From bc32ff5db9e507b8843b50bb8222dd9cabbf8de2 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Fri, 22 Oct 2021 17:03:57 +0200 Subject: [PATCH 01/31] Added Bell-47 --- Moose Development/Moose/Utilities/Utils.lua | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index a2ed997ed..a9511dd1e 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -1613,6 +1613,11 @@ function UTILS.IsLoadingDoorOpen( unit_name ) ret_val = true end + if string.find(type_name, "Bell-47") then -- bell aint got no doors so always ready to load injured soldiers + BASE:T(unit_name .. " door is open") + ret_val = true + end + if ret_val == false then BASE:T(unit_name .. " all doors are closed") end From ad36ab520b7ea3776c8ab00590da74ad386419ea Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Fri, 22 Oct 2021 17:04:03 +0200 Subject: [PATCH 02/31] Added Bell-47 --- Moose Development/Moose/Ops/CSAR.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/Ops/CSAR.lua b/Moose Development/Moose/Ops/CSAR.lua index 49a5ebb90..b166009df 100644 --- a/Moose Development/Moose/Ops/CSAR.lua +++ b/Moose Development/Moose/Ops/CSAR.lua @@ -238,10 +238,11 @@ CSAR.AircraftType["Mi-8MTV2"] = 12 CSAR.AircraftType["Mi-8MT"] = 12 CSAR.AircraftType["Mi-24P"] = 8 CSAR.AircraftType["Mi-24V"] = 8 +CSAR.AircraftType["Bell-47"] = 2 --- CSAR class version. -- @field #string version -CSAR.version="0.1.11r1" +CSAR.version="0.1.11r2" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToDo list From 61ac6b413151dbb86a2ea0eecd500d5fa26769c8 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Fri, 22 Oct 2021 17:04:19 +0200 Subject: [PATCH 03/31] Added Bell-47 --- Moose Development/Moose/Utilities/Utils.lua | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 9a8e7e62e..f99053ee1 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -1612,7 +1612,12 @@ function UTILS.IsLoadingDoorOpen( unit_name ) BASE:T(unit_name .. " side door is open") ret_val = true end - + + if string.find(type_name, "Bell-47") then -- bell aint got no doors so always ready to load injured soldiers + BASE:T(unit_name .. " door is open") + ret_val = true + end + if ret_val == false then BASE:T(unit_name .. " all doors are closed") end From fe3079caad397cbf93e291f978d4e0d46b33b974 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Fri, 22 Oct 2021 17:04:23 +0200 Subject: [PATCH 04/31] Added Bell-47 --- Moose Development/Moose/Ops/CSAR.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/Ops/CSAR.lua b/Moose Development/Moose/Ops/CSAR.lua index 49a5ebb90..b166009df 100644 --- a/Moose Development/Moose/Ops/CSAR.lua +++ b/Moose Development/Moose/Ops/CSAR.lua @@ -238,10 +238,11 @@ CSAR.AircraftType["Mi-8MTV2"] = 12 CSAR.AircraftType["Mi-8MT"] = 12 CSAR.AircraftType["Mi-24P"] = 8 CSAR.AircraftType["Mi-24V"] = 8 +CSAR.AircraftType["Bell-47"] = 2 --- CSAR class version. -- @field #string version -CSAR.version="0.1.11r1" +CSAR.version="0.1.11r2" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToDo list From c74c475a29d5214ffd109b07d107e79f4b98cff3 Mon Sep 17 00:00:00 2001 From: madmoney99 Date: Sat, 23 Oct 2021 16:34:21 -0700 Subject: [PATCH 05/31] Forrestal Wire Corrections Tested on MP and SP. Both were recording incorrect wires on 1/2 and 4 wires regularly with an occasional 3 wire miss. This tested correct in the Hornet. TonyG --- Moose Development/Moose/Ops/Airboss.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 1243b086f..edb660534 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -4404,10 +4404,10 @@ function AIRBOSS:_InitForrestal() self.carrierparam.rwywidth = 25 -- Wires. - self.carrierparam.wire1 = 42 -- Distance from stern to first wire. - self.carrierparam.wire2 = 51.5 - self.carrierparam.wire3 = 62 - self.carrierparam.wire4 = 72.5 + self.carrierparam.wire1 = 44 -- Distance from stern to first wire. Original from Frank - 42 + self.carrierparam.wire2 = 54 --51.5 + self.carrierparam.wire3 = 64 --62 + self.carrierparam.wire4 = 74 --72.5 end From 8af3f89c143582cdb00872ee89c41eed2b048410 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sun, 24 Oct 2021 14:35:55 +0200 Subject: [PATCH 06/31] Adjustments for Forrestal by Pene --- Moose Development/Moose/Ops/Airboss.lua | 272 ++++++++++++------------ 1 file changed, 136 insertions(+), 136 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 25ba1d265..6b7045538 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -27,11 +27,11 @@ -- **Supported Carriers:** -- -- * [USS John C. Stennis](https://en.wikipedia.org/wiki/USS_John_C._Stennis) (CVN-74) --- * [USS Theodore Roosevelt](https://en.wikipedia.org/wiki/USS_Theodore_Roosevelt_(CVN-71)) (CVN-71) [Super Carrier Module] --- * [USS Abraham Lincoln](https://en.wikipedia.org/wiki/USS_Abraham_Lincoln_(CVN-72)) (CVN-72) [Super Carrier Module] --- * [USS George Washington](https://en.wikipedia.org/wiki/USS_George_Washington_(CVN-73)) (CVN-73) [Super Carrier Module] --- * [USS Harry S. Truman](https://en.wikipedia.org/wiki/USS_Harry_S._Truman) (CVN-75) [Super Carrier Module] --- * [USS Forrestal](https://en.wikipedia.org/wiki/USS_Forrestal_(CV-59)) (CV-59) [Heatblur Carrier Module] +-- * [USS Theodore Roosevelt](https://en.wikipedia.org/wiki/USS_Theodore_Roosevelt_(CVN-71)) (CVN-71) [Super Carrier Module] +-- * [USS Abraham Lincoln](https://en.wikipedia.org/wiki/USS_Abraham_Lincoln_(CVN-72)) (CVN-72) [Super Carrier Module] +-- * [USS George Washington](https://en.wikipedia.org/wiki/USS_George_Washington_(CVN-73)) (CVN-73) [Super Carrier Module] +-- * [USS Harry S. Truman](https://en.wikipedia.org/wiki/USS_Harry_S._Truman) (CVN-75) [Super Carrier Module] +-- * [USS Forrestal](https://en.wikipedia.org/wiki/USS_Forrestal_(CV-59)) (CV-59) [Heatblur Carrier Module] -- * [USS Tarawa](https://en.wikipedia.org/wiki/USS_Tarawa_(LHA-1)) (LHA-1) [**WIP**] -- * [USS America](https://en.wikipedia.org/wiki/USS_America_(LHA-6)) (LHA-6) [**WIP**] -- * [Juan Carlos I](https://en.wikipedia.org/wiki/Spanish_amphibious_assault_ship_Juan_Carlos_I) (L61) [**WIP**] @@ -300,7 +300,7 @@ -- -- Once the aircraft reaches the Initial, the landing pattern begins. The important steps of the pattern are shown in the image above. -- The AV-8B Harrier pattern is very similar, the only differences are as there is no angled deck there is no wake check. from the ninety you wil fly a straight approach offset 26 ft to port (left) of the tram line. --- The aim is to arrive abeam the landing spot in a stable hover at 120 ft with forward speed matched to the boat. From there the LSO will call "cleared to land". You then level cross to the tram line at the designated landing spot at land vertcally. +-- The aim is to arrive abeam the landing spot in a stable hover at 120 ft with forward speed matched to the boat. From there the LSO will call "cleared to land". You then level cross to the tram line at the designated landing spot at land vertcally. -- -- -- ## CASE III @@ -2028,10 +2028,10 @@ function AIRBOSS:New(carriername, alias) self:_GetZonePlatform(case):SmokeZone(SMOKECOLOR.Blue, 45) self:_GetZoneCorridor(case):SmokeZone(SMOKECOLOR.Green, 45) self:_GetZoneHolding(case, 1):SmokeZone(SMOKECOLOR.White, 45) - self:_GetZoneHolding(case, 2):SmokeZone(SMOKECOLOR.White, 45) + self:_GetZoneHolding(case, 2):SmokeZone(SMOKECOLOR.White, 45) self:_GetZoneInitial(case):SmokeZone(SMOKECOLOR.Orange, 45) self:_GetZoneCommence(case, 1):SmokeZone(SMOKECOLOR.Red, 45) - self:_GetZoneCommence(case, 2):SmokeZone(SMOKECOLOR.Red, 45) + self:_GetZoneCommence(case, 2):SmokeZone(SMOKECOLOR.Red, 45) self:_GetZoneAbeamLandingSpot():SmokeZone(SMOKECOLOR.Red, 5) self:_GetZoneLandingSpot():SmokeZone(SMOKECOLOR.Red, 5) end @@ -2855,7 +2855,7 @@ function AIRBOSS:SetLineupErrorThresholds(_max,_min, Left, LeftMed, LEFT, Right, self.lue.LeftMed=LeftMed or -2.0 self.lue.LEFT=LEFT or -3.0 self.lue.Right=Right or 1.0 - self.lue.RightMed=RightMed or 2.0 + self.lue.RightMed=RightMed or 2.0 self.lue.RIGHT=RIGHT or 3.0 return self end @@ -4404,10 +4404,10 @@ function AIRBOSS:_InitForrestal() self.carrierparam.rwywidth = 25 -- Wires. - self.carrierparam.wire1 = 42 -- Distance from stern to first wire. - self.carrierparam.wire2 = 51.5 - self.carrierparam.wire3 = 62 - self.carrierparam.wire4 = 72.5 + self.carrierparam.wire1 = 44 -- Distance from stern to first wire. Original from Frank - 42 + self.carrierparam.wire2 = 54 --51.5 + self.carrierparam.wire3 = 64 --62 + self.carrierparam.wire4 = 74 --72.5 end @@ -5012,7 +5012,7 @@ function AIRBOSS:_InitVoiceOvers() duration=2.0, subduration=5, }, - EXPECTSPOT5={ + EXPECTSPOT5={ file="LSO-ExpectSpot5", suffix="ogg", loud=false, @@ -5851,8 +5851,8 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) elseif skyhawk then alt=UTILS.FeetToMeters(600) speed=UTILS.KnotsToMps(250) - elseif goshawk then - alt=UTILS.FeetToMeters(800) + elseif goshawk then + alt=UTILS.FeetToMeters(800) speed=UTILS.KnotsToMps(300) end @@ -5864,8 +5864,8 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) elseif skyhawk then alt=UTILS.FeetToMeters(600) speed=UTILS.KnotsToMps(250) - elseif goshawk then - alt=UTILS.FeetToMeters(800) + elseif goshawk then + alt=UTILS.FeetToMeters(800) speed=UTILS.KnotsToMps(300) end @@ -5902,7 +5902,7 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) dist=UTILS.NMToMeters(1.2) end - if goshawk then + if goshawk then -- 0.9 to 1.1 NM per natops ch.4 page 48 dist=UTILS.NMToMeters(0.9) else @@ -5913,7 +5913,7 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) if hornet or tomcat then alt=UTILS.FeetToMeters(500) - elseif goshawk then + elseif goshawk then alt=UTILS.FeetToMeters(450) elseif skyhawk then alt=UTILS.FeetToMeters(500) @@ -5946,7 +5946,7 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) alt=UTILS.FeetToMeters(300) --? elseif harrier then -- 300-325 ft - alt=UTILS.FeetToMeters(300)-- Need to verify + alt=UTILS.FeetToMeters(300)-- Need to verify end aoa=aoaac.OnSpeed @@ -6415,8 +6415,8 @@ function AIRBOSS:_MarshalPlayer(playerData, stack) -- Set stack flag. flight.flag=stack - -- Trigger Marshal event. - self:Marshal(flight) + -- Trigger Marshal event. + self:Marshal(flight) end else @@ -9674,7 +9674,7 @@ function AIRBOSS:_Bullseye(playerData) self:_PlayerHint(playerData) -- LSO expect spot 5 or 7.5 call - if playerData.actype==AIRBOSS.AircraftCarrier.AV8B and self.carriertype==AIRBOSS.CarrierType.JCARLOS then + if playerData.actype==AIRBOSS.AircraftCarrier.AV8B and self.carriertype==AIRBOSS.CarrierType.JCARLOS then self:RadioTransmission(self.LSORadio, self.LSOCall.EXPECTSPOT5, nil, nil, nil, true) elseif playerData.actype==AIRBOSS.AircraftCarrier.AV8B then self:RadioTransmission(self.LSORadio, self.LSOCall.EXPECTSPOT75, nil, nil, nil, true) @@ -9859,7 +9859,7 @@ function AIRBOSS:_Abeam(playerData) self:RadioTransmission(self.LSORadio, self.LSOCall.PADDLESCONTACT, nil, nil, nil, true) -- LSO expect spot 5 or 7.5 call - if playerData.actype==AIRBOSS.AircraftCarrier.AV8B and self.carriertype==AIRBOSS.CarrierType.JCARLOS then + if playerData.actype==AIRBOSS.AircraftCarrier.AV8B and self.carriertype==AIRBOSS.CarrierType.JCARLOS then self:RadioTransmission(self.LSORadio, self.LSOCall.EXPECTSPOT5, false, 5, nil, true) elseif playerData.actype==AIRBOSS.AircraftCarrier.AV8B then self:RadioTransmission(self.LSORadio, self.LSOCall.EXPECTSPOT75, false, 5, nil, true) @@ -10249,25 +10249,25 @@ function AIRBOSS:_Groove(playerData) -- Distance in NM. local d=UTILS.MetersToNM(rho) - -- Drift on lineup. - if rho>=RAR and rho<=RIM then - if gd.LUE>0.22 and lineupError<-0.22 then - env.info" Drift Right across centre ==> DR-" - gd.Drift=" DR" - self:T(self.lid..string.format("Got Drift Right across centre step %s, d=%.3f: Max LUE=%.3f, lower LUE=%.3f", gs, d, gd.LUE, lineupError)) - elseif gd.LUE<-0.22 and lineupError>0.22 then - env.info" Drift Left ==> DL-" - gd.Drift=" DL" - self:T(self.lid..string.format("Got Drift Left across centre at step %s, d=%.3f: Min LUE=%.3f, lower LUE=%.3f", gs, d, gd.LUE, lineupError)) - elseif gd.LUE>0.13 and lineupError<-0.14 then - env.info" Little Drift Right across centre ==> (DR-)" - gd.Drift=" (DR)" - self:T(self.lid..string.format("Got Little Drift Right across centre at step %s, d=%.3f: Max LUE=%.3f, lower LUE=%.3f", gs, d, gd.LUE, lineupError)) - elseif gd.LUE<-0.13 and lineupError>0.14 then - env.info" Little Drift Left across centre ==> (DL-)" - gd.Drift=" (DL)" - self:E(self.lid..string.format("Got Little Drift Left across centre at step %s, d=%.3f: Min LUE=%.3f, lower LUE=%.3f", gs, d, gd.LUE, lineupError)) - end + -- Drift on lineup. + if rho>=RAR and rho<=RIM then + if gd.LUE>0.22 and lineupError<-0.22 then + env.info" Drift Right across centre ==> DR-" + gd.Drift=" DR" + self:T(self.lid..string.format("Got Drift Right across centre step %s, d=%.3f: Max LUE=%.3f, lower LUE=%.3f", gs, d, gd.LUE, lineupError)) + elseif gd.LUE<-0.22 and lineupError>0.22 then + env.info" Drift Left ==> DL-" + gd.Drift=" DL" + self:T(self.lid..string.format("Got Drift Left across centre at step %s, d=%.3f: Min LUE=%.3f, lower LUE=%.3f", gs, d, gd.LUE, lineupError)) + elseif gd.LUE>0.13 and lineupError<-0.14 then + env.info" Little Drift Right across centre ==> (DR-)" + gd.Drift=" (DR)" + self:T(self.lid..string.format("Got Little Drift Right across centre at step %s, d=%.3f: Max LUE=%.3f, lower LUE=%.3f", gs, d, gd.LUE, lineupError)) + elseif gd.LUE<-0.13 and lineupError>0.14 then + env.info" Little Drift Left across centre ==> (DL-)" + gd.Drift=" (DL)" + self:E(self.lid..string.format("Got Little Drift Left across centre at step %s, d=%.3f: Min LUE=%.3f, lower LUE=%.3f", gs, d, gd.LUE, lineupError)) + end end -- Update max deviation of line up error. @@ -10613,10 +10613,10 @@ function AIRBOSS:_GetWire(Lcoord, dc) -- Corrected landing distance wrt to stern. Landing distance needs to be reduced due to delayed landing event for human players. local d=Ldist-dc - + -- Multiplayer wire correction. if self.mpWireCorrection then - d=d-self.mpWireCorrection + d=d-self.mpWireCorrection end -- Shift wires from stern to their correct position. @@ -10709,7 +10709,7 @@ function AIRBOSS:_Trapped(playerData) elseif playerData.actype==AIRBOSS.AircraftCarrier.A4EC then -- A-4E gets slowed down much faster the the F/A-18C! dcorr=56 - elseif playerData.actype==AIRBOSS.AircraftCarrier.T45C then + elseif playerData.actype==AIRBOSS.AircraftCarrier.T45C then -- T-45 also gets slowed down much faster the the F/A-18C. dcorr=56 end @@ -11340,7 +11340,7 @@ function AIRBOSS:_GetZoneHolding(case, stack) -- So stay 0-5 NM (+1 NM error margin) port of carrier. self.zoneHolding=self.zoneHolding or ZONE_POLYGON_BASE:New("CASE II/III Holding Zone") - self.zoneHolding:UpdateFromVec2(p) + self.zoneHolding:UpdateFromVec2(p) end return self.zoneHolding @@ -11386,12 +11386,12 @@ function AIRBOSS:_GetZoneCommence(case, stack) -- Create holding zone. self.zoneCommence=self.zoneCommence or ZONE_RADIUS:New("CASE I Commence Zone") - self.zoneCommence:UpdateFromVec2(Three:GetVec2(), R) + self.zoneCommence:UpdateFromVec2(Three:GetVec2(), R) else -- Case II/III - stack=stack or 1 + stack=stack or 1 -- Start point at 21 NM for stack=1. local l=20+stack @@ -11419,7 +11419,7 @@ function AIRBOSS:_GetZoneCommence(case, stack) -- Zone polygon. self.zoneCommence=self.zoneCommence or ZONE_POLYGON_BASE:New("CASE II/III Commence Zone") - self.zoneCommence:UpdateFromVec2(p) + self.zoneCommence:UpdateFromVec2(p) end @@ -11679,7 +11679,7 @@ function AIRBOSS:_GetOptLandingCoordinate() if self.carriertype==AIRBOSS.CarrierType.TARAWA then -- Landing 100 ft abeam, 120 ft alt. - self.landingcoord:UpdateFromCoordinate(self:_GetLandingSpotCoordinate()):Translate(35, FB-90, true, true) + self.landingcoord:UpdateFromCoordinate(self:_GetLandingSpotCoordinate()):Translate(35, FB-90, true, true) --stern=self:_GetLandingSpotCoordinate():Translate(35, FB-90) -- Alitude 120 ft. @@ -11687,21 +11687,21 @@ function AIRBOSS:_GetOptLandingCoordinate() elseif self.carriertype==AIRBOSS.CarrierType.AMERICA then -- Landing 100 ft abeam, 120 ft alt. To allow adjustments to match different deck configurations. - self.landingcoord:UpdateFromCoordinate(self:_GetLandingSpotCoordinate()):Translate(35, FB-90, true, true) + self.landingcoord:UpdateFromCoordinate(self:_GetLandingSpotCoordinate()):Translate(35, FB-90, true, true) --stern=self:_GetLandingSpotCoordinate():Translate(35, FB-90) -- Alitude 120 ft. self.landingcoord:SetAltitude(UTILS.FeetToMeters(120)) - + elseif self.carriertype==AIRBOSS.CarrierType.JCARLOS then -- Landing 100 ft abeam, 120 ft alt. - self.landingcoord:UpdateFromCoordinate(self:_GetLandingSpotCoordinate()):Translate(35, FB-100, true, true) + self.landingcoord:UpdateFromCoordinate(self:_GetLandingSpotCoordinate()):Translate(35, FB-100, true, true) --stern=self:_GetLandingSpotCoordinate():Translate(35, FB-100) -- Alitude 120 ft. self.landingcoord:SetAltitude(UTILS.FeetToMeters(120)) - + else -- Ideally we want to land between 2nd and 3rd wire. @@ -11743,7 +11743,7 @@ function AIRBOSS:_GetLandingSpotCoordinate() -- Primary landing spot 7.5 a little further forwad on the America self.landingspotcoord:Translate(59, hdg, true, true):SetAltitude(self.carrierparam.deckheight) - + elseif self.carriertype==AIRBOSS.CarrierType.JCARLOS then -- Landing 100 ft abeam, 120 alt. @@ -11751,7 +11751,7 @@ function AIRBOSS:_GetLandingSpotCoordinate() -- Primary landing spot 5.0 -- TODO voice for different landing Spots. self.landingspotcoord:Translate(89, hdg, true, true):SetAltitude(self.carrierparam.deckheight) - + end return self.landingspotcoord @@ -12241,10 +12241,10 @@ end -- * 12-21 seconds: OK (15-18 is ideal) -- * 22-24 seconds: Fair "(OK) -- * > 24 seconds: No Grade "--" --- +-- -- If you manage to be between 16.4 and and 16.6 seconds, you will even get and okay underline "\_OK\_". -- No groove time for Harrier on LHA, LHD set to Tgroove Unicorn as starting point to allow possible _OK_ 5.0. --- If time in the AV-8B +-- If time in the AV-8B -- -- * < 90 seconds: OK V/STOL -- * > 91 Seconds: SLOW V/STOL (Early hover stop selection) @@ -12256,7 +12256,7 @@ function AIRBOSS:_EvalGrooveTime(playerData) -- Time in groove. local t=playerData.Tgroove - + local grade="" if t<9 then grade="_NESA_" @@ -12276,12 +12276,12 @@ function AIRBOSS:_EvalGrooveTime(playerData) else grade="LIG" end - + -- The unicorn! if t>=16.4 and t<=16.6 then grade="_OK_" end - + -- V/STOL Unicorn! if playerData.actype==AIRBOSS.AircraftCarrier.AV8B and (t>=65.0 and t<=75.0) then grade="_OK_ V/STOL" @@ -12322,7 +12322,7 @@ function AIRBOSS:_LSOgrade(playerData) local Tgroove=playerData.Tgroove local TgrooveUnicorn=Tgroove and (Tgroove>=15.0 and Tgroove<=18.99) or false local TgrooveVstolUnicorn=Tgroove and (Tgroove>=65.0 and Tgroove<=70.0)and playerData.actype==AIRBOSS.AircraftCarrier.AV8B or false - + local grade local points if N==0 and (TgrooveUnicorn or TgrooveVstolUnicorn ) then @@ -12331,18 +12331,18 @@ function AIRBOSS:_LSOgrade(playerData) points=5.0 G="Unicorn" else - - -- Add AV-8B Harrier devation allowances due to lower groundspeed and 3x conventional groove time, this allows to maintain LSO tolerances while respecting the deviations are not unsafe. (WIP requires feedback) + + -- Add AV-8B Harrier devation allowances due to lower groundspeed and 3x conventional groove time, this allows to maintain LSO tolerances while respecting the deviations are not unsafe. (WIP requires feedback) -- Large devaitions still result in a No Grade, A Unicorn still requires a clean pass with no deviation. - if nL>3 and playerData.actype==AIRBOSS.AircraftCarrier.AV8B then + if nL>3 and playerData.actype==AIRBOSS.AircraftCarrier.AV8B then -- Larger deviations ==> "No grade" 2.0 points. grade="--" points=2.0 - elseif nN>2 and playerData.actype==AIRBOSS.AircraftCarrier.AV8B then + elseif nN>2 and playerData.actype==AIRBOSS.AircraftCarrier.AV8B then -- Only average deviations ==> "Fair Pass" Pass with average deviations and corrections. grade="(OK)" points=3.0 - elseif nL>0 then + elseif nL>0 then -- Larger deviations ==> "No grade" 2.0 points. grade="--" points=2.0 @@ -12355,7 +12355,7 @@ function AIRBOSS:_LSOgrade(playerData) grade="OK" points=4.0 end - + end -- Replace" )"( and "__" @@ -12466,35 +12466,35 @@ function AIRBOSS:_Flightdata2Text(playerData, groovestep) -- Aircraft specific AoA values. local acaoa=self:_GetAircraftAoA(playerData) - + --Angled Approach. local P=nil if step==AIRBOSS.PatternStep.GROOVE_XX and ROL<=4.0 and playerData.case<3 then - if LUE>self.lue.RIGHT then - P=underline("AA") - elseif - LUE>self.lue.RightMed then - P="AA " - elseif - LUE>self.lue.Right then - P=little("AA") - end + if LUE>self.lue.RIGHT then + P=underline("AA") + elseif + LUE>self.lue.RightMed then + P="AA " + elseif + LUE>self.lue.Right then + P=little("AA") + end end - + --Overshoot Start. local O=nil if step==AIRBOSS.PatternStep.GROOVE_XX then - if LUEacaoa.SLOW then @@ -12536,21 +12536,21 @@ function AIRBOSS:_Flightdata2Text(playerData, groovestep) elseif LUE>self.lue._max then D=little("LUL") elseif playerData.case<3 then - if LUE1 then - text=text..string.format(" Marshal radial %d°.", self.skipperOffset) - end + if case>1 then + text=text..string.format(" Marshal radial %d°.", self.skipperOffset) + end if self:IsRecovering() then text="negative, carrier is already recovering." self:MessageToPlayer(playerData, text, "AIRBOSS") @@ -18065,12 +18065,12 @@ function AIRBOSS:_SaveTrapSheet(playerData, grade) for i=1,9999 do -- Create file name - if self.trapprefix then - filename=string.format("%s_%s-%04d.csv", self.trapprefix, playerData.actype, i) - else - local name=UTILS.ReplaceIllegalCharacters(playerData.name, "_") - filename=string.format("AIRBOSS-%s_Trapsheet-%s_%s-%04d.csv", self.alias, name, playerData.actype, i) - end + if self.trapprefix then + filename=string.format("%s_%s-%04d.csv", self.trapprefix, playerData.actype, i) + else + local name=UTILS.ReplaceIllegalCharacters(playerData.name, "_") + filename=string.format("AIRBOSS-%s_Trapsheet-%s_%s-%04d.csv", self.alias, name, playerData.actype, i) + end -- Set path. if path~=nil then From c52e30ceae6a84b04236ff52f88b60364979d2b9 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Mon, 25 Oct 2021 16:28:11 +0200 Subject: [PATCH 07/31] Added smoke color per RECCE --- .../Moose/Functional/Autolase.lua | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/Functional/Autolase.lua b/Moose Development/Moose/Functional/Autolase.lua index a1085f1b6..1ce6f951b 100644 --- a/Moose Development/Moose/Functional/Autolase.lua +++ b/Moose Development/Moose/Functional/Autolase.lua @@ -31,6 +31,7 @@ -- * Targets are lased by threat priority order -- * Use FSM events to link functionality into your scripts -- * Easy set-up +-- * Set laser codes and smoke colors per Recce unit -- -- # 2 Basic usage -- @@ -46,7 +47,7 @@ -- -- local autolaser = AUTOLASE:New(FoxSet,coalition.side.BLUE,"Wolfpack",Pilotset) -- --- ## 2.5 Example - Using a fixed laser code for a specific Recce unit: +-- ## 2.5 Example - Using a fixed laser code and color for a specific Recce unit: -- -- local recce = SPAWN:New("Reaper") -- :InitDelayOff() @@ -55,6 +56,7 @@ -- local unit = group:GetUnit(1) -- local name = unit:GetName() -- autolaser:SetRecceLaserCode(name,1688) +-- autolaser:SetRecceSmokeColor(name,SMOKECOLOR.Red) -- end -- ) -- :InitCleanUp(60) @@ -107,7 +109,7 @@ AUTOLASE = { --- AUTOLASE class version. -- @field #string version -AUTOLASE.version = "0.0.8" +AUTOLASE.version = "0.0.9" ------------------------------------------------------------------- -- Begin Functional.Autolase.lua @@ -169,6 +171,7 @@ function AUTOLASE:New(RecceSet, Coalition, Alias, PilotSet) self.UnitsByThreat = {} self.RecceNames = {} self.RecceLaserCode = {} + self.RecceSmokeColor = {} self.RecceUnitNames= {} self.maxlasing = 4 self.CurrentLasing = {} @@ -330,6 +333,20 @@ function AUTOLASE:GetLaserCode(RecceName) return code end +--- (Internal) Function to get a smoke color by recce name +-- @param #AUTOLASE self +-- @param #string RecceName Unit(!) name of the Recce +-- @return #AUTOLASE self +function AUTOLASE:GetSmokeColor(RecceName) + local color = self.smokecolor + if self.RecceSmokeColor[RecceName] == nil then + self.RecceSmokeColor[RecceName] = color + else + color = self.RecceLaserCode[RecceName] + end + return color +end + --- (User) Function enable sending messages via SRS. -- @param #AUTOLASE self -- @param #boolean OnOff Switch usage on and off @@ -374,6 +391,17 @@ function AUTOLASE:SetRecceLaserCode(RecceName, Code) return self end +--- (User) Function to set a specific smoke color for a Recce. +-- @param #AUTOLASE self +-- @param #string RecceName (Unit!) Name of the Recce +-- @param #number Color The color, e.g. SMOKECOLOR.Red, SMOKECOLOR.Green etc +-- @return #AUTOLASE self +function AUTOLASE:SetRecceSmokeColor(RecceName, Color) + local color = Color or self.smokecolor + self.RecceSmokeColor[RecceName] = color + return self +end + --- (User) Function to force laser cooldown and cool down time -- @param #AUTOLASE self -- @param #boolean OnOff Switch cool down on (true) or off (false) - defaults to true @@ -812,7 +840,8 @@ function AUTOLASE:onafterMonitor(From, Event, To) } if self.smoketargets then local coord = unit:GetCoordinate() - coord:Smoke(self.smokecolor) + local color = self:GetSmokeColor(reccename) + coord:Smoke(color) end self.lasingindex = self.lasingindex + 1 self.CurrentLasing[self.lasingindex] = laserspot From e2eb13739c13b2299ebac782cb3876f39d16f79b Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Mon, 25 Oct 2021 16:28:40 +0200 Subject: [PATCH 08/31] Commented out wall of text when joining a unit as a player --- Moose Development/Moose/Tasking/CommandCenter.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/Tasking/CommandCenter.lua b/Moose Development/Moose/Tasking/CommandCenter.lua index 2bcfe972c..7de7f5b00 100644 --- a/Moose Development/Moose/Tasking/CommandCenter.lua +++ b/Moose Development/Moose/Tasking/CommandCenter.lua @@ -216,7 +216,7 @@ function COMMANDCENTER:New( CommandCenterPositionable, CommandCenterName ) local MenuReporting = MENU_GROUP:New( EventGroup, "Missions Reports", CommandCenterMenu ) local MenuMissionsSummary = MENU_GROUP_COMMAND:New( EventGroup, "Missions Status Report", MenuReporting, self.ReportSummary, self, EventGroup ) local MenuMissionsDetails = MENU_GROUP_COMMAND:New( EventGroup, "Missions Players Report", MenuReporting, self.ReportMissionsPlayers, self, EventGroup ) - self:ReportSummary( EventGroup ) + --self:ReportSummary( EventGroup ) local PlayerUnit = EventData.IniUnit for MissionID, Mission in pairs( self:GetMissions() ) do local Mission = Mission -- Tasking.Mission#MISSION From 3e1005aef124f4b3a3f81d5491a9e2c7f1ad5971 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Mon, 25 Oct 2021 16:29:14 +0200 Subject: [PATCH 09/31] Small correction to silence wall of text when adding tasks to a mission --- Moose Development/Moose/Tasking/Task_Cargo_Dispatcher.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/Tasking/Task_Cargo_Dispatcher.lua b/Moose Development/Moose/Tasking/Task_Cargo_Dispatcher.lua index e1018bdf0..fd3be1018 100644 --- a/Moose Development/Moose/Tasking/Task_Cargo_Dispatcher.lua +++ b/Moose Development/Moose/Tasking/Task_Cargo_Dispatcher.lua @@ -710,7 +710,7 @@ do -- TASK_CARGO_DISPATCHER self.TransportCount = self.TransportCount + 1 - local verbose = Silent and true + local verbose = Silent or false local TaskName = string.format( ( TaskPrefix or "Transport" ) .. ".%03d", self.TransportCount ) From dadfd803f77729e83552bffd27ec499e01749c82 Mon Sep 17 00:00:00 2001 From: Applevangelist <72444570+Applevangelist@users.noreply.github.com> Date: Tue, 26 Oct 2021 13:58:11 +0200 Subject: [PATCH 10/31] Update Autolase.lua added docu --- Moose Development/Moose/Functional/Autolase.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Functional/Autolase.lua b/Moose Development/Moose/Functional/Autolase.lua index 1ce6f951b..4c503d1da 100644 --- a/Moose Development/Moose/Functional/Autolase.lua +++ b/Moose Development/Moose/Functional/Autolase.lua @@ -30,8 +30,8 @@ -- * Detect and lase contacts automatically -- * Targets are lased by threat priority order -- * Use FSM events to link functionality into your scripts --- * Easy set-up -- * Set laser codes and smoke colors per Recce unit +-- * Easy set-up -- -- # 2 Basic usage -- @@ -73,7 +73,7 @@ -- @module Functional.Autolase -- @image Designation.JPG -- --- Date: Oct 2021 +-- Date: 24 Oct 2021 -- --- Class AUTOLASE -- @type AUTOLASE From 27e21e77f987459bcf7432ec4efd37ad5f15e8f3 Mon Sep 17 00:00:00 2001 From: Applevangelist <72444570+Applevangelist@users.noreply.github.com> Date: Wed, 27 Oct 2021 11:19:32 +0200 Subject: [PATCH 11/31] Update Task_Cargo_Transport.lua --- Moose Development/Moose/Tasking/Task_Cargo_Transport.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/Tasking/Task_Cargo_Transport.lua b/Moose Development/Moose/Tasking/Task_Cargo_Transport.lua index 484363978..1e8e37b4f 100644 --- a/Moose Development/Moose/Tasking/Task_Cargo_Transport.lua +++ b/Moose Development/Moose/Tasking/Task_Cargo_Transport.lua @@ -57,7 +57,7 @@ do -- TASK_CARGO_TRANSPORT - --- @type TASK_CARGO_TRANSPORT + -- @type TASK_CARGO_TRANSPORT -- @extends Tasking.Task_CARGO#TASK_CARGO --- Orchestrates the task for players to transport cargo to or between deployment zones. From 513406f0e5b5f25dee1e45f08d362d9606ad37ed Mon Sep 17 00:00:00 2001 From: Applevangelist <72444570+Applevangelist@users.noreply.github.com> Date: Wed, 27 Oct 2021 13:09:30 +0200 Subject: [PATCH 12/31] Update Task_Cargo_Transport.lua --- .../Moose/Tasking/Task_Cargo_Transport.lua | 80 +++++++++---------- 1 file changed, 39 insertions(+), 41 deletions(-) diff --git a/Moose Development/Moose/Tasking/Task_Cargo_Transport.lua b/Moose Development/Moose/Tasking/Task_Cargo_Transport.lua index 1e8e37b4f..8629871b2 100644 --- a/Moose Development/Moose/Tasking/Task_Cargo_Transport.lua +++ b/Moose Development/Moose/Tasking/Task_Cargo_Transport.lua @@ -2,7 +2,7 @@ -- -- **Specific features:** -- --- * Creates a task to transport @{Cargo.Cargo} to and between deployment zones. +-- * Creates a task to transport #Cargo.Cargo to and between deployment zones. -- * Derived from the TASK_CARGO class, which is derived from the TASK class. -- * Orchestrate the task flow, so go from Planned to Assigned to Success, Failed or Cancelled. -- * Co-operation tasking, so a player joins a group of players executing the same task. @@ -44,7 +44,7 @@ -- -- === -- --- Please read through the @{Tasking.Task_Cargo} process to understand the mechanisms of tasking and cargo tasking and handling. +-- Please read through the #Tasking.Task_Cargo process to understand the mechanisms of tasking and cargo tasking and handling. -- -- Enjoy! -- FC @@ -76,7 +76,7 @@ do -- TASK_CARGO_TRANSPORT -- -- ## 1.1) Create a command center. -- - -- First you need to create a command center using the @{Tasking.CommandCenter#COMMANDCENTER.New}() constructor. + -- First you need to create a command center using the Tasking.CommandCenter#COMMANDCENTER.New constructor. -- -- local CommandCenter = COMMANDCENTER -- :New( HQ, "Lima" ) -- Create the CommandCenter. @@ -85,7 +85,7 @@ do -- TASK_CARGO_TRANSPORT -- -- Tasks work in a mission, which groups these tasks to achieve a joint mission goal. -- A command center can govern multiple missions. - -- Create a new mission, using the @{Tasking.Mission#MISSION.New}() constructor. + -- Create a new mission, using the Tasking.Mission#MISSION.New constructor. -- -- -- Declare the Mission for the Command Center. -- local Mission = MISSION @@ -99,7 +99,7 @@ do -- TASK_CARGO_TRANSPORT -- ## 1.3) Create the transport cargo task. -- -- So, now that we have a command center and a mission, we now create the transport task. - -- We create the transport task using the @{#TASK_CARGO_TRANSPORT.New}() constructor. + -- We create the transport task using the #TASK_CARGO_TRANSPORT.New constructor. -- -- Because a transport task will not generate the cargo itself, you'll need to create it first. -- The cargo in this case will be the downed pilot! @@ -118,7 +118,7 @@ do -- TASK_CARGO_TRANSPORT -- -- The cargoset "CargoSet" will embed all defined cargo of type "Pilots" (prefix) into its set. -- local CargoGroup = CARGO_GROUP:New( PilotGroup, "Cargo", "Engineer Team 1", 500 ) -- - -- What is also needed, is to have a set of @{Core.Group}s defined that contains the clients of the players. + -- What is also needed, is to have a set of Core.Groups defined that contains the clients of the players. -- -- -- Allocate the Transport, which are the helicopter to retrieve the pilot, that can be manned by players. -- local GroupSet = SET_GROUP:New():FilterPrefixes( "Transport" ):FilterStart() @@ -139,48 +139,48 @@ do -- TASK_CARGO_TRANSPORT -- By doing this, cargo transport tasking will become a dynamic experience. -- -- - -- # 2) Create a task using the @{Tasking.Task_Cargo_Dispatcher} module. + -- # 2) Create a task using the Tasking.Task_Cargo_Dispatcher module. -- - -- Actually, it is better to **GENERATE** these tasks using the @{Tasking.Task_Cargo_Dispatcher} module. - -- Using the dispatcher module, transport tasks can be created much more easy. + -- Actually, it is better to **GENERATE** these tasks using the Tasking.Task_Cargo_Dispatcher module. + -- Using the dispatcher module, transport tasks can be created easier. -- -- Find below an example how to use the TASK_CARGO_DISPATCHER class: -- -- - -- -- Find the HQ group. - -- HQ = GROUP:FindByName( "HQ", "Bravo" ) + -- -- Find the HQ group. + -- HQ = GROUP:FindByName( "HQ", "Bravo" ) -- - -- -- Create the command center with the name "Lima". - -- CommandCenter = COMMANDCENTER - -- :New( HQ, "Lima" ) + -- -- Create the command center with the name "Lima". + -- CommandCenter = COMMANDCENTER + -- :New( HQ, "Lima" ) -- - -- -- Create the mission, for the command center, with the name "Operation Cargo Fun", a "Tactical" mission, with the mission briefing "Transport Cargo", for the BLUE coalition. - -- Mission = MISSION - -- :New( CommandCenter, "Operation Cargo Fun", "Tactical", "Transport Cargo", coalition.side.BLUE ) + -- -- Create the mission, for the command center, with the name "Operation Cargo Fun", a "Tactical" mission, with the mission briefing "Transport Cargo", for the BLUE coalition. + -- Mission = MISSION + -- :New( CommandCenter, "Operation Cargo Fun", "Tactical", "Transport Cargo", coalition.side.BLUE ) -- - -- -- Create the SET of GROUPs containing clients (players) that will transport the cargo. - -- -- These are have a name that start with "Transport" and are of the "blue" coalition. - -- TransportGroups = SET_GROUP:New():FilterCoalitions( "blue" ):FilterPrefixes( "Transport" ):FilterStart() + -- -- Create the SET of GROUPs containing clients (players) that will transport the cargo. + -- -- These are have a name that start with "Transport" and are of the "blue" coalition. + -- TransportGroups = SET_GROUP:New():FilterCoalitions( "blue" ):FilterPrefixes( "Transport" ):FilterStart() -- -- - -- -- Here we create the TASK_CARGO_DISPATCHER object! This is where we assign the dispatcher to generate tasks in the Mission for the TransportGroups. - -- TaskDispatcher = TASK_CARGO_DISPATCHER:New( Mission, TransportGroups ) + -- -- Here we create the TASK_CARGO_DISPATCHER object! This is where we assign the dispatcher to generate tasks in the Mission for the TransportGroups. + -- TaskDispatcher = TASK_CARGO_DISPATCHER:New( Mission, TransportGroups ) -- -- - -- -- Here we declare the SET of CARGOs called "Workmaterials". - -- local CargoSetWorkmaterials = SET_CARGO:New():FilterTypes( "Workmaterials" ):FilterStart() + -- -- Here we declare the SET of CARGOs called "Workmaterials". + -- local CargoSetWorkmaterials = SET_CARGO:New():FilterTypes( "Workmaterials" ):FilterStart() -- - -- -- Here we declare (add) CARGO_GROUP objects of various types, that are filtered and added in the CargoSetworkmaterials cargo set. - -- -- These cargo objects have the type "Workmaterials" which is exactly the type of cargo the CargoSetworkmaterials is filtering on. - -- local EngineerCargoGroup = CARGO_GROUP:New( GROUP:FindByName( "Engineers" ), "Workmaterials", "Engineers", 250 ) - -- local ConcreteCargo = CARGO_SLINGLOAD:New( STATIC:FindByName( "Concrete" ), "Workmaterials", "Concrete", 150, 50 ) - -- local CrateCargo = CARGO_CRATE:New( STATIC:FindByName( "Crate" ), "Workmaterials", "Crate", 150, 50 ) - -- local EnginesCargo = CARGO_CRATE:New( STATIC:FindByName( "Engines" ), "Workmaterials", "Engines", 150, 50 ) - -- local MetalCargo = CARGO_CRATE:New( STATIC:FindByName( "Metal" ), "Workmaterials", "Metal", 150, 50 ) + -- -- Here we declare (add) CARGO_GROUP objects of various types, that are filtered and added in the CargoSetworkmaterials cargo set. + -- -- These cargo objects have the type "Workmaterials" which is exactly the type of cargo the CargoSetworkmaterials is filtering on. + -- local EngineerCargoGroup = CARGO_GROUP:New( GROUP:FindByName( "Engineers" ), "Workmaterials", "Engineers", 250 ) + -- local ConcreteCargo = CARGO_SLINGLOAD:New( STATIC:FindByName( "Concrete" ), "Workmaterials", "Concrete", 150, 50 ) + -- local CrateCargo = CARGO_CRATE:New( STATIC:FindByName( "Crate" ), "Workmaterials", "Crate", 150, 50 ) + -- local EnginesCargo = CARGO_CRATE:New( STATIC:FindByName( "Engines" ), "Workmaterials", "Engines", 150, 50 ) + -- local MetalCargo = CARGO_CRATE:New( STATIC:FindByName( "Metal" ), "Workmaterials", "Metal", 150, 50 ) -- - -- -- And here we create a new WorkplaceTask, using the :AddTransportTask method of the TaskDispatcher. - -- local WorkplaceTask = TaskDispatcher:AddTransportTask( "Build a Workplace", CargoSetWorkmaterials, "Transport the workers, engineers and the equipment near the Workplace." ) - -- TaskDispatcher:SetTransportDeployZone( WorkplaceTask, ZONE:New( "Workplace" ) ) + -- -- And here we create a new WorkplaceTask, using the :AddTransportTask method of the TaskDispatcher. + -- local WorkplaceTask = TaskDispatcher:AddTransportTask( "Build a Workplace", CargoSetWorkmaterials, "Transport the workers, engineers and the equipment near the Workplace." ) + -- TaskDispatcher:SetTransportDeployZone( WorkplaceTask, ZONE:New( "Workplace" ) ) -- -- # 3) Handle cargo task events. -- @@ -189,7 +189,7 @@ do -- TASK_CARGO_TRANSPORT -- In order to properly capture the events and avoid mistakes using the documentation, it is advised that you execute the following actions: -- -- * **Copy / Paste** the code section into your script. - -- * **Change** the CLASS literal to the task object name you have in your script. + -- * **Change** the "myclass" literal to the task object name you have in your script. -- * Within the function, you can now **write your own code**! -- * **IntelliSense** will recognize the type of the variables provided by the function. Note: the From, Event and To variables can be safely ignored, -- but you need to declare them as they are automatically provided by the event handling system of MOOSE. @@ -210,14 +210,13 @@ do -- TASK_CARGO_TRANSPORT -- If you want to code your own event handler, use this code fragment to tailor the event when a player carrier has picked up a cargo object in the CarrierGroup. -- You can use this event handler to post messages to players, or provide status updates etc. -- - -- --- CargoPickedUp event handler OnAfter for CLASS. - -- -- @param #CLASS self + -- --- CargoPickedUp event handler OnAfter for "myclass". -- -- @param #string From A string that contains the "*from state name*" when the event was triggered. -- -- @param #string Event A string that contains the "*event name*" when the event was triggered. -- -- @param #string To A string that contains the "*to state name*" when the event was triggered. -- -- @param Wrapper.Unit#UNIT TaskUnit The unit (client) of the player that has picked up the cargo. -- -- @param Cargo.Cargo#CARGO Cargo The cargo object that has been picked up. Note that this can be a CARGO_GROUP, CARGO_CRATE or CARGO_SLINGLOAD object! - -- function CLASS:OnAfterCargoPickedUp( From, Event, To, TaskUnit, Cargo ) + -- function myclass:OnAfterCargoPickedUp( From, Event, To, TaskUnit, Cargo ) -- -- -- Write here your own code. -- @@ -240,15 +239,14 @@ do -- TASK_CARGO_TRANSPORT -- You can use this event handler to post messages to players, or provide status updates etc. -- -- - -- --- CargoDeployed event handler OnAfter for CLASS. - -- -- @param #CLASS self + -- --- CargoDeployed event handler OnAfter foR "myclass". -- -- @param #string From A string that contains the "*from state name*" when the event was triggered. -- -- @param #string Event A string that contains the "*event name*" when the event was triggered. -- -- @param #string To A string that contains the "*to state name*" when the event was triggered. -- -- @param Wrapper.Unit#UNIT TaskUnit The unit (client) of the player that has deployed the cargo. -- -- @param Cargo.Cargo#CARGO Cargo The cargo object that has been deployed. Note that this can be a CARGO_GROUP, CARGO_CRATE or CARGO_SLINGLOAD object! -- -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. - -- function CLASS:OnAfterCargoDeployed( From, Event, To, TaskUnit, Cargo, DeployZone ) + -- function myclass:OnAfterCargoDeployed( From, Event, To, TaskUnit, Cargo, DeployZone ) -- -- -- Write here your own code. -- From c3f7deb602975f97d268190dae5379fc078bd655 Mon Sep 17 00:00:00 2001 From: Andrew Waugh Date: Wed, 27 Oct 2021 20:18:10 -0700 Subject: [PATCH 13/31] Fixes broken external tools Several small changes that should hopefully be a nice QoL upgrade for generating the imports and lead to less newbie confusion. * Adds a lua binary directly to the repository instead of just expecting the user to have installed it via choco. Added binary is 5.4 as that's the lowest 5.x exe that's easily downloaded, and it works fine for what we need. * Modifies the launch targets to use workspace_loc macros instead of resource_loc macros. Workspace_loc requires the user to have correctly set the name of their project, but that is already stressed in the documentation. resource_loc was just wrong. (project_loc would cause problems if the user had selected something outside of the moose project before running). * Modifies launch targets to use the folder structure that the project is actually structured with. * Adds the include folder and files so Eclipse doesn't explode when they are missing. * Small modifications to Moose_Create, also includes a path conversion function that in my testing doesn't make a difference eitherway on Windows, but is there for a troubleshooting option. * Adds courtesy instructions when generating the dynamic include file. --- MOOSE_INCLUDE/Moose_Include_Dynamic/Moose.lua | 24 + MOOSE_INCLUDE/Moose_Include_Static/Moose.lua | 190282 +++++++++++++++ .../Eclipse/Moose Loader Dynamic.launch | 6 +- .../Eclipse/Moose Loader Static.launch | 6 +- Moose Setup/Moose_Create.lua | 29 +- lua54/lua54.dll | Bin 0 -> 356234 bytes lua54/lua54.exe | Bin 0 -> 122006 bytes 7 files changed, 190336 insertions(+), 11 deletions(-) create mode 100644 MOOSE_INCLUDE/Moose_Include_Dynamic/Moose.lua create mode 100644 MOOSE_INCLUDE/Moose_Include_Static/Moose.lua create mode 100644 lua54/lua54.dll create mode 100644 lua54/lua54.exe diff --git a/MOOSE_INCLUDE/Moose_Include_Dynamic/Moose.lua b/MOOSE_INCLUDE/Moose_Include_Dynamic/Moose.lua new file mode 100644 index 000000000..51defff79 --- /dev/null +++ b/MOOSE_INCLUDE/Moose_Include_Dynamic/Moose.lua @@ -0,0 +1,24 @@ +env.info( '*** MOOSE DYNAMIC INCLUDE START *** ' ) + +local base = _G + +__Moose = {} + +__Moose.Include = function( IncludeFile ) + if not __Moose.Includes[ IncludeFile ] then + __Moose.Includes[IncludeFile] = IncludeFile + local f = assert( base.loadfile( IncludeFile ) ) + if f == nil then + error ("Moose: Could not load Moose file " .. IncludeFile ) + else + env.info( "Moose: " .. IncludeFile .. " dynamically loaded." ) + return f() + end + end +end + +__Moose.Includes = {} + +__Moose.Include( 'Scripts/Moose/Modules.lua' ) +BASE:TraceOnOff( true ) +env.info( '*** MOOSE INCLUDE END *** ' ) diff --git a/MOOSE_INCLUDE/Moose_Include_Static/Moose.lua b/MOOSE_INCLUDE/Moose_Include_Static/Moose.lua new file mode 100644 index 000000000..b0c1854b8 --- /dev/null +++ b/MOOSE_INCLUDE/Moose_Include_Static/Moose.lua @@ -0,0 +1,190282 @@ +env.info( '*** MOOSE GITHUB Commit Hash ID: LOCAL ***' ) +env.info( '*** MOOSE STATIC INCLUDE START *** ' ) + +--- **Utilities** Enumerators. +-- +-- An enumerator is a variable that holds a constant value. Enumerators are very useful because they make the code easier to read and to change in general. +-- +-- For example, instead of using the same value at multiple different places in your code, you should use a variable set to that value. +-- If, for whatever reason, the value needs to be changed, you only have to change the variable once and do not have to search through you code and reset +-- every value by hand. +-- +-- Another big advantage is that the LDT intellisense "knows" the enumerators. So you can use the autocompletion feature and do not have to keep all the +-- values in your head or look them up in the docs. +-- +-- DCS itself provides a lot of enumerators for various things. See [Enumerators](https://wiki.hoggitworld.com/view/Category:Enumerators) on Hoggit. +-- +-- Other Moose classe also have enumerators. For example, the AIRBASE class has enumerators for airbase names. +-- +-- @module ENUMS +-- @image MOOSE.JPG + +--- [DCS Enum world](https://wiki.hoggitworld.com/view/DCS_enum_world) +-- @type ENUMS + +--- Because ENUMS are just better practice. +-- +-- The ENUMS class adds some handy variables, which help you to make your code better and more general. +-- +-- @field #ENUMS +ENUMS = {} + +--- Rules of Engagement. +-- @type ENUMS.ROE +-- @field #number WeaponFree AI will engage any enemy group it detects. Target prioritization is based based on the threat of the target. +-- @field #number OpenFireWeaponFree AI will engage any enemy group it detects, but will prioritize targets specified in the groups tasking. +-- @field #number OpenFire AI will engage only targets specified in its taskings. +-- @field #number ReturnFire AI will only engage threats that shoot first. +-- @field #number WeaponHold AI will hold fire under all circumstances. +ENUMS.ROE = { + WeaponFree=0, + OpenFireWeaponFree=1, + OpenFire=2, + ReturnFire=3, + WeaponHold=4, + } + +--- Reaction On Threat. +-- @type ENUMS.ROT +-- @field #number NoReaction No defensive actions will take place to counter threats. +-- @field #number PassiveDefense AI will use jammers and other countermeasures in an attempt to defeat the threat. AI will not attempt a maneuver to defeat a threat. +-- @field #number EvadeFire AI will react by performing defensive maneuvers against incoming threats. AI will also use passive defense. +-- @field #number BypassAndEscape AI will attempt to avoid enemy threat zones all together. This includes attempting to fly above or around threats. +-- @field #number AllowAbortMission If a threat is deemed severe enough the AI will abort its mission and return to base. +ENUMS.ROT = { + NoReaction=0, + PassiveDefense=1, + EvadeFire=2, + BypassAndEscape=3, + AllowAbortMission=4, +} + +--- Alarm state. +-- @type ENUMS.AlarmState +-- @field #number Auto AI will automatically switch alarm states based on the presence of threats. The AI kind of cheats in this regard. +-- @field #number Green Group is not combat ready. Sensors are stowed if possible. +-- @field #number Red Group is combat ready and actively searching for targets. Some groups like infantry will not move in this state. +ENUMS.AlarmState = { + Auto=0, + Green=1, + Red=2, +} + +--- Weapon types. See the [Weapon Flag](https://wiki.hoggitworld.com/view/DCS_enum_weapon_flag) enumerotor on hoggit wiki. +-- @type ENUMS.WeaponFlag +ENUMS.WeaponFlag={ + -- Bombs + LGB = 2, + TvGB = 4, + SNSGB = 8, + HEBomb = 16, + Penetrator = 32, + NapalmBomb = 64, + FAEBomb = 128, + ClusterBomb = 256, + Dispencer = 512, + CandleBomb = 1024, + ParachuteBomb = 2147483648, + -- Rockets + LightRocket = 2048, + MarkerRocket = 4096, + CandleRocket = 8192, + HeavyRocket = 16384, + -- Air-To-Surface Missiles + AntiRadarMissile = 32768, + AntiShipMissile = 65536, + AntiTankMissile = 131072, + FireAndForgetASM = 262144, + LaserASM = 524288, + TeleASM = 1048576, + CruiseMissile = 2097152, + AntiRadarMissile2 = 1073741824, + -- Air-To-Air Missiles + SRAM = 4194304, + MRAAM = 8388608, + LRAAM = 16777216, + IR_AAM = 33554432, + SAR_AAM = 67108864, + AR_AAM = 134217728, + --- Guns + GunPod = 268435456, + BuiltInCannon = 536870912, + --- + -- Combinations + -- + -- Bombs + GuidedBomb = 14, -- (LGB + TvGB + SNSGB) + AnyUnguidedBomb = 2147485680, -- (HeBomb + Penetrator + NapalmBomb + FAEBomb + ClusterBomb + Dispencer + CandleBomb + ParachuteBomb) + AnyBomb = 2147485694, -- (GuidedBomb + AnyUnguidedBomb) + --- Rockets + AnyRocket = 30720, -- LightRocket + MarkerRocket + CandleRocket + HeavyRocket + --- Air-To-Surface Missiles + GuidedASM = 1572864, -- (LaserASM + TeleASM) + TacticalASM = 1835008, -- (GuidedASM + FireAndForgetASM) + AnyASM = 4161536, -- (AntiRadarMissile + AntiShipMissile + AntiTankMissile + FireAndForgetASM + GuidedASM + CruiseMissile) + AnyASM2 = 1077903360, -- 4161536+1073741824, + --- Air-To-Air Missiles + AnyAAM = 264241152, -- IR_AAM + SAR_AAM + AR_AAM + SRAAM + MRAAM + LRAAM + AnyAutonomousMissile = 36012032, -- IR_AAM + AntiRadarMissile + AntiShipMissile + FireAndForgetASM + CruiseMissile + AnyMissile = 268402688, -- AnyASM + AnyAAM + --- Guns + Cannons = 805306368, -- GUN_POD + BuiltInCannon + --- + -- Even More Genral + Auto = 3221225470, -- Any Weapon (AnyBomb + AnyRocket + AnyMissile + Cannons) + AutoDCS = 1073741822, -- Something if often see + AnyAG = 2956984318, -- Any Air-To-Ground Weapon + AnyAA = 264241152, -- Any Air-To-Air Weapon + AnyUnguided = 2952822768, -- Any Unguided Weapon + AnyGuided = 268402702, -- Any Guided Weapon +} + +--- Mission tasks. +-- @type ENUMS.MissionTask +-- @field #string NOTHING No special task. Group can perform the minimal tasks: Orbit, Refuelling, Follow and Aerobatics. +-- @field #string AFAC Forward Air Controller Air. Can perform the tasks: Attack Group, Attack Unit, FAC assign group, Bombing, Attack Map Object. +-- @field #string ANTISHIPSTRIKE Naval ops. Can perform the tasks: Attack Group, Attack Unit. +-- @field #string AWACS AWACS. +-- @field #string CAP Combat Air Patrol. +-- @field #string CAS Close Air Support. +-- @field #string ESCORT Escort another group. +-- @field #string FIGHTERSWEEP Fighter sweep. +-- @field #string GROUNDATTACK Ground attack. +-- @field #string INTERCEPT Intercept. +-- @field #string PINPOINTSTRIKE Pinpoint strike. +-- @field #string RECONNAISSANCE Reconnaissance mission. +-- @field #string REFUELING Refueling mission. +-- @field #string RUNWAYATTACK Attack the runway of an airdrome. +-- @field #string SEAD Suppression of Enemy Air Defenses. +-- @field #string TRANSPORT Troop transport. +ENUMS.MissionTask={ + NOTHING="Nothing", + AFAC="AFAC", + ANTISHIPSTRIKE="Antiship Strike", + AWACS="AWACS", + CAP="CAP", + CAS="CAS", + ESCORT="Escort", + FIGHTERSWEEP="Fighter Sweep", + GROUNDATTACK="Ground Attack", + INTERCEPT="Intercept", + PINPOINTSTRIKE="Pinpoint Strike", + RECONNAISSANCE="Reconnaissance", + REFUELING="Refueling", + RUNWAYATTACK="Runway Attack", + SEAD="SEAD", + TRANSPORT="Transport", +} + +--- Formations (new). See the [Formations](https://wiki.hoggitworld.com/view/DCS_enum_formation) on hoggit wiki. +-- @type ENUMS.Formation +ENUMS.Formation={} +ENUMS.Formation.FixedWing={} +ENUMS.Formation.FixedWing.LineAbreast={} +ENUMS.Formation.FixedWing.LineAbreast.Close = 65537 +ENUMS.Formation.FixedWing.LineAbreast.Open = 65538 +ENUMS.Formation.FixedWing.LineAbreast.Group = 65539 +ENUMS.Formation.FixedWing.Trail={} +ENUMS.Formation.FixedWing.Trail.Close = 131073 +ENUMS.Formation.FixedWing.Trail.Open = 131074 +ENUMS.Formation.FixedWing.Trail.Group = 131075 +ENUMS.Formation.FixedWing.Wedge={} +ENUMS.Formation.FixedWing.Wedge.Close = 196609 +ENUMS.Formation.FixedWing.Wedge.Open = 196610 +ENUMS.Formation.FixedWing.Wedge.Group = 196611 +ENUMS.Formation.FixedWing.EchelonRight={} +ENUMS.Formation.FixedWing.EchelonRight.Close = 262145 +ENUMS.Formation.FixedWing.EchelonRight.Open = 262146 +ENUMS.Formation.FixedWing.EchelonRight.Group = 262147 +ENUMS.Formation.FixedWing.EchelonLeft={} +ENUMS.Formation.FixedWing.EchelonLeft.Close = 327681 +ENUMS.Formation.FixedWing.EchelonLeft.Open = 327682 +ENUMS.Formation.FixedWing.EchelonLeft.Group = 327683 +ENUMS.Formation.FixedWing.FingerFour={} +ENUMS.Formation.FixedWing.FingerFour.Close = 393217 +ENUMS.Formation.FixedWing.FingerFour.Open = 393218 +ENUMS.Formation.FixedWing.FingerFour.Group = 393219 +ENUMS.Formation.FixedWing.Spread={} +ENUMS.Formation.FixedWing.Spread.Close = 458753 +ENUMS.Formation.FixedWing.Spread.Open = 458754 +ENUMS.Formation.FixedWing.Spread.Group = 458755 +ENUMS.Formation.FixedWing.BomberElement={} +ENUMS.Formation.FixedWing.BomberElement.Close = 786433 +ENUMS.Formation.FixedWing.BomberElement.Open = 786434 +ENUMS.Formation.FixedWing.BomberElement.Group = 786435 +ENUMS.Formation.FixedWing.BomberElementHeight={} +ENUMS.Formation.FixedWing.BomberElementHeight.Close = 851968 +ENUMS.Formation.FixedWing.FighterVic={} +ENUMS.Formation.FixedWing.FighterVic.Close = 917505 +ENUMS.Formation.FixedWing.FighterVic.Open = 917506 +ENUMS.Formation.RotaryWing={} +ENUMS.Formation.RotaryWing.Column={} +ENUMS.Formation.RotaryWing.Column.D70=720896 +ENUMS.Formation.RotaryWing.Wedge={} +ENUMS.Formation.RotaryWing.Wedge.D70=8 +ENUMS.Formation.RotaryWing.FrontRight={} +ENUMS.Formation.RotaryWing.FrontRight.D300=655361 +ENUMS.Formation.RotaryWing.FrontRight.D600=655362 +ENUMS.Formation.RotaryWing.FrontLeft={} +ENUMS.Formation.RotaryWing.FrontLeft.D300=655617 +ENUMS.Formation.RotaryWing.FrontLeft.D600=655618 +ENUMS.Formation.RotaryWing.EchelonRight={} +ENUMS.Formation.RotaryWing.EchelonRight.D70 =589825 +ENUMS.Formation.RotaryWing.EchelonRight.D300=589826 +ENUMS.Formation.RotaryWing.EchelonRight.D600=589827 +ENUMS.Formation.RotaryWing.EchelonLeft={} +ENUMS.Formation.RotaryWing.EchelonLeft.D70 =590081 +ENUMS.Formation.RotaryWing.EchelonLeft.D300=590082 +ENUMS.Formation.RotaryWing.EchelonLeft.D600=590083 +ENUMS.Formation.Vehicle={} +ENUMS.Formation.Vehicle.Vee="Vee" +ENUMS.Formation.Vehicle.EchelonRight="EchelonR" +ENUMS.Formation.Vehicle.OffRoad="Off Road" +ENUMS.Formation.Vehicle.Rank="Rank" +ENUMS.Formation.Vehicle.EchelonLeft="EchelonL" +ENUMS.Formation.Vehicle.OnRoad="On Road" +ENUMS.Formation.Vehicle.Cone="Cone" +ENUMS.Formation.Vehicle.Diamond="Diamond" + +--- Formations (old). The old format is a simplified version of the new formation enums, which allow more sophisticated settings. +-- See the [Formations](https://wiki.hoggitworld.com/view/DCS_enum_formation) on hoggit wiki. +-- @type ENUMS.FormationOld +ENUMS.FormationOld={} +ENUMS.FormationOld.FixedWing={} +ENUMS.FormationOld.FixedWing.LineAbreast=1 +ENUMS.FormationOld.FixedWing.Trail=2 +ENUMS.FormationOld.FixedWing.Wedge=3 +ENUMS.FormationOld.FixedWing.EchelonRight=4 +ENUMS.FormationOld.FixedWing.EchelonLeft=5 +ENUMS.FormationOld.FixedWing.FingerFour=6 +ENUMS.FormationOld.FixedWing.SpreadFour=7 +ENUMS.FormationOld.FixedWing.BomberElement=12 +ENUMS.FormationOld.FixedWing.BomberElementHeight=13 +ENUMS.FormationOld.FixedWing.FighterVic=14 +ENUMS.FormationOld.RotaryWing={} +ENUMS.FormationOld.RotaryWing.Wedge=8 +ENUMS.FormationOld.RotaryWing.Echelon=9 +ENUMS.FormationOld.RotaryWing.Front=10 +ENUMS.FormationOld.RotaryWing.Column=11 + + +--- Morse Code. See the [Wikipedia](https://en.wikipedia.org/wiki/Morse_code). +-- +-- * Short pulse "*" +-- * Long pulse "-" +-- +-- Pulses are separated by a blank character " ". +-- +-- @type ENUMS.Morse +ENUMS.Morse={} +ENUMS.Morse.A="* -" +ENUMS.Morse.B="- * * *" +ENUMS.Morse.C="- * - *" +ENUMS.Morse.D="- * *" +ENUMS.Morse.E="*" +ENUMS.Morse.F="* * - *" +ENUMS.Morse.G="- - *" +ENUMS.Morse.H="* * * *" +ENUMS.Morse.I="* *" +ENUMS.Morse.J="* - - -" +ENUMS.Morse.K="- * -" +ENUMS.Morse.L="* - * *" +ENUMS.Morse.M="- -" +ENUMS.Morse.N="- *" +ENUMS.Morse.O="- - -" +ENUMS.Morse.P="* - - *" +ENUMS.Morse.Q="- - * -" +ENUMS.Morse.R="* - *" +ENUMS.Morse.S="* * *" +ENUMS.Morse.T="-" +ENUMS.Morse.U="* * -" +ENUMS.Morse.V="* * * -" +ENUMS.Morse.W="* - -" +ENUMS.Morse.X="- * * -" +ENUMS.Morse.Y="- * - -" +ENUMS.Morse.Z="- - * *" +ENUMS.Morse.N1="* - - - -" +ENUMS.Morse.N2="* * - - -" +ENUMS.Morse.N3="* * * - -" +ENUMS.Morse.N4="* * * * -" +ENUMS.Morse.N5="* * * * *" +ENUMS.Morse.N6="- * * * *" +ENUMS.Morse.N7="- - * * *" +ENUMS.Morse.N8="- - - * *" +ENUMS.Morse.N9="- - - - *" +ENUMS.Morse.N0="- - - - -" +ENUMS.Morse[" "]=" " + +--- ISO (639-1) 2-letter Language Codes. See the [Wikipedia](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes). +-- +-- @type ENUMS.ISOLang +ENUMS.ISOLang = +{ + Arabic = 'AR', + Chinese = 'ZH', + English = 'EN', + French = 'FR', + German = 'DE', + Russian = 'RU', + Spanish = 'ES', + Japanese = 'JA', + Italian = 'IT', +} + +--- Phonetic Alphabet (NATO). See the [Wikipedia](https://en.wikipedia.org/wiki/NATO_phonetic_alphabet). +-- +-- @type ENUMS.Phonetic +ENUMS.Phonetic = +{ + A = 'Alpha', + B = 'Bravo', + C = 'Charlie', + D = 'Delta', + E = 'Echo', + F = 'Foxtrot', + G = 'Golf', + H = 'Hotel', + I = 'India', + J = 'Juliett', + K = 'Kilo', + L = 'Lima', + M = 'Mike', + N = 'November', + O = 'Oscar', + P = 'Papa', + Q = 'Quebec', + R = 'Romeo', + S = 'Sierra', + T = 'Tango', + U = 'Uniform', + V = 'Victor', + W = 'Whiskey', + X = 'Xray', + Y = 'Yankee', + Z = 'Zulu', +}--- Various routines +-- @module routines +-- @image MOOSE.JPG + +env.setErrorMessageBoxEnabled(false) + +--- Extract of MIST functions. +-- @author Grimes + +routines = {} + + +-- don't change these +routines.majorVersion = 3 +routines.minorVersion = 3 +routines.build = 22 + +----------------------------------------------------------------------------------------------------------------- + +---------------------------------------------------------------------------------------------- +-- Utils- conversion, Lua utils, etc. +routines.utils = {} + +routines.utils.round = function(number, decimals) + local power = 10^decimals + return math.floor(number * power) / power +end + +--from http://lua-users.org/wiki/CopyTable +routines.utils.deepCopy = function(object) + local lookup_table = {} + local function _copy(object) + if type(object) ~= "table" then + return object + elseif lookup_table[object] then + return lookup_table[object] + end + local new_table = {} + lookup_table[object] = new_table + for index, value in pairs(object) do + new_table[_copy(index)] = _copy(value) + end + return setmetatable(new_table, getmetatable(object)) + end + local objectreturn = _copy(object) + return objectreturn +end + + +-- porting in Slmod's serialize_slmod2 +routines.utils.oneLineSerialize = function(tbl) -- serialization of a table all on a single line, no comments, made to replace old get_table_string function + + lookup_table = {} + + local function _Serialize( tbl ) + + if type(tbl) == 'table' then --function only works for tables! + + if lookup_table[tbl] then + return lookup_table[object] + end + + local tbl_str = {} + + lookup_table[tbl] = tbl_str + + tbl_str[#tbl_str + 1] = '{' + + for ind,val in pairs(tbl) do -- serialize its fields + local ind_str = {} + if type(ind) == "number" then + ind_str[#ind_str + 1] = '[' + ind_str[#ind_str + 1] = tostring(ind) + ind_str[#ind_str + 1] = ']=' + else --must be a string + ind_str[#ind_str + 1] = '[' + ind_str[#ind_str + 1] = routines.utils.basicSerialize(ind) + ind_str[#ind_str + 1] = ']=' + end + + local val_str = {} + if ((type(val) == 'number') or (type(val) == 'boolean')) then + val_str[#val_str + 1] = tostring(val) + val_str[#val_str + 1] = ',' + tbl_str[#tbl_str + 1] = table.concat(ind_str) + tbl_str[#tbl_str + 1] = table.concat(val_str) + elseif type(val) == 'string' then + val_str[#val_str + 1] = routines.utils.basicSerialize(val) + val_str[#val_str + 1] = ',' + tbl_str[#tbl_str + 1] = table.concat(ind_str) + tbl_str[#tbl_str + 1] = table.concat(val_str) + elseif type(val) == 'nil' then -- won't ever happen, right? + val_str[#val_str + 1] = 'nil,' + tbl_str[#tbl_str + 1] = table.concat(ind_str) + tbl_str[#tbl_str + 1] = table.concat(val_str) + elseif type(val) == 'table' then + if ind == "__index" then + -- tbl_str[#tbl_str + 1] = "__index" + -- tbl_str[#tbl_str + 1] = ',' --I think this is right, I just added it + else + + val_str[#val_str + 1] = _Serialize(val) + val_str[#val_str + 1] = ',' --I think this is right, I just added it + tbl_str[#tbl_str + 1] = table.concat(ind_str) + tbl_str[#tbl_str + 1] = table.concat(val_str) + end + elseif type(val) == 'function' then + -- tbl_str[#tbl_str + 1] = "function " .. tostring(ind) + -- tbl_str[#tbl_str + 1] = ',' --I think this is right, I just added it + else +-- env.info('unable to serialize value type ' .. routines.utils.basicSerialize(type(val)) .. ' at index ' .. tostring(ind)) +-- env.info( debug.traceback() ) + end + + end + tbl_str[#tbl_str + 1] = '}' + return table.concat(tbl_str) + else + if type(tbl) == 'string' then + return tbl + else + return tostring(tbl) + end + end + end + + local objectreturn = _Serialize(tbl) + return objectreturn +end + +--porting in Slmod's "safestring" basic serialize +routines.utils.basicSerialize = function(s) + if s == nil then + return "\"\"" + else + if ((type(s) == 'number') or (type(s) == 'boolean') or (type(s) == 'function') or (type(s) == 'table') or (type(s) == 'userdata') ) then + return tostring(s) + elseif type(s) == 'string' then + s = string.format('%s', s:gsub( "%%", "%%%%" ) ) + return s + end + end +end + + +routines.utils.toDegree = function(angle) + return angle*180/math.pi +end + +routines.utils.toRadian = function(angle) + return angle*math.pi/180 +end + +routines.utils.metersToNM = function(meters) + return meters/1852 +end + +routines.utils.metersToFeet = function(meters) + return meters/0.3048 +end + +routines.utils.NMToMeters = function(NM) + return NM*1852 +end + +routines.utils.feetToMeters = function(feet) + return feet*0.3048 +end + +routines.utils.mpsToKnots = function(mps) + return mps*3600/1852 +end + +routines.utils.mpsToKmph = function(mps) + return mps*3.6 +end + +routines.utils.knotsToMps = function(knots) + return knots*1852/3600 +end + +routines.utils.kmphToMps = function(kmph) + return kmph/3.6 +end + +function routines.utils.makeVec2(Vec3) + if Vec3.z then + return {x = Vec3.x, y = Vec3.z} + else + return {x = Vec3.x, y = Vec3.y} -- it was actually already vec2. + end +end + +function routines.utils.makeVec3(Vec2, y) + if not Vec2.z then + if not y then + y = 0 + end + return {x = Vec2.x, y = y, z = Vec2.y} + else + return {x = Vec2.x, y = Vec2.y, z = Vec2.z} -- it was already Vec3, actually. + end +end + +function routines.utils.makeVec3GL(Vec2, offset) + local adj = offset or 0 + + if not Vec2.z then + return {x = Vec2.x, y = (land.getHeight(Vec2) + adj), z = Vec2.y} + else + return {x = Vec2.x, y = (land.getHeight({x = Vec2.x, y = Vec2.z}) + adj), z = Vec2.z} + end +end + +routines.utils.zoneToVec3 = function(zone) + local new = {} + if type(zone) == 'table' and zone.point then + new.x = zone.point.x + new.y = zone.point.y + new.z = zone.point.z + return new + elseif type(zone) == 'string' then + zone = trigger.misc.getZone(zone) + if zone then + new.x = zone.point.x + new.y = zone.point.y + new.z = zone.point.z + return new + end + end +end + +-- gets heading-error corrected direction from point along vector vec. +function routines.utils.getDir(vec, point) + local dir = math.atan2(vec.z, vec.x) + dir = dir + routines.getNorthCorrection(point) + if dir < 0 then + dir = dir + 2*math.pi -- put dir in range of 0 to 2*pi + end + return dir +end + +-- gets distance in meters between two points (2 dimensional) +function routines.utils.get2DDist(point1, point2) + point1 = routines.utils.makeVec3(point1) + point2 = routines.utils.makeVec3(point2) + return routines.vec.mag({x = point1.x - point2.x, y = 0, z = point1.z - point2.z}) +end + +-- gets distance in meters between two points (3 dimensional) +function routines.utils.get3DDist(point1, point2) + return routines.vec.mag({x = point1.x - point2.x, y = point1.y - point2.y, z = point1.z - point2.z}) +end + + + + + +--3D Vector manipulation +routines.vec = {} + +routines.vec.add = function(vec1, vec2) + return {x = vec1.x + vec2.x, y = vec1.y + vec2.y, z = vec1.z + vec2.z} +end + +routines.vec.sub = function(vec1, vec2) + return {x = vec1.x - vec2.x, y = vec1.y - vec2.y, z = vec1.z - vec2.z} +end + +routines.vec.scalarMult = function(vec, mult) + return {x = vec.x*mult, y = vec.y*mult, z = vec.z*mult} +end + +routines.vec.scalar_mult = routines.vec.scalarMult + +routines.vec.dp = function(vec1, vec2) + return vec1.x*vec2.x + vec1.y*vec2.y + vec1.z*vec2.z +end + +routines.vec.cp = function(vec1, vec2) + return { x = vec1.y*vec2.z - vec1.z*vec2.y, y = vec1.z*vec2.x - vec1.x*vec2.z, z = vec1.x*vec2.y - vec1.y*vec2.x} +end + +routines.vec.mag = function(vec) + return (vec.x^2 + vec.y^2 + vec.z^2)^0.5 +end + +routines.vec.getUnitVec = function(vec) + local mag = routines.vec.mag(vec) + return { x = vec.x/mag, y = vec.y/mag, z = vec.z/mag } +end + +routines.vec.rotateVec2 = function(vec2, theta) + return { x = vec2.x*math.cos(theta) - vec2.y*math.sin(theta), y = vec2.x*math.sin(theta) + vec2.y*math.cos(theta)} +end +--------------------------------------------------------------------------------------------------------------------------- + + + + +-- acc- the accuracy of each easting/northing. 0, 1, 2, 3, 4, or 5. +routines.tostringMGRS = function(MGRS, acc) + if acc == 0 then + return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph + else + return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph .. ' ' .. string.format('%0' .. acc .. 'd', routines.utils.round(MGRS.Easting/(10^(5-acc)), 0)) + .. ' ' .. string.format('%0' .. acc .. 'd', routines.utils.round(MGRS.Northing/(10^(5-acc)), 0)) + end +end + +--[[acc: +in DM: decimal point of minutes. +In DMS: decimal point of seconds. +position after the decimal of the least significant digit: +So: +42.32 - acc of 2. +]] +routines.tostringLL = function(lat, lon, acc, DMS) + + local latHemi, lonHemi + if lat > 0 then + latHemi = 'N' + else + latHemi = 'S' + end + + if lon > 0 then + lonHemi = 'E' + else + lonHemi = 'W' + end + + lat = math.abs(lat) + lon = math.abs(lon) + + local latDeg = math.floor(lat) + local latMin = (lat - latDeg)*60 + + local lonDeg = math.floor(lon) + local lonMin = (lon - lonDeg)*60 + + if DMS then -- degrees, minutes, and seconds. + local oldLatMin = latMin + latMin = math.floor(latMin) + local latSec = routines.utils.round((oldLatMin - latMin)*60, acc) + + local oldLonMin = lonMin + lonMin = math.floor(lonMin) + local lonSec = routines.utils.round((oldLonMin - lonMin)*60, acc) + + if latSec == 60 then + latSec = 0 + latMin = latMin + 1 + end + + if lonSec == 60 then + lonSec = 0 + lonMin = lonMin + 1 + end + + local secFrmtStr -- create the formatting string for the seconds place + if acc <= 0 then -- no decimal place. + secFrmtStr = '%02d' + else + local width = 3 + acc -- 01.310 - that's a width of 6, for example. + secFrmtStr = '%0' .. width .. '.' .. acc .. 'f' + end + + return string.format('%02d', latDeg) .. ' ' .. string.format('%02d', latMin) .. '\' ' .. string.format(secFrmtStr, latSec) .. '"' .. latHemi .. ' ' + .. string.format('%02d', lonDeg) .. ' ' .. string.format('%02d', lonMin) .. '\' ' .. string.format(secFrmtStr, lonSec) .. '"' .. lonHemi + + else -- degrees, decimal minutes. + latMin = routines.utils.round(latMin, acc) + lonMin = routines.utils.round(lonMin, acc) + + if latMin == 60 then + latMin = 0 + latDeg = latDeg + 1 + end + + if lonMin == 60 then + lonMin = 0 + lonDeg = lonDeg + 1 + end + + local minFrmtStr -- create the formatting string for the minutes place + if acc <= 0 then -- no decimal place. + minFrmtStr = '%02d' + else + local width = 3 + acc -- 01.310 - that's a width of 6, for example. + minFrmtStr = '%0' .. width .. '.' .. acc .. 'f' + end + + return string.format('%02d', latDeg) .. ' ' .. string.format(minFrmtStr, latMin) .. '\'' .. latHemi .. ' ' + .. string.format('%02d', lonDeg) .. ' ' .. string.format(minFrmtStr, lonMin) .. '\'' .. lonHemi + + end +end + +--[[ required: az - radian + required: dist - meters + optional: alt - meters (set to false or nil if you don't want to use it). + optional: metric - set true to get dist and alt in km and m. + precision will always be nearest degree and NM or km.]] +routines.tostringBR = function(az, dist, alt, metric) + az = routines.utils.round(routines.utils.toDegree(az), 0) + + if metric then + dist = routines.utils.round(dist/1000, 2) + else + dist = routines.utils.round(routines.utils.metersToNM(dist), 2) + end + + local s = string.format('%03d', az) .. ' for ' .. dist + + if alt then + if metric then + s = s .. ' at ' .. routines.utils.round(alt, 0) + else + s = s .. ' at ' .. routines.utils.round(routines.utils.metersToFeet(alt), 0) + end + end + return s +end + +routines.getNorthCorrection = function(point) --gets the correction needed for true north + if not point.z then --Vec2; convert to Vec3 + point.z = point.y + point.y = 0 + end + local lat, lon = coord.LOtoLL(point) + local north_posit = coord.LLtoLO(lat + 1, lon) + return math.atan2(north_posit.z - point.z, north_posit.x - point.x) +end + + +do + local idNum = 0 + + --Simplified event handler + routines.addEventHandler = function(f) --id is optional! + local handler = {} + idNum = idNum + 1 + handler.id = idNum + handler.f = f + handler.onEvent = function(self, event) + self.f(event) + end + world.addEventHandler(handler) + end + + routines.removeEventHandler = function(id) + for key, handler in pairs(world.eventHandlers) do + if handler.id and handler.id == id then + world.eventHandlers[key] = nil + return true + end + end + return false + end +end + +-- need to return a Vec3 or Vec2? +function routines.getRandPointInCircle(point, radius, innerRadius) + local theta = 2*math.pi*math.random() + local rad = math.random() + math.random() + if rad > 1 then + rad = 2 - rad + end + + local radMult + if innerRadius and innerRadius <= radius then + radMult = (radius - innerRadius)*rad + innerRadius + else + radMult = radius*rad + end + + if not point.z then --might as well work with vec2/3 + point.z = point.y + end + + local rndCoord + if radius > 0 then + rndCoord = {x = math.cos(theta)*radMult + point.x, y = math.sin(theta)*radMult + point.z} + else + rndCoord = {x = point.x, y = point.z} + end + return rndCoord +end + +routines.goRoute = function(group, path) + local misTask = { + id = 'Mission', + params = { + route = { + points = routines.utils.deepCopy(path), + }, + }, + } + if type(group) == 'string' then + group = Group.getByName(group) + end + local groupCon = group:getController() + if groupCon then + groupCon:setTask(misTask) + return true + end + + Controller.setTask(groupCon, misTask) + return false +end + + +-- Useful atomic functions from mist, ported. + +routines.ground = {} +routines.fixedWing = {} +routines.heli = {} + +routines.ground.buildWP = function(point, overRideForm, overRideSpeed) + + local wp = {} + wp.x = point.x + + if point.z then + wp.y = point.z + else + wp.y = point.y + end + local form, speed + + if point.speed and not overRideSpeed then + wp.speed = point.speed + elseif type(overRideSpeed) == 'number' then + wp.speed = overRideSpeed + else + wp.speed = routines.utils.kmphToMps(20) + end + + if point.form and not overRideForm then + form = point.form + else + form = overRideForm + end + + if not form then + wp.action = 'Cone' + else + form = string.lower(form) + if form == 'off_road' or form == 'off road' then + wp.action = 'Off Road' + elseif form == 'on_road' or form == 'on road' then + wp.action = 'On Road' + elseif form == 'rank' or form == 'line_abrest' or form == 'line abrest' or form == 'lineabrest'then + wp.action = 'Rank' + elseif form == 'cone' then + wp.action = 'Cone' + elseif form == 'diamond' then + wp.action = 'Diamond' + elseif form == 'vee' then + wp.action = 'Vee' + elseif form == 'echelon_left' or form == 'echelon left' or form == 'echelonl' then + wp.action = 'EchelonL' + elseif form == 'echelon_right' or form == 'echelon right' or form == 'echelonr' then + wp.action = 'EchelonR' + else + wp.action = 'Cone' -- if nothing matched + end + end + + wp.type = 'Turning Point' + + return wp + +end + +routines.fixedWing.buildWP = function(point, WPtype, speed, alt, altType) + + local wp = {} + wp.x = point.x + + if point.z then + wp.y = point.z + else + wp.y = point.y + end + + if alt and type(alt) == 'number' then + wp.alt = alt + else + wp.alt = 2000 + end + + if altType then + altType = string.lower(altType) + if altType == 'radio' or 'agl' then + wp.alt_type = 'RADIO' + elseif altType == 'baro' or 'asl' then + wp.alt_type = 'BARO' + end + else + wp.alt_type = 'RADIO' + end + + if point.speed then + speed = point.speed + end + + if point.type then + WPtype = point.type + end + + if not speed then + wp.speed = routines.utils.kmphToMps(500) + else + wp.speed = speed + end + + if not WPtype then + wp.action = 'Turning Point' + else + WPtype = string.lower(WPtype) + if WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then + wp.action = 'Fly Over Point' + elseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then + wp.action = 'Turning Point' + else + wp.action = 'Turning Point' + end + end + + wp.type = 'Turning Point' + return wp +end + +routines.heli.buildWP = function(point, WPtype, speed, alt, altType) + + local wp = {} + wp.x = point.x + + if point.z then + wp.y = point.z + else + wp.y = point.y + end + + if alt and type(alt) == 'number' then + wp.alt = alt + else + wp.alt = 500 + end + + if altType then + altType = string.lower(altType) + if altType == 'radio' or 'agl' then + wp.alt_type = 'RADIO' + elseif altType == 'baro' or 'asl' then + wp.alt_type = 'BARO' + end + else + wp.alt_type = 'RADIO' + end + + if point.speed then + speed = point.speed + end + + if point.type then + WPtype = point.type + end + + if not speed then + wp.speed = routines.utils.kmphToMps(200) + else + wp.speed = speed + end + + if not WPtype then + wp.action = 'Turning Point' + else + WPtype = string.lower(WPtype) + if WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then + wp.action = 'Fly Over Point' + elseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then + wp.action = 'Turning Point' + else + wp.action = 'Turning Point' + end + end + + wp.type = 'Turning Point' + return wp +end + +routines.groupToRandomPoint = function(vars) + local group = vars.group --Required + local point = vars.point --required + local radius = vars.radius or 0 + local innerRadius = vars.innerRadius + local form = vars.form or 'Cone' + local heading = vars.heading or math.random()*2*math.pi + local headingDegrees = vars.headingDegrees + local speed = vars.speed or routines.utils.kmphToMps(20) + + + local useRoads + if not vars.disableRoads then + useRoads = true + else + useRoads = false + end + + local path = {} + + if headingDegrees then + heading = headingDegrees*math.pi/180 + end + + if heading >= 2*math.pi then + heading = heading - 2*math.pi + end + + local rndCoord = routines.getRandPointInCircle(point, radius, innerRadius) + + local offset = {} + local posStart = routines.getLeadPos(group) + + offset.x = routines.utils.round(math.sin(heading - (math.pi/2)) * 50 + rndCoord.x, 3) + offset.z = routines.utils.round(math.cos(heading + (math.pi/2)) * 50 + rndCoord.y, 3) + path[#path + 1] = routines.ground.buildWP(posStart, form, speed) + + + if useRoads == true and ((point.x - posStart.x)^2 + (point.z - posStart.z)^2)^0.5 > radius * 1.3 then + path[#path + 1] = routines.ground.buildWP({['x'] = posStart.x + 11, ['z'] = posStart.z + 11}, 'off_road', speed) + path[#path + 1] = routines.ground.buildWP(posStart, 'on_road', speed) + path[#path + 1] = routines.ground.buildWP(offset, 'on_road', speed) + else + path[#path + 1] = routines.ground.buildWP({['x'] = posStart.x + 25, ['z'] = posStart.z + 25}, form, speed) + end + + path[#path + 1] = routines.ground.buildWP(offset, form, speed) + path[#path + 1] = routines.ground.buildWP(rndCoord, form, speed) + + routines.goRoute(group, path) + + return +end + +routines.groupRandomDistSelf = function(gpData, dist, form, heading, speed) + local pos = routines.getLeadPos(gpData) + local fakeZone = {} + fakeZone.radius = dist or math.random(300, 1000) + fakeZone.point = {x = pos.x, y, pos.y, z = pos.z} + routines.groupToRandomZone(gpData, fakeZone, form, heading, speed) + + return +end + +routines.groupToRandomZone = function(gpData, zone, form, heading, speed) + if type(gpData) == 'string' then + gpData = Group.getByName(gpData) + end + + if type(zone) == 'string' then + zone = trigger.misc.getZone(zone) + elseif type(zone) == 'table' and not zone.radius then + zone = trigger.misc.getZone(zone[math.random(1, #zone)]) + end + + if speed then + speed = routines.utils.kmphToMps(speed) + end + + local vars = {} + vars.group = gpData + vars.radius = zone.radius + vars.form = form + vars.headingDegrees = heading + vars.speed = speed + vars.point = routines.utils.zoneToVec3(zone) + + routines.groupToRandomPoint(vars) + + return +end + +routines.isTerrainValid = function(coord, terrainTypes) -- vec2/3 and enum or table of acceptable terrain types + if coord.z then + coord.y = coord.z + end + local typeConverted = {} + + if type(terrainTypes) == 'string' then -- if its a string it does this check + for constId, constData in pairs(land.SurfaceType) do + if string.lower(constId) == string.lower(terrainTypes) or string.lower(constData) == string.lower(terrainTypes) then + table.insert(typeConverted, constId) + end + end + elseif type(terrainTypes) == 'table' then -- if its a table it does this check + for typeId, typeData in pairs(terrainTypes) do + for constId, constData in pairs(land.SurfaceType) do + if string.lower(constId) == string.lower(typeData) or string.lower(constData) == string.lower(typeId) then + table.insert(typeConverted, constId) + end + end + end + end + for validIndex, validData in pairs(typeConverted) do + if land.getSurfaceType(coord) == land.SurfaceType[validData] then + return true + end + end + return false +end + +routines.groupToPoint = function(gpData, point, form, heading, speed, useRoads) + if type(point) == 'string' then + point = trigger.misc.getZone(point) + end + if speed then + speed = routines.utils.kmphToMps(speed) + end + + local vars = {} + vars.group = gpData + vars.form = form + vars.headingDegrees = heading + vars.speed = speed + vars.disableRoads = useRoads + vars.point = routines.utils.zoneToVec3(point) + routines.groupToRandomPoint(vars) + + return +end + + +routines.getLeadPos = function(group) + if type(group) == 'string' then -- group name + group = Group.getByName(group) + end + + local units = group:getUnits() + + local leader = units[1] + if not leader then -- SHOULD be good, but if there is a bug, this code future-proofs it then. + local lowestInd = math.huge + for ind, unit in pairs(units) do + if ind < lowestInd then + lowestInd = ind + leader = unit + end + end + end + if leader and Unit.isExist(leader) then -- maybe a little too paranoid now... + return leader:getPosition().p + end +end + +--[[ vars for routines.getMGRSString: +vars.units - table of unit names (NOT unitNameTable- maybe this should change). +vars.acc - integer between 0 and 5, inclusive +]] +routines.getMGRSString = function(vars) + local units = vars.units + local acc = vars.acc or 5 + local avgPos = routines.getAvgPos(units) + if avgPos then + return routines.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(avgPos)), acc) + end +end + +--[[ vars for routines.getLLString +vars.units - table of unit names (NOT unitNameTable- maybe this should change). +vars.acc - integer, number of numbers after decimal place +vars.DMS - if true, output in degrees, minutes, seconds. Otherwise, output in degrees, minutes. + + +]] +routines.getLLString = function(vars) + local units = vars.units + local acc = vars.acc or 3 + local DMS = vars.DMS + local avgPos = routines.getAvgPos(units) + if avgPos then + local lat, lon = coord.LOtoLL(avgPos) + return routines.tostringLL(lat, lon, acc, DMS) + end +end + +--[[ +vars.zone - table of a zone name. +vars.ref - vec3 ref point, maybe overload for vec2 as well? +vars.alt - boolean, if used, includes altitude in string +vars.metric - boolean, gives distance in km instead of NM. +]] +routines.getBRStringZone = function(vars) + local zone = trigger.misc.getZone( vars.zone ) + local ref = routines.utils.makeVec3(vars.ref, 0) -- turn it into Vec3 if it is not already. + local alt = vars.alt + local metric = vars.metric + if zone then + local vec = {x = zone.point.x - ref.x, y = zone.point.y - ref.y, z = zone.point.z - ref.z} + local dir = routines.utils.getDir(vec, ref) + local dist = routines.utils.get2DDist(zone.point, ref) + if alt then + alt = zone.y + end + return routines.tostringBR(dir, dist, alt, metric) + else + env.info( 'routines.getBRStringZone: error: zone is nil' ) + end +end + +--[[ +vars.units- table of unit names (NOT unitNameTable- maybe this should change). +vars.ref - vec3 ref point, maybe overload for vec2 as well? +vars.alt - boolean, if used, includes altitude in string +vars.metric - boolean, gives distance in km instead of NM. +]] +routines.getBRString = function(vars) + local units = vars.units + local ref = routines.utils.makeVec3(vars.ref, 0) -- turn it into Vec3 if it is not already. + local alt = vars.alt + local metric = vars.metric + local avgPos = routines.getAvgPos(units) + if avgPos then + local vec = {x = avgPos.x - ref.x, y = avgPos.y - ref.y, z = avgPos.z - ref.z} + local dir = routines.utils.getDir(vec, ref) + local dist = routines.utils.get2DDist(avgPos, ref) + if alt then + alt = avgPos.y + end + return routines.tostringBR(dir, dist, alt, metric) + end +end + + +-- Returns the Vec3 coordinates of the average position of the concentration of units most in the heading direction. +--[[ vars for routines.getLeadingPos: +vars.units - table of unit names +vars.heading - direction +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees +]] +routines.getLeadingPos = function(vars) + local units = vars.units + local heading = vars.heading + local radius = vars.radius + if vars.headingDegrees then + heading = routines.utils.toRadian(vars.headingDegrees) + end + + local unitPosTbl = {} + for i = 1, #units do + local unit = Unit.getByName(units[i]) + if unit and unit:isExist() then + unitPosTbl[#unitPosTbl + 1] = unit:getPosition().p + end + end + if #unitPosTbl > 0 then -- one more more units found. + -- first, find the unit most in the heading direction + local maxPos = -math.huge + + local maxPosInd -- maxPos - the furthest in direction defined by heading; maxPosInd = + for i = 1, #unitPosTbl do + local rotatedVec2 = routines.vec.rotateVec2(routines.utils.makeVec2(unitPosTbl[i]), heading) + if (not maxPos) or maxPos < rotatedVec2.x then + maxPos = rotatedVec2.x + maxPosInd = i + end + end + + --now, get all the units around this unit... + local avgPos + if radius then + local maxUnitPos = unitPosTbl[maxPosInd] + local avgx, avgy, avgz, totNum = 0, 0, 0, 0 + for i = 1, #unitPosTbl do + if routines.utils.get2DDist(maxUnitPos, unitPosTbl[i]) <= radius then + avgx = avgx + unitPosTbl[i].x + avgy = avgy + unitPosTbl[i].y + avgz = avgz + unitPosTbl[i].z + totNum = totNum + 1 + end + end + avgPos = { x = avgx/totNum, y = avgy/totNum, z = avgz/totNum} + else + avgPos = unitPosTbl[maxPosInd] + end + + return avgPos + end +end + + +--[[ vars for routines.getLeadingMGRSString: +vars.units - table of unit names +vars.heading - direction +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees +vars.acc - number, 0 to 5. +]] +routines.getLeadingMGRSString = function(vars) + local pos = routines.getLeadingPos(vars) + if pos then + local acc = vars.acc or 5 + return routines.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(pos)), acc) + end +end + +--[[ vars for routines.getLeadingLLString: +vars.units - table of unit names +vars.heading - direction, number +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees +vars.acc - number of digits after decimal point (can be negative) +vars.DMS - boolean, true if you want DMS. +]] +routines.getLeadingLLString = function(vars) + local pos = routines.getLeadingPos(vars) + if pos then + local acc = vars.acc or 3 + local DMS = vars.DMS + local lat, lon = coord.LOtoLL(pos) + return routines.tostringLL(lat, lon, acc, DMS) + end +end + + + +--[[ vars for routines.getLeadingBRString: +vars.units - table of unit names +vars.heading - direction, number +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees +vars.metric - boolean, if true, use km instead of NM. +vars.alt - boolean, if true, include altitude. +vars.ref - vec3/vec2 reference point. +]] +routines.getLeadingBRString = function(vars) + local pos = routines.getLeadingPos(vars) + if pos then + local ref = vars.ref + local alt = vars.alt + local metric = vars.metric + + local vec = {x = pos.x - ref.x, y = pos.y - ref.y, z = pos.z - ref.z} + local dir = routines.utils.getDir(vec, ref) + local dist = routines.utils.get2DDist(pos, ref) + if alt then + alt = pos.y + end + return routines.tostringBR(dir, dist, alt, metric) + end +end + +--[[ vars for routines.message.add + vars.text = 'Hello World' + vars.displayTime = 20 + vars.msgFor = {coa = {'red'}, countries = {'Ukraine', 'Georgia'}, unitTypes = {'A-10C'}} + +]] + +--[[ vars for routines.msgMGRS +vars.units - table of unit names (NOT unitNameTable- maybe this should change). +vars.acc - integer between 0 and 5, inclusive +vars.text - text in the message +vars.displayTime - self explanatory +vars.msgFor - scope +]] +routines.msgMGRS = function(vars) + local units = vars.units + local acc = vars.acc + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor + + local s = routines.getMGRSString{units = units, acc = acc} + local newText + if string.find(text, '%%s') then -- look for %s + newText = string.format(text, s) -- insert the coordinates into the message + else -- else, just append to the end. + newText = text .. s + end + + routines.message.add{ + text = newText, + displayTime = displayTime, + msgFor = msgFor + } +end + +--[[ vars for routines.msgLL +vars.units - table of unit names (NOT unitNameTable- maybe this should change) (Yes). +vars.acc - integer, number of numbers after decimal place +vars.DMS - if true, output in degrees, minutes, seconds. Otherwise, output in degrees, minutes. +vars.text - text in the message +vars.displayTime - self explanatory +vars.msgFor - scope +]] +routines.msgLL = function(vars) + local units = vars.units -- technically, I don't really need to do this, but it helps readability. + local acc = vars.acc + local DMS = vars.DMS + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor + + local s = routines.getLLString{units = units, acc = acc, DMS = DMS} + local newText + if string.find(text, '%%s') then -- look for %s + newText = string.format(text, s) -- insert the coordinates into the message + else -- else, just append to the end. + newText = text .. s + end + + routines.message.add{ + text = newText, + displayTime = displayTime, + msgFor = msgFor + } + +end + + +--[[ +vars.units- table of unit names (NOT unitNameTable- maybe this should change). +vars.ref - vec3 ref point, maybe overload for vec2 as well? +vars.alt - boolean, if used, includes altitude in string +vars.metric - boolean, gives distance in km instead of NM. +vars.text - text of the message +vars.displayTime +vars.msgFor - scope +]] +routines.msgBR = function(vars) + local units = vars.units -- technically, I don't really need to do this, but it helps readability. + local ref = vars.ref -- vec2/vec3 will be handled in routines.getBRString + local alt = vars.alt + local metric = vars.metric + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor + + local s = routines.getBRString{units = units, ref = ref, alt = alt, metric = metric} + local newText + if string.find(text, '%%s') then -- look for %s + newText = string.format(text, s) -- insert the coordinates into the message + else -- else, just append to the end. + newText = text .. s + end + + routines.message.add{ + text = newText, + displayTime = displayTime, + msgFor = msgFor + } + +end + + +-------------------------------------------------------------------------------------------- +-- basically, just sub-types of routines.msgBR... saves folks the work of getting the ref point. +--[[ +vars.units- table of unit names (NOT unitNameTable- maybe this should change). +vars.ref - string red, blue +vars.alt - boolean, if used, includes altitude in string +vars.metric - boolean, gives distance in km instead of NM. +vars.text - text of the message +vars.displayTime +vars.msgFor - scope +]] +routines.msgBullseye = function(vars) + if string.lower(vars.ref) == 'red' then + vars.ref = routines.DBs.missionData.bullseye.red + routines.msgBR(vars) + elseif string.lower(vars.ref) == 'blue' then + vars.ref = routines.DBs.missionData.bullseye.blue + routines.msgBR(vars) + end +end + +--[[ +vars.units- table of unit names (NOT unitNameTable- maybe this should change). +vars.ref - unit name of reference point +vars.alt - boolean, if used, includes altitude in string +vars.metric - boolean, gives distance in km instead of NM. +vars.text - text of the message +vars.displayTime +vars.msgFor - scope +]] + +routines.msgBRA = function(vars) + if Unit.getByName(vars.ref) then + vars.ref = Unit.getByName(vars.ref):getPosition().p + if not vars.alt then + vars.alt = true + end + routines.msgBR(vars) + end +end +-------------------------------------------------------------------------------------------- + +--[[ vars for routines.msgLeadingMGRS: +vars.units - table of unit names +vars.heading - direction +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees (optional) +vars.acc - number, 0 to 5. +vars.text - text of the message +vars.displayTime +vars.msgFor - scope +]] +routines.msgLeadingMGRS = function(vars) + local units = vars.units -- technically, I don't really need to do this, but it helps readability. + local heading = vars.heading + local radius = vars.radius + local headingDegrees = vars.headingDegrees + local acc = vars.acc + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor + + local s = routines.getLeadingMGRSString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc} + local newText + if string.find(text, '%%s') then -- look for %s + newText = string.format(text, s) -- insert the coordinates into the message + else -- else, just append to the end. + newText = text .. s + end + + routines.message.add{ + text = newText, + displayTime = displayTime, + msgFor = msgFor + } + + +end +--[[ vars for routines.msgLeadingLL: +vars.units - table of unit names +vars.heading - direction, number +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees (optional) +vars.acc - number of digits after decimal point (can be negative) +vars.DMS - boolean, true if you want DMS. (optional) +vars.text - text of the message +vars.displayTime +vars.msgFor - scope +]] +routines.msgLeadingLL = function(vars) + local units = vars.units -- technically, I don't really need to do this, but it helps readability. + local heading = vars.heading + local radius = vars.radius + local headingDegrees = vars.headingDegrees + local acc = vars.acc + local DMS = vars.DMS + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor + + local s = routines.getLeadingLLString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc, DMS = DMS} + local newText + if string.find(text, '%%s') then -- look for %s + newText = string.format(text, s) -- insert the coordinates into the message + else -- else, just append to the end. + newText = text .. s + end + + routines.message.add{ + text = newText, + displayTime = displayTime, + msgFor = msgFor + } + +end + +--[[ +vars.units - table of unit names +vars.heading - direction, number +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees (optional) +vars.metric - boolean, if true, use km instead of NM. (optional) +vars.alt - boolean, if true, include altitude. (optional) +vars.ref - vec3/vec2 reference point. +vars.text - text of the message +vars.displayTime +vars.msgFor - scope +]] +routines.msgLeadingBR = function(vars) + local units = vars.units -- technically, I don't really need to do this, but it helps readability. + local heading = vars.heading + local radius = vars.radius + local headingDegrees = vars.headingDegrees + local metric = vars.metric + local alt = vars.alt + local ref = vars.ref -- vec2/vec3 will be handled in routines.getBRString + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor + + local s = routines.getLeadingBRString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, metric = metric, alt = alt, ref = ref} + local newText + if string.find(text, '%%s') then -- look for %s + newText = string.format(text, s) -- insert the coordinates into the message + else -- else, just append to the end. + newText = text .. s + end + + routines.message.add{ + text = newText, + displayTime = displayTime, + msgFor = msgFor + } +end + + +function spairs(t, order) + -- collect the keys + local keys = {} + for k in pairs(t) do keys[#keys+1] = k end + + -- if order function given, sort by it by passing the table and keys a, b, + -- otherwise just sort the keys + if order then + table.sort(keys, function(a,b) return order(t, a, b) end) + else + table.sort(keys) + end + + -- return the iterator function + local i = 0 + return function() + i = i + 1 + if keys[i] then + return keys[i], t[keys[i]] + end + end +end + + +function routines.IsPartOfGroupInZones( CargoGroup, LandingZones ) +--trace.f() + + local CurrentZoneID = nil + + if CargoGroup then + local CargoUnits = CargoGroup:getUnits() + for CargoUnitID, CargoUnit in pairs( CargoUnits ) do + if CargoUnit and CargoUnit:getLife() >= 1.0 then + CurrentZoneID = routines.IsUnitInZones( CargoUnit, LandingZones ) + if CurrentZoneID then + break + end + end + end + end + +--trace.r( "", "", { CurrentZoneID } ) + return CurrentZoneID +end + + + +function routines.IsUnitInZones( TransportUnit, LandingZones ) +--trace.f("", "routines.IsUnitInZones" ) + + local TransportZoneResult = nil + local TransportZonePos = nil + local TransportZone = nil + + -- fill-up some local variables to support further calculations to determine location of units within the zone. + if TransportUnit then + local TransportUnitPos = TransportUnit:getPosition().p + if type( LandingZones ) == "table" then + for LandingZoneID, LandingZoneName in pairs( LandingZones ) do + TransportZone = trigger.misc.getZone( LandingZoneName ) + if TransportZone then + TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} + if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then + TransportZoneResult = LandingZoneID + break + end + end + end + else + TransportZone = trigger.misc.getZone( LandingZones ) + TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} + if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then + TransportZoneResult = 1 + end + end + if TransportZoneResult then + --trace.i( "routines", "TransportZone:" .. TransportZoneResult ) + else + --trace.i( "routines", "TransportZone:nil logic" ) + end + return TransportZoneResult + else + --trace.i( "routines", "TransportZone:nil hard" ) + return nil + end +end + +function routines.IsUnitNearZonesRadius( TransportUnit, LandingZones, ZoneRadius ) +--trace.f("", "routines.IsUnitInZones" ) + + local TransportZoneResult = nil + local TransportZonePos = nil + local TransportZone = nil + + -- fill-up some local variables to support further calculations to determine location of units within the zone. + if TransportUnit then + local TransportUnitPos = TransportUnit:getPosition().p + if type( LandingZones ) == "table" then + for LandingZoneID, LandingZoneName in pairs( LandingZones ) do + TransportZone = trigger.misc.getZone( LandingZoneName ) + if TransportZone then + TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} + if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= ZoneRadius ) then + TransportZoneResult = LandingZoneID + break + end + end + end + else + TransportZone = trigger.misc.getZone( LandingZones ) + TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} + if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= ZoneRadius ) then + TransportZoneResult = 1 + end + end + if TransportZoneResult then + --trace.i( "routines", "TransportZone:" .. TransportZoneResult ) + else + --trace.i( "routines", "TransportZone:nil logic" ) + end + return TransportZoneResult + else + --trace.i( "routines", "TransportZone:nil hard" ) + return nil + end +end + + +function routines.IsStaticInZones( TransportStatic, LandingZones ) +--trace.f() + + local TransportZoneResult = nil + local TransportZonePos = nil + local TransportZone = nil + + -- fill-up some local variables to support further calculations to determine location of units within the zone. + local TransportStaticPos = TransportStatic:getPosition().p + if type( LandingZones ) == "table" then + for LandingZoneID, LandingZoneName in pairs( LandingZones ) do + TransportZone = trigger.misc.getZone( LandingZoneName ) + if TransportZone then + TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} + if ((( TransportStaticPos.x - TransportZonePos.x)^2 + (TransportStaticPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then + TransportZoneResult = LandingZoneID + break + end + end + end + else + TransportZone = trigger.misc.getZone( LandingZones ) + TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} + if ((( TransportStaticPos.x - TransportZonePos.x)^2 + (TransportStaticPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then + TransportZoneResult = 1 + end + end + +--trace.r( "", "", { TransportZoneResult } ) + return TransportZoneResult +end + + +function routines.IsUnitInRadius( CargoUnit, ReferencePosition, Radius ) +--trace.f() + + local Valid = true + + -- fill-up some local variables to support further calculations to determine location of units within the zone. + local CargoPos = CargoUnit:getPosition().p + local ReferenceP = ReferencePosition.p + + if (((CargoPos.x - ReferenceP.x)^2 + (CargoPos.z - ReferenceP.z)^2)^0.5 <= Radius) then + else + Valid = false + end + + return Valid +end + +function routines.IsPartOfGroupInRadius( CargoGroup, ReferencePosition, Radius ) +--trace.f() + + local Valid = true + + Valid = routines.ValidateGroup( CargoGroup, "CargoGroup", Valid ) + + -- fill-up some local variables to support further calculations to determine location of units within the zone + local CargoUnits = CargoGroup:getUnits() + for CargoUnitId, CargoUnit in pairs( CargoUnits ) do + local CargoUnitPos = CargoUnit:getPosition().p +-- env.info( 'routines.IsPartOfGroupInRadius: CargoUnitPos.x = ' .. CargoUnitPos.x .. ' CargoUnitPos.z = ' .. CargoUnitPos.z ) + local ReferenceP = ReferencePosition.p +-- env.info( 'routines.IsPartOfGroupInRadius: ReferenceGroupPos.x = ' .. ReferenceGroupPos.x .. ' ReferenceGroupPos.z = ' .. ReferenceGroupPos.z ) + + if ((( CargoUnitPos.x - ReferenceP.x)^2 + (CargoUnitPos.z - ReferenceP.z)^2)^0.5 <= Radius) then + else + Valid = false + break + end + end + + return Valid +end + + +function routines.ValidateString( Variable, VariableName, Valid ) +--trace.f() + + if type( Variable ) == "string" then + if Variable == "" then + error( "routines.ValidateString: error: " .. VariableName .. " must be filled out!" ) + Valid = false + end + else + error( "routines.ValidateString: error: " .. VariableName .. " is not a string." ) + Valid = false + end + +--trace.r( "", "", { Valid } ) + return Valid +end + +function routines.ValidateNumber( Variable, VariableName, Valid ) +--trace.f() + + if type( Variable ) == "number" then + else + error( "routines.ValidateNumber: error: " .. VariableName .. " is not a number." ) + Valid = false + end + +--trace.r( "", "", { Valid } ) + return Valid + +end + +function routines.ValidateGroup( Variable, VariableName, Valid ) +--trace.f() + + if Variable == nil then + error( "routines.ValidateGroup: error: " .. VariableName .. " is a nil value!" ) + Valid = false + end + +--trace.r( "", "", { Valid } ) + return Valid +end + +function routines.ValidateZone( LandingZones, VariableName, Valid ) +--trace.f() + + if LandingZones == nil then + error( "routines.ValidateGroup: error: " .. VariableName .. " is a nil value!" ) + Valid = false + end + + if type( LandingZones ) == "table" then + for LandingZoneID, LandingZoneName in pairs( LandingZones ) do + if trigger.misc.getZone( LandingZoneName ) == nil then + error( "routines.ValidateGroup: error: Zone " .. LandingZoneName .. " does not exist!" ) + Valid = false + break + end + end + else + if trigger.misc.getZone( LandingZones ) == nil then + error( "routines.ValidateGroup: error: Zone " .. LandingZones .. " does not exist!" ) + Valid = false + end + end + +--trace.r( "", "", { Valid } ) + return Valid +end + +function routines.ValidateEnumeration( Variable, VariableName, Enum, Valid ) +--trace.f() + + local ValidVariable = false + + for EnumId, EnumData in pairs( Enum ) do + if Variable == EnumData then + ValidVariable = true + break + end + end + + if ValidVariable then + else + error( 'TransportValidateEnum: " .. VariableName .. " is not a valid type.' .. Variable ) + Valid = false + end + +--trace.r( "", "", { Valid } ) + return Valid +end + +function routines.getGroupRoute(groupIdent, task) -- same as getGroupPoints but returns speed and formation type along with vec2 of point} + -- refactor to search by groupId and allow groupId and groupName as inputs + local gpId = groupIdent + if type(groupIdent) == 'string' and not tonumber(groupIdent) then + gpId = _DATABASE.Templates.Groups[groupIdent].groupId + end + + for coa_name, coa_data in pairs(env.mission.coalition) do + if (coa_name == 'red' or coa_name == 'blue') and type(coa_data) == 'table' then + if coa_data.country then --there is a country table + for cntry_id, cntry_data in pairs(coa_data.country) do + for obj_type_name, obj_type_data in pairs(cntry_data) do + if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" then -- only these types have points + if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! + for group_num, group_data in pairs(obj_type_data.group) do + if group_data and group_data.groupId == gpId then -- this is the group we are looking for + if group_data.route and group_data.route.points and #group_data.route.points > 0 then + local points = {} + + for point_num, point in pairs(group_data.route.points) do + local routeData = {} + if env.mission.version > 7 then + routeData.name = env.getValueDictByKey(point.name) + else + routeData.name = point.name + end + if not point.point then + routeData.x = point.x + routeData.y = point.y + else + routeData.point = point.point --it's possible that the ME could move to the point = Vec2 notation. + end + routeData.form = point.action + routeData.speed = point.speed + routeData.alt = point.alt + routeData.alt_type = point.alt_type + routeData.airdromeId = point.airdromeId + routeData.helipadId = point.helipadId + routeData.type = point.type + routeData.action = point.action + if task then + routeData.task = point.task + end + points[point_num] = routeData + end + + return points + end + return + end --if group_data and group_data.name and group_data.name == 'groupname' + end --for group_num, group_data in pairs(obj_type_data.group) do + end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then + end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then + end --for obj_type_name, obj_type_data in pairs(cntry_data) do + end --for cntry_id, cntry_data in pairs(coa_data.country) do + end --if coa_data.country then --there is a country table + end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then + end --for coa_name, coa_data in pairs(mission.coalition) do +end + +routines.ground.patrolRoute = function(vars) + + + local tempRoute = {} + local useRoute = {} + local gpData = vars.gpData + if type(gpData) == 'string' then + gpData = Group.getByName(gpData) + end + + local useGroupRoute + if not vars.useGroupRoute then + useGroupRoute = vars.gpData + else + useGroupRoute = vars.useGroupRoute + end + local routeProvided = false + if not vars.route then + if useGroupRoute then + tempRoute = routines.getGroupRoute(useGroupRoute) + end + else + useRoute = vars.route + local posStart = routines.getLeadPos(gpData) + useRoute[1] = routines.ground.buildWP(posStart, useRoute[1].action, useRoute[1].speed) + routeProvided = true + end + + + local overRideSpeed = vars.speed or 'default' + local pType = vars.pType + local offRoadForm = vars.offRoadForm or 'default' + local onRoadForm = vars.onRoadForm or 'default' + + if routeProvided == false and #tempRoute > 0 then + local posStart = routines.getLeadPos(gpData) + + + useRoute[#useRoute + 1] = routines.ground.buildWP(posStart, offRoadForm, overRideSpeed) + for i = 1, #tempRoute do + local tempForm = tempRoute[i].action + local tempSpeed = tempRoute[i].speed + + if offRoadForm == 'default' then + tempForm = tempRoute[i].action + end + if onRoadForm == 'default' then + onRoadForm = 'On Road' + end + if (string.lower(tempRoute[i].action) == 'on road' or string.lower(tempRoute[i].action) == 'onroad' or string.lower(tempRoute[i].action) == 'on_road') then + tempForm = onRoadForm + else + tempForm = offRoadForm + end + + if type(overRideSpeed) == 'number' then + tempSpeed = overRideSpeed + end + + + useRoute[#useRoute + 1] = routines.ground.buildWP(tempRoute[i], tempForm, tempSpeed) + end + + if pType and string.lower(pType) == 'doubleback' then + local curRoute = routines.utils.deepCopy(useRoute) + for i = #curRoute, 2, -1 do + useRoute[#useRoute + 1] = routines.ground.buildWP(curRoute[i], curRoute[i].action, curRoute[i].speed) + end + end + + useRoute[1].action = useRoute[#useRoute].action -- make it so the first WP matches the last WP + end + + local cTask3 = {} + local newPatrol = {} + newPatrol.route = useRoute + newPatrol.gpData = gpData:getName() + cTask3[#cTask3 + 1] = 'routines.ground.patrolRoute(' + cTask3[#cTask3 + 1] = routines.utils.oneLineSerialize(newPatrol) + cTask3[#cTask3 + 1] = ')' + cTask3 = table.concat(cTask3) + local tempTask = { + id = 'WrappedAction', + params = { + action = { + id = 'Script', + params = { + command = cTask3, + + }, + }, + }, + } + + + useRoute[#useRoute].task = tempTask + routines.goRoute(gpData, useRoute) + + return +end + +routines.ground.patrol = function(gpData, pType, form, speed) + local vars = {} + + if type(gpData) == 'table' and gpData:getName() then + gpData = gpData:getName() + end + + vars.useGroupRoute = gpData + vars.gpData = gpData + vars.pType = pType + vars.offRoadForm = form + vars.speed = speed + + routines.ground.patrolRoute(vars) + + return +end + +function routines.GetUnitHeight( CheckUnit ) +--trace.f( "routines" ) + + local UnitPoint = CheckUnit:getPoint() + local UnitPosition = { x = UnitPoint.x, y = UnitPoint.z } + local UnitHeight = UnitPoint.y + + local LandHeight = land.getHeight( UnitPosition ) + + --env.info(( 'CarrierHeight: LandHeight = ' .. LandHeight .. ' CarrierHeight = ' .. CarrierHeight )) + + --trace.f( "routines", "Unit Height = " .. UnitHeight - LandHeight ) + + return UnitHeight - LandHeight + +end + + + +Su34Status = { status = {} } +boardMsgRed = { statusMsg = "" } +boardMsgAll = { timeMsg = "" } +SpawnSettings = {} +Su34MenuPath = {} +Su34Menus = 0 + + +function Su34AttackCarlVinson(groupName) +--trace.menu("", "Su34AttackCarlVinson") + local groupSu34 = Group.getByName( groupName ) + local controllerSu34 = groupSu34.getController(groupSu34) + local groupCarlVinson = Group.getByName("US Carl Vinson #001") + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE ) + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) + if groupCarlVinson ~= nil then + controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupCarlVinson:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = true}}) + end + Su34Status.status[groupName] = 1 + MessageToRed( string.format('%s: ',groupName) .. 'Attacking carrier Carl Vinson. ', 10, 'RedStatus' .. groupName ) +end + +function Su34AttackWest(groupName) +--trace.f("","Su34AttackWest") + local groupSu34 = Group.getByName( groupName ) + local controllerSu34 = groupSu34.getController(groupSu34) + local groupShipWest1 = Group.getByName("US Ship West #001") + local groupShipWest2 = Group.getByName("US Ship West #002") + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE ) + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) + if groupShipWest1 ~= nil then + controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipWest1:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = true}}) + end + if groupShipWest2 ~= nil then + controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipWest2:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = true}}) + end + Su34Status.status[groupName] = 2 + MessageToRed( string.format('%s: ',groupName) .. 'Attacking invading ships in the west. ', 10, 'RedStatus' .. groupName ) +end + +function Su34AttackNorth(groupName) +--trace.menu("","Su34AttackNorth") + local groupSu34 = Group.getByName( groupName ) + local controllerSu34 = groupSu34.getController(groupSu34) + local groupShipNorth1 = Group.getByName("US Ship North #001") + local groupShipNorth2 = Group.getByName("US Ship North #002") + local groupShipNorth3 = Group.getByName("US Ship North #003") + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE ) + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) + if groupShipNorth1 ~= nil then + controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipNorth1:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = false}}) + end + if groupShipNorth2 ~= nil then + controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipNorth2:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = false}}) + end + if groupShipNorth3 ~= nil then + controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipNorth3:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = false}}) + end + Su34Status.status[groupName] = 3 + MessageToRed( string.format('%s: ',groupName) .. 'Attacking invading ships in the north. ', 10, 'RedStatus' .. groupName ) +end + +function Su34Orbit(groupName) +--trace.menu("","Su34Orbit") + local groupSu34 = Group.getByName( groupName ) + local controllerSu34 = groupSu34:getController() + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD ) + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) + controllerSu34:pushTask( {id = 'ControlledTask', params = { task = { id = 'Orbit', params = { pattern = AI.Task.OrbitPattern.RACE_TRACK } }, stopCondition = { duration = 600 } } } ) + Su34Status.status[groupName] = 4 + MessageToRed( string.format('%s: ',groupName) .. 'In orbit and awaiting further instructions. ', 10, 'RedStatus' .. groupName ) +end + +function Su34TakeOff(groupName) +--trace.menu("","Su34TakeOff") + local groupSu34 = Group.getByName( groupName ) + local controllerSu34 = groupSu34:getController() + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD ) + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.BYPASS_AND_ESCAPE ) + Su34Status.status[groupName] = 8 + MessageToRed( string.format('%s: ',groupName) .. 'Take-Off. ', 10, 'RedStatus' .. groupName ) +end + +function Su34Hold(groupName) +--trace.menu("","Su34Hold") + local groupSu34 = Group.getByName( groupName ) + local controllerSu34 = groupSu34:getController() + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD ) + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.BYPASS_AND_ESCAPE ) + Su34Status.status[groupName] = 5 + MessageToRed( string.format('%s: ',groupName) .. 'Holding Weapons. ', 10, 'RedStatus' .. groupName ) +end + +function Su34RTB(groupName) +--trace.menu("","Su34RTB") + Su34Status.status[groupName] = 6 + MessageToRed( string.format('%s: ',groupName) .. 'Return to Krasnodar. ', 10, 'RedStatus' .. groupName ) +end + +function Su34Destroyed(groupName) +--trace.menu("","Su34Destroyed") + Su34Status.status[groupName] = 7 + MessageToRed( string.format('%s: ',groupName) .. 'Destroyed. ', 30, 'RedStatus' .. groupName ) +end + +function GroupAlive( groupName ) +--trace.menu("","GroupAlive") + local groupTest = Group.getByName( groupName ) + + local groupExists = false + + if groupTest then + groupExists = groupTest:isExist() + end + + --trace.r( "", "", { groupExists } ) + return groupExists +end + +function Su34IsDead() +--trace.f() + +end + +function Su34OverviewStatus() +--trace.menu("","Su34OverviewStatus") + local msg = "" + local currentStatus = 0 + local Exists = false + + for groupName, currentStatus in pairs(Su34Status.status) do + + env.info(('Su34 Overview Status: GroupName = ' .. groupName )) + Alive = GroupAlive( groupName ) + + if Alive then + if currentStatus == 1 then + msg = msg .. string.format("%s: ",groupName) + msg = msg .. "Attacking carrier Carl Vinson. " + elseif currentStatus == 2 then + msg = msg .. string.format("%s: ",groupName) + msg = msg .. "Attacking supporting ships in the west. " + elseif currentStatus == 3 then + msg = msg .. string.format("%s: ",groupName) + msg = msg .. "Attacking invading ships in the north. " + elseif currentStatus == 4 then + msg = msg .. string.format("%s: ",groupName) + msg = msg .. "In orbit and awaiting further instructions. " + elseif currentStatus == 5 then + msg = msg .. string.format("%s: ",groupName) + msg = msg .. "Holding Weapons. " + elseif currentStatus == 6 then + msg = msg .. string.format("%s: ",groupName) + msg = msg .. "Return to Krasnodar. " + elseif currentStatus == 7 then + msg = msg .. string.format("%s: ",groupName) + msg = msg .. "Destroyed. " + elseif currentStatus == 8 then + msg = msg .. string.format("%s: ",groupName) + msg = msg .. "Take-Off. " + end + else + if currentStatus == 7 then + msg = msg .. string.format("%s: ",groupName) + msg = msg .. "Destroyed. " + else + Su34Destroyed(groupName) + end + end + end + + boardMsgRed.statusMsg = msg +end + + +function UpdateBoardMsg() +--trace.f() + Su34OverviewStatus() + MessageToRed( boardMsgRed.statusMsg, 15, 'RedStatus' ) +end + +function MusicReset( flg ) +--trace.f() + trigger.action.setUserFlag(95,flg) +end + +function PlaneActivate(groupNameFormat, flg) +--trace.f() + local groupName = groupNameFormat .. string.format("#%03d", trigger.misc.getUserFlag(flg)) + --trigger.action.outText(groupName,10) + trigger.action.activateGroup(Group.getByName(groupName)) +end + +function Su34Menu(groupName) +--trace.f() + + --env.info(( 'Su34Menu(' .. groupName .. ')' )) + local groupSu34 = Group.getByName( groupName ) + + if Su34Status.status[groupName] == 1 or + Su34Status.status[groupName] == 2 or + Su34Status.status[groupName] == 3 or + Su34Status.status[groupName] == 4 or + Su34Status.status[groupName] == 5 then + if Su34MenuPath[groupName] == nil then + if planeMenuPath == nil then + planeMenuPath = missionCommands.addSubMenuForCoalition( + coalition.side.RED, + "SU-34 anti-ship flights", + nil + ) + end + Su34MenuPath[groupName] = missionCommands.addSubMenuForCoalition( + coalition.side.RED, + "Flight " .. groupName, + planeMenuPath + ) + + missionCommands.addCommandForCoalition( + coalition.side.RED, + "Attack carrier Carl Vinson", + Su34MenuPath[groupName], + Su34AttackCarlVinson, + groupName + ) + + missionCommands.addCommandForCoalition( + coalition.side.RED, + "Attack ships in the west", + Su34MenuPath[groupName], + Su34AttackWest, + groupName + ) + + missionCommands.addCommandForCoalition( + coalition.side.RED, + "Attack ships in the north", + Su34MenuPath[groupName], + Su34AttackNorth, + groupName + ) + + missionCommands.addCommandForCoalition( + coalition.side.RED, + "Hold position and await instructions", + Su34MenuPath[groupName], + Su34Orbit, + groupName + ) + + missionCommands.addCommandForCoalition( + coalition.side.RED, + "Report status", + Su34MenuPath[groupName], + Su34OverviewStatus + ) + end + else + if Su34MenuPath[groupName] then + missionCommands.removeItemForCoalition(coalition.side.RED, Su34MenuPath[groupName]) + end + end +end + +--- Obsolete function, but kept to rework in framework. + +function ChooseInfantry ( TeleportPrefixTable, TeleportMax ) +--trace.f("Spawn") + --env.info(( 'ChooseInfantry: ' )) + + TeleportPrefixTableCount = #TeleportPrefixTable + TeleportPrefixTableIndex = math.random( 1, TeleportPrefixTableCount ) + + --env.info(( 'ChooseInfantry: TeleportPrefixTableIndex = ' .. TeleportPrefixTableIndex .. ' TeleportPrefixTableCount = ' .. TeleportPrefixTableCount .. ' TeleportMax = ' .. TeleportMax )) + + local TeleportFound = false + local TeleportLoop = true + local Index = TeleportPrefixTableIndex + local TeleportPrefix = '' + + while TeleportLoop do + TeleportPrefix = TeleportPrefixTable[Index] + if SpawnSettings[TeleportPrefix] then + if SpawnSettings[TeleportPrefix]['SpawnCount'] - 1 < TeleportMax then + SpawnSettings[TeleportPrefix]['SpawnCount'] = SpawnSettings[TeleportPrefix]['SpawnCount'] + 1 + TeleportFound = true + else + TeleportFound = false + end + else + SpawnSettings[TeleportPrefix] = {} + SpawnSettings[TeleportPrefix]['SpawnCount'] = 0 + TeleportFound = true + end + if TeleportFound then + TeleportLoop = false + else + if Index < TeleportPrefixTableCount then + Index = Index + 1 + else + TeleportLoop = false + end + end + --env.info(( 'ChooseInfantry: Loop 1 - TeleportPrefix = ' .. TeleportPrefix .. ' Index = ' .. Index )) + end + + if TeleportFound == false then + TeleportLoop = true + Index = 1 + while TeleportLoop do + TeleportPrefix = TeleportPrefixTable[Index] + if SpawnSettings[TeleportPrefix] then + if SpawnSettings[TeleportPrefix]['SpawnCount'] - 1 < TeleportMax then + SpawnSettings[TeleportPrefix]['SpawnCount'] = SpawnSettings[TeleportPrefix]['SpawnCount'] + 1 + TeleportFound = true + else + TeleportFound = false + end + else + SpawnSettings[TeleportPrefix] = {} + SpawnSettings[TeleportPrefix]['SpawnCount'] = 0 + TeleportFound = true + end + if TeleportFound then + TeleportLoop = false + else + if Index < TeleportPrefixTableIndex then + Index = Index + 1 + else + TeleportLoop = false + end + end + --env.info(( 'ChooseInfantry: Loop 2 - TeleportPrefix = ' .. TeleportPrefix .. ' Index = ' .. Index )) + end + end + + local TeleportGroupName = '' + if TeleportFound == true then + TeleportGroupName = TeleportPrefix .. string.format("#%03d", SpawnSettings[TeleportPrefix]['SpawnCount'] ) + else + TeleportGroupName = '' + end + + --env.info(('ChooseInfantry: TeleportGroupName = ' .. TeleportGroupName )) + --env.info(('ChooseInfantry: return')) + + return TeleportGroupName +end + +SpawnedInfantry = 0 + +function LandCarrier ( CarrierGroup, LandingZonePrefix ) +--trace.f() + --env.info(( 'LandCarrier: ' )) + --env.info(( 'LandCarrier: CarrierGroup = ' .. CarrierGroup:getName() )) + --env.info(( 'LandCarrier: LandingZone = ' .. LandingZonePrefix )) + + local controllerGroup = CarrierGroup:getController() + + local LandingZone = trigger.misc.getZone(LandingZonePrefix) + local LandingZonePos = {} + LandingZonePos.x = LandingZone.point.x + math.random(LandingZone.radius * -1, LandingZone.radius) + LandingZonePos.y = LandingZone.point.z + math.random(LandingZone.radius * -1, LandingZone.radius) + + controllerGroup:pushTask( { id = 'Land', params = { point = LandingZonePos, durationFlag = true, duration = 10 } } ) + + --env.info(( 'LandCarrier: end' )) +end + +EscortCount = 0 +function EscortCarrier ( CarrierGroup, EscortPrefix, EscortLastWayPoint, EscortEngagementDistanceMax, EscortTargetTypes ) +--trace.f() + --env.info(( 'EscortCarrier: ' )) + --env.info(( 'EscortCarrier: CarrierGroup = ' .. CarrierGroup:getName() )) + --env.info(( 'EscortCarrier: EscortPrefix = ' .. EscortPrefix )) + + local CarrierName = CarrierGroup:getName() + + local EscortMission = {} + local CarrierMission = {} + + local EscortMission = SpawnMissionGroup( EscortPrefix ) + local CarrierMission = SpawnMissionGroup( CarrierGroup:getName() ) + + if EscortMission ~= nil and CarrierMission ~= nil then + + EscortCount = EscortCount + 1 + EscortMissionName = string.format( EscortPrefix .. '#Escort %s', CarrierName ) + EscortMission.name = EscortMissionName + EscortMission.groupId = nil + EscortMission.lateActivation = false + EscortMission.taskSelected = false + + local EscortUnits = #EscortMission.units + for u = 1, EscortUnits do + EscortMission.units[u].name = string.format( EscortPrefix .. '#Escort %s %02d', CarrierName, u ) + EscortMission.units[u].unitId = nil + end + + + EscortMission.route.points[1].task = { id = "ComboTask", + params = + { + tasks = + { + [1] = + { + enabled = true, + auto = false, + id = "Escort", + number = 1, + params = + { + lastWptIndexFlagChangedManually = false, + groupId = CarrierGroup:getID(), + lastWptIndex = nil, + lastWptIndexFlag = false, + engagementDistMax = EscortEngagementDistanceMax, + targetTypes = EscortTargetTypes, + pos = + { + y = 20, + x = 20, + z = 0, + } -- end of ["pos"] + } -- end of ["params"] + } -- end of [1] + } -- end of ["tasks"] + } -- end of ["params"] + } -- end of ["task"] + + SpawnGroupAdd( EscortPrefix, EscortMission ) + + end +end + +function SendMessageToCarrier( CarrierGroup, CarrierMessage ) +--trace.f() + + if CarrierGroup ~= nil then + MessageToGroup( CarrierGroup, CarrierMessage, 30, 'Carrier/' .. CarrierGroup:getName() ) + end + +end + +function MessageToGroup( MsgGroup, MsgText, MsgTime, MsgName ) +--trace.f() + + if type(MsgGroup) == 'string' then + --env.info( 'MessageToGroup: Converted MsgGroup string "' .. MsgGroup .. '" into a Group structure.' ) + MsgGroup = Group.getByName( MsgGroup ) + end + + if MsgGroup ~= nil then + local MsgTable = {} + MsgTable.text = MsgText + MsgTable.displayTime = MsgTime + MsgTable.msgFor = { units = { MsgGroup:getUnits()[1]:getName() } } + MsgTable.name = MsgName + --routines.message.add( MsgTable ) + --env.info(('MessageToGroup: Message sent to ' .. MsgGroup:getUnits()[1]:getName() .. ' -> ' .. MsgText )) + end +end + +function MessageToUnit( UnitName, MsgText, MsgTime, MsgName ) +--trace.f() + + if UnitName ~= nil then + local MsgTable = {} + MsgTable.text = MsgText + MsgTable.displayTime = MsgTime + MsgTable.msgFor = { units = { UnitName } } + MsgTable.name = MsgName + --routines.message.add( MsgTable ) + end +end + +function MessageToAll( MsgText, MsgTime, MsgName ) +--trace.f() + + MESSAGE:New( MsgText, MsgTime, "Message" ):ToCoalition( coalition.side.RED ):ToCoalition( coalition.side.BLUE ) +end + +function MessageToRed( MsgText, MsgTime, MsgName ) +--trace.f() + + MESSAGE:New( MsgText, MsgTime, "To Red Coalition" ):ToCoalition( coalition.side.RED ) +end + +function MessageToBlue( MsgText, MsgTime, MsgName ) +--trace.f() + + MESSAGE:New( MsgText, MsgTime, "To Blue Coalition" ):ToCoalition( coalition.side.BLUE ) +end + +function getCarrierHeight( CarrierGroup ) +--trace.f() + + if CarrierGroup ~= nil then + if table.getn(CarrierGroup:getUnits()) == 1 then + local CarrierUnit = CarrierGroup:getUnits()[1] + local CurrentPoint = CarrierUnit:getPoint() + + local CurrentPosition = { x = CurrentPoint.x, y = CurrentPoint.z } + local CarrierHeight = CurrentPoint.y + + local LandHeight = land.getHeight( CurrentPosition ) + + --env.info(( 'CarrierHeight: LandHeight = ' .. LandHeight .. ' CarrierHeight = ' .. CarrierHeight )) + + return CarrierHeight - LandHeight + else + return 999999 + end + else + return 999999 + end + +end + +function GetUnitHeight( CheckUnit ) +--trace.f() + + local UnitPoint = CheckUnit:getPoint() + local UnitPosition = { x = CurrentPoint.x, y = CurrentPoint.z } + local UnitHeight = CurrentPoint.y + + local LandHeight = land.getHeight( CurrentPosition ) + + --env.info(( 'CarrierHeight: LandHeight = ' .. LandHeight .. ' CarrierHeight = ' .. CarrierHeight )) + + return UnitHeight - LandHeight + +end + + +_MusicTable = {} +_MusicTable.Files = {} +_MusicTable.Queue = {} +_MusicTable.FileCnt = 0 + + +function MusicRegister( SndRef, SndFile, SndTime ) +--trace.f() + + env.info(( 'MusicRegister: SndRef = ' .. SndRef )) + env.info(( 'MusicRegister: SndFile = ' .. SndFile )) + env.info(( 'MusicRegister: SndTime = ' .. SndTime )) + + + _MusicTable.FileCnt = _MusicTable.FileCnt + 1 + + _MusicTable.Files[_MusicTable.FileCnt] = {} + _MusicTable.Files[_MusicTable.FileCnt].Ref = SndRef + _MusicTable.Files[_MusicTable.FileCnt].File = SndFile + _MusicTable.Files[_MusicTable.FileCnt].Time = SndTime + + if not _MusicTable.Function then + _MusicTable.Function = routines.scheduleFunction( MusicScheduler, { }, timer.getTime() + 10, 10) + end + +end + +function MusicToPlayer( SndRef, PlayerName, SndContinue ) +--trace.f() + + --env.info(( 'MusicToPlayer: SndRef = ' .. SndRef )) + + local PlayerUnits = AlivePlayerUnits() + for PlayerUnitIdx, PlayerUnit in pairs(PlayerUnits) do + local PlayerUnitName = PlayerUnit:getPlayerName() + --env.info(( 'MusicToPlayer: PlayerUnitName = ' .. PlayerUnitName )) + if PlayerName == PlayerUnitName then + PlayerGroup = PlayerUnit:getGroup() + if PlayerGroup then + --env.info(( 'MusicToPlayer: PlayerGroup = ' .. PlayerGroup:getName() )) + MusicToGroup( SndRef, PlayerGroup, SndContinue ) + end + break + end + end + + --env.info(( 'MusicToPlayer: end' )) + +end + +function MusicToGroup( SndRef, SndGroup, SndContinue ) +--trace.f() + + --env.info(( 'MusicToGroup: SndRef = ' .. SndRef )) + + if SndGroup ~= nil then + if _MusicTable and _MusicTable.FileCnt > 0 then + if SndGroup:isExist() then + if MusicCanStart(SndGroup:getUnit(1):getPlayerName()) then + --env.info(( 'MusicToGroup: OK for Sound.' )) + local SndIdx = 0 + if SndRef == '' then + --env.info(( 'MusicToGroup: SndRef as empty. Queueing at random.' )) + SndIdx = math.random( 1, _MusicTable.FileCnt ) + else + for SndIdx = 1, _MusicTable.FileCnt do + if _MusicTable.Files[SndIdx].Ref == SndRef then + break + end + end + end + --env.info(( 'MusicToGroup: SndIdx = ' .. SndIdx )) + --env.info(( 'MusicToGroup: Queueing Music ' .. _MusicTable.Files[SndIdx].File .. ' for Group ' .. SndGroup:getID() )) + trigger.action.outSoundForGroup( SndGroup:getID(), _MusicTable.Files[SndIdx].File ) + MessageToGroup( SndGroup, 'Playing ' .. _MusicTable.Files[SndIdx].File, 15, 'Music-' .. SndGroup:getUnit(1):getPlayerName() ) + + local SndQueueRef = SndGroup:getUnit(1):getPlayerName() + if _MusicTable.Queue[SndQueueRef] == nil then + _MusicTable.Queue[SndQueueRef] = {} + end + _MusicTable.Queue[SndQueueRef].Start = timer.getTime() + _MusicTable.Queue[SndQueueRef].PlayerName = SndGroup:getUnit(1):getPlayerName() + _MusicTable.Queue[SndQueueRef].Group = SndGroup + _MusicTable.Queue[SndQueueRef].ID = SndGroup:getID() + _MusicTable.Queue[SndQueueRef].Ref = SndIdx + _MusicTable.Queue[SndQueueRef].Continue = SndContinue + _MusicTable.Queue[SndQueueRef].Type = Group + end + end + end + end +end + +function MusicCanStart(PlayerName) +--trace.f() + + --env.info(( 'MusicCanStart:' )) + + local MusicOut = false + + if _MusicTable['Queue'] ~= nil and _MusicTable.FileCnt > 0 then + --env.info(( 'MusicCanStart: PlayerName = ' .. PlayerName )) + local PlayerFound = false + local MusicStart = 0 + local MusicTime = 0 + for SndQueueIdx, SndQueue in pairs( _MusicTable.Queue ) do + if SndQueue.PlayerName == PlayerName then + PlayerFound = true + MusicStart = SndQueue.Start + MusicTime = _MusicTable.Files[SndQueue.Ref].Time + break + end + end + if PlayerFound then + --env.info(( 'MusicCanStart: MusicStart = ' .. MusicStart )) + --env.info(( 'MusicCanStart: MusicTime = ' .. MusicTime )) + --env.info(( 'MusicCanStart: timer.getTime() = ' .. timer.getTime() )) + + if MusicStart + MusicTime <= timer.getTime() then + MusicOut = true + end + else + MusicOut = true + end + end + + if MusicOut then + --env.info(( 'MusicCanStart: true' )) + else + --env.info(( 'MusicCanStart: false' )) + end + + return MusicOut +end + +function MusicScheduler() +--trace.scheduled("", "MusicScheduler") + + --env.info(( 'MusicScheduler:' )) + if _MusicTable['Queue'] ~= nil and _MusicTable.FileCnt > 0 then + --env.info(( 'MusicScheduler: Walking Sound Queue.')) + for SndQueueIdx, SndQueue in pairs( _MusicTable.Queue ) do + if SndQueue.Continue then + if MusicCanStart(SndQueue.PlayerName) then + --env.info(('MusicScheduler: MusicToGroup')) + MusicToPlayer( '', SndQueue.PlayerName, true ) + end + end + end + end + +end + + +env.info(( 'Init: Scripts Loaded v1.1' )) + +--- This module contains derived utilities taken from the MIST framework, which are excellent tools to be reused in an OO environment. +-- +-- ### Authors: +-- +-- * Grimes : Design & Programming of the MIST framework. +-- +-- ### Contributions: +-- +-- * FlightControl : Rework to OO framework. +-- +-- @module Utils +-- @image MOOSE.JPG + + +--- @type SMOKECOLOR +-- @field Green +-- @field Red +-- @field White +-- @field Orange +-- @field Blue + +SMOKECOLOR = trigger.smokeColor -- #SMOKECOLOR + +--- @type FLARECOLOR +-- @field Green +-- @field Red +-- @field White +-- @field Yellow + +FLARECOLOR = trigger.flareColor -- #FLARECOLOR + +--- Big smoke preset enum. +-- @type BIGSMOKEPRESET +BIGSMOKEPRESET = { + SmallSmokeAndFire=1, + MediumSmokeAndFire=2, + LargeSmokeAndFire=3, + HugeSmokeAndFire=4, + SmallSmoke=5, + MediumSmoke=6, + LargeSmoke=7, + HugeSmoke=8, +} + +--- DCS map as returned by env.mission.theatre. +-- @type DCSMAP +-- @field #string Caucasus Caucasus map. +-- @field #string Normandy Normandy map. +-- @field #string NTTR Nevada Test and Training Range map. +-- @field #string PersianGulf Persian Gulf map. +-- @field #string TheChannel The Channel map. +-- @field #string Syria Syria map. +-- @field #string MarianaIslands Mariana Islands map. +DCSMAP = { + Caucasus="Caucasus", + NTTR="Nevada", + Normandy="Normandy", + PersianGulf="PersianGulf", + TheChannel="TheChannel", + Syria="Syria", + MarianaIslands="MarianaIslands" +} + + +--- See [DCS_enum_callsigns](https://wiki.hoggitworld.com/view/DCS_enum_callsigns) +-- @type CALLSIGN +CALLSIGN={ + -- Aircraft + Aircraft={ + Enfield=1, + Springfield=2, + Uzi=3, + Colt=4, + Dodge=5, + Ford=6, + Chevy=7, + Pontiac=8, + -- A-10A or A-10C + Hawg=9, + Boar=10, + Pig=11, + Tusk=12, + }, + -- AWACS + AWACS={ + Overlord=1, + Magic=2, + Wizard=3, + Focus=4, + Darkstar=5, + }, + -- Tanker + Tanker={ + Texaco=1, + Arco=2, + Shell=3, + }, + -- JTAC + JTAC={ + Axeman=1, + Darknight=2, + Warrior=3, + Pointer=4, + Eyeball=5, + Moonbeam=6, + Whiplash=7, + Finger=8, + Pinpoint=9, + Ferret=10, + Shaba=11, + Playboy=12, + Hammer=13, + Jaguar=14, + Deathstar=15, + Anvil=16, + Firefly=17, + Mantis=18, + Badger=19, + }, + -- FARP + FARP={ + London=1, + Dallas=2, + Paris=3, + Moscow=4, + Berlin=5, + Rome=6, + Madrid=7, + Warsaw=8, + Dublin=9, + Perth=10, + }, +} --#CALLSIGN + +--- Utilities static class. +-- @type UTILS +-- @field #number _MarkID Marker index counter. Running number when marker is added. +UTILS = { + _MarkID = 1 +} + +--- Function to infer instance of an object +-- +-- ### Examples: +-- +-- * UTILS.IsInstanceOf( 'some text', 'string' ) will return true +-- * UTILS.IsInstanceOf( some_function, 'function' ) will return true +-- * UTILS.IsInstanceOf( 10, 'number' ) will return true +-- * UTILS.IsInstanceOf( false, 'boolean' ) will return true +-- * UTILS.IsInstanceOf( nil, 'nil' ) will return true +-- +-- * UTILS.IsInstanceOf( ZONE:New( 'some zone', ZONE ) will return true +-- * UTILS.IsInstanceOf( ZONE:New( 'some zone', 'ZONE' ) will return true +-- * UTILS.IsInstanceOf( ZONE:New( 'some zone', 'zone' ) will return true +-- * UTILS.IsInstanceOf( ZONE:New( 'some zone', 'BASE' ) will return true +-- +-- * UTILS.IsInstanceOf( ZONE:New( 'some zone', 'GROUP' ) will return false +-- +-- +-- @param object is the object to be evaluated +-- @param className is the name of the class to evaluate (can be either a string or a Moose class) +-- @return #boolean +UTILS.IsInstanceOf = function( object, className ) + -- Is className NOT a string ? + if not type( className ) == 'string' then + + -- Is className a Moose class ? + if type( className ) == 'table' and className.IsInstanceOf ~= nil then + + -- Get the name of the Moose class as a string + className = className.ClassName + + -- className is neither a string nor a Moose class, throw an error + else + + -- I'm not sure if this should take advantage of MOOSE logging function, or throw an error for pcall + local err_str = 'className parameter should be a string; parameter received: '..type( className ) + return false + -- error( err_str ) + + end + end + + -- Is the object a Moose class instance ? + if type( object ) == 'table' and object.IsInstanceOf ~= nil then + + -- Use the IsInstanceOf method of the BASE class + return object:IsInstanceOf( className ) + else + + -- If the object is not an instance of a Moose class, evaluate against lua basic data types + local basicDataTypes = { 'string', 'number', 'function', 'boolean', 'nil', 'table' } + for _, basicDataType in ipairs( basicDataTypes ) do + if className == basicDataType then + return type( object ) == basicDataType + end + end + end + + -- Check failed + return false +end + + +--- Deep copy a table. See http://lua-users.org/wiki/CopyTable +-- @param #table object The input table. +-- @return #table Copy of the input table. +UTILS.DeepCopy = function(object) + + local lookup_table = {} + + -- Copy function. + local function _copy(object) + if type(object) ~= "table" then + return object + elseif lookup_table[object] then + return lookup_table[object] + end + + local new_table = {} + + lookup_table[object] = new_table + + for index, value in pairs(object) do + new_table[_copy(index)] = _copy(value) + end + + return setmetatable(new_table, getmetatable(object)) + end + + local objectreturn = _copy(object) + + return objectreturn +end + + +--- Porting in Slmod's serialize_slmod2. +-- @param #table tbl Input table. +UTILS.OneLineSerialize = function( tbl ) -- serialization of a table all on a single line, no comments, made to replace old get_table_string function + + lookup_table = {} + + local function _Serialize( tbl ) + + if type(tbl) == 'table' then --function only works for tables! + + if lookup_table[tbl] then + return lookup_table[object] + end + + local tbl_str = {} + + lookup_table[tbl] = tbl_str + + tbl_str[#tbl_str + 1] = '{' + + for ind,val in pairs(tbl) do -- serialize its fields + local ind_str = {} + if type(ind) == "number" then + ind_str[#ind_str + 1] = '[' + ind_str[#ind_str + 1] = tostring(ind) + ind_str[#ind_str + 1] = ']=' + else --must be a string + ind_str[#ind_str + 1] = '[' + ind_str[#ind_str + 1] = routines.utils.basicSerialize(ind) + ind_str[#ind_str + 1] = ']=' + end + + local val_str = {} + if ((type(val) == 'number') or (type(val) == 'boolean')) then + val_str[#val_str + 1] = tostring(val) + val_str[#val_str + 1] = ',' + tbl_str[#tbl_str + 1] = table.concat(ind_str) + tbl_str[#tbl_str + 1] = table.concat(val_str) + elseif type(val) == 'string' then + val_str[#val_str + 1] = routines.utils.basicSerialize(val) + val_str[#val_str + 1] = ',' + tbl_str[#tbl_str + 1] = table.concat(ind_str) + tbl_str[#tbl_str + 1] = table.concat(val_str) + elseif type(val) == 'nil' then -- won't ever happen, right? + val_str[#val_str + 1] = 'nil,' + tbl_str[#tbl_str + 1] = table.concat(ind_str) + tbl_str[#tbl_str + 1] = table.concat(val_str) + elseif type(val) == 'table' then + if ind == "__index" then + -- tbl_str[#tbl_str + 1] = "__index" + -- tbl_str[#tbl_str + 1] = ',' --I think this is right, I just added it + else + + val_str[#val_str + 1] = _Serialize(val) + val_str[#val_str + 1] = ',' --I think this is right, I just added it + tbl_str[#tbl_str + 1] = table.concat(ind_str) + tbl_str[#tbl_str + 1] = table.concat(val_str) + end + elseif type(val) == 'function' then + tbl_str[#tbl_str + 1] = "f() " .. tostring(ind) + tbl_str[#tbl_str + 1] = ',' --I think this is right, I just added it + else + env.info('unable to serialize value type ' .. routines.utils.basicSerialize(type(val)) .. ' at index ' .. tostring(ind)) + env.info( debug.traceback() ) + end + + end + tbl_str[#tbl_str + 1] = '}' + return table.concat(tbl_str) + else + return tostring(tbl) + end + end + + local objectreturn = _Serialize(tbl) + return objectreturn +end + +--porting in Slmod's "safestring" basic serialize +UTILS.BasicSerialize = function(s) + if s == nil then + return "\"\"" + else + if ((type(s) == 'number') or (type(s) == 'boolean') or (type(s) == 'function') or (type(s) == 'table') or (type(s) == 'userdata') ) then + return tostring(s) + elseif type(s) == 'string' then + s = string.format('%q', s) + return s + end + end +end + + +UTILS.ToDegree = function(angle) + return angle*180/math.pi +end + +UTILS.ToRadian = function(angle) + return angle*math.pi/180 +end + +UTILS.MetersToNM = function(meters) + return meters/1852 +end + +UTILS.KiloMetersToNM = function(kilometers) + return kilometers/1852*1000 +end + +UTILS.MetersToSM = function(meters) + return meters/1609.34 +end + +UTILS.KiloMetersToSM = function(kilometers) + return kilometers/1609.34*1000 +end + +UTILS.MetersToFeet = function(meters) + return meters/0.3048 +end + +UTILS.KiloMetersToFeet = function(kilometers) + return kilometers/0.3048*1000 +end + +UTILS.NMToMeters = function(NM) + return NM*1852 +end + +UTILS.NMToKiloMeters = function(NM) + return NM*1852/1000 +end + +UTILS.FeetToMeters = function(feet) + return feet*0.3048 +end + +UTILS.KnotsToKmph = function(knots) + return knots * 1.852 +end + +UTILS.KmphToKnots = function(knots) + return knots / 1.852 +end + +UTILS.KmphToMps = function( kmph ) + return kmph / 3.6 +end + +UTILS.MpsToKmph = function( mps ) + return mps * 3.6 +end + +UTILS.MiphToMps = function( miph ) + return miph * 0.44704 +end + +--- Convert meters per second to miles per hour. +-- @param #number mps Speed in m/s. +-- @return #number Speed in miles per hour. +UTILS.MpsToMiph = function( mps ) + return mps / 0.44704 +end + +--- Convert meters per second to knots. +-- @param #number mps Speed in m/s. +-- @return #number Speed in knots. +UTILS.MpsToKnots = function( mps ) + return mps * 1.94384 --3600 / 1852 +end + +--- Convert knots to meters per second. +-- @param #number knots Speed in knots. +-- @return #number Speed in m/s. +UTILS.KnotsToMps = function( knots ) + return knots / 1.94384 --* 1852 / 3600 +end + +--- Convert temperature from Celsius to Farenheit. +-- @param #number Celcius Temperature in degrees Celsius. +-- @return #number Temperature in degrees Farenheit. +UTILS.CelciusToFarenheit = function( Celcius ) + return Celcius * 9/5 + 32 +end + +--- Convert pressure from hecto Pascal (hPa) to inches of mercury (inHg). +-- @param #number hPa Pressure in hPa. +-- @return #number Pressure in inHg. +UTILS.hPa2inHg = function( hPa ) + return hPa * 0.0295299830714 +end + +--- Convert knots to alitude corrected KIAS, e.g. for tankers. +-- @param #number knots Speed in knots. +-- @param #number altitude Altitude in feet +-- @return #number Corrected KIAS +UTILS.KnotsToAltKIAS = function( knots, altitude ) + return (knots * 0.018 * (altitude / 1000)) + knots +end + +--- Convert pressure from hecto Pascal (hPa) to millimeters of mercury (mmHg). +-- @param #number hPa Pressure in hPa. +-- @return #number Pressure in mmHg. +UTILS.hPa2mmHg = function( hPa ) + return hPa * 0.7500615613030 +end + +--- Convert kilo gramms (kg) to pounds (lbs). +-- @param #number kg Mass in kg. +-- @return #number Mass in lbs. +UTILS.kg2lbs = function( kg ) + return kg * 2.20462 +end + +--[[acc: +in DM: decimal point of minutes. +In DMS: decimal point of seconds. +position after the decimal of the least significant digit: +So: +42.32 - acc of 2. +]] +UTILS.tostringLL = function( lat, lon, acc, DMS) + + local latHemi, lonHemi + if lat > 0 then + latHemi = 'N' + else + latHemi = 'S' + end + + if lon > 0 then + lonHemi = 'E' + else + lonHemi = 'W' + end + + lat = math.abs(lat) + lon = math.abs(lon) + + local latDeg = math.floor(lat) + local latMin = (lat - latDeg)*60 + + local lonDeg = math.floor(lon) + local lonMin = (lon - lonDeg)*60 + + if DMS then -- degrees, minutes, and seconds. + local oldLatMin = latMin + latMin = math.floor(latMin) + local latSec = UTILS.Round((oldLatMin - latMin)*60, acc) + + local oldLonMin = lonMin + lonMin = math.floor(lonMin) + local lonSec = UTILS.Round((oldLonMin - lonMin)*60, acc) + + if latSec == 60 then + latSec = 0 + latMin = latMin + 1 + end + + if lonSec == 60 then + lonSec = 0 + lonMin = lonMin + 1 + end + + local secFrmtStr -- create the formatting string for the seconds place + secFrmtStr = '%02d' + if acc <= 0 then -- no decimal place. + secFrmtStr = '%02d' + else + local width = 3 + acc -- 01.310 - that's a width of 6, for example. Acc is limited to 2 for DMS! + secFrmtStr = '%0' .. width .. '.' .. acc .. 'f' + end + + -- 024° 23' 12"N or 024° 23' 12.03"N + return string.format('%03d°', latDeg)..string.format('%02d', latMin)..'\''..string.format(secFrmtStr, latSec)..'"'..latHemi..' ' + .. string.format('%03d°', lonDeg)..string.format('%02d', lonMin)..'\''..string.format(secFrmtStr, lonSec)..'"'..lonHemi + + else -- degrees, decimal minutes. + latMin = UTILS.Round(latMin, acc) + lonMin = UTILS.Round(lonMin, acc) + + if latMin == 60 then + latMin = 0 + latDeg = latDeg + 1 + end + + if lonMin == 60 then + lonMin = 0 + lonDeg = lonDeg + 1 + end + + local minFrmtStr -- create the formatting string for the minutes place + if acc <= 0 then -- no decimal place. + minFrmtStr = '%02d' + else + local width = 3 + acc -- 01.310 - that's a width of 6, for example. + minFrmtStr = '%0' .. width .. '.' .. acc .. 'f' + end + + -- 024 23'N or 024 23.123'N + return string.format('%03d°', latDeg) .. ' ' .. string.format(minFrmtStr, latMin) .. '\'' .. latHemi .. ' ' + .. string.format('%03d°', lonDeg) .. ' ' .. string.format(minFrmtStr, lonMin) .. '\'' .. lonHemi + + end +end + +-- acc- the accuracy of each easting/northing. 0, 1, 2, 3, 4, or 5. +UTILS.tostringMGRS = function(MGRS, acc) --R2.1 + + if acc == 0 then + return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph + else + + -- Test if Easting/Northing have less than 4 digits. + --MGRS.Easting=123 -- should be 00123 + --MGRS.Northing=5432 -- should be 05432 + + -- Truncate rather than round MGRS grid! + local Easting=tostring(MGRS.Easting) + local Northing=tostring(MGRS.Northing) + + -- Count number of missing digits. Easting/Northing should have 5 digits. However, it is passed as a number. Therefore, any leading zeros would not be displayed by lua. + local nE=5-string.len(Easting) + local nN=5-string.len(Northing) + + -- Get leading zeros (if any). + for i=1,nE do Easting="0"..Easting end + for i=1,nN do Northing="0"..Northing end + + -- Return MGRS string. + return string.format("%s %s %s %s", MGRS.UTMZone, MGRS.MGRSDigraph, string.sub(Easting, 1, acc), string.sub(Northing, 1, acc)) + end + +end + + +--- From http://lua-users.org/wiki/SimpleRound +-- use negative idp for rounding ahead of decimal place, positive for rounding after decimal place +function UTILS.Round( num, idp ) + local mult = 10 ^ ( idp or 0 ) + return math.floor( num * mult + 0.5 ) / mult +end + +-- porting in Slmod's dostring +function UTILS.DoString( s ) + local f, err = loadstring( s ) + if f then + return true, f() + else + return false, err + end +end + +-- Here is a customized version of pairs, which I called spairs because it iterates over the table in a sorted order. +function UTILS.spairs( t, order ) + -- collect the keys + local keys = {} + for k in pairs(t) do keys[#keys+1] = k end + + -- if order function given, sort by it by passing the table and keys a, b, + -- otherwise just sort the keys + if order then + table.sort(keys, function(a,b) return order(t, a, b) end) + else + table.sort(keys) + end + + -- return the iterator function + local i = 0 + return function() + i = i + 1 + if keys[i] then + return keys[i], t[keys[i]] + end + end +end + + +-- Here is a customized version of pairs, which I called kpairs because it iterates over the table in a sorted order, based on a function that will determine the keys as reference first. +function UTILS.kpairs( t, getkey, order ) + -- collect the keys + local keys = {} + local keyso = {} + for k, o in pairs(t) do keys[#keys+1] = k keyso[#keyso+1] = getkey( o ) end + + -- if order function given, sort by it by passing the table and keys a, b, + -- otherwise just sort the keys + if order then + table.sort(keys, function(a,b) return order(t, a, b) end) + else + table.sort(keys) + end + + -- return the iterator function + local i = 0 + return function() + i = i + 1 + if keys[i] then + return keyso[i], t[keys[i]] + end + end +end + +-- Here is a customized version of pairs, which I called rpairs because it iterates over the table in a random order. +function UTILS.rpairs( t ) + -- collect the keys + + local keys = {} + for k in pairs(t) do keys[#keys+1] = k end + + local random = {} + local j = #keys + for i = 1, j do + local k = math.random( 1, #keys ) + random[i] = keys[k] + table.remove( keys, k ) + end + + -- return the iterator function + local i = 0 + return function() + i = i + 1 + if random[i] then + return random[i], t[random[i]] + end + end +end + +-- get a new mark ID for markings +function UTILS.GetMarkID() + + UTILS._MarkID = UTILS._MarkID + 1 + return UTILS._MarkID + +end + +--- Remove an object (marker, circle, arrow, text, quad, ...) on the F10 map. +-- @param #number MarkID Unique ID of the object. +-- @param #number Delay (Optional) Delay in seconds before the mark is removed. +function UTILS.RemoveMark(MarkID, Delay) + if Delay and Delay>0 then + TIMER:New(UTILS.RemoveMark, MarkID):Start(Delay) + else + trigger.action.removeMark(MarkID) + end +end + + +-- Test if a Vec2 is in a radius of another Vec2 +function UTILS.IsInRadius( InVec2, Vec2, Radius ) + + local InRadius = ( ( InVec2.x - Vec2.x ) ^2 + ( InVec2.y - Vec2.y ) ^2 ) ^ 0.5 <= Radius + + return InRadius +end + +-- Test if a Vec3 is in the sphere of another Vec3 +function UTILS.IsInSphere( InVec3, Vec3, Radius ) + + local InSphere = ( ( InVec3.x - Vec3.x ) ^2 + ( InVec3.y - Vec3.y ) ^2 + ( InVec3.z - Vec3.z ) ^2 ) ^ 0.5 <= Radius + + return InSphere +end + +--- Beaufort scale: returns Beaufort number and wind description as a function of wind speed in m/s. +-- @param #number speed Wind speed in m/s. +-- @return #number Beaufort number. +-- @return #string Beauford wind description. +function UTILS.BeaufortScale(speed) + local bn=nil + local bd=nil + if speed<0.51 then + bn=0 + bd="Calm" + elseif speed<2.06 then + bn=1 + bd="Light Air" + elseif speed<3.60 then + bn=2 + bd="Light Breeze" + elseif speed<5.66 then + bn=3 + bd="Gentle Breeze" + elseif speed<8.23 then + bn=4 + bd="Moderate Breeze" + elseif speed<11.32 then + bn=5 + bd="Fresh Breeze" + elseif speed<14.40 then + bn=6 + bd="Strong Breeze" + elseif speed<17.49 then + bn=7 + bd="Moderate Gale" + elseif speed<21.09 then + bn=8 + bd="Fresh Gale" + elseif speed<24.69 then + bn=9 + bd="Strong Gale" + elseif speed<28.81 then + bn=10 + bd="Storm" + elseif speed<32.92 then + bn=11 + bd="Violent Storm" + else + bn=12 + bd="Hurricane" + end + return bn,bd +end + +--- Split string at seperators. C.f. http://stackoverflow.com/questions/1426954/split-string-in-lua +-- @param #string str Sting to split. +-- @param #string sep Speparator for split. +-- @return #table Split text. +function UTILS.Split(str, sep) + local result = {} + local regex = ("([^%s]+)"):format(sep) + for each in str:gmatch(regex) do + table.insert(result, each) + end + return result +end + +--- Get a table of all characters in a string. +-- @param #string str Sting. +-- @return #table Individual characters. +function UTILS.GetCharacters(str) + + local chars={} + + for i=1,#str do + local c=str:sub(i,i) + table.insert(chars, c) + end + + return chars +end + +--- Convert time in seconds to hours, minutes and seconds. +-- @param #number seconds Time in seconds, e.g. from timer.getAbsTime() function. +-- @param #boolean short (Optional) If true, use short output, i.e. (HH:)MM:SS without day. +-- @return #string Time in format Hours:Minutes:Seconds+Days (HH:MM:SS+D). +function UTILS.SecondsToClock(seconds, short) + + -- Nil check. + if seconds==nil then + return nil + end + + -- Seconds + local seconds = tonumber(seconds) + + -- Seconds of this day. + local _seconds=seconds%(60*60*24) + + if seconds<0 then + return nil + else + local hours = string.format("%02.f", math.floor(_seconds/3600)) + local mins = string.format("%02.f", math.floor(_seconds/60 - (hours*60))) + local secs = string.format("%02.f", math.floor(_seconds - hours*3600 - mins *60)) + local days = string.format("%d", seconds/(60*60*24)) + local clock=hours..":"..mins..":"..secs.."+"..days + if short then + if hours=="00" then + --clock=mins..":"..secs + clock=hours..":"..mins..":"..secs + else + clock=hours..":"..mins..":"..secs + end + end + return clock + end +end + +--- Seconds of today. +-- @return #number Seconds passed since last midnight. +function UTILS.SecondsOfToday() + + -- Time in seconds. + local time=timer.getAbsTime() + + -- Short format without days since mission start. + local clock=UTILS.SecondsToClock(time, true) + + -- Time is now the seconds passed since last midnight. + return UTILS.ClockToSeconds(clock) +end + +--- Cound seconds until next midnight. +-- @return #number Seconds to midnight. +function UTILS.SecondsToMidnight() + return 24*60*60-UTILS.SecondsOfToday() +end + +--- Convert clock time from hours, minutes and seconds to seconds. +-- @param #string clock String of clock time. E.g., "06:12:35" or "5:1:30+1". Format is (H)H:(M)M:((S)S)(+D) H=Hours, M=Minutes, S=Seconds, D=Days. +-- @return #number Seconds. Corresponds to what you cet from timer.getAbsTime() function. +function UTILS.ClockToSeconds(clock) + + -- Nil check. + if clock==nil then + return nil + end + + -- Seconds init. + local seconds=0 + + -- Split additional days. + local dsplit=UTILS.Split(clock, "+") + + -- Convert days to seconds. + if #dsplit>1 then + seconds=seconds+tonumber(dsplit[2])*60*60*24 + end + + -- Split hours, minutes, seconds + local tsplit=UTILS.Split(dsplit[1], ":") + + -- Get time in seconds + local i=1 + for _,time in ipairs(tsplit) do + if i==1 then + -- Hours + seconds=seconds+tonumber(time)*60*60 + elseif i==2 then + -- Minutes + seconds=seconds+tonumber(time)*60 + elseif i==3 then + -- Seconds + seconds=seconds+tonumber(time) + end + i=i+1 + end + + return seconds +end + +--- Display clock and mission time on screen as a message to all. +-- @param #number duration Duration in seconds how long the time is displayed. Default is 5 seconds. +function UTILS.DisplayMissionTime(duration) + duration=duration or 5 + local Tnow=timer.getAbsTime() + local mission_time=Tnow-timer.getTime0() + local mission_time_minutes=mission_time/60 + local mission_time_seconds=mission_time%60 + local local_time=UTILS.SecondsToClock(Tnow) + local text=string.format("Time: %s - %02d:%02d", local_time, mission_time_minutes, mission_time_seconds) + MESSAGE:New(text, duration):ToAll() +end + +--- Replace illegal characters [<>|/?*:\\] in a string. +-- @param #string Text Input text. +-- @param #string ReplaceBy Replace illegal characters by this character or string. Default underscore "_". +-- @return #string The input text with illegal chars replaced. +function UTILS.ReplaceIllegalCharacters(Text, ReplaceBy) + ReplaceBy=ReplaceBy or "_" + local text=Text:gsub("[<>|/?*:\\]", ReplaceBy) + return text +end + +--- Generate a Gaussian pseudo-random number. +-- @param #number x0 Expectation value of distribution. +-- @param #number sigma (Optional) Standard deviation. Default 10. +-- @param #number xmin (Optional) Lower cut-off value. +-- @param #number xmax (Optional) Upper cut-off value. +-- @param #number imax (Optional) Max number of tries to get a value between xmin and xmax (if specified). Default 100. +-- @return #number Gaussian random number. +function UTILS.RandomGaussian(x0, sigma, xmin, xmax, imax) + + -- Standard deviation. Default 10 if not given. + sigma=sigma or 10 + + -- Max attempts. + imax=imax or 100 + + local r + local gotit=false + local i=0 + while not gotit do + + -- Uniform numbers in [0,1). We need two. + local x1=math.random() + local x2=math.random() + + -- Transform to Gaussian exp(-(x-x0)°/(2*sigma°). + r = math.sqrt(-2*sigma*sigma * math.log(x1)) * math.cos(2*math.pi * x2) + x0 + + i=i+1 + if (r>=xmin and r<=xmax) or i>imax then + gotit=true + end + end + + return r +end + +--- Randomize a value by a certain amount. +-- @param #number value The value which should be randomized +-- @param #number fac Randomization factor. +-- @param #number lower (Optional) Lower limit of the returned value. +-- @param #number upper (Optional) Upper limit of the returned value. +-- @return #number Randomized value. +-- @usage UTILS.Randomize(100, 0.1) returns a value between 90 and 110, i.e. a plus/minus ten percent variation. +-- @usage UTILS.Randomize(100, 0.5, nil, 120) returns a value between 50 and 120, i.e. a plus/minus fivty percent variation with upper bound 120. +function UTILS.Randomize(value, fac, lower, upper) + local min + if lower then + min=math.max(value-value*fac, lower) + else + min=value-value*fac + end + local max + if upper then + max=math.min(value+value*fac, upper) + else + max=value+value*fac + end + + local r=math.random(min, max) + + return r +end + +--- Calculate the [dot product](https://en.wikipedia.org/wiki/Dot_product) of two vectors. The result is a number. +-- @param DCS#Vec3 a Vector in 3D with x, y, z components. +-- @param DCS#Vec3 b Vector in 3D with x, y, z components. +-- @return #number Scalar product of the two vectors a*b. +function UTILS.VecDot(a, b) + return a.x*b.x + a.y*b.y + a.z*b.z +end + +--- Calculate the [euclidean norm](https://en.wikipedia.org/wiki/Euclidean_distance) (length) of a 3D vector. +-- @param DCS#Vec3 a Vector in 3D with x, y, z components. +-- @return #number Norm of the vector. +function UTILS.VecNorm(a) + return math.sqrt(UTILS.VecDot(a, a)) +end + +--- Calculate the distance between two 2D vectors. +-- @param DCS#Vec2 a Vector in 3D with x, y components. +-- @param DCS#Vec2 b Vector in 3D with x, y components. +-- @return #number Distance between the vectors. +function UTILS.VecDist2D(a, b) + + local c={x=b.x-a.x, y=b.y-a.y} + + local d=math.sqrt(c.x*c.x+c.y*c.y) + + return d +end + + +--- Calculate the distance between two 3D vectors. +-- @param DCS#Vec3 a Vector in 3D with x, y, z components. +-- @param DCS#Vec3 b Vector in 3D with x, y, z components. +-- @return #number Distance between the vectors. +function UTILS.VecDist3D(a, b) + + local c={x=b.x-a.x, y=b.y-a.y, z=b.z-a.z} + + local d=math.sqrt(UTILS.VecDot(c, c)) + + return d +end + +--- Calculate the [cross product](https://en.wikipedia.org/wiki/Cross_product) of two 3D vectors. The result is a 3D vector. +-- @param DCS#Vec3 a Vector in 3D with x, y, z components. +-- @param DCS#Vec3 b Vector in 3D with x, y, z components. +-- @return DCS#Vec3 Vector +function UTILS.VecCross(a, b) + return {x=a.y*b.z - a.z*b.y, y=a.z*b.x - a.x*b.z, z=a.x*b.y - a.y*b.x} +end + +--- Calculate the difference between two 3D vectors by substracting the x,y,z components from each other. +-- @param DCS#Vec3 a Vector in 3D with x, y, z components. +-- @param DCS#Vec3 b Vector in 3D with x, y, z components. +-- @return DCS#Vec3 Vector c=a-b with c(i)=a(i)-b(i), i=x,y,z. +function UTILS.VecSubstract(a, b) + return {x=a.x-b.x, y=a.y-b.y, z=a.z-b.z} +end + +--- Calculate the total vector of two 3D vectors by adding the x,y,z components of each other. +-- @param DCS#Vec3 a Vector in 3D with x, y, z components. +-- @param DCS#Vec3 b Vector in 3D with x, y, z components. +-- @return DCS#Vec3 Vector c=a+b with c(i)=a(i)+b(i), i=x,y,z. +function UTILS.VecAdd(a, b) + return {x=a.x+b.x, y=a.y+b.y, z=a.z+b.z} +end + +--- Calculate the angle between two 3D vectors. +-- @param DCS#Vec3 a Vector in 3D with x, y, z components. +-- @param DCS#Vec3 b Vector in 3D with x, y, z components. +-- @return #number Angle alpha between and b in degrees. alpha=acos(a*b)/(|a||b|), (* denotes the dot product). +function UTILS.VecAngle(a, b) + + local cosalpha=UTILS.VecDot(a,b)/(UTILS.VecNorm(a)*UTILS.VecNorm(b)) + + local alpha=0 + if cosalpha>=0.9999999999 then --acos(1) is not defined. + alpha=0 + elseif cosalpha<=-0.999999999 then --acos(-1) is not defined. + alpha=math.pi + else + alpha=math.acos(cosalpha) + end + + return math.deg(alpha) +end + +--- Calculate "heading" of a 3D vector in the X-Z plane. +-- @param DCS#Vec3 a Vector in 3D with x, y, z components. +-- @return #number Heading in degrees in [0,360). +function UTILS.VecHdg(a) + local h=math.deg(math.atan2(a.z, a.x)) + if h<0 then + h=h+360 + end + return h +end + +--- Calculate the difference between two "heading", i.e. angles in [0,360) deg. +-- @param #number h1 Heading one. +-- @param #number h2 Heading two. +-- @return #number Heading difference in degrees. +function UTILS.HdgDiff(h1, h2) + + -- Angle in rad. + local alpha= math.rad(tonumber(h1)) + local beta = math.rad(tonumber(h2)) + + -- Runway vector. + local v1={x=math.cos(alpha), y=0, z=math.sin(alpha)} + local v2={x=math.cos(beta), y=0, z=math.sin(beta)} + + local delta=UTILS.VecAngle(v1, v2) + + return math.abs(delta) +end + + +--- Translate 3D vector in the 2D (x,z) plane. y-component (usually altitude) unchanged. +-- @param DCS#Vec3 a Vector in 3D with x, y, z components. +-- @param #number distance The distance to translate. +-- @param #number angle Rotation angle in degrees. +-- @return DCS#Vec3 Vector rotated in the (x,z) plane. +function UTILS.VecTranslate(a, distance, angle) + + local SX = a.x + local SY = a.z + local Radians=math.rad(angle or 0) + local TX=distance*math.cos(Radians)+SX + local TY=distance*math.sin(Radians)+SY + + return {x=TX, y=a.y, z=TY} +end + +--- Rotate 3D vector in the 2D (x,z) plane. y-component (usually altitude) unchanged. +-- @param DCS#Vec3 a Vector in 3D with x, y, z components. +-- @param #number angle Rotation angle in degrees. +-- @return DCS#Vec3 Vector rotated in the (x,z) plane. +function UTILS.Rotate2D(a, angle) + + local phi=math.rad(angle) + + local x=a.z + local y=a.x + + local Z=x*math.cos(phi)-y*math.sin(phi) + local X=x*math.sin(phi)+y*math.cos(phi) + local Y=a.y + + local A={x=X, y=Y, z=Z} + + return A +end + + +--- Converts a TACAN Channel/Mode couple into a frequency in Hz. +-- @param #number TACANChannel The TACAN channel, i.e. the 10 in "10X". +-- @param #string TACANMode The TACAN mode, i.e. the "X" in "10X". +-- @return #number Frequency in Hz or #nil if parameters are invalid. +function UTILS.TACANToFrequency(TACANChannel, TACANMode) + + if type(TACANChannel) ~= "number" then + return nil -- error in arguments + end + if TACANMode ~= "X" and TACANMode ~= "Y" then + return nil -- error in arguments + end + +-- This code is largely based on ED's code, in DCS World\Scripts\World\Radio\BeaconTypes.lua, line 137. +-- I have no idea what it does but it seems to work + local A = 1151 -- 'X', channel >= 64 + local B = 64 -- channel >= 64 + + if TACANChannel < 64 then + B = 1 + end + + if TACANMode == 'Y' then + A = 1025 + if TACANChannel < 64 then + A = 1088 + end + else -- 'X' + if TACANChannel < 64 then + A = 962 + end + end + + return (A + TACANChannel - B) * 1000000 +end + + +--- Returns the DCS map/theatre as optained by env.mission.theatre +-- @return #string DCS map name. +function UTILS.GetDCSMap() + return env.mission.theatre +end + +--- Returns the mission date. This is the date the mission **started**. +-- @return #string Mission date in yyyy/mm/dd format. +-- @return #number The year anno domini. +-- @return #number The month. +-- @return #number The day. +function UTILS.GetDCSMissionDate() + local year=tostring(env.mission.date.Year) + local month=tostring(env.mission.date.Month) + local day=tostring(env.mission.date.Day) + return string.format("%s/%s/%s", year, month, day), tonumber(year), tonumber(month), tonumber(day) +end + +--- Returns the day of the mission. +-- @param #number Time (Optional) Abs. time in seconds. Default now, i.e. the value return from timer.getAbsTime(). +-- @return #number Day of the mission. Mission starts on day 0. +function UTILS.GetMissionDay(Time) + + Time=Time or timer.getAbsTime() + + local clock=UTILS.SecondsToClock(Time, false) + + local x=tonumber(UTILS.Split(clock, "+")[2]) + + return x +end + +--- Returns the current day of the year of the mission. +-- @param #number Time (Optional) Abs. time in seconds. Default now, i.e. the value return from timer.getAbsTime(). +-- @return #number Current day of year of the mission. For example, January 1st returns 0, January 2nd returns 1 etc. +function UTILS.GetMissionDayOfYear(Time) + + local Date, Year, Month, Day=UTILS.GetDCSMissionDate() + + local d=UTILS.GetMissionDay(Time) + + return UTILS.GetDayOfYear(Year, Month, Day)+d + +end + +--- Returns the current date. +-- @return #string Mission date in yyyy/mm/dd format. +-- @return #number The year anno domini. +-- @return #number The month. +-- @return #number The day. +function UTILS.GetDate() + + -- Mission start date + local date, year, month, day=UTILS.GetDCSMissionDate() + + local time=timer.getAbsTime() + + local clock=UTILS.SecondsToClock(time, false) + + local x=tonumber(UTILS.Split(clock, "+")[2]) + + local day=day+x + +end + +--- Returns the magnetic declination of the map. +-- Returned values for the current maps are: +-- +-- * Caucasus +6 (East), year ~ 2011 +-- * NTTR +12 (East), year ~ 2011 +-- * Normandy -10 (West), year ~ 1944 +-- * Persian Gulf +2 (East), year ~ 2011 +-- * The Cannel Map -10 (West) +-- * Syria +5 (East) +-- * Mariana Islands +2 (East) +-- @param #string map (Optional) Map for which the declination is returned. Default is from env.mission.theatre +-- @return #number Declination in degrees. +function UTILS.GetMagneticDeclination(map) + + -- Map. + map=map or UTILS.GetDCSMap() + + local declination=0 + if map==DCSMAP.Caucasus then + declination=6 + elseif map==DCSMAP.NTTR then + declination=12 + elseif map==DCSMAP.Normandy then + declination=-10 + elseif map==DCSMAP.PersianGulf then + declination=2 + elseif map==DCSMAP.TheChannel then + declination=-10 + elseif map==DCSMAP.Syria then + declination=5 + elseif map==DCSMAP.MarianaIslands then + declination=2 + else + declination=0 + end + + return declination +end + +--- Checks if a file exists or not. This requires **io** to be desanitized. +-- @param #string file File that should be checked. +-- @return #boolean True if the file exists, false if the file does not exist or nil if the io module is not available and the check could not be performed. +function UTILS.FileExists(file) + if io then + local f=io.open(file, "r") + if f~=nil then + io.close(f) + return true + else + return false + end + else + return nil + end +end + +--- Checks the current memory usage collectgarbage("count"). Info is printed to the DCS log file. Time stamp is the current mission runtime. +-- @param #boolean output If true, print to DCS log file. +-- @return #number Memory usage in kByte. +function UTILS.CheckMemory(output) + local time=timer.getTime() + local clock=UTILS.SecondsToClock(time) + local mem=collectgarbage("count") + if output then + env.info(string.format("T=%s Memory usage %d kByte = %.2f MByte", clock, mem, mem/1024)) + end + return mem +end + + +--- Get the coalition name from its numerical ID, e.g. coaliton.side.RED. +-- @param #number Coalition The coalition ID. +-- @return #string The coalition name, i.e. "Neutral", "Red" or "Blue" (or "Unknown"). +function UTILS.GetCoalitionName(Coalition) + + if Coalition then + if Coalition==coalition.side.BLUE then + return "Blue" + elseif Coalition==coalition.side.RED then + return "Red" + elseif Coalition==coalition.side.NEUTRAL then + return "Neutral" + else + return "Unknown" + end + else + return "Unknown" + end + +end + +--- Get the modulation name from its numerical value. +-- @param #number Modulation The modulation enumerator number. Can be either 0 or 1. +-- @return #string The modulation name, i.e. "AM"=0 or "FM"=1. Anything else will return "Unknown". +function UTILS.GetModulationName(Modulation) + + if Modulation then + if Modulation==0 then + return "AM" + elseif Modulation==1 then + return "FM" + else + return "Unknown" + end + else + return "Unknown" + end + +end + +--- Get the callsign name from its enumerator value +-- @param #number Callsign The enumerator callsign. +-- @return #string The callsign name or "Ghostrider". +function UTILS.GetCallsignName(Callsign) + + for name, value in pairs(CALLSIGN.Aircraft) do + if value==Callsign then + return name + end + end + + for name, value in pairs(CALLSIGN.AWACS) do + if value==Callsign then + return name + end + end + + for name, value in pairs(CALLSIGN.JTAC) do + if value==Callsign then + return name + end + end + + for name, value in pairs(CALLSIGN.Tanker) do + if value==Callsign then + return name + end + end + + return "Ghostrider" +end + +--- Get the time difference between GMT and local time. +-- @return #number Local time difference in hours compared to GMT. E.g. Dubai is GMT+4 ==> +4 is returned. +function UTILS.GMTToLocalTimeDifference() + + local theatre=UTILS.GetDCSMap() + + if theatre==DCSMAP.Caucasus then + return 4 -- Caucasus UTC+4 hours + elseif theatre==DCSMAP.PersianGulf then + return 4 -- Abu Dhabi UTC+4 hours + elseif theatre==DCSMAP.NTTR then + return -8 -- Las Vegas UTC-8 hours + elseif theatre==DCSMAP.Normandy then + return 0 -- Calais UTC+1 hour + elseif theatre==DCSMAP.TheChannel then + return 2 -- This map currently needs +2 + elseif theatre==DCSMAP.Syria then + return 3 -- Damascus is UTC+3 hours + elseif theatre==DCSMAP.MarianaIslands then + return 10 -- Guam is UTC+10 hours. + else + BASE:E(string.format("ERROR: Unknown Map %s in UTILS.GMTToLocal function. Returning 0", tostring(theatre))) + return 0 + end + +end + + +--- Get the day of the year. Counting starts on 1st of January. +-- @param #number Year The year. +-- @param #number Month The month. +-- @param #number Day The day. +-- @return #number The day of the year. +function UTILS.GetDayOfYear(Year, Month, Day) + + local floor = math.floor + + local n1 = floor(275 * Month / 9) + local n2 = floor((Month + 9) / 12) + local n3 = (1 + floor((Year - 4 * floor(Year / 4) + 2) / 3)) + + return n1 - (n2 * n3) + Day - 30 +end + +--- Get sunrise or sun set of a specific day of the year at a specific location. +-- @param #number DayOfYear The day of the year. +-- @param #number Latitude Latitude. +-- @param #number Longitude Longitude. +-- @param #boolean Rising If true, calc sun rise, or sun set otherwise. +-- @param #number Tlocal Local time offset in hours. E.g. +4 for a location which has GMT+4. +-- @return #number Sun rise/set in seconds of the day. +function UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, Rising, Tlocal) + + -- Defaults + local zenith=90.83 + local latitude=Latitude + local longitude=Longitude + local rising=Rising + local n=DayOfYear + Tlocal=Tlocal or 0 + + + -- Short cuts. + local rad = math.rad + local deg = math.deg + local floor = math.floor + local frac = function(n) return n - floor(n) end + local cos = function(d) return math.cos(rad(d)) end + local acos = function(d) return deg(math.acos(d)) end + local sin = function(d) return math.sin(rad(d)) end + local asin = function(d) return deg(math.asin(d)) end + local tan = function(d) return math.tan(rad(d)) end + local atan = function(d) return deg(math.atan(d)) end + + local function fit_into_range(val, min, max) + local range = max - min + local count + if val < min then + count = floor((min - val) / range) + 1 + return val + count * range + elseif val >= max then + count = floor((val - max) / range) + 1 + return val - count * range + else + return val + end + end + + -- Convert the longitude to hour value and calculate an approximate time + local lng_hour = longitude / 15 + + local t + if rising then -- Rising time is desired + t = n + ((6 - lng_hour) / 24) + else -- Setting time is desired + t = n + ((18 - lng_hour) / 24) + end + + -- Calculate the Sun's mean anomaly + local M = (0.9856 * t) - 3.289 + + -- Calculate the Sun's true longitude + local L = fit_into_range(M + (1.916 * sin(M)) + (0.020 * sin(2 * M)) + 282.634, 0, 360) + + -- Calculate the Sun's right ascension + local RA = fit_into_range(atan(0.91764 * tan(L)), 0, 360) + + -- Right ascension value needs to be in the same quadrant as L + local Lquadrant = floor(L / 90) * 90 + local RAquadrant = floor(RA / 90) * 90 + RA = RA + Lquadrant - RAquadrant + + -- Right ascension value needs to be converted into hours + RA = RA / 15 + + -- Calculate the Sun's declination + local sinDec = 0.39782 * sin(L) + local cosDec = cos(asin(sinDec)) + + -- Calculate the Sun's local hour angle + local cosH = (cos(zenith) - (sinDec * sin(latitude))) / (cosDec * cos(latitude)) + + if rising and cosH > 1 then + return "N/R" -- The sun never rises on this location on the specified date + elseif cosH < -1 then + return "N/S" -- The sun never sets on this location on the specified date + end + + -- Finish calculating H and convert into hours + local H + if rising then + H = 360 - acos(cosH) + else + H = acos(cosH) + end + H = H / 15 + + -- Calculate local mean time of rising/setting + local T = H + RA - (0.06571 * t) - 6.622 + + -- Adjust back to UTC + local UT = fit_into_range(T - lng_hour +Tlocal, 0, 24) + + return floor(UT)*60*60+frac(UT)*60*60--+Tlocal*60*60 + end + +--- Get sun rise of a specific day of the year at a specific location. +-- @param #number Day Day of the year. +-- @param #number Month Month of the year. +-- @param #number Year Year. +-- @param #number Latitude Latitude. +-- @param #number Longitude Longitude. +-- @param #boolean Rising If true, calc sun rise, or sun set otherwise. +-- @param #number Tlocal Local time offset in hours. E.g. +4 for a location which has GMT+4. Default 0. +-- @return #number Sun rise in seconds of the day. +function UTILS.GetSunrise(Day, Month, Year, Latitude, Longitude, Tlocal) + + local DayOfYear=UTILS.GetDayOfYear(Year, Month, Day) + + return UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, true, Tlocal) +end + +--- Get sun set of a specific day of the year at a specific location. +-- @param #number Day Day of the year. +-- @param #number Month Month of the year. +-- @param #number Year Year. +-- @param #number Latitude Latitude. +-- @param #number Longitude Longitude. +-- @param #boolean Rising If true, calc sun rise, or sun set otherwise. +-- @param #number Tlocal Local time offset in hours. E.g. +4 for a location which has GMT+4. Default 0. +-- @return #number Sun rise in seconds of the day. +function UTILS.GetSunset(Day, Month, Year, Latitude, Longitude, Tlocal) + + local DayOfYear=UTILS.GetDayOfYear(Year, Month, Day) + + return UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, false, Tlocal) +end + +--- Get OS time. Needs os to be desanitized! +-- @return #number Os time in seconds. +function UTILS.GetOSTime() + if os then + return os.clock() + end + + return nil +end + +--- Shuffle a table accoring to Fisher Yeates algorithm +--@param #table t Table to be shuffled +--@return #table +function UTILS.ShuffleTable(t) + if t == nil or type(t) ~= "table" then + BASE:I("Error in ShuffleTable: Missing or wrong type of Argument") + return + end + math.random() + math.random() + math.random() + local TempTable = {} + for i = 1, #t do + local r = math.random(1,#t) + TempTable[i] = t[r] + table.remove(t,r) + end + return TempTable +end + +--- (Helicopter) Check if one loading door is open. +--@param #string unit_name Unit name to be checked +--@return #boolean Outcome - true if a (loading door) is open, false if not, nil if none exists. +function UTILS.IsLoadingDoorOpen( unit_name ) + + local ret_val = false + local unit = Unit.getByName(unit_name) + if unit ~= nil then + local type_name = unit:getTypeName() + + if type_name == "Mi-8MT" and unit:getDrawArgumentValue(38) == 1 or unit:getDrawArgumentValue(86) == 1 or unit:getDrawArgumentValue(250) < 0 then + BASE: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 + BASE: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 + BASE: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 + BASE:T(unit_name .. " front door(s) are open") + ret_val = true + end + + if string.find(type_name, "Hercules") and unit:getDrawArgumentValue(1215) == 1 and unit:getDrawArgumentValue(1216) == 1 then + BASE:T(unit_name .. " rear doors are open") + ret_val = true + end + + if string.find(type_name, "Hercules") and (unit:getDrawArgumentValue(1220) == 1 or unit:getDrawArgumentValue(1221) == 1) then + BASE:T(unit_name .. " para doors are open") + ret_val = true + end + + if string.find(type_name, "Hercules") and unit:getDrawArgumentValue(1217) == 1 then + BASE:T(unit_name .. " side door is open") + ret_val = true + end + + if string.find(type_name, "Bell-47") then -- bell aint got no doors so always ready to load injured soldiers + BASE:T(unit_name .. " door is open") + ret_val = true + end + + if ret_val == false then + BASE:T(unit_name .. " all doors are closed") + end + return ret_val + + 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 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 +--- **Utils** - Lua Profiler. +-- +-- Find out how many times functions are called and how much real time it costs. +-- +-- === +-- +-- ### Author: **TAW CougarNL**, *funkyfranky* +-- +-- @module Utilities.PROFILER +-- @image Utils_Profiler.jpg + + +--- PROFILER class. +-- @type PROFILER +-- @field #string ClassName Name of the class. +-- @field #table Counters Function counters. +-- @field #table dInfo Info. +-- @field #table fTime Function time. +-- @field #table fTimeTotal Total function time. +-- @field #table eventhandler Event handler to get mission end event. +-- @field #number TstartGame Game start time timer.getTime(). +-- @field #number TstartOS OS real start time os.clock. +-- @field #boolean logUnknown Log unknown functions. Default is off. +-- @field #number ThreshCPS Low calls per second threshold. Only write output if function has more calls per second than this value. +-- @field #number ThreshTtot Total time threshold. Only write output if total function CPU time is more than this value. +-- @field #string fileNamePrefix Output file name prefix, e.g. "MooseProfiler". +-- @field #string fileNameSuffix Output file name prefix, e.g. "txt" + +--- *The emperor counsels simplicity.* *First principles. Of each particular thing, ask: What is it in itself, in its own constitution? What is its causal nature?* +-- +-- === +-- +-- ![Banner Image](..\Presentations\Utilities\PROFILER_Main.jpg) +-- +-- # The PROFILER Concept +-- +-- Profile your lua code. This tells you, which functions are called very often and which consume most real time. +-- With this information you can optimize the perfomance of your code. +-- +-- # Prerequisites +-- +-- The modules **os**, **io** and **lfs** need to be desanizied. Comment out the lines +-- +-- --sanitizeModule('os') +-- --sanitizeModule('io') +-- --sanitizeModule('lfs') +-- +-- in your *"DCS World OpenBeta/Scripts/MissionScripting.lua"* file. +-- +-- But be aware that these changes can make you system vulnerable to attacks. +-- +-- # Disclaimer +-- +-- **Profiling itself is CPU expensive!** Don't use this when you want to fly or host a mission. +-- +-- +-- # Start +-- +-- The profiler can simply be started with the @{#PROFILER.Start}(*Delay, Duration*) function +-- +-- PROFILER.Start() +-- +-- The optional parameter *Delay* can be used to delay the start by a certain amount of seconds and the optional parameter *Duration* can be used to +-- stop the profiler after a certain amount of seconds. +-- +-- # Stop +-- +-- The profiler automatically stops when the mission ends. But it can be stopped any time with the @{#PROFILER.Stop}(*Delay*) function +-- +-- PROFILER.Stop() +-- +-- The optional parameter *Delay* can be used to specify a delay after which the profiler is stopped. +-- +-- When the profiler is stopped, the output is written to a file. +-- +-- # Output +-- +-- The profiler output is written to a file in your DCS home folder +-- +-- X:\User\\Saved Games\DCS OpenBeta\Logs +-- +-- The default file name is "MooseProfiler.txt". If that file exists, the file name is "MooseProfiler-001.txt" etc. +-- +-- ## Data +-- +-- The data in the output file provides information on the functions that were called in the mission. +-- +-- It will tell you how many times a function was called in total, how many times per second, how much time in total and the percentage of time. +-- +-- If you only want output for functions that are called more than *X* times per second, you can set +-- +-- PROFILER.ThreshCPS=1.5 +-- +-- With this setting, only functions which are called more than 1.5 times per second are displayed. The default setting is PROFILER.ThreshCPS=0.0 (no threshold). +-- +-- Furthermore, you can limit the output for functions that consumed a certain amount of CPU time in total by +-- +-- PROFILER.ThreshTtot=0.005 +-- +-- With this setting, which is also the default, only functions which in total used more than 5 milliseconds CPU time. +-- +-- @field #PROFILER +PROFILER = { + ClassName = "PROFILER", + Counters = {}, + dInfo = {}, + fTime = {}, + fTimeTotal = {}, + eventHandler = {}, + logUnknown = false, + ThreshCPS = 0.0, + ThreshTtot = 0.005, + fileNamePrefix = "MooseProfiler", + fileNameSuffix = "txt" +} + +--- Waypoint data. +-- @type PROFILER.Data +-- @field #string func The function name. +-- @field #string src The source file. +-- @field #number line The line number +-- @field #number count Number of function calls. +-- @field #number tm Total time in seconds. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Start/Stop Profiler +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Start profiler. +-- @param #number Delay Delay in seconds before profiler is stated. Default is immediately. +-- @param #number Duration Duration in (game) seconds before the profiler is stopped. Default is when mission ends. +function PROFILER.Start(Delay, Duration) + + -- Check if os, io and lfs are available. + local go=true + if not os then + env.error("ERROR: Profiler needs os to be desanitized!") + go=false + end + if not io then + env.error("ERROR: Profiler needs io to be desanitized!") + go=false + end + if not lfs then + env.error("ERROR: Profiler needs lfs to be desanitized!") + go=false + end + if not go then + return + end + + if Delay and Delay>0 then + BASE:ScheduleOnce(Delay, PROFILER.Start, 0, Duration) + else + + -- Set start time. + PROFILER.TstartGame=timer.getTime() + PROFILER.TstartOS=os.clock() + + -- Add event handler. + world.addEventHandler(PROFILER.eventHandler) + + -- Info in log. + env.info('############################ Profiler Started ############################') + if Duration then + env.info(string.format("- Will be running for %d seconds", Duration)) + else + env.info(string.format("- Will be stopped when mission ends")) + end + env.info(string.format("- Calls per second threshold %.3f/sec", PROFILER.ThreshCPS)) + env.info(string.format("- Total function time threshold %.3f sec", PROFILER.ThreshTtot)) + env.info(string.format("- Output file \"%s\" in your DCS log file folder", PROFILER.getfilename(PROFILER.fileNameSuffix))) + env.info(string.format("- Output file \"%s\" in CSV format", PROFILER.getfilename("csv"))) + env.info('###############################################################################') + + + -- Message on screen + local duration=Duration or 600 + trigger.action.outText("### Profiler running ###", duration) + + -- Set hook. + debug.sethook(PROFILER.hook, "cr") + + -- Auto stop profiler. + if Duration then + PROFILER.Stop(Duration) + end + + end + +end + +--- Stop profiler. +-- @param #number Delay Delay before stop in seconds. +function PROFILER.Stop(Delay) + + if Delay and Delay>0 then + + BASE:ScheduleOnce(Delay, PROFILER.Stop) + + else + + -- Remove hook. + debug.sethook() + + + -- Run time game. + local runTimeGame=timer.getTime()-PROFILER.TstartGame + + -- Run time real OS. + local runTimeOS=os.clock()-PROFILER.TstartOS + + -- Show info. + PROFILER.showInfo(runTimeGame, runTimeOS) + + end + +end + +--- Event handler. +function PROFILER.eventHandler:onEvent(event) + if event.id==world.event.S_EVENT_MISSION_END then + PROFILER.Stop() + end +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Hook +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Debug hook. +-- @param #table event Event. +function PROFILER.hook(event) + + local f=debug.getinfo(2, "f").func + + if event=='call' then + + if PROFILER.Counters[f]==nil then + + PROFILER.Counters[f]=1 + PROFILER.dInfo[f]=debug.getinfo(2,"Sn") + + if PROFILER.fTimeTotal[f]==nil then + PROFILER.fTimeTotal[f]=0 + end + + else + PROFILER.Counters[f]=PROFILER.Counters[f]+1 + end + + if PROFILER.fTime[f]==nil then + PROFILER.fTime[f]=os.clock() + end + + elseif (event=='return') then + + if PROFILER.fTime[f]~=nil then + PROFILER.fTimeTotal[f]=PROFILER.fTimeTotal[f]+(os.clock()-PROFILER.fTime[f]) + PROFILER.fTime[f]=nil + end + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Data +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get data. +-- @param #function func Function. +-- @return #string Function name. +-- @return #string Source file name. +-- @return #string Line number. +-- @return #number Function time in seconds. +function PROFILER.getData(func) + + local n=PROFILER.dInfo[func] + + if n.what=="C" then + return n.name, "?", "?", PROFILER.fTimeTotal[func] + end + + return n.name, n.short_src, n.linedefined, PROFILER.fTimeTotal[func] +end + +--- Write text to log file. +-- @param #function f The file. +-- @param #string txt The text. +function PROFILER._flog(f, txt) + f:write(txt.."\r\n") +end + +--- Show table. +-- @param #table data Data table. +-- @param #function f The file. +-- @param #number runTimeGame Game run time in seconds. +function PROFILER.showTable(data, f, runTimeGame) + + -- Loop over data. + for i=1, #data do + local t=data[i] --#PROFILER.Data + + -- Calls per second. + local cps=t.count/runTimeGame + + local threshCPS=cps>=PROFILER.ThreshCPS + local threshTot=t.tm>=PROFILER.ThreshTtot + + if threshCPS and threshTot then + + -- Output + local text=string.format("%30s: %8d calls %8.1f/sec - Time Total %8.3f sec (%.3f %%) %5.3f sec/call %s line %s", t.func, t.count, cps, t.tm, t.tm/runTimeGame*100, t.tm/t.count, tostring(t.src), tostring(t.line)) + PROFILER._flog(f, text) + + end + end + +end + +--- Print csv file. +-- @param #table data Data table. +-- @param #number runTimeGame Game run time in seconds. +function PROFILER.printCSV(data, runTimeGame) + + -- Output file. + local file=PROFILER.getfilename("csv") + local g=io.open(file, 'w') + + -- Header. + local text="Function,Total Calls,Calls per Sec,Total Time,Total in %,Sec per Call,Source File;Line Number," + g:write(text.."\r\n") + + -- Loop over data. + for i=1, #data do + local t=data[i] --#PROFILER.Data + + -- Calls per second. + local cps=t.count/runTimeGame + + -- Output + local txt=string.format("%s,%d,%.1f,%.3f,%.3f,%.3f,%s,%s,", t.func, t.count, cps, t.tm, t.tm/runTimeGame*100, t.tm/t.count, tostring(t.src), tostring(t.line)) + g:write(txt.."\r\n") + + end + + -- Close file. + g:close() +end + + +--- Write info to output file. +-- @param #string ext Extension. +-- @return #string File name. +function PROFILER.getfilename(ext) + + local dir=lfs.writedir()..[[Logs\]] + + ext=ext or PROFILER.fileNameSuffix + + local file=dir..PROFILER.fileNamePrefix.."."..ext + + if not UTILS.FileExists(file) then + return file + end + + for i=1,999 do + + local file=string.format("%s%s-%03d.%s", dir,PROFILER.fileNamePrefix, i, ext) + + if not UTILS.FileExists(file) then + return file + end + + end + +end + +--- Write info to output file. +-- @param #number runTimeGame Game time in seconds. +-- @param #number runTimeOS OS time in seconds. +function PROFILER.showInfo(runTimeGame, runTimeOS) + + -- Output file. + local file=PROFILER.getfilename(PROFILER.fileNameSuffix) + local f=io.open(file, 'w') + + -- Gather data. + local Ttot=0 + local Calls=0 + + local t={} + + local tcopy=nil --#PROFILER.Data + local tserialize=nil --#PROFILER.Data + local tforgen=nil --#PROFILER.Data + local tpairs=nil --#PROFILER.Data + + + for func, count in pairs(PROFILER.Counters) do + + local s,src,line,tm=PROFILER.getData(func) + + if PROFILER.logUnknown==true then + if s==nil then s="" end + end + + if s~=nil then + + -- Profile data. + local T= + { func=s, + src=src, + line=line, + count=count, + tm=tm, + } --#PROFILER.Data + + -- Collect special cases. Somehow, e.g. "_copy" appears multiple times so we try to gather all data. + if s=="_copy" then + if tcopy==nil then + tcopy=T + else + tcopy.count=tcopy.count+T.count + tcopy.tm=tcopy.tm+T.tm + end + elseif s=="_Serialize" then + if tserialize==nil then + tserialize=T + else + tserialize.count=tserialize.count+T.count + tserialize.tm=tserialize.tm+T.tm + end + elseif s=="(for generator)" then + if tforgen==nil then + tforgen=T + else + tforgen.count=tforgen.count+T.count + tforgen.tm=tforgen.tm+T.tm + end + elseif s=="pairs" then + if tpairs==nil then + tpairs=T + else + tpairs.count=tpairs.count+T.count + tpairs.tm=tpairs.tm+T.tm + end + else + table.insert(t, T) + end + + -- Total function time. + Ttot=Ttot+tm + + -- Total number of calls. + Calls=Calls+count + + end + + end + + -- Add special cases. + if tcopy then + table.insert(t, tcopy) + end + if tserialize then + table.insert(t, tserialize) + end + if tforgen then + table.insert(t, tforgen) + end + if tpairs then + table.insert(t, tpairs) + end + + env.info('############################ Profiler Stopped ############################') + env.info(string.format("* Runtime Game : %s = %d sec", UTILS.SecondsToClock(runTimeGame, true), runTimeGame)) + env.info(string.format("* Runtime Real : %s = %d sec", UTILS.SecondsToClock(runTimeOS, true), runTimeOS)) + env.info(string.format("* Function time : %s = %.1f sec (%.1f percent of runtime game)", UTILS.SecondsToClock(Ttot, true), Ttot, Ttot/runTimeGame*100)) + env.info(string.format("* Total functions : %d", #t)) + env.info(string.format("* Total func calls : %d", Calls)) + env.info(string.format("* Writing to file : \"%s\"", file)) + env.info(string.format("* Writing to file : \"%s\"", PROFILER.getfilename("csv"))) + env.info("##############################################################################") + + -- Sort by total time. + table.sort(t, function(a,b) return a.tm>b.tm end) + + -- Write data. + PROFILER._flog(f,"") + PROFILER._flog(f,"************************************************************************************************************************") + PROFILER._flog(f,"************************************************************************************************************************") + PROFILER._flog(f,"************************************************************************************************************************") + PROFILER._flog(f,"") + PROFILER._flog(f,"-------------------------") + PROFILER._flog(f,"---- Profiler Report ----") + PROFILER._flog(f,"-------------------------") + PROFILER._flog(f,"") + PROFILER._flog(f,string.format("* Runtime Game : %s = %.1f sec", UTILS.SecondsToClock(runTimeGame, true), runTimeGame)) + PROFILER._flog(f,string.format("* Runtime Real : %s = %.1f sec", UTILS.SecondsToClock(runTimeOS, true), runTimeOS)) + PROFILER._flog(f,string.format("* Function time : %s = %.1f sec (%.1f %% of runtime game)", UTILS.SecondsToClock(Ttot, true), Ttot, Ttot/runTimeGame*100)) + PROFILER._flog(f,"") + PROFILER._flog(f,string.format("* Total functions = %d", #t)) + PROFILER._flog(f,string.format("* Total func calls = %d", Calls)) + PROFILER._flog(f,"") + PROFILER._flog(f,string.format("* Calls per second threshold = %.3f/sec", PROFILER.ThreshCPS)) + PROFILER._flog(f,string.format("* Total func time threshold = %.3f sec", PROFILER.ThreshTtot)) + PROFILER._flog(f,"") + PROFILER._flog(f,"************************************************************************************************************************") + PROFILER._flog(f,"") + PROFILER.showTable(t, f, runTimeGame) + + -- Sort by number of calls. + table.sort(t, function(a,b) return a.tm/a.count>b.tm/b.count end) + + -- Detailed data. + PROFILER._flog(f,"") + PROFILER._flog(f,"************************************************************************************************************************") + PROFILER._flog(f,"") + PROFILER._flog(f,"--------------------------------------") + PROFILER._flog(f,"---- Data Sorted by Time per Call ----") + PROFILER._flog(f,"--------------------------------------") + PROFILER._flog(f,"") + PROFILER.showTable(t, f, runTimeGame) + + -- Sort by number of calls. + table.sort(t, function(a,b) return a.count>b.count end) + + -- Detailed data. + PROFILER._flog(f,"") + PROFILER._flog(f,"************************************************************************************************************************") + PROFILER._flog(f,"") + PROFILER._flog(f,"------------------------------------") + PROFILER._flog(f,"---- Data Sorted by Total Calls ----") + PROFILER._flog(f,"------------------------------------") + PROFILER._flog(f,"") + PROFILER.showTable(t, f, runTimeGame) + + -- Closing. + PROFILER._flog(f,"") + PROFILER._flog(f,"************************************************************************************************************************") + PROFILER._flog(f,"************************************************************************************************************************") + PROFILER._flog(f,"************************************************************************************************************************") + -- Close file. + f:close() + + -- Print csv file. + PROFILER.printCSV(t, runTimeGame) +end +--- **Utils** Templates +-- +-- DCS unit templates +-- +-- @module Utilities.Templates +-- @image MOOSE.JPG + +--- TEMPLATE class. +-- @type TEMPLATE +-- @field #string ClassName Name of the class. + +--- *Templates* +-- +-- === +-- +-- ![Banner Image](..\Presentations\Utilities\PROFILER_Main.jpg) +-- +-- Get DCS templates from thin air. +-- +-- # Ground Units +-- +-- Ground units. +-- +-- # Naval Units +-- +-- Ships are not implemented yet. +-- +-- # Aircraft +-- +-- ## Airplanes +-- +-- Airplanes are not implemented yet. +-- +-- ## Helicopters +-- +-- Helicopters are not implemented yet. +-- +-- @field #TEMPLATE +TEMPLATE = { + ClassName = "TEMPLATE", + Ground = {}, + Naval = {}, + Airplane = {}, + Helicopter = {}, +} + +--- Ground unit type names. +-- @type TEMPLATE.TypeGround +-- @param #string InfantryAK +TEMPLATE.TypeGround={ + InfantryAK="Infantry AK", + ParatrooperAKS74="Paratrooper AKS-74", + ParatrooperRPG16="Paratrooper RPG-16", + SoldierWWIIUS="soldier_wwii_us", + InfantryM248="Infantry M249", + SoldierM4="Soldier M4", +} + +--- Naval unit type names. +-- @type TEMPLATE.TypeNaval +-- @param #string Ticonderoga +TEMPLATE.TypeNaval={ + Ticonderoga="TICONDEROG", +} + +--- Rotary wing unit type names. +-- @type TEMPLATE.TypeAirplane +-- @param #string A10C +TEMPLATE.TypeAirplane={ + A10C="A-10C", +} + +--- Rotary wing unit type names. +-- @type TEMPLATE.TypeHelicopter +-- @param #string AH1W +TEMPLATE.TypeHelicopter={ + AH1W="AH-1W", +} + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Ground Template +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get template for ground units. +-- @param #string TypeName Type name of the unit(s) in the groups. See `TEMPLATE.Ground`. +-- @param #string GroupName Name of the spawned group. **Must be unique!** +-- @param #number CountryID Country ID. Default `country.id.USA`. Coalition is automatically determined by the one the country belongs to. +-- @param DCS#Vec3 Vec3 Position of the group and the first unit. +-- @param #number Nunits Number of units. Default 1. +-- @param #number Radius Spawn radius for additonal units in meters. Default 50 m. +-- @return #table Template Template table. +function TEMPLATE.GetGround(TypeName, GroupName, CountryID, Vec3, Nunits, Radius) + + -- Defaults. + TypeName=TypeName or TEMPLATE.TypeGround.SoldierM4 + GroupName=GroupName or "Ground-1" + CountryID=CountryID or country.id.USA + Vec3=Vec3 or {x=0, y=0, z=0} + Nunits=Nunits or 1 + Radius=Radius or 50 + + + -- Get generic template. + local template=UTILS.DeepCopy(TEMPLATE.GenericGround) + + -- Set group name. + template.name=GroupName + + -- These are additional entries required by the MOOSE _DATABASE:Spawn() function. + template.CountryID=CountryID + template.CoalitionID=coalition.getCountryCoalition(template.CountryID) + template.CategoryID=Unit.Category.GROUND_UNIT + + -- Set first unit. + template.units[1].type=TypeName + template.units[1].name=GroupName.."-1" + + if Vec3 then + TEMPLATE.SetPositionFromVec3(template, Vec3) + end + + TEMPLATE.SetUnits(template, Nunits, COORDINATE:NewFromVec3(Vec3), Radius) + + return template +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Naval Template +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get template for ground units. +-- @param #string TypeName Type name of the unit(s) in the groups. See `TEMPLATE.Ground`. +-- @param #string GroupName Name of the spawned group. **Must be unique!** +-- @param #number CountryID Country ID. Default `country.id.USA`. Coalition is automatically determined by the one the country belongs to. +-- @param DCS#Vec3 Vec3 Position of the group and the first unit. +-- @param #number Nunits Number of units. Default 1. +-- @param #number Radius Spawn radius for additonal units in meters. Default 500 m. +-- @return #table Template Template table. +function TEMPLATE.GetNaval(TypeName, GroupName, CountryID, Vec3, Nunits, Radius) + + -- Defaults. + TypeName=TypeName or TEMPLATE.TypeNaval.Ticonderoga + GroupName=GroupName or "Naval-1" + CountryID=CountryID or country.id.USA + Vec3=Vec3 or {x=0, y=0, z=0} + Nunits=Nunits or 1 + Radius=Radius or 500 + + + -- Get generic template. + local template=UTILS.DeepCopy(TEMPLATE.GenericNaval) + + -- Set group name. + template.name=GroupName + + -- These are additional entries required by the MOOSE _DATABASE:Spawn() function. + template.CountryID=CountryID + template.CoalitionID=coalition.getCountryCoalition(template.CountryID) + template.CategoryID=Unit.Category.SHIP + + -- Set first unit. + template.units[1].type=TypeName + template.units[1].name=GroupName.."-1" + + if Vec3 then + TEMPLATE.SetPositionFromVec3(template, Vec3) + end + + TEMPLATE.SetUnits(template, Nunits, COORDINATE:NewFromVec3(Vec3), Radius) + + return template +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Aircraft Template +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get template for fixed wing units. +-- @param #string TypeName Type name of the unit(s) in the groups. See `TEMPLATE.Ground`. +-- @param #string GroupName Name of the spawned group. **Must be unique!** +-- @param #number CountryID Country ID. Default `country.id.USA`. Coalition is automatically determined by the one the country belongs to. +-- @param DCS#Vec3 Vec3 Position of the group and the first unit. +-- @param #number Nunits Number of units. Default 1. +-- @param #number Radius Spawn radius for additonal units in meters. Default 500 m. +-- @return #table Template Template table. +function TEMPLATE.GetAirplane(TypeName, GroupName, CountryID, Vec3, Nunits, Radius) + + -- Defaults. + TypeName=TypeName or TEMPLATE.TypeAirplane.A10C + GroupName=GroupName or "Airplane-1" + CountryID=CountryID or country.id.USA + Vec3=Vec3 or {x=0, y=1000, z=0} + Nunits=Nunits or 1 + Radius=Radius or 100 + + local template=TEMPLATE._GetAircraft(true, TypeName, GroupName, CountryID, Vec3, Nunits, Radius) + + return template +end + +--- Get template for fixed wing units. +-- @param #string TypeName Type name of the unit(s) in the groups. See `TEMPLATE.Ground`. +-- @param #string GroupName Name of the spawned group. **Must be unique!** +-- @param #number CountryID Country ID. Default `country.id.USA`. Coalition is automatically determined by the one the country belongs to. +-- @param DCS#Vec3 Vec3 Position of the group and the first unit. +-- @param #number Nunits Number of units. Default 1. +-- @param #number Radius Spawn radius for additonal units in meters. Default 500 m. +-- @return #table Template Template table. +function TEMPLATE.GetHelicopter(TypeName, GroupName, CountryID, Vec3, Nunits, Radius) + + -- Defaults. + TypeName=TypeName or TEMPLATE.TypeHelicopter.AH1W + GroupName=GroupName or "Helicopter-1" + CountryID=CountryID or country.id.USA + Vec3=Vec3 or {x=0, y=500, z=0} + Nunits=Nunits or 1 + Radius=Radius or 100 + + -- Limit unis to 4. + Nunits=math.min(Nunits, 4) + + local template=TEMPLATE._GetAircraft(false, TypeName, GroupName, CountryID, Vec3, Nunits, Radius) + + return template +end + + +--- Get template for aircraft units. +-- @param #boolean Airplane If true, this is a fixed wing. Else, rotary wing. +-- @param #string TypeName Type name of the unit(s) in the groups. See `TEMPLATE.Ground`. +-- @param #string GroupName Name of the spawned group. **Must be unique!** +-- @param #number CountryID Country ID. Default `country.id.USA`. Coalition is automatically determined by the one the country belongs to. +-- @param DCS#Vec3 Vec3 Position of the group and the first unit. +-- @param #number Nunits Number of units. Default 1. +-- @param #number Radius Spawn radius for additonal units in meters. Default 500 m. +-- @return #table Template Template table. +function TEMPLATE._GetAircraft(Airplane, TypeName, GroupName, CountryID, Vec3, Nunits, Radius) + + -- Defaults. + TypeName=TypeName + GroupName=GroupName or "Aircraft-1" + CountryID=CountryID or country.id.USA + Vec3=Vec3 or {x=0, y=0, z=0} + Nunits=Nunits or 1 + Radius=Radius or 100 + + -- Get generic template. + local template=UTILS.DeepCopy(TEMPLATE.GenericAircraft) + + -- Set group name. + template.name=GroupName + + -- These are additional entries required by the MOOSE _DATABASE:Spawn() function. + template.CountryID=CountryID + template.CoalitionID=coalition.getCountryCoalition(template.CountryID) + if Airplane then + template.CategoryID=Unit.Category.AIRPLANE + else + template.CategoryID=Unit.Category.HELICOPTER + end + + -- Set first unit. + template.units[1].type=TypeName + template.units[1].name=GroupName.."-1" + + -- Set position. + if Vec3 then + TEMPLATE.SetPositionFromVec3(template, Vec3) + end + + -- Set number of units. + TEMPLATE.SetUnits(template, Nunits, COORDINATE:NewFromVec3(Vec3), Radius) + + return template +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set the position of the template. +-- @param #table Template The template to be modified. +-- @param DCS#Vec2 Vec2 2D Position vector with x and y components of the group. +function TEMPLATE.SetPositionFromVec2(Template, Vec2) + + Template.x=Vec2.x + Template.y=Vec2.y + + for _,unit in pairs(Template.units) do + unit.x=Vec2.x + unit.y=Vec2.y + end + + Template.route.points[1].x=Vec2.x + Template.route.points[1].y=Vec2.y + Template.route.points[1].alt=0 --TODO: Use land height. + +end + +--- Set the position of the template. +-- @param #table Template The template to be modified. +-- @param DCS#Vec3 Vec3 Position vector of the group. +function TEMPLATE.SetPositionFromVec3(Template, Vec3) + + local Vec2={x=Vec3.x, y=Vec3.z} + + TEMPLATE.SetPositionFromVec2(Template, Vec2) + +end + +--- Set the position of the template. +-- @param #table Template The template to be modified. +-- @param #number N Total number of units in the group. +-- @param Core.Point#COORDINATE Coordinate Position of the first unit. +-- @param #number Radius Radius in meters to randomly place the additional units. +function TEMPLATE.SetUnits(Template, N, Coordinate, Radius) + + local units=Template.units + + local unit1=units[1] + + local Vec3=Coordinate:GetVec3() + + unit1.x=Vec3.x + unit1.y=Vec3.z + unit1.alt=Vec3.y + + for i=2,N do + units[i]=UTILS.DeepCopy(unit1) + end + + for i=1,N do + local unit=units[i] + unit.name=string.format("%s-%d", Template.name, i) + if i>1 then + local vec2=Coordinate:GetRandomCoordinateInRadius(Radius, 5):GetVec2() + unit.x=vec2.x + unit.y=vec2.y + unit.alt=unit1.alt + end + end + +end + +--- Set the position of the template. +-- @param #table Template The template to be modified. +-- @param Wrapper.Airbase#AIRBASE AirBase The airbase where the aircraft are spawned. +-- @param #table ParkingSpots List of parking spot IDs. Every unit needs one! +-- @param #boolean EngineOn If true, aircraft are spawned hot. +function TEMPLATE.SetAirbase(Template, AirBase, ParkingSpots, EngineOn) + + -- Airbase ID. + local AirbaseID=AirBase:GetID() + + -- Spawn point. + local point=Template.route.points[1] + + -- Set ID. + if AirBase:IsAirdrome() then + point.airdromeId=AirbaseID + else + point.helipadId=AirbaseID + point.linkUnit=AirbaseID + end + + if EngineOn then + point.action=COORDINATE.WaypointAction.FromParkingAreaHot + point.type=COORDINATE.WaypointType.TakeOffParkingHot + else + point.action=COORDINATE.WaypointAction.FromParkingArea + point.type=COORDINATE.WaypointType.TakeOffParking + end + + for i,unit in ipairs(Template.units) do + unit.parking_id=ParkingSpots[i] + end + +end + +--- Add a waypoint. +-- @param #table Template The template to be modified. +-- @param #table Waypoint Waypoint table. +function TEMPLATE.AddWaypoint(Template, Waypoint) + + table.insert(Template.route.points, Waypoint) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Generic Ground Template +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +TEMPLATE.GenericGround= +{ + ["visible"] = false, + ["tasks"] = {}, -- end of ["tasks"] + ["uncontrollable"] = false, + ["task"] = "Ground Nothing", + ["route"] = + { + ["spans"] = {}, -- end of ["spans"] + ["points"] = + { + [1] = + { + ["alt"] = 0, + ["type"] = "Turning Point", + ["ETA"] = 0, + ["alt_type"] = "BARO", + ["formation_template"] = "", + ["y"] = 0, + ["x"] = 0, + ["ETA_locked"] = true, + ["speed"] = 0, + ["action"] = "Off Road", + ["task"] = + { + ["id"] = "ComboTask", + ["params"] = + { + ["tasks"] = + { + }, -- end of ["tasks"] + }, -- end of ["params"] + }, -- end of ["task"] + ["speed_locked"] = true, + }, -- end of [1] + }, -- end of ["points"] + }, -- end of ["route"] + ["groupId"] = nil, + ["hidden"] = false, + ["units"] = + { + [1] = + { + ["transportable"] = + { + ["randomTransportable"] = false, + }, -- end of ["transportable"] + ["skill"] = "Average", + ["type"] = "Infantry AK", + ["unitId"] = nil, + ["y"] = 0, + ["x"] = 0, + ["name"] = "Infantry AK-47 Rus", + ["heading"] = 0, + ["playerCanDrive"] = false, + }, -- end of [1] + }, -- end of ["units"] + ["y"] = 0, + ["x"] = 0, + ["name"] = "Infantry AK-47 Rus", + ["start_time"] = 0, +} + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Generic Ship Template +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +TEMPLATE.GenericNaval= +{ + ["visible"] = false, + ["tasks"] = {}, -- end of ["tasks"] + ["uncontrollable"] = false, + ["route"] = + { + ["points"] = + { + [1] = + { + ["alt"] = 0, + ["type"] = "Turning Point", + ["ETA"] = 0, + ["alt_type"] = "BARO", + ["formation_template"] = "", + ["y"] = 0, + ["x"] = 0, + ["ETA_locked"] = true, + ["speed"] = 0, + ["action"] = "Turning Point", + ["task"] = + { + ["id"] = "ComboTask", + ["params"] = + { + ["tasks"] = + { + }, -- end of ["tasks"] + }, -- end of ["params"] + }, -- end of ["task"] + ["speed_locked"] = true, + }, -- end of [1] + }, -- end of ["points"] + }, -- end of ["route"] + ["groupId"] = nil, + ["hidden"] = false, + ["units"] = + { + [1] = + { + ["transportable"] = + { + ["randomTransportable"] = false, + }, -- end of ["transportable"] + ["skill"] = "Average", + ["type"] = "TICONDEROG", + ["unitId"] = nil, + ["y"] = 0, + ["x"] = 0, + ["name"] = "Naval-1-1", + ["heading"] = 0, + ["modulation"] = 0, + ["frequency"] = 127500000, + }, -- end of [1] + }, -- end of ["units"] + ["y"] = 0, + ["x"] = 0, + ["name"] = "Naval-1", + ["start_time"] = 0, +} + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Generic Aircraft Template +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +TEMPLATE.GenericAircraft= +{ + ["groupId"] = nil, + ["name"] = "Rotary-1", + ["uncontrolled"] = false, + ["hidden"] = false, + ["task"] = "Nothing", + ["y"] = 0, + ["x"] = 0, + ["start_time"] = 0, + ["communication"] = true, + ["radioSet"] = false, + ["frequency"] = 127.5, + ["modulation"] = 0, + ["taskSelected"] = true, + ["tasks"] = {}, -- end of ["tasks"] + ["route"] = + { + ["points"] = + { + [1] = + { + ["y"] = 0, + ["x"] = 0, + ["alt"] = 1000, + ["alt_type"] = "BARO", + ["action"] = "Turning Point", + ["type"] = "Turning Point", + ["airdromeId"] = nil, + ["task"] = + { + ["id"] = "ComboTask", + ["params"] = + { + ["tasks"] = {}, -- end of ["tasks"] + }, -- end of ["params"] + }, -- end of ["task"] + ["ETA"] = 0, + ["ETA_locked"] = true, + ["speed"] = 100, + ["speed_locked"] = true, + ["formation_template"] = "", + }, -- end of [1] + }, -- end of ["points"] + }, -- end of ["route"] + ["units"] = + { + [1] = + { + ["name"] = "Rotary-1-1", + ["unitId"] = nil, + ["type"] = "AH-1W", + ["onboard_num"] = "050", + ["livery_id"] = "USA X Black", + ["skill"] = "High", + ["ropeLength"] = 15, + ["speed"] = 0, + ["x"] = 0, + ["y"] = 0, + ["alt"] = 10, + ["alt_type"] = "BARO", + ["heading"] = 0, + ["psi"] = 0, + ["parking"] = nil, + ["parking_id"] = nil, + ["payload"] = + { + ["pylons"] = {}, -- end of ["pylons"] + ["fuel"] = "1250.0", + ["flare"] = 30, + ["chaff"] = 30, + ["gun"] = 100, + }, -- end of ["payload"] + ["callsign"] = + { + [1] = 2, + [2] = 1, + [3] = 1, + ["name"] = "Springfield11", + }, -- end of ["callsign"] + }, -- end of [1] + }, -- end of ["units"] +} +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- **Utilities** DCS Simple Text-To-Speech (STTS). +-- +-- +-- +-- @module Utils.STTS +-- @image MOOSE.JPG + +--- [DCS Enum world](https://wiki.hoggitworld.com/view/DCS_enum_world) +-- @type STTS +-- @field #string DIRECTORY Path of the SRS directory. + +--- Simple Text-To-Speech +-- +-- Version 0.4 - Compatible with SRS version 1.9.6.0+ +-- +-- # DCS Modification Required +-- +-- You will need to edit MissionScripting.lua in DCS World/Scripts/MissionScripting.lua and remove the sanitisation. +-- To do this remove all the code below the comment - the line starts "local function sanitizeModule(name)" +-- Do this without DCS running to allow mission scripts to use os functions. +-- +-- *You WILL HAVE TO REAPPLY AFTER EVERY DCS UPDATE* +-- +-- # USAGE: +-- +-- Add this script into the mission as a DO SCRIPT or DO SCRIPT FROM FILE to initialise it +-- Make sure to edit the STTS.SRS_PORT and STTS.DIRECTORY to the correct values before adding to the mission. +-- Then its as simple as calling the correct function in LUA as a DO SCRIPT or in your own scripts. +-- +-- Example calls: +-- +-- STTS.TextToSpeech("Hello DCS WORLD","251","AM","1.0","SRS",2) +-- +-- Arguments in order are: +-- +-- * Message to say, make sure not to use a newline (\n) ! +-- * Frequency in MHz +-- * Modulation - AM/FM +-- * Volume - 1.0 max, 0.5 half +-- * Name of the transmitter - ATC, RockFM etc +-- * Coalition - 0 spectator, 1 red 2 blue +-- * OPTIONAL - Vec3 Point i.e Unit.getByName("A UNIT"):getPoint() - needs Vec3 for Height! OR null if not needed +-- * OPTIONAL - Speed -10 to +10 +-- * OPTIONAL - Gender male, female or neuter +-- * OPTIONAL - Culture - en-US, en-GB etc +-- * OPTIONAL - Voice - a specfic voice by name. Run DCS-SR-ExternalAudio.exe with --help to get the ones you can use on the command line +-- * OPTIONAL - Google TTS - Switch to Google Text To Speech - Requires STTS.GOOGLE_CREDENTIALS path and Google project setup correctly +-- +-- +-- ## Example +-- +-- This example will say the words "Hello DCS WORLD" on 251 MHz AM at maximum volume with a client called SRS and to the Blue coalition only +-- +-- STTS.TextToSpeech("Hello DCS WORLD","251","AM","1.0","SRS",2,null,-5,"male","en-GB") +-- +-- ## Example +-- +--This example will say the words "Hello DCS WORLD" on 251 MHz AM at maximum volume with a client called SRS and to the Blue coalition only centered on the position of the Unit called "A UNIT" +-- +-- STTS.TextToSpeech("Hello DCS WORLD","251","AM","1.0","SRS",2,Unit.getByName("A UNIT"):getPoint(),-5,"male","en-GB") +-- +-- Arguments in order are: +-- +-- * FULL path to the MP3 OR OGG to play +-- * Frequency in MHz - to use multiple separate with a comma - Number of frequencies MUST match number of Modulations +-- * Modulation - AM/FM - to use multiple +-- * Volume - 1.0 max, 0.5 half +-- * Name of the transmitter - ATC, RockFM etc +-- * Coalition - 0 spectator, 1 red 2 blue +-- +-- ## Example +-- +-- This will play that MP3 on 255MHz AM & 31 FM at half volume with a client called "Multiple" and to Spectators only +-- +-- STTS.PlayMP3("C:\\Users\\Ciaran\\Downloads\\PR-Music.mp3","255,31","AM,FM","0.5","Multiple",0) +-- +-- @field #STTS +STTS={ + ClassName="STTS", + DIRECTORY="", + SRS_PORT=5002, + GOOGLE_CREDENTIALS="C:\\Users\\Ciaran\\Downloads\\googletts.json", + EXECUTABLE="DCS-SR-ExternalAudio.exe", +} + +--- FULL Path to the FOLDER containing DCS-SR-ExternalAudio.exe - EDIT TO CORRECT FOLDER +STTS.DIRECTORY = "D:/DCS/_SRS" + +--- LOCAL SRS PORT - DEFAULT IS 5002 +STTS.SRS_PORT = 5002 + +--- Google credentials file +STTS.GOOGLE_CREDENTIALS = "C:\\Users\\Ciaran\\Downloads\\googletts.json" + +--- DONT CHANGE THIS UNLESS YOU KNOW WHAT YOU'RE DOING +STTS.EXECUTABLE = "DCS-SR-ExternalAudio.exe" + + +--- Function for UUID. +function STTS.uuid() + local random = math.random + local template ='yxxx-xxxxxxxxxxxx' + return string.gsub(template, '[xy]', function (c) + local v = (c == 'x') and random(0, 0xf) or random(8, 0xb) + return string.format('%x', v) + end) +end + +--- Round a number. +-- @param #number x Number. +-- @param #number n Precision. +function STTS.round(x, n) + n = math.pow(10, n or 0) + x = x * n + if x >= 0 then x = math.floor(x + 0.5) else x = math.ceil(x - 0.5) end + return x / n +end + +--- Function returns estimated speech time in seconds. +-- Assumptions for time calc: 100 Words per min, avarage of 5 letters for english word so +-- +-- * 5 chars * 100wpm = 500 characters per min = 8.3 chars per second +-- +-- So lengh of msg / 8.3 = number of seconds needed to read it. rounded down to 8 chars per sec map function: +-- +-- * (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min +-- +function STTS.getSpeechTime(length,speed,isGoogle) + + local maxRateRatio = 3 + + speed = speed or 1.0 + isGoogle = isGoogle or false + + local speedFactor = 1.0 + if isGoogle then + speedFactor = speed + else + if speed ~= 0 then + speedFactor = math.abs(speed) * (maxRateRatio - 1) / 10 + 1 + end + if speed < 0 then + speedFactor = 1/speedFactor + end + end + + local wpm = math.ceil(100 * speedFactor) + local cps = math.floor((wpm * 5)/60) + + if type(length) == "string" then + length = string.len(length) + end + + return math.ceil(length/cps) +end + +--- Text to speech function. +function STTS.TextToSpeech(message, freqs, modulations, volume, name, coalition, point, speed, gender, culture, voice, googleTTS) + if os == nil or io == nil then + env.info("[DCS-STTS] LUA modules os or io are sanitized. skipping. ") + return + end + + speed = speed or 1 + gender = gender or "female" + culture = culture or "" + voice = voice or "" + coalition=coalition or "0" + name=name or "ROBOT" + volume=1 + speed=1 + + + message = message:gsub("\"","\\\"") + + local cmd = string.format("start /min \"\" /d \"%s\" /b \"%s\" -f %s -m %s -c %s -p %s -n \"%s\" -h", STTS.DIRECTORY, STTS.EXECUTABLE, freqs or "305", modulations or "AM", coalition, STTS.SRS_PORT, name) + + if voice ~= "" then + cmd = cmd .. string.format(" -V \"%s\"",voice) + else + + if culture ~= "" then + cmd = cmd .. string.format(" -l %s",culture) + end + + if gender ~= "" then + cmd = cmd .. string.format(" -g %s",gender) + end + end + + if googleTTS == true then + cmd = cmd .. string.format(" -G \"%s\"",STTS.GOOGLE_CREDENTIALS) + end + + if speed ~= 1 then + cmd = cmd .. string.format(" -s %s",speed) + end + + if volume ~= 1.0 then + cmd = cmd .. string.format(" -v %s",volume) + end + + if point and type(point) == "table" and point.x then + local lat, lon, alt = coord.LOtoLL(point) + + lat = STTS.round(lat,4) + lon = STTS.round(lon,4) + alt = math.floor(alt) + + cmd = cmd .. string.format(" -L %s -O %s -A %s",lat,lon,alt) + end + + cmd = cmd ..string.format(" -t \"%s\"",message) + + if string.len(cmd) > 255 then + local filename = os.getenv('TMP') .. "\\DCS_STTS-" .. STTS.uuid() .. ".bat" + local script = io.open(filename,"w+") + script:write(cmd .. " && exit" ) + script:close() + cmd = string.format("\"%s\"",filename) + timer.scheduleFunction(os.remove, filename, timer.getTime() + 1) + end + + if string.len(cmd) > 255 then + env.info("[DCS-STTS] - cmd string too long") + env.info("[DCS-STTS] TextToSpeech Command :\n" .. cmd.."\n") + end + os.execute(cmd) + + return STTS.getSpeechTime(message,speed,googleTTS) +end + +--- Play mp3 function. +-- @param #string pathToMP3 Path to the sound file. +-- @param #string freqs Frequencies, e.g. "305, 256". +-- @param #string modulations Modulations, e.g. "AM, FM". +-- @param #string volume Volume, e.g. "0.5". +function STTS.PlayMP3(pathToMP3, freqs, modulations, volume, name, coalition, point) + + local cmd = string.format("start \"\" /d \"%s\" /b /min \"%s\" -i \"%s\" -f %s -m %s -c %s -p %s -n \"%s\" -v %s -h", + STTS.DIRECTORY, STTS.EXECUTABLE, pathToMP3, freqs or "305", modulations or "AM", coalition or "0", STTS.SRS_PORT, name or "ROBOT", volume or "1") + + if point and type(point) == "table" and point.x then + local lat, lon, alt = coord.LOtoLL(point) + + lat = STTS.round(lat,4) + lon = STTS.round(lon,4) + alt = math.floor(alt) + + cmd = cmd .. string.format(" -L %s -O %s -A %s",lat,lon,alt) + end + + env.info("[DCS-STTS] MP3/OGG Command :\n" .. cmd.."\n") + os.execute(cmd) + +end--- **Core** - The base class within the framework. +-- +-- === +-- +-- ## Features: +-- +-- * The construction and inheritance of MOOSE classes. +-- * The class naming and numbering system. +-- * The class hierarchy search system. +-- * The tracing of information or objects during mission execution for debuggin purposes. +-- * The subscription to DCS events for event handling in MOOSE objects. +-- * Object inspection. +-- +-- === +-- +-- All classes within the MOOSE framework are derived from the BASE class. +-- Note: The BASE class is an abstract class and is not meant to be used directly. +-- +-- === +-- +-- ### Author: **FlightControl** +-- ### Contributions: +-- +-- === +-- +-- @module Core.Base +-- @image Core_Base.JPG + +local _TraceOnOff = true +local _TraceLevel = 1 +local _TraceAll = false +local _TraceClass = {} +local _TraceClassMethod = {} + +local _ClassID = 0 + +--- @type BASE +-- @field ClassName The name of the class. +-- @field ClassID The ID number of the class. +-- @field ClassNameAndID The name of the class concatenated with the ID number of the class. + +--- BASE class +-- +-- # 1. BASE constructor. +-- +-- Any class derived from BASE, will use the @{Core.Base#BASE.New} constructor embedded in the @{Core.Base#BASE.Inherit} method. +-- See an example at the @{Core.Base#BASE.New} method how this is done. +-- +-- # 2. Trace information for debugging. +-- +-- The BASE class contains trace methods to trace progress within a mission execution of a certain object. +-- These trace methods are inherited by each MOOSE class interiting BASE, soeach object created from derived class from BASE can use the tracing methods to trace its execution. +-- +-- Any type of information can be passed to these tracing methods. See the following examples: +-- +-- self:E( "Hello" ) +-- +-- Result in the word "Hello" in the dcs.log. +-- +-- local Array = { 1, nil, "h", { "a","b" }, "x" } +-- self:E( Array ) +-- +-- Results with the text [1]=1,[3]="h",[4]={[1]="a",[2]="b"},[5]="x"} in the dcs.log. +-- +-- local Object1 = "Object1" +-- local Object2 = 3 +-- local Object3 = { Object 1, Object 2 } +-- self:E( { Object1, Object2, Object3 } ) +-- +-- Results with the text [1]={[1]="Object",[2]=3,[3]={[1]="Object",[2]=3}} in the dcs.log. +-- +-- local SpawnObject = SPAWN:New( "Plane" ) +-- local GroupObject = GROUP:FindByName( "Group" ) +-- self:E( { Spawn = SpawnObject, Group = GroupObject } ) +-- +-- Results with the text [1]={Spawn={....),Group={...}} in the dcs.log. +-- +-- Below a more detailed explanation of the different method types for tracing. +-- +-- ## 2.1. Tracing methods categories. +-- +-- There are basically 3 types of tracing methods available: +-- +-- * @{#BASE.F}: Used to trace the entrance of a function and its given parameters. An F is indicated at column 44 in the DCS.log file. +-- * @{#BASE.T}: Used to trace further logic within a function giving optional variables or parameters. A T is indicated at column 44 in the DCS.log file. +-- * @{#BASE.E}: Used to always trace information giving optional variables or parameters. An E is indicated at column 44 in the DCS.log file. +-- +-- ## 2.2 Tracing levels. +-- +-- There are 3 tracing levels within MOOSE. +-- These tracing levels were defined to avoid bulks of tracing to be generated by lots of objects. +-- +-- As such, the F and T methods have additional variants to trace level 2 and 3 respectively: +-- +-- * @{#BASE.F2}: Trace the beginning of a function and its given parameters with tracing level 2. +-- * @{#BASE.F3}: Trace the beginning of a function and its given parameters with tracing level 3. +-- * @{#BASE.T2}: Trace further logic within a function giving optional variables or parameters with tracing level 2. +-- * @{#BASE.T3}: Trace further logic within a function giving optional variables or parameters with tracing level 3. +-- +-- ## 2.3. Trace activation. +-- +-- Tracing can be activated in several ways: +-- +-- * Switch tracing on or off through the @{#BASE.TraceOnOff}() method. +-- * Activate all tracing through the @{#BASE.TraceAll}() method. +-- * Activate only the tracing of a certain class (name) through the @{#BASE.TraceClass}() method. +-- * Activate only the tracing of a certain method of a certain class through the @{#BASE.TraceClassMethod}() method. +-- * Activate only the tracing of a certain level through the @{#BASE.TraceLevel}() method. +-- +-- ## 2.4. Check if tracing is on. +-- +-- The method @{#BASE.IsTrace}() will validate if tracing is activated or not. +-- +-- +-- # 3. DCS simulator Event Handling. +-- +-- The BASE class provides methods to catch DCS Events. These are events that are triggered from within the DCS simulator, +-- and handled through lua scripting. MOOSE provides an encapsulation to handle these events more efficiently. +-- +-- ## 3.1. Subscribe / Unsubscribe to DCS Events. +-- +-- At first, the mission designer will need to **Subscribe** to a specific DCS event for the class. +-- So, when the DCS event occurs, the class will be notified of that event. +-- There are two methods which you use to subscribe to or unsubscribe from an event. +-- +-- * @{#BASE.HandleEvent}(): Subscribe to a DCS Event. +-- * @{#BASE.UnHandleEvent}(): Unsubscribe from a DCS Event. +-- +-- ## 3.2. Event Handling of DCS Events. +-- +-- Once the class is subscribed to the event, an **Event Handling** method on the object or class needs to be written that will be called +-- when the DCS event occurs. The Event Handling method receives an @{Core.Event#EVENTDATA} structure, which contains a lot of information +-- about the event that occurred. +-- +-- Find below an example of the prototype how to write an event handling function for two units: +-- +-- local Tank1 = UNIT:FindByName( "Tank A" ) +-- local Tank2 = UNIT:FindByName( "Tank B" ) +-- +-- -- Here we subscribe to the Dead events. So, if one of these tanks dies, the Tank1 or Tank2 objects will be notified. +-- Tank1:HandleEvent( EVENTS.Dead ) +-- Tank2:HandleEvent( EVENTS.Dead ) +-- +-- --- This function is an Event Handling function that will be called when Tank1 is Dead. +-- -- @param Wrapper.Unit#UNIT self +-- -- @param Core.Event#EVENTDATA EventData +-- function Tank1:OnEventDead( EventData ) +-- +-- self:SmokeGreen() +-- end +-- +-- --- This function is an Event Handling function that will be called when Tank2 is Dead. +-- -- @param Wrapper.Unit#UNIT self +-- -- @param Core.Event#EVENTDATA EventData +-- function Tank2:OnEventDead( EventData ) +-- +-- self:SmokeBlue() +-- end +-- +-- +-- +-- See the @{Event} module for more information about event handling. +-- +-- # 4. Class identification methods. +-- +-- BASE provides methods to get more information of each object: +-- +-- * @{#BASE.GetClassID}(): Gets the ID (number) of the object. Each object created is assigned a number, that is incremented by one. +-- * @{#BASE.GetClassName}(): Gets the name of the object, which is the name of the class the object was instantiated from. +-- * @{#BASE.GetClassNameAndID}(): Gets the name and ID of the object. +-- +-- # 5. All objects derived from BASE can have "States". +-- +-- A mechanism is in place in MOOSE, that allows to let the objects administer **states**. +-- States are essentially properties of objects, which are identified by a **Key** and a **Value**. +-- +-- The method @{#BASE.SetState}() can be used to set a Value with a reference Key to the object. +-- To **read or retrieve** a state Value based on a Key, use the @{#BASE.GetState} method. +-- +-- These two methods provide a very handy way to keep state at long lasting processes. +-- Values can be stored within the objects, and later retrieved or changed when needed. +-- There is one other important thing to note, the @{#BASE.SetState}() and @{#BASE.GetState} methods +-- receive as the **first parameter the object for which the state needs to be set**. +-- Thus, if the state is to be set for the same object as the object for which the method is used, then provide the same +-- object name to the method. +-- +-- # 6. Inheritance. +-- +-- The following methods are available to implement inheritance +-- +-- * @{#BASE.Inherit}: Inherits from a class. +-- * @{#BASE.GetParent}: Returns the parent object from the object it is handling, or nil if there is no parent object. +-- +-- === +-- +-- @field #BASE +BASE = { + ClassName = "BASE", + ClassID = 0, + Events = {}, + States = {}, + Debug = debug, + Scheduler = nil, +} + + +--- @field #BASE.__ +BASE.__ = {} + +--- @field #BASE._ +BASE._ = { + Schedules = {} --- Contains the Schedulers Active +} + +--- The Formation Class +-- @type FORMATION +-- @field Cone A cone formation. +FORMATION = { + Cone = "Cone", + Vee = "Vee" +} + + + +--- BASE constructor. +-- +-- This is an example how to use the BASE:New() constructor in a new class definition when inheriting from BASE. +-- +-- function EVENT:New() +-- local self = BASE:Inherit( self, BASE:New() ) -- #EVENT +-- return self +-- end +-- +-- @param #BASE self +-- @return #BASE +function BASE:New() + local self = routines.utils.deepCopy( self ) -- Create a new self instance + + _ClassID = _ClassID + 1 + self.ClassID = _ClassID + + -- This is for "private" methods... + -- When a __ is passed to a method as "self", the __index will search for the method on the public method list too! +-- if rawget( self, "__" ) then + --setmetatable( self, { __index = self.__ } ) +-- end + + return self +end + +--- This is the worker method to inherit from a parent class. +-- @param #BASE self +-- @param Child is the Child class that inherits. +-- @param #BASE Parent is the Parent class that the Child inherits from. +-- @return #BASE Child +function BASE:Inherit( Child, Parent ) + + -- Create child. + local Child = routines.utils.deepCopy( Child ) + + if Child ~= nil then + + -- This is for "private" methods... + -- When a __ is passed to a method as "self", the __index will search for the method on the public method list of the same object too! + if rawget( Child, "__" ) then + setmetatable( Child, { __index = Child.__ } ) + setmetatable( Child.__, { __index = Parent } ) + else + setmetatable( Child, { __index = Parent } ) + end + + --Child:_SetDestructor() + end + + return Child +end + + +local function getParent( Child ) + local Parent = nil + + if Child.ClassName == 'BASE' then + Parent = nil + else + if rawget( Child, "__" ) then + Parent = getmetatable( Child.__ ).__index + else + Parent = getmetatable( Child ).__index + end + end + return Parent +end + + +--- This is the worker method to retrieve the Parent class. +-- Note that the Parent class must be passed to call the parent class method. +-- +-- self:GetParent(self):ParentMethod() +-- +-- +-- @param #BASE self +-- @param #BASE Child This is the Child class from which the Parent class needs to be retrieved. +-- @param #BASE FromClass (Optional) The class from which to get the parent. +-- @return #BASE +function BASE:GetParent( Child, FromClass ) + + + local Parent + -- BASE class has no parent + if Child.ClassName == 'BASE' then + Parent = nil + else + + --self:E({FromClass = FromClass}) + --self:E({Child = Child.ClassName}) + if FromClass then + while( Child.ClassName ~= "BASE" and Child.ClassName ~= FromClass.ClassName ) do + Child = getParent( Child ) + --self:E({Child.ClassName}) + end + end + if Child.ClassName == 'BASE' then + Parent = nil + else + Parent = getParent( Child ) + end + end + --self:E({Parent.ClassName}) + return Parent +end + +--- This is the worker method to check if an object is an (sub)instance of a class. +-- +-- ### Examples: +-- +-- * ZONE:New( 'some zone' ):IsInstanceOf( ZONE ) will return true +-- * ZONE:New( 'some zone' ):IsInstanceOf( 'ZONE' ) will return true +-- * ZONE:New( 'some zone' ):IsInstanceOf( 'zone' ) will return true +-- * ZONE:New( 'some zone' ):IsInstanceOf( 'BASE' ) will return true +-- +-- * ZONE:New( 'some zone' ):IsInstanceOf( 'GROUP' ) will return false +-- +-- @param #BASE self +-- @param ClassName is the name of the class or the class itself to run the check against +-- @return #boolean +function BASE:IsInstanceOf( ClassName ) + + -- Is className NOT a string ? + if type( ClassName ) ~= 'string' then + + -- Is className a Moose class ? + if type( ClassName ) == 'table' and ClassName.ClassName ~= nil then + + -- Get the name of the Moose class as a string + ClassName = ClassName.ClassName + + -- className is neither a string nor a Moose class, throw an error + else + + -- I'm not sure if this should take advantage of MOOSE logging function, or throw an error for pcall + local err_str = 'className parameter should be a string; parameter received: '..type( ClassName ) + self:E( err_str ) + -- error( err_str ) + return false + + end + end + + ClassName = string.upper( ClassName ) + + if string.upper( self.ClassName ) == ClassName then + return true + end + + local Parent = getParent(self) + + while Parent do + + if string.upper( Parent.ClassName ) == ClassName then + return true + end + + Parent = getParent( Parent ) + + end + + return false + +end +--- Get the ClassName + ClassID of the class instance. +-- The ClassName + ClassID is formatted as '%s#%09d'. +-- @param #BASE self +-- @return #string The ClassName + ClassID of the class instance. +function BASE:GetClassNameAndID() + return string.format( '%s#%09d', self.ClassName, self.ClassID ) +end + +--- Get the ClassName of the class instance. +-- @param #BASE self +-- @return #string The ClassName of the class instance. +function BASE:GetClassName() + return self.ClassName +end + +--- Get the ClassID of the class instance. +-- @param #BASE self +-- @return #string The ClassID of the class instance. +function BASE:GetClassID() + return self.ClassID +end + +do -- Event Handling + + --- Returns the event dispatcher + -- @param #BASE self + -- @return Core.Event#EVENT + function BASE:EventDispatcher() + + return _EVENTDISPATCHER + end + + + --- Get the Class @{Event} processing Priority. + -- The Event processing Priority is a number from 1 to 10, + -- reflecting the order of the classes subscribed to the Event to be processed. + -- @param #BASE self + -- @return #number The @{Event} processing Priority. + function BASE:GetEventPriority() + return self._.EventPriority or 5 + end + + --- Set the Class @{Event} processing Priority. + -- The Event processing Priority is a number from 1 to 10, + -- reflecting the order of the classes subscribed to the Event to be processed. + -- @param #BASE self + -- @param #number EventPriority The @{Event} processing Priority. + -- @return #BASE self + function BASE:SetEventPriority( EventPriority ) + self._.EventPriority = EventPriority + end + + --- Remove all subscribed events + -- @param #BASE self + -- @return #BASE + function BASE:EventRemoveAll() + + self:EventDispatcher():RemoveAll( self ) + + return self + end + + --- Subscribe to a DCS Event. + -- @param #BASE self + -- @param Core.Event#EVENTS EventID Event ID. + -- @param #function EventFunction (optional) The function to be called when the event occurs for the unit. + -- @return #BASE + function BASE:HandleEvent( EventID, EventFunction ) + + self:EventDispatcher():OnEventGeneric( EventFunction, self, EventID ) + + return self + end + + --- UnSubscribe to a DCS event. + -- @param #BASE self + -- @param Core.Event#EVENTS EventID Event ID. + -- @return #BASE + function BASE:UnHandleEvent( EventID ) + + self:EventDispatcher():RemoveEvent( self, EventID ) + + return self + end + + -- Event handling function prototypes + + --- Occurs whenever any unit in a mission fires a weapon. But not any machine gun or autocannon based weapon, those are handled by EVENT.ShootingStart. + -- @function [parent=#BASE] OnEventShot + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs whenever an object is hit by a weapon. + -- initiator : The unit object the fired the weapon + -- weapon: Weapon object that hit the target + -- target: The Object that was hit. + -- @function [parent=#BASE] OnEventHit + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when an aircraft takes off from an airbase, farp, or ship. + -- initiator : The unit that tookoff + -- place: Object from where the AI took-off from. Can be an Airbase Object, FARP, or Ships + -- @function [parent=#BASE] OnEventTakeoff + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when an aircraft lands at an airbase, farp or ship + -- initiator : The unit that has landed + -- place: Object that the unit landed on. Can be an Airbase Object, FARP, or Ships + -- @function [parent=#BASE] OnEventLand + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when any aircraft crashes into the ground and is completely destroyed. + -- initiator : The unit that has crashed + -- @function [parent=#BASE] OnEventCrash + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when a pilot ejects from an aircraft + -- initiator : The unit that has ejected + -- @function [parent=#BASE] OnEventEjection + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when an aircraft connects with a tanker and begins taking on fuel. + -- initiator : The unit that is receiving fuel. + -- @function [parent=#BASE] OnEventRefueling + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when an object is dead. + -- initiator : The unit that is dead. + -- @function [parent=#BASE] OnEventDead + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when an object is completely destroyed. + -- initiator : The unit that is was destroyed. + -- @function [parent=#BASE] OnEvent + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when the pilot of an aircraft is killed. Can occur either if the player is alive and crashes or if a weapon kills the pilot without completely destroying the plane. + -- initiator : The unit that the pilot has died in. + -- @function [parent=#BASE] OnEventPilotDead + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when a ground unit captures either an airbase or a farp. + -- initiator : The unit that captured the base + -- place: The airbase that was captured, can be a FARP or Airbase. When calling place:getCoalition() the faction will already be the new owning faction. + -- @function [parent=#BASE] OnEventBaseCaptured + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when a mission starts + -- @function [parent=#BASE] OnEventMissionStart + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when a mission ends + -- @function [parent=#BASE] OnEventMissionEnd + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when an aircraft is finished taking fuel. + -- initiator : The unit that was receiving fuel. + -- @function [parent=#BASE] OnEventRefuelingStop + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when any object is spawned into the mission. + -- initiator : The unit that was spawned + -- @function [parent=#BASE] OnEventBirth + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when any system fails on a human controlled aircraft. + -- initiator : The unit that had the failure + -- @function [parent=#BASE] OnEventHumanFailure + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when any aircraft starts its engines. + -- initiator : The unit that is starting its engines. + -- @function [parent=#BASE] OnEventEngineStartup + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when any aircraft shuts down its engines. + -- initiator : The unit that is stopping its engines. + -- @function [parent=#BASE] OnEventEngineShutdown + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when any player assumes direct control of a unit. + -- initiator : The unit that is being taken control of. + -- @function [parent=#BASE] OnEventPlayerEnterUnit + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when any player relieves control of a unit to the AI. + -- initiator : The unit that the player left. + -- @function [parent=#BASE] OnEventPlayerLeaveUnit + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when any unit begins firing a weapon that has a high rate of fire. Most common with aircraft cannons (GAU-8), autocannons, and machine guns. + -- initiator : The unit that is doing the shooting. + -- target: The unit that is being targeted. + -- @function [parent=#BASE] OnEventShootingStart + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when any unit stops firing its weapon. Event will always correspond with a shooting start event. + -- initiator : The unit that was doing the shooting. + -- @function [parent=#BASE] OnEventShootingEnd + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when a new mark was added. + -- MarkID: ID of the mark. + -- @function [parent=#BASE] OnEventMarkAdded + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when a mark was removed. + -- MarkID: ID of the mark. + -- @function [parent=#BASE] OnEventMarkRemoved + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when a mark text was changed. + -- MarkID: ID of the mark. + -- @function [parent=#BASE] OnEventMarkChange + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + + --- Unknown precisely what creates this event, likely tied into newer damage model. Will update this page when new information become available. + -- + -- * initiator: The unit that had the failure. + -- + -- @function [parent=#BASE] OnEventDetailedFailure + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when any modification to the "Score" as seen on the debrief menu would occur. + -- There is no information on what values the score was changed to. Event is likely similar to player_comment in this regard. + -- @function [parent=#BASE] OnEventScore + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs on the death of a unit. Contains more and different information. Similar to unit_lost it will occur for aircraft before the aircraft crash event occurs. + -- + -- * initiator: The unit that killed the target + -- * target: Target Object + -- * weapon: Weapon Object + -- + -- @function [parent=#BASE] OnEventKill + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when any modification to the "Score" as seen on the debrief menu would occur. + -- There is no information on what values the score was changed to. Event is likely similar to player_comment in this regard. + -- @function [parent=#BASE] OnEventScore + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when the game thinks an object is destroyed. + -- + -- * initiator: The unit that is was destroyed. + -- + -- @function [parent=#BASE] OnEventUnitLost + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs shortly after the landing animation of an ejected pilot touching the ground and standing up. Event does not occur if the pilot lands in the water and sub combs to Davey Jones Locker. + -- + -- * initiator: Static object representing the ejected pilot. Place : Aircraft that the pilot ejected from. + -- * place: may not return as a valid object if the aircraft has crashed into the ground and no longer exists. + -- * subplace: is always 0 for unknown reasons. + -- + -- @function [parent=#BASE] OnEventLandingAfterEjection + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Paratrooper landing. + -- @function [parent=#BASE] OnEventParatrooperLanding + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Discard chair after ejection. + -- @function [parent=#BASE] OnEventDiscardChairAfterEjection + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Weapon add. Fires when entering a mission per pylon with the name of the weapon (double pylons not counted, infinite wep reload not counted. + -- @function [parent=#BASE] OnEventParatrooperLanding + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Trigger zone. + -- @function [parent=#BASE] OnEventTriggerZone + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Landing quality mark. + -- @function [parent=#BASE] OnEventLandingQualityMark + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- BDA. + -- @function [parent=#BASE] OnEventBDA + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + + --- Occurs when a player enters a slot and takes control of an aircraft. + -- **NOTE**: This is a workaround of a long standing DCS bug with the PLAYER_ENTER_UNIT event. + -- initiator : The unit that is being taken control of. + -- @function [parent=#BASE] OnEventPlayerEnterAircraft + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + +end + + +--- Creation of a Birth Event. +-- @param #BASE self +-- @param DCS#Time EventTime The time stamp of the event. +-- @param DCS#Object Initiator The initiating object of the event. +-- @param #string IniUnitName The initiating unit name. +-- @param place +-- @param subplace +function BASE:CreateEventBirth( EventTime, Initiator, IniUnitName, place, subplace ) + self:F( { EventTime, Initiator, IniUnitName, place, subplace } ) + + local Event = { + id = world.event.S_EVENT_BIRTH, + time = EventTime, + initiator = Initiator, + IniUnitName = IniUnitName, + place = place, + subplace = subplace + } + + world.onEvent( Event ) +end + +--- Creation of a Crash Event. +-- @param #BASE self +-- @param DCS#Time EventTime The time stamp of the event. +-- @param DCS#Object Initiator The initiating object of the event. +function BASE:CreateEventCrash( EventTime, Initiator ) + self:F( { EventTime, Initiator } ) + + local Event = { + id = world.event.S_EVENT_CRASH, + time = EventTime, + initiator = Initiator, + } + + world.onEvent( Event ) +end + +--- Creation of a Crash Event. +-- @param #BASE self +-- @param DCS#Time EventTime The time stamp of the event. +-- @param DCS#Object Initiator The initiating object of the event. +function BASE:CreateEventUnitLost(EventTime, Initiator) + self:F( { EventTime, Initiator } ) + + local Event = { + id = world.event.S_EVENT_UNIT_LOST, + time = EventTime, + initiator = Initiator, + } + + world.onEvent( Event ) +end + +--- Creation of a Dead Event. +-- @param #BASE self +-- @param DCS#Time EventTime The time stamp of the event. +-- @param DCS#Object Initiator The initiating object of the event. +function BASE:CreateEventDead( EventTime, Initiator ) + self:F( { EventTime, Initiator } ) + + local Event = { + id = world.event.S_EVENT_DEAD, + time = EventTime, + initiator = Initiator, + } + + world.onEvent( Event ) +end + +--- Creation of a Remove Unit Event. +-- @param #BASE self +-- @param DCS#Time EventTime The time stamp of the event. +-- @param DCS#Object Initiator The initiating object of the event. +function BASE:CreateEventRemoveUnit( EventTime, Initiator ) + self:F( { EventTime, Initiator } ) + + local Event = { + id = EVENTS.RemoveUnit, + time = EventTime, + initiator = Initiator, + } + + world.onEvent( Event ) +end + +--- Creation of a Takeoff Event. +-- @param #BASE self +-- @param DCS#Time EventTime The time stamp of the event. +-- @param DCS#Object Initiator The initiating object of the event. +function BASE:CreateEventTakeoff( EventTime, Initiator ) + self:F( { EventTime, Initiator } ) + + local Event = { + id = world.event.S_EVENT_TAKEOFF, + time = EventTime, + initiator = Initiator, + } + + world.onEvent( Event ) +end + + --- Creation of a `S_EVENT_PLAYER_ENTER_AIRCRAFT` event. + -- @param #BASE self + -- @param Wrapper.Unit#UNIT PlayerUnit The aircraft unit the player entered. + function BASE:CreateEventPlayerEnterAircraft( PlayerUnit ) + self:F( { PlayerUnit } ) + + local Event = { + id = EVENTS.PlayerEnterAircraft, + time = timer.getTime(), + initiator = PlayerUnit:GetDCSObject() + } + + world.onEvent(Event) + end + +-- TODO: Complete DCS#Event structure. +--- The main event handling function... This function captures all events generated for the class. +-- @param #BASE self +-- @param DCS#Event event +function BASE:onEvent(event) + + if self then + + for EventID, EventObject in pairs(self.Events) do + if EventObject.EventEnabled then + + if event.id == EventObject.Event then + + if self == EventObject.Self then + + if event.initiator and event.initiator:isExist() then + event.IniUnitName = event.initiator:getName() + end + + if event.target and event.target:isExist() then + event.TgtUnitName = event.target:getName() + end + + end + + end + + end + end + end +end + +do -- Scheduling + + --- Schedule a new time event. Note that the schedule will only take place if the scheduler is *started*. Even for a single schedule event, the scheduler needs to be started also. + -- @param #BASE self + -- @param #number Start Specifies the amount of seconds that will be waited before the scheduling is started, and the event function is called. + -- @param #function SchedulerFunction The event function to be called when a timer event occurs. The event function needs to accept the parameters specified in SchedulerArguments. + -- @param #table ... Optional arguments that can be given as part of scheduler. The arguments need to be given as a table { param1, param 2, ... }. + -- @return #number The ScheduleID of the planned schedule. + function BASE:ScheduleOnce( Start, SchedulerFunction, ... ) + self:F2( { Start } ) + self:T3( { ... } ) + + local ObjectName = "-" + ObjectName = self.ClassName .. self.ClassID + + self:F3( { "ScheduleOnce: ", ObjectName, Start } ) + + if not self.Scheduler then + self.Scheduler = SCHEDULER:New( self ) + end + + local ScheduleID = _SCHEDULEDISPATCHER:AddSchedule( + self, + SchedulerFunction, + { ... }, + Start, + nil, + nil, + nil + ) + + self._.Schedules[#self._.Schedules+1] = ScheduleID + + return self._.Schedules[#self._.Schedules] + end + + --- Schedule a new time event. Note that the schedule will only take place if the scheduler is *started*. Even for a single schedule event, the scheduler needs to be started also. + -- @param #BASE self + -- @param #number Start Specifies the amount of seconds that will be waited before the scheduling is started, and the event function is called. + -- @param #number Repeat Specifies the interval in seconds when the scheduler will call the event function. + -- @param #number RandomizeFactor Specifies a randomization factor between 0 and 1 to randomize the Repeat. + -- @param #number Stop Specifies the amount of seconds when the scheduler will be stopped. + -- @param #function SchedulerFunction The event function to be called when a timer event occurs. The event function needs to accept the parameters specified in SchedulerArguments. + -- @param #table ... Optional arguments that can be given as part of scheduler. The arguments need to be given as a table { param1, param 2, ... }. + -- @return #number The ScheduleID of the planned schedule. + function BASE:ScheduleRepeat( Start, Repeat, RandomizeFactor, Stop, SchedulerFunction, ... ) + self:F2( { Start } ) + self:T3( { ... } ) + + local ObjectName = "-" + ObjectName = self.ClassName .. self.ClassID + + self:F3( { "ScheduleRepeat: ", ObjectName, Start, Repeat, RandomizeFactor, Stop } ) + + if not self.Scheduler then + self.Scheduler = SCHEDULER:New( self ) + end + + local ScheduleID = self.Scheduler:Schedule( + self, + SchedulerFunction, + { ... }, + Start, + Repeat, + RandomizeFactor, + Stop, + 4 + ) + + self._.Schedules[#self._.Schedules+1] = ScheduleID + + return self._.Schedules[#self._.Schedules] + end + + --- Stops the Schedule. + -- @param #BASE self + -- @param #function SchedulerFunction The event function to be called when a timer event occurs. The event function needs to accept the parameters specified in SchedulerArguments. + function BASE:ScheduleStop( SchedulerFunction ) + + self:F3( { "ScheduleStop:" } ) + + if self.Scheduler then + _SCHEDULEDISPATCHER:Stop( self.Scheduler, self._.Schedules[SchedulerFunction] ) + end + end + +end + + +--- Set a state or property of the Object given a Key and a Value. +-- Note that if the Object is destroyed, nillified or garbage collected, then the Values and Keys will also be gone. +-- @param #BASE self +-- @param Object The object that will hold the Value set by the Key. +-- @param Key The key that is used as a reference of the value. Note that the key can be a #string, but it can also be any other type! +-- @param Value The value to is stored in the object. +-- @return The Value set. +function BASE:SetState( Object, Key, Value ) + + local ClassNameAndID = Object:GetClassNameAndID() + + self.States[ClassNameAndID] = self.States[ClassNameAndID] or {} + self.States[ClassNameAndID][Key] = Value + + return self.States[ClassNameAndID][Key] +end + + +--- Get a Value given a Key from the Object. +-- Note that if the Object is destroyed, nillified or garbage collected, then the Values and Keys will also be gone. +-- @param #BASE self +-- @param Object The object that holds the Value set by the Key. +-- @param Key The key that is used to retrieve the value. Note that the key can be a #string, but it can also be any other type! +-- @return The Value retrieved or nil if the Key was not found and thus the Value could not be retrieved. +function BASE:GetState( Object, Key ) + + local ClassNameAndID = Object:GetClassNameAndID() + + if self.States[ClassNameAndID] then + local Value = self.States[ClassNameAndID][Key] or false + return Value + end + + return nil +end + +--- Clear the state of an object. +-- @param #BASE self +-- @param Object The object that holds the Value set by the Key. +-- @param StateName The key that is should be cleared. +function BASE:ClearState( Object, StateName ) + + local ClassNameAndID = Object:GetClassNameAndID() + if self.States[ClassNameAndID] then + self.States[ClassNameAndID][StateName] = nil + end +end + +-- Trace section + +-- Log a trace (only shown when trace is on) +-- TODO: Make trace function using variable parameters. + +--- Set trace on. +-- @param #BASE self +-- @usage +-- -- Switch the tracing On +-- BASE:TraceOn() +function BASE:TraceOn() + self:TraceOnOff( true ) +end + +--- Set trace off. +-- @param #BASE self +-- @usage +-- -- Switch the tracing Off +-- BASE:TraceOff() +function BASE:TraceOff() + self:TraceOnOff( false ) +end + + + +--- Set trace on or off +-- Note that when trace is off, no BASE.Debug statement is performed, increasing performance! +-- When Moose is loaded statically, (as one file), tracing is switched off by default. +-- So tracing must be switched on manually in your mission if you are using Moose statically. +-- When moose is loading dynamically (for moose class development), tracing is switched on by default. +-- @param #BASE self +-- @param #boolean TraceOnOff Switch the tracing on or off. +-- @usage +-- -- Switch the tracing On +-- BASE:TraceOnOff( true ) +-- +-- -- Switch the tracing Off +-- BASE:TraceOnOff( false ) +function BASE:TraceOnOff( TraceOnOff ) + if TraceOnOff==false then + self:I( "Tracing in MOOSE is OFF" ) + _TraceOnOff = false + else + self:I( "Tracing in MOOSE is ON" ) + _TraceOnOff = true + end +end + + +--- Enquires if tracing is on (for the class). +-- @param #BASE self +-- @return #boolean +function BASE:IsTrace() + + if BASE.Debug and ( _TraceAll == true ) or ( _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName] ) then + return true + else + return false + end +end + +--- Set trace level +-- @param #BASE self +-- @param #number Level +function BASE:TraceLevel( Level ) + _TraceLevel = Level or 1 + self:I( "Tracing level " .. _TraceLevel ) +end + +--- Trace all methods in MOOSE +-- @param #BASE self +-- @param #boolean TraceAll true = trace all methods in MOOSE. +function BASE:TraceAll( TraceAll ) + + if TraceAll==false then + _TraceAll=false + else + _TraceAll = true + end + + if _TraceAll then + self:I( "Tracing all methods in MOOSE " ) + else + self:I( "Switched off tracing all methods in MOOSE" ) + end +end + +--- Set tracing for a class +-- @param #BASE self +-- @param #string Class +function BASE:TraceClass( Class ) + _TraceClass[Class] = true + _TraceClassMethod[Class] = {} + self:I( "Tracing class " .. Class ) +end + +--- Set tracing for a specific method of class +-- @param #BASE self +-- @param #string Class +-- @param #string Method +function BASE:TraceClassMethod( Class, Method ) + if not _TraceClassMethod[Class] then + _TraceClassMethod[Class] = {} + _TraceClassMethod[Class].Method = {} + end + _TraceClassMethod[Class].Method[Method] = true + self:I( "Tracing method " .. Method .. " of class " .. Class ) +end + +--- Trace a function call. This function is private. +-- @param #BASE self +-- @param Arguments A #table or any field. +function BASE:_F( Arguments, DebugInfoCurrentParam, DebugInfoFromParam ) + + if BASE.Debug and ( _TraceAll == true ) or ( _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName] ) then + + local DebugInfoCurrent = DebugInfoCurrentParam and DebugInfoCurrentParam or BASE.Debug.getinfo( 2, "nl" ) + local DebugInfoFrom = DebugInfoFromParam and DebugInfoFromParam or BASE.Debug.getinfo( 3, "l" ) + + local Function = "function" + if DebugInfoCurrent.name then + Function = DebugInfoCurrent.name + end + + if _TraceAll == true or _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName].Method[Function] then + local LineCurrent = 0 + if DebugInfoCurrent.currentline then + LineCurrent = DebugInfoCurrent.currentline + end + local LineFrom = 0 + if DebugInfoFrom then + LineFrom = DebugInfoFrom.currentline + end + env.info( string.format( "%6d(%6d)/%1s:%30s%05d.%s(%s)" , LineCurrent, LineFrom, "F", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) + end + end +end + +--- Trace a function call. Must be at the beginning of the function logic. +-- @param #BASE self +-- @param Arguments A #table or any field. +function BASE:F( Arguments ) + + if BASE.Debug and _TraceOnOff then + local DebugInfoCurrent = BASE.Debug.getinfo( 2, "nl" ) + local DebugInfoFrom = BASE.Debug.getinfo( 3, "l" ) + + if _TraceLevel >= 1 then + self:_F( Arguments, DebugInfoCurrent, DebugInfoFrom ) + end + end +end + + +--- Trace a function call level 2. Must be at the beginning of the function logic. +-- @param #BASE self +-- @param Arguments A #table or any field. +function BASE:F2( Arguments ) + + if BASE.Debug and _TraceOnOff then + local DebugInfoCurrent = BASE.Debug.getinfo( 2, "nl" ) + local DebugInfoFrom = BASE.Debug.getinfo( 3, "l" ) + + if _TraceLevel >= 2 then + self:_F( Arguments, DebugInfoCurrent, DebugInfoFrom ) + end + end +end + +--- Trace a function call level 3. Must be at the beginning of the function logic. +-- @param #BASE self +-- @param Arguments A #table or any field. +function BASE:F3( Arguments ) + + if BASE.Debug and _TraceOnOff then + local DebugInfoCurrent = BASE.Debug.getinfo( 2, "nl" ) + local DebugInfoFrom = BASE.Debug.getinfo( 3, "l" ) + + if _TraceLevel >= 3 then + self:_F( Arguments, DebugInfoCurrent, DebugInfoFrom ) + end + end +end + +--- Trace a function logic. +-- @param #BASE self +-- @param Arguments A #table or any field. +function BASE:_T( Arguments, DebugInfoCurrentParam, DebugInfoFromParam ) + + if BASE.Debug and ( _TraceAll == true ) or ( _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName] ) then + + local DebugInfoCurrent = DebugInfoCurrentParam and DebugInfoCurrentParam or BASE.Debug.getinfo( 2, "nl" ) + local DebugInfoFrom = DebugInfoFromParam and DebugInfoFromParam or BASE.Debug.getinfo( 3, "l" ) + + local Function = "function" + if DebugInfoCurrent.name then + Function = DebugInfoCurrent.name + end + + if _TraceAll == true or _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName].Method[Function] then + local LineCurrent = 0 + if DebugInfoCurrent.currentline then + LineCurrent = DebugInfoCurrent.currentline + end + local LineFrom = 0 + if DebugInfoFrom then + LineFrom = DebugInfoFrom.currentline + end + env.info( string.format( "%6d(%6d)/%1s:%30s%05d.%s" , LineCurrent, LineFrom, "T", self.ClassName, self.ClassID, routines.utils.oneLineSerialize( Arguments ) ) ) + end + end +end + +--- Trace a function logic level 1. Can be anywhere within the function logic. +-- @param #BASE self +-- @param Arguments A #table or any field. +function BASE:T( Arguments ) + + if BASE.Debug and _TraceOnOff then + local DebugInfoCurrent = BASE.Debug.getinfo( 2, "nl" ) + local DebugInfoFrom = BASE.Debug.getinfo( 3, "l" ) + + if _TraceLevel >= 1 then + self:_T( Arguments, DebugInfoCurrent, DebugInfoFrom ) + end + end +end + + +--- Trace a function logic level 2. Can be anywhere within the function logic. +-- @param #BASE self +-- @param Arguments A #table or any field. +function BASE:T2( Arguments ) + + if BASE.Debug and _TraceOnOff then + local DebugInfoCurrent = BASE.Debug.getinfo( 2, "nl" ) + local DebugInfoFrom = BASE.Debug.getinfo( 3, "l" ) + + if _TraceLevel >= 2 then + self:_T( Arguments, DebugInfoCurrent, DebugInfoFrom ) + end + end +end + +--- Trace a function logic level 3. Can be anywhere within the function logic. +-- @param #BASE self +-- @param Arguments A #table or any field. +function BASE:T3( Arguments ) + + if BASE.Debug and _TraceOnOff then + local DebugInfoCurrent = BASE.Debug.getinfo( 2, "nl" ) + local DebugInfoFrom = BASE.Debug.getinfo( 3, "l" ) + + if _TraceLevel >= 3 then + self:_T( Arguments, DebugInfoCurrent, DebugInfoFrom ) + end + end +end + +--- Log an exception which will be traced always. Can be anywhere within the function logic. +-- @param #BASE self +-- @param Arguments A #table or any field. +function BASE:E( Arguments ) + + if BASE.Debug then + local DebugInfoCurrent = BASE.Debug.getinfo( 2, "nl" ) + local DebugInfoFrom = BASE.Debug.getinfo( 3, "l" ) + + local Function = "function" + if DebugInfoCurrent.name then + Function = DebugInfoCurrent.name + end + + local LineCurrent = DebugInfoCurrent.currentline + local LineFrom = -1 + if DebugInfoFrom then + LineFrom = DebugInfoFrom.currentline + end + + env.info( string.format( "%6d(%6d)/%1s:%30s%05d.%s(%s)" , LineCurrent, LineFrom, "E", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) + else + env.info( string.format( "%1s:%30s%05d(%s)" , "E", self.ClassName, self.ClassID, routines.utils.oneLineSerialize( Arguments ) ) ) + end + +end + + +--- Log an information which will be traced always. Can be anywhere within the function logic. +-- @param #BASE self +-- @param Arguments A #table or any field. +function BASE:I( Arguments ) + + if BASE.Debug then + local DebugInfoCurrent = BASE.Debug.getinfo( 2, "nl" ) + local DebugInfoFrom = BASE.Debug.getinfo( 3, "l" ) + + local Function = "function" + if DebugInfoCurrent.name then + Function = DebugInfoCurrent.name + end + + local LineCurrent = DebugInfoCurrent.currentline + local LineFrom = -1 + if DebugInfoFrom then + LineFrom = DebugInfoFrom.currentline + end + + env.info( string.format( "%6d(%6d)/%1s:%30s%05d.%s(%s)" , LineCurrent, LineFrom, "I", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) + else + env.info( string.format( "%1s:%30s%05d(%s)" , "I", self.ClassName, self.ClassID, routines.utils.oneLineSerialize( Arguments ) ) ) + end + +end + + + +--- old stuff + +--function BASE:_Destructor() +-- --self:E("_Destructor") +-- +-- --self:EventRemoveAll() +--end + + +-- THIS IS WHY WE NEED LUA 5.2 ... +--function BASE:_SetDestructor() +-- +-- -- TODO: Okay, this is really technical... +-- -- When you set a proxy to a table to catch __gc, weak tables don't behave like weak... +-- -- Therefore, I am parking this logic until I've properly discussed all this with the community. +-- +-- local proxy = newproxy(true) +-- local proxyMeta = getmetatable(proxy) +-- +-- proxyMeta.__gc = function () +-- env.info("In __gc for " .. self:GetClassNameAndID() ) +-- if self._Destructor then +-- self:_Destructor() +-- end +-- end +-- +-- -- keep the userdata from newproxy reachable until the object +-- -- table is about to be garbage-collected - then the __gc hook +-- -- will be invoked and the destructor called +-- rawset( self, '__proxy', proxy ) +-- +--end--- **Core** - TACAN and other beacons. +-- +-- === +-- +-- ## Features: +-- +-- * Provide beacon functionality to assist pilots. +-- +-- === +-- +-- ### Authors: Hugues "Grey_Echo" Bousquet, funkyfranky +-- +-- @module Core.Beacon +-- @image Core_Radio.JPG + +--- *In order for the light to shine so brightly, the darkness must be present.* -- Francis Bacon +-- +-- After attaching a @{#BEACON} to your @{Wrapper.Positionable#POSITIONABLE}, you need to select the right function to activate the kind of beacon you want. +-- There are two types of BEACONs available : the AA TACAN Beacon and the general purpose Radio Beacon. +-- Note that in both case, you can set an optional parameter : the `BeaconDuration`. This can be very usefull to simulate the battery time if your BEACON is +-- attach to a cargo crate, for exemple. +-- +-- ## AA TACAN Beacon usage +-- +-- This beacon only works with airborne @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP}. Use @{#BEACON:AATACAN}() to set the beacon parameters and start the beacon. +-- Use @#BEACON:StopAATACAN}() to stop it. +-- +-- ## General Purpose Radio Beacon usage +-- +-- This beacon will work with any @{Wrapper.Positionable#POSITIONABLE}, but **it won't follow the @{Wrapper.Positionable#POSITIONABLE}** ! This means that you should only use it with +-- @{Wrapper.Positionable#POSITIONABLE} that don't move, or move very slowly. Use @{#BEACON:RadioBeacon}() to set the beacon parameters and start the beacon. +-- Use @{#BEACON:StopRadioBeacon}() to stop it. +-- +-- @type BEACON +-- @field #string ClassName Name of the class "BEACON". +-- @field Wrapper.Controllable#CONTROLLABLE Positionable The @{#CONTROLLABLE} that will receive radio capabilities. +-- @extends Core.Base#BASE +BEACON = { + ClassName = "BEACON", + Positionable = nil, + name = nil, +} + +--- Beacon types supported by DCS. +-- @type BEACON.Type +-- @field #number NULL +-- @field #number VOR +-- @field #number DME +-- @field #number VOR_DME +-- @field #number TACAN TACtical Air Navigation system. +-- @field #number VORTAC +-- @field #number RSBN +-- @field #number BROADCAST_STATION +-- @field #number HOMER +-- @field #number AIRPORT_HOMER +-- @field #number AIRPORT_HOMER_WITH_MARKER +-- @field #number ILS_FAR_HOMER +-- @field #number ILS_NEAR_HOMER +-- @field #number ILS_LOCALIZER +-- @field #number ILS_GLIDESLOPE +-- @field #number PRMG_LOCALIZER +-- @field #number PRMG_GLIDESLOPE +-- @field #number ICLS Same as ICLS glideslope. +-- @field #number ICLS_LOCALIZER +-- @field #number ICLS_GLIDESLOPE +-- @field #number NAUTICAL_HOMER +BEACON.Type={ + NULL = 0, + VOR = 1, + DME = 2, + VOR_DME = 3, + TACAN = 4, + VORTAC = 5, + RSBN = 128, + BROADCAST_STATION = 1024, + HOMER = 8, + AIRPORT_HOMER = 4104, + AIRPORT_HOMER_WITH_MARKER = 4136, + ILS_FAR_HOMER = 16408, + ILS_NEAR_HOMER = 16424, + ILS_LOCALIZER = 16640, + ILS_GLIDESLOPE = 16896, + PRMG_LOCALIZER = 33024, + PRMG_GLIDESLOPE = 33280, + ICLS = 131584, --leaving this in here but it is the same as ICLS_GLIDESLOPE + ICLS_LOCALIZER = 131328, + ICLS_GLIDESLOPE = 131584, + NAUTICAL_HOMER = 65536, +} + +--- Beacon systems supported by DCS. https://wiki.hoggitworld.com/view/DCS_command_activateBeacon +-- @type BEACON.System +-- @field #number PAR_10 ? +-- @field #number RSBN_5 Russian VOR/DME system. +-- @field #number TACAN TACtical Air Navigation system on ground. +-- @field #number TACAN_TANKER_X TACtical Air Navigation system for tankers on X band. +-- @field #number TACAN_TANKER_Y TACtical Air Navigation system for tankers on Y band. +-- @field #number VOR Very High Frequency Omni-Directional Range +-- @field #number ILS_LOCALIZER ILS localizer +-- @field #number ILS_GLIDESLOPE ILS glideslope. +-- @field #number PRGM_LOCALIZER PRGM localizer. +-- @field #number PRGM_GLIDESLOPE PRGM glideslope. +-- @field #number BROADCAST_STATION Broadcast station. +-- @field #number VORTAC Radio-based navigational aid for aircraft pilots consisting of a co-located VHF omnidirectional range (VOR) beacon and a tactical air navigation system (TACAN) beacon. +-- @field #number TACAN_AA_MODE_X TACtical Air Navigation for aircraft on X band. +-- @field #number TACAN_AA_MODE_Y TACtical Air Navigation for aircraft on Y band. +-- @field #number VORDME Radio beacon that combines a VHF omnidirectional range (VOR) with a distance measuring equipment (DME). +-- @field #number ICLS_LOCALIZER Carrier landing system. +-- @field #number ICLS_GLIDESLOPE Carrier landing system. +BEACON.System={ + PAR_10 = 1, + RSBN_5 = 2, + TACAN = 3, + TACAN_TANKER_X = 4, + TACAN_TANKER_Y = 5, + VOR = 6, + ILS_LOCALIZER = 7, + ILS_GLIDESLOPE = 8, + PRMG_LOCALIZER = 9, + PRMG_GLIDESLOPE = 10, + BROADCAST_STATION = 11, + VORTAC = 12, + TACAN_AA_MODE_X = 13, + TACAN_AA_MODE_Y = 14, + VORDME = 15, + ICLS_LOCALIZER = 16, + ICLS_GLIDESLOPE = 17, +} + +--- Create a new BEACON Object. This doesn't activate the beacon, though, use @{#BEACON.ActivateTACAN} etc. +-- If you want to create a BEACON, you probably should use @{Wrapper.Positionable#POSITIONABLE.GetBeacon}() instead. +-- @param #BEACON self +-- @param Wrapper.Positionable#POSITIONABLE Positionable The @{Positionable} that will receive radio capabilities. +-- @return #BEACON Beacon object or #nil if the positionable is invalid. +function BEACON:New(Positionable) + + -- Inherit BASE. + local self=BASE:Inherit(self, BASE:New()) --#BEACON + + -- Debug. + self:F(Positionable) + + -- Set positionable. + if Positionable:GetPointVec2() then -- It's stupid, but the only way I found to make sure positionable is valid + self.Positionable = Positionable + self.name=Positionable:GetName() + self:I(string.format("New BEACON %s", tostring(self.name))) + return self + end + + self:E({"The passed positionable is invalid, no BEACON created", Positionable}) + return nil +end + + +--- Activates a TACAN BEACON. +-- @param #BEACON self +-- @param #number Channel TACAN channel, i.e. the "10" part in "10Y". +-- @param #string Mode TACAN mode, i.e. the "Y" part in "10Y". +-- @param #string Message The Message that is going to be coded in Morse and broadcasted by the beacon. +-- @param #boolean Bearing If true, beacon provides bearing information. If false (or nil), only distance information is available. +-- @param #number Duration How long will the beacon last in seconds. Omit for forever. +-- @return #BEACON self +-- @usage +-- -- Let's create a TACAN Beacon for a tanker +-- local myUnit = UNIT:FindByName("MyUnit") +-- local myBeacon = myUnit:GetBeacon() -- Creates the beacon +-- +-- myBeacon:ActivateTACAN(20, "Y", "TEXACO", true) -- Activate the beacon +function BEACON:ActivateTACAN(Channel, Mode, Message, Bearing, Duration) + self:T({channel=Channel, mode=Mode, callsign=Message, bearing=Bearing, duration=Duration}) + + -- Get frequency. + local Frequency=UTILS.TACANToFrequency(Channel, Mode) + + -- Check. + if not Frequency then + self:E({"The passed TACAN channel is invalid, the BEACON is not emitting"}) + return self + end + + -- Beacon type. + local Type=BEACON.Type.TACAN + + -- Beacon system. + local System=BEACON.System.TACAN + + -- Check if unit is an aircraft and set system accordingly. + local AA=self.Positionable:IsAir() + if AA then + System=5 --NOTE: 5 is how you cat the correct tanker behaviour! --BEACON.System.TACAN_TANKER + -- Check if "Y" mode is selected for aircraft. + if Mode~="Y" then + self:E({"WARNING: The POSITIONABLE you want to attach the AA Tacan Beacon is an aircraft: Mode should Y !The BEACON is not emitting.", self.Positionable}) + end + end + + -- Attached unit. + local UnitID=self.Positionable:GetID() + + -- Debug. + self:I({string.format("BEACON Activating TACAN %s: Channel=%d%s, Morse=%s, Bearing=%s, Duration=%s!", tostring(self.name), Channel, Mode, Message, tostring(Bearing), tostring(Duration))}) + + -- Start beacon. + self.Positionable:CommandActivateBeacon(Type, System, Frequency, UnitID, Channel, Mode, AA, Message, Bearing) + + -- Stop sheduler. + if Duration then + self.Positionable:DeactivateBeacon(Duration) + end + + return self +end + +--- Activates an ICLS BEACON. The unit the BEACON is attached to should be an aircraft carrier supporting this system. +-- @param #BEACON self +-- @param #number Channel ICLS channel. +-- @param #string Callsign The Message that is going to be coded in Morse and broadcasted by the beacon. +-- @param #number Duration How long will the beacon last in seconds. Omit for forever. +-- @return #BEACON self +function BEACON:ActivateICLS(Channel, Callsign, Duration) + self:F({Channel=Channel, Callsign=Callsign, Duration=Duration}) + + -- Attached unit. + local UnitID=self.Positionable:GetID() + + -- Debug + self:T2({"ICLS BEACON started!"}) + + -- Start beacon. + self.Positionable:CommandActivateICLS(Channel, UnitID, Callsign) + + -- Stop sheduler + if Duration then -- Schedule the stop of the BEACON if asked by the MD + self.Positionable:DeactivateBeacon(Duration) + end + + return self +end + +--- Activates a TACAN BEACON on an Aircraft. +-- @param #BEACON self +-- @param #number TACANChannel (the "10" part in "10Y"). Note that AA TACAN are only available on Y Channels +-- @param #string Message The Message that is going to be coded in Morse and broadcasted by the beacon +-- @param #boolean Bearing Can the BEACON be homed on ? +-- @param #number BeaconDuration How long will the beacon last in seconds. Omit for forever. +-- @return #BEACON self +-- @usage +-- -- Let's create a TACAN Beacon for a tanker +-- local myUnit = UNIT:FindByName("MyUnit") +-- local myBeacon = myUnit:GetBeacon() -- Creates the beacon +-- +-- myBeacon:AATACAN(20, "TEXACO", true) -- Activate the beacon +function BEACON:AATACAN(TACANChannel, Message, Bearing, BeaconDuration) + self:F({TACANChannel, Message, Bearing, BeaconDuration}) + + local IsValid = true + + if not self.Positionable:IsAir() then + self:E({"The POSITIONABLE you want to attach the AA Tacan Beacon is not an aircraft ! The BEACON is not emitting", self.Positionable}) + IsValid = false + end + + local Frequency = self:_TACANToFrequency(TACANChannel, "Y") + if not Frequency then + self:E({"The passed TACAN channel is invalid, the BEACON is not emitting"}) + IsValid = false + end + + -- I'm using the beacon type 4 (BEACON_TYPE_TACAN). For System, I'm using 5 (TACAN_TANKER_MODE_Y) if the bearing shows its bearing + -- or 14 (TACAN_AA_MODE_Y) if it does not + local System + if Bearing then + System = 5 + else + System = 14 + end + + if IsValid then -- Starts the BEACON + self:T2({"AA TACAN BEACON started !"}) + self.Positionable:SetCommand({ + id = "ActivateBeacon", + params = { + type = 4, + system = System, + callsign = Message, + frequency = Frequency, + } + }) + + if BeaconDuration then -- Schedule the stop of the BEACON if asked by the MD + SCHEDULER:New(nil, + function() + self:StopAATACAN() + end, {}, BeaconDuration) + end + end + + return self +end + +--- Stops the AA TACAN BEACON +-- @param #BEACON self +-- @return #BEACON self +function BEACON:StopAATACAN() + self:F() + if not self.Positionable then + self:E({"Start the beacon first before stoping it !"}) + else + self.Positionable:SetCommand({ + id = 'DeactivateBeacon', + params = { + } + }) + end +end + + +--- Activates a general pupose Radio Beacon +-- This uses the very generic singleton function "trigger.action.radioTransmission()" provided by DCS to broadcast a sound file on a specific frequency. +-- Although any frequency could be used, only 2 DCS Modules can home on radio beacons at the time of writing : the Huey and the Mi-8. +-- They can home in on these specific frequencies : +-- * **Mi8** +-- * R-828 -> 20-60MHz +-- * ARKUD -> 100-150MHz (canal 1 : 114166, canal 2 : 114333, canal 3 : 114583, canal 4 : 121500, canal 5 : 123100, canal 6 : 124100) AM +-- * ARK9 -> 150-1300KHz +-- * **Huey** +-- * AN/ARC-131 -> 30-76 Mhz FM +-- @param #BEACON self +-- @param #string FileName The name of the audio file +-- @param #number Frequency in MHz +-- @param #number Modulation either radio.modulation.AM or radio.modulation.FM +-- @param #number Power in W +-- @param #number BeaconDuration How long will the beacon last in seconds. Omit for forever. +-- @return #BEACON self +-- @usage +-- -- Let's create a beacon for a unit in distress. +-- -- Frequency will be 40MHz FM (home-able by a Huey's AN/ARC-131) +-- -- The beacon they use is battery-powered, and only lasts for 5 min +-- local UnitInDistress = UNIT:FindByName("Unit1") +-- local UnitBeacon = UnitInDistress:GetBeacon() +-- +-- -- Set the beacon and start it +-- UnitBeacon:RadioBeacon("MySoundFileSOS.ogg", 40, radio.modulation.FM, 20, 5*60) +function BEACON:RadioBeacon(FileName, Frequency, Modulation, Power, BeaconDuration) + self:F({FileName, Frequency, Modulation, Power, BeaconDuration}) + local IsValid = false + + -- Check the filename + if type(FileName) == "string" then + if FileName:find(".ogg") or FileName:find(".wav") then + if not FileName:find("l10n/DEFAULT/") then + FileName = "l10n/DEFAULT/" .. FileName + end + IsValid = true + end + end + if not IsValid then + self:E({"File name invalid. Maybe something wrong with the extension ? ", FileName}) + end + + -- Check the Frequency + if type(Frequency) ~= "number" and IsValid then + self:E({"Frequency invalid. ", Frequency}) + IsValid = false + end + Frequency = Frequency * 1000000 -- Conversion to Hz + + -- Check the modulation + if Modulation ~= radio.modulation.AM and Modulation ~= radio.modulation.FM and IsValid then --TODO Maybe make this future proof if ED decides to add an other modulation ? + self:E({"Modulation is invalid. Use DCS's enum radio.modulation.", Modulation}) + IsValid = false + end + + -- Check the Power + if type(Power) ~= "number" and IsValid then + self:E({"Power is invalid. ", Power}) + IsValid = false + end + Power = math.floor(math.abs(Power)) --TODO Find what is the maximum power allowed by DCS and limit power to that + + if IsValid then + self:T2({"Activating Beacon on ", Frequency, Modulation}) + -- Note that this is looped. I have to give this transmission a unique name, I use the class ID + trigger.action.radioTransmission(FileName, self.Positionable:GetPositionVec3(), Modulation, true, Frequency, Power, tostring(self.ID)) + + if BeaconDuration then -- Schedule the stop of the BEACON if asked by the MD + SCHEDULER:New( nil, + function() + self:StopRadioBeacon() + end, {}, BeaconDuration) + end + end +end + +--- Stops the AA TACAN BEACON +-- @param #BEACON self +-- @return #BEACON self +function BEACON:StopRadioBeacon() + self:F() + -- The unique name of the transmission is the class ID + trigger.action.stopRadioTransmission(tostring(self.ID)) + return self +end + +--- Converts a TACAN Channel/Mode couple into a frequency in Hz +-- @param #BEACON self +-- @param #number TACANChannel +-- @param #string TACANMode +-- @return #number Frequecy +-- @return #nil if parameters are invalid +function BEACON:_TACANToFrequency(TACANChannel, TACANMode) + self:F3({TACANChannel, TACANMode}) + + if type(TACANChannel) ~= "number" then + if TACANMode ~= "X" and TACANMode ~= "Y" then + return nil -- error in arguments + end + end + +-- This code is largely based on ED's code, in DCS World\Scripts\World\Radio\BeaconTypes.lua, line 137. +-- I have no idea what it does but it seems to work + local A = 1151 -- 'X', channel >= 64 + local B = 64 -- channel >= 64 + + if TACANChannel < 64 then + B = 1 + end + + if TACANMode == 'Y' then + A = 1025 + if TACANChannel < 64 then + A = 1088 + end + else -- 'X' + if TACANChannel < 64 then + A = 962 + end + end + + return (A + TACANChannel - B) * 1000000 +end--- **Core** - Manage user flags to interact with the mission editor trigger system and server side scripts. +-- +-- === +-- +-- ## Features: +-- +-- * Set or get DCS user flags within running missions. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module Core.UserFlag +-- @image Core_Userflag.JPG +-- + +do -- UserFlag + + --- @type USERFLAG + -- @field #string ClassName Name of the class + -- @field #string UserFlagName Name of the flag. + -- @extends Core.Base#BASE + + + --- Management of DCS User Flags. + -- + -- # 1. USERFLAG constructor + -- + -- * @{#USERFLAG.New}(): Creates a new USERFLAG object. + -- + -- @field #USERFLAG + USERFLAG = { + ClassName = "USERFLAG", + UserFlagName = nil, + } + + --- USERFLAG Constructor. + -- @param #USERFLAG self + -- @param #string UserFlagName The name of the userflag, which is a free text string. + -- @return #USERFLAG + function USERFLAG:New( UserFlagName ) --R2.3 + + local self = BASE:Inherit( self, BASE:New() ) -- #USERFLAG + + self.UserFlagName = UserFlagName + + return self + end + + --- Get the userflag name. + -- @param #USERFLAG self + -- @return #string Name of the user flag. + function USERFLAG:GetName() + return self.UserFlagName + end + + --- Set the userflag to a given Number. + -- @param #USERFLAG self + -- @param #number Number The number value to be checked if it is the same as the userflag. + -- @param #number Delay Delay in seconds, before the flag is set. + -- @return #USERFLAG The userflag instance. + -- @usage + -- local BlueVictory = USERFLAG:New( "VictoryBlue" ) + -- BlueVictory:Set( 100 ) -- Set the UserFlag VictoryBlue to 100. + -- + function USERFLAG:Set( Number, Delay ) --R2.3 + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, USERFLAG.Set, self, Number) + else + --env.info(string.format("Setting flag \"%s\" to %d at T=%.1f", self.UserFlagName, Number, timer.getTime())) + trigger.action.setUserFlag( self.UserFlagName, Number ) + end + + return self + end + + + --- Get the userflag Number. + -- @param #USERFLAG self + -- @return #number Number The number value to be checked if it is the same as the userflag. + -- @usage + -- local BlueVictory = USERFLAG:New( "VictoryBlue" ) + -- local BlueVictoryValue = BlueVictory:Get() -- Get the UserFlag VictoryBlue value. + -- + function USERFLAG:Get() --R2.3 + + return trigger.misc.getUserFlag( self.UserFlagName ) + end + + + + --- Check if the userflag has a value of Number. + -- @param #USERFLAG self + -- @param #number Number The number value to be checked if it is the same as the userflag. + -- @return #boolean true if the Number is the value of the userflag. + -- @usage + -- local BlueVictory = USERFLAG:New( "VictoryBlue" ) + -- if BlueVictory:Is( 1 ) then + -- return "Blue has won" + -- end + function USERFLAG:Is( Number ) --R2.3 + + return trigger.misc.getUserFlag( self.UserFlagName ) == Number + + end + +end--- **Core** - Provides a handy means to create messages and reports. +-- +-- === +-- +-- ## Features: +-- +-- * Create text blocks that are formatted. +-- * Create automatic indents. +-- * Variate the delimiters between reporting lines. +-- +-- === +-- +-- ### Authors: FlightControl : Design & Programming +-- +-- @module Core.Report +-- @image Core_Report.JPG + + +--- @type REPORT +-- @extends Core.Base#BASE + +--- Provides a handy means to create messages and reports. +-- @field #REPORT +REPORT = { + ClassName = "REPORT", + Title = "", +} + +--- Create a new REPORT. +-- @param #REPORT self +-- @param #string Title +-- @return #REPORT +function REPORT:New( Title ) + + local self = BASE:Inherit( self, BASE:New() ) -- #REPORT + + self.Report = {} + + self:SetTitle( Title or "" ) + self:SetIndent( 3 ) + + return self +end + +--- Has the REPORT Text? +-- @param #REPORT self +-- @return #boolean +function REPORT:HasText() --R2.1 + + return #self.Report > 0 +end + + +--- Set indent of a REPORT. +-- @param #REPORT self +-- @param #number Indent +-- @return #REPORT +function REPORT:SetIndent( Indent ) --R2.1 + self.Indent = Indent + return self +end + + +--- Add a new line to a REPORT. +-- @param #REPORT self +-- @param #string Text +-- @return #REPORT +function REPORT:Add( Text ) + self.Report[#self.Report+1] = Text + return self +end + +--- Add a new line to a REPORT, but indented. A separator character can be specified to separate the reported lines visually. +-- @param #REPORT self +-- @param #string Text The report text. +-- @param #string Separator (optional) The start of each report line can begin with an optional separator character. This can be a "-", or "#", or "*". You're free to choose what you find the best. +-- @return #REPORT +function REPORT:AddIndent( Text, Separator ) + self.Report[#self.Report+1] = ( ( Separator and Separator .. string.rep( " ", self.Indent - 1 ) ) or string.rep(" ", self.Indent ) ) .. Text:gsub("\n","\n"..string.rep( " ", self.Indent ) ) + return self +end + +--- Produces the text of the report, taking into account an optional delimeter, which is \n by default. +-- @param #REPORT self +-- @param #string Delimiter (optional) A delimiter text. +-- @return #string The report text. +function REPORT:Text( Delimiter ) + Delimiter = Delimiter or "\n" + local ReportText = ( self.Title ~= "" and self.Title .. Delimiter or self.Title ) .. table.concat( self.Report, Delimiter ) or "" + return ReportText +end + +--- Sets the title of the report. +-- @param #REPORT self +-- @param #string Title The title of the report. +-- @return #REPORT +function REPORT:SetTitle( Title ) + self.Title = Title + return self +end + +--- Gets the amount of report items contained in the report. +-- @param #REPORT self +-- @return #number Returns the number of report items contained in the report. 0 is returned if no report items are contained in the report. The title is not counted for. +function REPORT:GetCount() + return #self.Report +end +--- **Core** - Prepares and handles the execution of functions over scheduled time (intervals). +-- +-- === +-- +-- ## Features: +-- +-- * Schedule functions over time, +-- * optionally in an optional specified time interval, +-- * optionally **repeating** with a specified time repeat interval, +-- * optionally **randomizing** with a specified time interval randomization factor, +-- * optionally **stop** the repeating after a specified time interval. +-- +-- === +-- +-- # Demo Missions +-- +-- ### [SCHEDULER Demo Missions source code](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master-release/SCH%20-%20Scheduler) +-- +-- ### [SCHEDULER Demo Missions, only for beta testers](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/SCH%20-%20Scheduler) +-- +-- ### [ALL Demo Missions pack of the last release](https://github.com/FlightControl-Master/MOOSE_MISSIONS/releases) +-- +-- === +-- +-- # YouTube Channel +-- +-- ### [SCHEDULER YouTube Channel (none)]() +-- +-- === +-- +-- ### Contributions: +-- +-- * FlightControl : Concept & Testing +-- +-- ### Authors: +-- +-- * FlightControl : Design & Programming +-- +-- === +-- +-- @module Core.Scheduler +-- @image Core_Scheduler.JPG + +--- The SCHEDULER class +-- @type SCHEDULER +-- @field #table Schedules Table of schedules. +-- @field #table MasterObject Master object. +-- @field #boolean ShowTrace Trace info if true. +-- @extends Core.Base#BASE + + +--- Creates and handles schedules over time, which allow to execute code at specific time intervals with randomization. +-- +-- A SCHEDULER can manage **multiple** (repeating) schedules. Each planned or executing schedule has a unique **ScheduleID**. +-- The ScheduleID is returned when the method @{#SCHEDULER.Schedule}() is called. +-- It is recommended to store the ScheduleID in a variable, as it is used in the methods @{SCHEDULER.Start}() and @{SCHEDULER.Stop}(), +-- which can start and stop specific repeating schedules respectively within a SCHEDULER object. +-- +-- ## SCHEDULER constructor +-- +-- The SCHEDULER class is quite easy to use, but note that the New constructor has variable parameters: +-- +-- The @{#SCHEDULER.New}() method returns 2 variables: +-- +-- 1. The SCHEDULER object reference. +-- 2. The first schedule planned in the SCHEDULER object. +-- +-- To clarify the different appliances, lets have a look at the following examples: +-- +-- ### Construct a SCHEDULER object without a persistent schedule. +-- +-- * @{#SCHEDULER.New}( nil ): Setup a new SCHEDULER object, which is persistently executed after garbage collection. +-- +-- MasterObject = SCHEDULER:New() +-- SchedulerID = MasterObject:Schedule( nil, ScheduleFunction, {} ) +-- +-- The above example creates a new MasterObject, but does not schedule anything. +-- A separate schedule is created by using the MasterObject using the method :Schedule..., which returns a ScheduleID +-- +-- ### Construct a SCHEDULER object without a volatile schedule, but volatile to the Object existence... +-- +-- * @{#SCHEDULER.New}( Object ): Setup a new SCHEDULER object, which is linked to the Object. When the Object is nillified or destroyed, the SCHEDULER object will also be destroyed and stopped after garbage collection. +-- +-- ZoneObject = ZONE:New( "ZoneName" ) +-- MasterObject = SCHEDULER:New( ZoneObject ) +-- SchedulerID = MasterObject:Schedule( ZoneObject, ScheduleFunction, {} ) +-- ... +-- ZoneObject = nil +-- garbagecollect() +-- +-- The above example creates a new MasterObject, but does not schedule anything, and is bound to the existence of ZoneObject, which is a ZONE. +-- A separate schedule is created by using the MasterObject using the method :Schedule()..., which returns a ScheduleID +-- Later in the logic, the ZoneObject is put to nil, and garbage is collected. +-- As a result, the MasterObject will cancel any planned schedule. +-- +-- ### Construct a SCHEDULER object with a persistent schedule. +-- +-- * @{#SCHEDULER.New}( nil, Function, FunctionArguments, Start, ... ): Setup a new persistent SCHEDULER object, and start a new schedule for the Function with the defined FunctionArguments according the Start and sequent parameters. +-- +-- MasterObject, SchedulerID = SCHEDULER:New( nil, ScheduleFunction, {} ) +-- +-- The above example creates a new MasterObject, and does schedule the first schedule as part of the call. +-- Note that 2 variables are returned here: MasterObject, ScheduleID... +-- +-- ### Construct a SCHEDULER object without a schedule, but volatile to the Object existence... +-- +-- * @{#SCHEDULER.New}( Object, Function, FunctionArguments, Start, ... ): Setup a new SCHEDULER object, linked to Object, and start a new schedule for the Function with the defined FunctionArguments according the Start and sequent parameters. +-- +-- ZoneObject = ZONE:New( "ZoneName" ) +-- MasterObject, SchedulerID = SCHEDULER:New( ZoneObject, ScheduleFunction, {} ) +-- SchedulerID = MasterObject:Schedule( ZoneObject, ScheduleFunction, {} ) +-- ... +-- ZoneObject = nil +-- garbagecollect() +-- +-- The above example creates a new MasterObject, and schedules a method call (ScheduleFunction), +-- and is bound to the existence of ZoneObject, which is a ZONE object (ZoneObject). +-- Both a MasterObject and a SchedulerID variable are returned. +-- Later in the logic, the ZoneObject is put to nil, and garbage is collected. +-- As a result, the MasterObject will cancel the planned schedule. +-- +-- ## SCHEDULER timer stopping and (re-)starting. +-- +-- The SCHEDULER can be stopped and restarted with the following methods: +-- +-- * @{#SCHEDULER.Start}(): (Re-)Start the schedules within the SCHEDULER object. If a CallID is provided to :Start(), only the schedule referenced by CallID will be (re-)started. +-- * @{#SCHEDULER.Stop}(): Stop the schedules within the SCHEDULER object. If a CallID is provided to :Stop(), then only the schedule referenced by CallID will be stopped. +-- +-- ZoneObject = ZONE:New( "ZoneName" ) +-- MasterObject, SchedulerID = SCHEDULER:New( ZoneObject, ScheduleFunction, {} ) +-- SchedulerID = MasterObject:Schedule( ZoneObject, ScheduleFunction, {}, 10, 10 ) +-- ... +-- MasterObject:Stop( SchedulerID ) +-- ... +-- MasterObject:Start( SchedulerID ) +-- +-- The above example creates a new MasterObject, and does schedule the first schedule as part of the call. +-- Note that 2 variables are returned here: MasterObject, ScheduleID... +-- Later in the logic, the repeating schedule with SchedulerID is stopped. +-- A bit later, the repeating schedule with SchedulerId is (re)-started. +-- +-- ## Create a new schedule +-- +-- With the method @{#SCHEDULER.Schedule}() a new time event can be scheduled. +-- This method is used by the :New() constructor when a new schedule is planned. +-- +-- Consider the following code fragment of the SCHEDULER object creation. +-- +-- ZoneObject = ZONE:New( "ZoneName" ) +-- MasterObject = SCHEDULER:New( ZoneObject ) +-- +-- Several parameters can be specified that influence the behaviour of a Schedule. +-- +-- ### A single schedule, immediately executed +-- +-- SchedulerID = MasterObject:Schedule( ZoneObject, ScheduleFunction, {} ) +-- +-- The above example schedules a new ScheduleFunction call to be executed asynchronously, within milleseconds ... +-- +-- ### A single schedule, planned over time +-- +-- SchedulerID = MasterObject:Schedule( ZoneObject, ScheduleFunction, {}, 10 ) +-- +-- The above example schedules a new ScheduleFunction call to be executed asynchronously, within 10 seconds ... +-- +-- ### A schedule with a repeating time interval, planned over time +-- +-- SchedulerID = MasterObject:Schedule( ZoneObject, ScheduleFunction, {}, 10, 60 ) +-- +-- The above example schedules a new ScheduleFunction call to be executed asynchronously, within 10 seconds, +-- and repeating 60 every seconds ... +-- +-- ### A schedule with a repeating time interval, planned over time, with time interval randomization +-- +-- SchedulerID = MasterObject:Schedule( ZoneObject, ScheduleFunction, {}, 10, 60, 0.5 ) +-- +-- The above example schedules a new ScheduleFunction call to be executed asynchronously, within 10 seconds, +-- and repeating 60 seconds, with a 50% time interval randomization ... +-- So the repeating time interval will be randomized using the **0.5**, +-- and will calculate between **60 - ( 60 * 0.5 )** and **60 + ( 60 * 0.5 )** for each repeat, +-- which is in this example between **30** and **90** seconds. +-- +-- ### A schedule with a repeating time interval, planned over time, with time interval randomization, and stop after a time interval +-- +-- SchedulerID = MasterObject:Schedule( ZoneObject, ScheduleFunction, {}, 10, 60, 0.5, 300 ) +-- +-- The above example schedules a new ScheduleFunction call to be executed asynchronously, within 10 seconds, +-- The schedule will repeat every 60 seconds. +-- So the repeating time interval will be randomized using the **0.5**, +-- and will calculate between **60 - ( 60 * 0.5 )** and **60 + ( 60 * 0.5 )** for each repeat, +-- which is in this example between **30** and **90** seconds. +-- The schedule will stop after **300** seconds. +-- +-- @field #SCHEDULER +SCHEDULER = { + ClassName = "SCHEDULER", + Schedules = {}, + MasterObject = nil, + ShowTrace = nil, +} + +--- SCHEDULER constructor. +-- @param #SCHEDULER self +-- @param #table MasterObject Specified for which Moose object the timer is setup. If a value of nil is provided, a scheduler will be setup without an object reference. +-- @param #function SchedulerFunction The event function to be called when a timer event occurs. The event function needs to accept the parameters specified in SchedulerArguments. +-- @param #table SchedulerArguments Optional arguments that can be given as part of scheduler. The arguments need to be given as a table { param1, param 2, ... }. +-- @param #number Start Specifies the amount of seconds that will be waited before the scheduling is started, and the event function is called. +-- @param #number Repeat Specifies the interval in seconds when the scheduler will call the event function. +-- @param #number RandomizeFactor Specifies a randomization factor between 0 and 1 to randomize the Repeat. +-- @param #number Stop Specifies the amount of seconds when the scheduler will be stopped. +-- @return #SCHEDULER self. +-- @return #table The ScheduleID of the planned schedule. +function SCHEDULER:New( MasterObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop ) + + local self = BASE:Inherit( self, BASE:New() ) -- #SCHEDULER + self:F2( { Start, Repeat, RandomizeFactor, Stop } ) + + local ScheduleID = nil + + self.MasterObject = MasterObject + self.ShowTrace = false + + if SchedulerFunction then + ScheduleID = self:Schedule( MasterObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop, 3 ) + end + + return self, ScheduleID +end + +--- Schedule a new time event. Note that the schedule will only take place if the scheduler is *started*. Even for a single schedule event, the scheduler needs to be started also. +-- @param #SCHEDULER self +-- @param #table MasterObject Specified for which Moose object the timer is setup. If a value of nil is provided, a scheduler will be setup without an object reference. +-- @param #function SchedulerFunction The event function to be called when a timer event occurs. The event function needs to accept the parameters specified in SchedulerArguments. +-- @param #table SchedulerArguments Optional arguments that can be given as part of scheduler. The arguments need to be given as a table { param1, param 2, ... }. +-- @param #number Start Specifies the amount of seconds that will be waited before the scheduling is started, and the event function is called. +-- @param #number Repeat Specifies the time interval in seconds when the scheduler will call the event function. +-- @param #number RandomizeFactor Specifies a randomization factor between 0 and 1 to randomize the Repeat. +-- @param #number Stop Time interval in seconds after which the scheduler will be stoppe. +-- @param #number TraceLevel Trace level [0,3]. Default 3. +-- @param Core.Fsm#FSM Fsm Finite state model. +-- @return #table The ScheduleID of the planned schedule. +function SCHEDULER:Schedule( MasterObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop, TraceLevel, Fsm ) + self:F2( { Start, Repeat, RandomizeFactor, Stop } ) + self:T3( { SchedulerArguments } ) + + -- Debug info. + local ObjectName = "-" + if MasterObject and MasterObject.ClassName and MasterObject.ClassID then + ObjectName = MasterObject.ClassName .. MasterObject.ClassID + end + self:F3( { "Schedule :", ObjectName, tostring( MasterObject ), Start, Repeat, RandomizeFactor, Stop } ) + + -- Set master object. + self.MasterObject = MasterObject + + -- Add schedule. + local ScheduleID = _SCHEDULEDISPATCHER:AddSchedule( + self, + SchedulerFunction, + SchedulerArguments, + Start, + Repeat, + RandomizeFactor, + Stop, + TraceLevel or 3, + Fsm + ) + + self.Schedules[#self.Schedules+1] = ScheduleID + + return ScheduleID +end + +--- (Re-)Starts the schedules or a specific schedule if a valid ScheduleID is provided. +-- @param #SCHEDULER self +-- @param #string ScheduleID (Optional) The ScheduleID of the planned (repeating) schedule. +function SCHEDULER:Start( ScheduleID ) + self:F3( { ScheduleID } ) + self:T(string.format("Starting scheduler ID=%s", tostring(ScheduleID))) + _SCHEDULEDISPATCHER:Start( self, ScheduleID ) +end + +--- Stops the schedules or a specific schedule if a valid ScheduleID is provided. +-- @param #SCHEDULER self +-- @param #string ScheduleID (Optional) The ScheduleID of the planned (repeating) schedule. +function SCHEDULER:Stop( ScheduleID ) + self:F3( { ScheduleID } ) + self:T(string.format("Stopping scheduler ID=%s", tostring(ScheduleID))) + _SCHEDULEDISPATCHER:Stop( self, ScheduleID ) +end + +--- Removes a specific schedule if a valid ScheduleID is provided. +-- @param #SCHEDULER self +-- @param #string ScheduleID (optional) The ScheduleID of the planned (repeating) schedule. +function SCHEDULER:Remove( ScheduleID ) + self:F3( { ScheduleID } ) + self:T(string.format("Removing scheduler ID=%s", tostring(ScheduleID))) + _SCHEDULEDISPATCHER:RemoveSchedule( self, ScheduleID ) +end + +--- Clears all pending schedules. +-- @param #SCHEDULER self +function SCHEDULER:Clear() + self:F3( ) + self:T(string.format("Clearing scheduler")) + _SCHEDULEDISPATCHER:Clear( self ) +end + +--- Show tracing for this scheduler. +-- @param #SCHEDULER self +function SCHEDULER:ShowTrace() + _SCHEDULEDISPATCHER:ShowTrace( self ) +end + +--- No tracing for this scheduler. +-- @param #SCHEDULER self +function SCHEDULER:NoTrace() + _SCHEDULEDISPATCHER:NoTrace( self ) +end +--- **Core** -- SCHEDULEDISPATCHER dispatches the different schedules. +-- +-- === +-- +-- Takes care of the creation and dispatching of scheduled functions for SCHEDULER objects. +-- +-- This class is tricky and needs some thorough explanation. +-- SCHEDULE classes are used to schedule functions for objects, or as persistent objects. +-- The SCHEDULEDISPATCHER class ensures that: +-- +-- - Scheduled functions are planned according the SCHEDULER object parameters. +-- - Scheduled functions are repeated when requested, according the SCHEDULER object parameters. +-- - Scheduled functions are automatically removed when the schedule is finished, according the SCHEDULER object parameters. +-- +-- The SCHEDULEDISPATCHER class will manage SCHEDULER object in memory during garbage collection: +-- +-- - When a SCHEDULER object is not attached to another object (that is, it's first :Schedule() parameter is nil), then the SCHEDULER object is _persistent_ within memory. +-- - When a SCHEDULER object *is* attached to another object, then the SCHEDULER object is _not persistent_ within memory after a garbage collection! +-- +-- The none persistency of SCHEDULERS attached to objects is required to allow SCHEDULER objects to be garbage collectged, when the parent object is also desroyed or nillified and garbage collected. +-- Even when there are pending timer scheduled functions to be executed for the SCHEDULER object, +-- these will not be executed anymore when the SCHEDULER object has been destroyed. +-- +-- The SCHEDULEDISPATCHER allows multiple scheduled functions to be planned and executed for one SCHEDULER object. +-- The SCHEDULER object therefore keeps a table of "CallID's", which are returned after each planning of a new scheduled function by the SCHEDULEDISPATCHER. +-- The SCHEDULER object plans new scheduled functions through the @{Core.Scheduler#SCHEDULER.Schedule}() method. +-- The Schedule() method returns the CallID that is the reference ID for each planned schedule. +-- +-- === +-- +-- ### Contributions: - +-- ### Authors: FlightControl : Design & Programming +-- +-- @module Core.ScheduleDispatcher +-- @image Core_Schedule_Dispatcher.JPG + +--- SCHEDULEDISPATCHER class. +-- @type SCHEDULEDISPATCHER +-- @field #string ClassName Name of the class. +-- @field #number CallID Call ID counter. +-- @field #table PersistentSchedulers Persistant schedulers. +-- @field #table ObjectSchedulers Schedulers that only exist as long as the master object exists. +-- @field #table Schedule Meta table setmetatable( {}, { __mode = "k" } ). +-- @extends Core.Base#BASE + +--- The SCHEDULEDISPATCHER structure +-- @type SCHEDULEDISPATCHER +SCHEDULEDISPATCHER = { + ClassName = "SCHEDULEDISPATCHER", + CallID = 0, + PersistentSchedulers = {}, + ObjectSchedulers = {}, + Schedule = nil, +} + +--- Player data table holding all important parameters of each player. +-- @type SCHEDULEDISPATCHER.ScheduleData +-- @field #function Function The schedule function to be called. +-- @field #table Arguments Schedule function arguments. +-- @field #number Start Start time in seconds. +-- @field #number Repeat Repeat time intervall in seconds. +-- @field #number Randomize Randomization factor [0,1]. +-- @field #number Stop Stop time in seconds. +-- @field #number StartTime Time in seconds when the scheduler is created. +-- @field #number ScheduleID Schedule ID. +-- @field #function CallHandler Function to be passed to the DCS timer.scheduleFunction(). +-- @field #boolean ShowTrace If true, show tracing info. + +--- Create a new schedule dispatcher object. +-- @param #SCHEDULEDISPATCHER self +-- @return #SCHEDULEDISPATCHER self +function SCHEDULEDISPATCHER:New() + local self = BASE:Inherit( self, BASE:New() ) + self:F3() + return self +end + +--- Add a Schedule to the ScheduleDispatcher. +-- The development of this method was really tidy. +-- It is constructed as such that a garbage collection is executed on the weak tables, when the Scheduler is nillified. +-- Nothing of this code should be modified without testing it thoroughly. +-- @param #SCHEDULEDISPATCHER self +-- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object. +-- @param #function ScheduleFunction Scheduler function. +-- @param #table ScheduleArguments Table of arguments passed to the ScheduleFunction. +-- @param #number Start Start time in seconds. +-- @param #number Repeat Repeat interval in seconds. +-- @param #number Randomize Radomization factor [0,1]. +-- @param #number Stop Stop time in seconds. +-- @param #number TraceLevel Trace level [0,3]. +-- @param Core.Fsm#FSM Fsm Finite state model. +-- @return #string Call ID or nil. +function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleArguments, Start, Repeat, Randomize, Stop, TraceLevel, Fsm ) + self:F2( { Scheduler, ScheduleFunction, ScheduleArguments, Start, Repeat, Randomize, Stop, TraceLevel, Fsm } ) + + -- Increase counter. + self.CallID = self.CallID + 1 + + -- Create ID. + local CallID = self.CallID .. "#" .. ( Scheduler.MasterObject and Scheduler.MasterObject.GetClassNameAndID and Scheduler.MasterObject:GetClassNameAndID() or "" ) or "" + + self:T2(string.format("Adding schedule #%d CallID=%s", self.CallID, CallID)) + + -- Initialize PersistentSchedulers + self.PersistentSchedulers = self.PersistentSchedulers or {} + + -- Initialize the ObjectSchedulers array, which is a weakly coupled table. + -- If the object used as the key is nil, then the garbage collector will remove the item from the Functions array. + self.ObjectSchedulers = self.ObjectSchedulers or setmetatable( {}, { __mode = "v" } ) + + if Scheduler.MasterObject then + self.ObjectSchedulers[CallID] = Scheduler + self:F3( { CallID = CallID, ObjectScheduler = tostring(self.ObjectSchedulers[CallID]), MasterObject = tostring(Scheduler.MasterObject) } ) + else + self.PersistentSchedulers[CallID] = Scheduler + self:F3( { CallID = CallID, PersistentScheduler = self.PersistentSchedulers[CallID] } ) + end + + self.Schedule = self.Schedule or setmetatable( {}, { __mode = "k" } ) + self.Schedule[Scheduler] = self.Schedule[Scheduler] or {} + self.Schedule[Scheduler][CallID] = {} --#SCHEDULEDISPATCHER.ScheduleData + self.Schedule[Scheduler][CallID].Function = ScheduleFunction + self.Schedule[Scheduler][CallID].Arguments = ScheduleArguments + self.Schedule[Scheduler][CallID].StartTime = timer.getTime() + ( Start or 0 ) + self.Schedule[Scheduler][CallID].Start = Start + 0.001 + self.Schedule[Scheduler][CallID].Repeat = Repeat or 0 + self.Schedule[Scheduler][CallID].Randomize = Randomize or 0 + self.Schedule[Scheduler][CallID].Stop = Stop + + + -- This section handles the tracing of the scheduled calls. + -- Because these calls will be executed with a delay, we inspect the place where these scheduled calls are initiated. + -- The Info structure contains the output of the debug.getinfo() calls, which inspects the call stack for the function name, line number and source name. + -- The call stack has many levels, and the correct semantical function call depends on where in the code AddSchedule was "used". + -- - Using SCHEDULER:New() + -- - Using Schedule:AddSchedule() + -- - Using Fsm:__Func() + -- - Using Class:ScheduleOnce() + -- - Using Class:ScheduleRepeat() + -- - ... + -- So for each of these scheduled call variations, AddSchedule is the workhorse which will schedule the call. + -- But the correct level with the correct semantical function location will differ depending on the above scheduled call invocation forms. + -- That's where the field TraceLevel contains optionally the level in the call stack where the call information is obtained. + -- The TraceLevel field indicates the correct level where the semantical scheduled call was invoked within the source, ensuring that function name, line number and source name are correct. + -- There is one quick ... + -- The FSM class models scheduled calls using the __Func syntax. However, these functions are "tailed". + -- There aren't defined anywhere within the source code, but rather implemented as triggers within the FSM logic, + -- and using the onbefore, onafter, onenter, onleave prefixes. (See the FSM for details). + -- Therefore, in the call stack, at the TraceLevel these functions are mentioned as "tail calls", and the Info.name field will be nil as a result. + -- To obtain the correct function name for FSM object calls, the function is mentioned in the call stack at a higher stack level. + -- So when function name stored in Info.name is nil, then I inspect the function name within the call stack one level higher. + -- So this little piece of code does its magic wonderfully, preformance overhead is neglectible, as scheduled calls don't happen that often. + + local Info = {} + + if debug then + TraceLevel = TraceLevel or 2 + Info = debug.getinfo( TraceLevel, "nlS" ) + local name_fsm = debug.getinfo( TraceLevel - 1, "n" ).name -- #string + if name_fsm then + Info.name = name_fsm + end + end + + self:T3( self.Schedule[Scheduler][CallID] ) + + --- Function passed to the DCS timer.scheduleFunction() + self.Schedule[Scheduler][CallID].CallHandler = function( Params ) + + local CallID = Params.CallID + local Info = Params.Info or {} + local Source = Info.source or "?" + local Line = Info.currentline or "?" + local Name = Info.name or "?" + + local ErrorHandler = function( errmsg ) + env.info( "Error in timer function: " .. errmsg ) + if BASE.Debug ~= nil then + env.info( BASE.Debug.traceback() ) + end + return errmsg + end + + -- Get object or persistant scheduler object. + local Scheduler = self.ObjectSchedulers[CallID] --Core.Scheduler#SCHEDULER + if not Scheduler then + Scheduler = self.PersistentSchedulers[CallID] + end + + --self:T3( { Scheduler = Scheduler } ) + + if Scheduler then + + local MasterObject = tostring(Scheduler.MasterObject) + + -- Schedule object. + local Schedule = self.Schedule[Scheduler][CallID] --#SCHEDULEDISPATCHER.ScheduleData + + --self:T3( { Schedule = Schedule } ) + + local SchedulerObject = Scheduler.MasterObject --Scheduler.SchedulerObject Now is this the Maste or Scheduler object? + local ShowTrace = Scheduler.ShowTrace + + local ScheduleFunction = Schedule.Function + local ScheduleArguments = Schedule.Arguments or {} + local Start = Schedule.Start + local Repeat = Schedule.Repeat or 0 + local Randomize = Schedule.Randomize or 0 + local Stop = Schedule.Stop or 0 + local ScheduleID = Schedule.ScheduleID + + + local Prefix = ( Repeat == 0 ) and "--->" or "+++>" + + local Status, Result + --self:E( { SchedulerObject = SchedulerObject } ) + if SchedulerObject then + local function Timer() + if ShowTrace then + SchedulerObject:T( Prefix .. Name .. ":" .. Line .. " (" .. Source .. ")" ) + end + return ScheduleFunction( SchedulerObject, unpack( ScheduleArguments ) ) + end + Status, Result = xpcall( Timer, ErrorHandler ) + else + local function Timer() + if ShowTrace then + self:T( Prefix .. Name .. ":" .. Line .. " (" .. Source .. ")" ) + end + return ScheduleFunction( unpack( ScheduleArguments ) ) + end + Status, Result = xpcall( Timer, ErrorHandler ) + end + + local CurrentTime = timer.getTime() + local StartTime = Schedule.StartTime + + -- Debug info. + self:F3( { CallID=CallID, ScheduleID=ScheduleID, Master = MasterObject, CurrentTime = CurrentTime, StartTime = StartTime, Start = Start, Repeat = Repeat, Randomize = Randomize, Stop = Stop } ) + + + if Status and (( Result == nil ) or ( Result and Result ~= false ) ) then + + if Repeat ~= 0 and ( ( Stop == 0 ) or ( Stop ~= 0 and CurrentTime <= StartTime + Stop ) ) then + local ScheduleTime = CurrentTime + Repeat + math.random(- ( Randomize * Repeat / 2 ), ( Randomize * Repeat / 2 )) + 0.0001 -- Accuracy + --self:T3( { Repeat = CallID, CurrentTime, ScheduleTime, ScheduleArguments } ) + return ScheduleTime -- returns the next time the function needs to be called. + else + self:Stop( Scheduler, CallID ) + end + + else + self:Stop( Scheduler, CallID ) + end + else + self:I( "<<<>" .. Name .. ":" .. Line .. " (" .. Source .. ")" ) + end + + return nil + end + + self:Start( Scheduler, CallID, Info ) + + return CallID +end + +--- Remove schedule. +-- @param #SCHEDULEDISPATCHER self +-- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object. +-- @param #table CallID Call ID. +function SCHEDULEDISPATCHER:RemoveSchedule( Scheduler, CallID ) + self:F2( { Remove = CallID, Scheduler = Scheduler } ) + + if CallID then + self:Stop( Scheduler, CallID ) + self.Schedule[Scheduler][CallID] = nil + end +end + +--- Start dispatcher. +-- @param #SCHEDULEDISPATCHER self +-- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object. +-- @param #table CallID (Optional) Call ID. +-- @param #string Info (Optional) Debug info. +function SCHEDULEDISPATCHER:Start( Scheduler, CallID, Info ) + self:F2( { Start = CallID, Scheduler = Scheduler } ) + + if CallID then + + local Schedule = self.Schedule[Scheduler][CallID] --#SCHEDULEDISPATCHER.ScheduleData + + -- Only start when there is no ScheduleID defined! + -- This prevents to "Start" the scheduler twice with the same CallID... + if not Schedule.ScheduleID then + + -- Current time in seconds. + local Tnow=timer.getTime() + + Schedule.StartTime = Tnow -- Set the StartTime field to indicate when the scheduler started. + + -- Start DCS schedule function https://wiki.hoggitworld.com/view/DCS_func_scheduleFunction + Schedule.ScheduleID = timer.scheduleFunction(Schedule.CallHandler, { CallID = CallID, Info = Info }, Tnow + Schedule.Start) + + self:T(string.format("Starting scheduledispatcher Call ID=%s ==> Schedule ID=%s", tostring(CallID), tostring(Schedule.ScheduleID))) + end + + else + + -- Recursive. + for CallID, Schedule in pairs( self.Schedule[Scheduler] or {} ) do + self:Start( Scheduler, CallID, Info ) -- Recursive + end + + end +end + +--- Stop dispatcher. +-- @param #SCHEDULEDISPATCHER self +-- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object. +-- @param #table CallID Call ID. +function SCHEDULEDISPATCHER:Stop( Scheduler, CallID ) + self:F2( { Stop = CallID, Scheduler = Scheduler } ) + + if CallID then + + local Schedule = self.Schedule[Scheduler][CallID] --#SCHEDULEDISPATCHER.ScheduleData + + -- Only stop when there is a ScheduleID defined for the CallID. So, when the scheduler was stopped before, do nothing. + if Schedule.ScheduleID then + + self:T(string.format("scheduledispatcher stopping scheduler CallID=%s, ScheduleID=%s", tostring(CallID), tostring(Schedule.ScheduleID))) + + -- Remove schedule function https://wiki.hoggitworld.com/view/DCS_func_removeFunction + timer.removeFunction(Schedule.ScheduleID) + + Schedule.ScheduleID = nil + + else + self:T(string.format("Error no ScheduleID for CallID=%s", tostring(CallID))) + end + + else + + for CallID, Schedule in pairs( self.Schedule[Scheduler] or {} ) do + self:Stop( Scheduler, CallID ) -- Recursive + end + + end +end + +--- Clear all schedules by stopping all dispatchers. +-- @param #SCHEDULEDISPATCHER self +-- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object. +function SCHEDULEDISPATCHER:Clear( Scheduler ) + self:F2( { Scheduler = Scheduler } ) + + for CallID, Schedule in pairs( self.Schedule[Scheduler] or {} ) do + self:Stop( Scheduler, CallID ) -- Recursive + end +end + +--- Shopw tracing info. +-- @param #SCHEDULEDISPATCHER self +-- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object. +function SCHEDULEDISPATCHER:ShowTrace( Scheduler ) + self:F2( { Scheduler = Scheduler } ) + Scheduler.ShowTrace = true +end + +--- No tracing info. +-- @param #SCHEDULEDISPATCHER self +-- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object. +function SCHEDULEDISPATCHER:NoTrace( Scheduler ) + self:F2( { Scheduler = Scheduler } ) + Scheduler.ShowTrace = false +end + +--- **Core** - Models DCS event dispatching using a publish-subscribe model. +-- +-- === +-- +-- ## Features: +-- +-- * Capture DCS events and dispatch them to the subscribed objects. +-- * Generate DCS events to the subscribed objects from within the code. +-- +-- === +-- +-- # Event Handling Overview +-- +-- ![Objects](..\Presentations\EVENT\Dia2.JPG) +-- +-- Within a running mission, various DCS events occur. Units are dynamically created, crash, die, shoot stuff, get hit etc. +-- This module provides a mechanism to dispatch those events occuring within your running mission, to the different objects orchestrating your mission. +-- +-- ![Objects](..\Presentations\EVENT\Dia3.JPG) +-- +-- Objects can subscribe to different events. The Event dispatcher will publish the received DCS events to the subscribed MOOSE objects, in a specified order. +-- In this way, the subscribed MOOSE objects are kept in sync with your evolving running mission. +-- +-- ## 1. Event Dispatching +-- +-- ![Objects](..\Presentations\EVENT\Dia4.JPG) +-- +-- The _EVENTDISPATCHER object is automatically created within MOOSE, +-- and handles the dispatching of DCS Events occurring +-- in the simulator to the subscribed objects +-- in the correct processing order. +-- +-- ![Objects](..\Presentations\EVENT\Dia5.JPG) +-- +-- There are 5 levels of kind of objects that the _EVENTDISPATCHER services: +-- +-- * _DATABASE object: The core of the MOOSE objects. Any object that is created, deleted or updated, is done in this database. +-- * SET_ derived classes: Subsets of the _DATABASE object. These subsets are updated by the _EVENTDISPATCHER as the second priority. +-- * UNIT objects: UNIT objects can subscribe to DCS events. Each DCS event will be directly published to teh subscribed UNIT object. +-- * GROUP objects: GROUP objects can subscribe to DCS events. Each DCS event will be directly published to the subscribed GROUP object. +-- * Any other object: Various other objects can subscribe to DCS events. Each DCS event triggered will be published to each subscribed object. +-- +-- ![Objects](..\Presentations\EVENT\Dia6.JPG) +-- +-- For most DCS events, the above order of updating will be followed. +-- +-- ![Objects](..\Presentations\EVENT\Dia7.JPG) +-- +-- But for some DCS events, the publishing order is reversed. This is due to the fact that objects need to be **erased** instead of added. +-- +-- # 2. Event Handling +-- +-- ![Objects](..\Presentations\EVENT\Dia8.JPG) +-- +-- The actual event subscribing and handling is not facilitated through the _EVENTDISPATCHER, but it is done through the @{BASE} class, @{UNIT} class and @{GROUP} class. +-- The _EVENTDISPATCHER is a component that is quietly working in the background of MOOSE. +-- +-- ![Objects](..\Presentations\EVENT\Dia9.JPG) +-- +-- The BASE class provides methods to catch DCS Events. These are events that are triggered from within the DCS simulator, +-- and handled through lua scripting. MOOSE provides an encapsulation to handle these events more efficiently. +-- +-- ## 2.1. Subscribe to / Unsubscribe from DCS Events. +-- +-- At first, the mission designer will need to **Subscribe** to a specific DCS event for the class. +-- So, when the DCS event occurs, the class will be notified of that event. +-- There are two functions which you use to subscribe to or unsubscribe from an event. +-- +-- * @{Core.Base#BASE.HandleEvent}(): Subscribe to a DCS Event. +-- * @{Core.Base#BASE.UnHandleEvent}(): Unsubscribe from a DCS Event. +-- +-- Note that for a UNIT, the event will be handled **for that UNIT only**! +-- Note that for a GROUP, the event will be handled **for all the UNITs in that GROUP only**! +-- +-- For all objects of other classes, the subscribed events will be handled for **all UNITs within the Mission**! +-- So if a UNIT within the mission has the subscribed event for that object, +-- then the object event handler will receive the event for that UNIT! +-- +-- ## 2.2 Event Handling of DCS Events +-- +-- Once the class is subscribed to the event, an **Event Handling** method on the object or class needs to be written that will be called +-- when the DCS event occurs. The Event Handling method receives an @{Core.Event#EVENTDATA} structure, which contains a lot of information +-- about the event that occurred. +-- +-- Find below an example of the prototype how to write an event handling function for two units: +-- +-- local Tank1 = UNIT:FindByName( "Tank A" ) +-- local Tank2 = UNIT:FindByName( "Tank B" ) +-- +-- -- Here we subscribe to the Dead events. So, if one of these tanks dies, the Tank1 or Tank2 objects will be notified. +-- Tank1:HandleEvent( EVENTS.Dead ) +-- Tank2:HandleEvent( EVENTS.Dead ) +-- +-- --- This function is an Event Handling function that will be called when Tank1 is Dead. +-- -- @param Wrapper.Unit#UNIT self +-- -- @param Core.Event#EVENTDATA EventData +-- function Tank1:OnEventDead( EventData ) +-- +-- self:SmokeGreen() +-- end +-- +-- --- This function is an Event Handling function that will be called when Tank2 is Dead. +-- -- @param Wrapper.Unit#UNIT self +-- -- @param Core.Event#EVENTDATA EventData +-- function Tank2:OnEventDead( EventData ) +-- +-- self:SmokeBlue() +-- end +-- +-- ## 2.3 Event Handling methods that are automatically called upon subscribed DCS events. +-- +-- ![Objects](..\Presentations\EVENT\Dia10.JPG) +-- +-- The following list outlines which EVENTS item in the structure corresponds to which Event Handling method. +-- Always ensure that your event handling methods align with the events being subscribed to, or nothing will be executed. +-- +-- # 3. EVENTS type +-- +-- The EVENTS structure contains names for all the different DCS events that objects can subscribe to using the +-- @{Core.Base#BASE.HandleEvent}() method. +-- +-- # 4. EVENTDATA type +-- +-- The @{Core.Event#EVENTDATA} structure contains all the fields that are populated with event information before +-- an Event Handler method is being called by the event dispatcher. +-- The Event Handler received the EVENTDATA object as a parameter, and can be used to investigate further the different events. +-- There are basically 4 main categories of information stored in the EVENTDATA structure: +-- +-- * Initiator Unit data: Several fields documenting the initiator unit related to the event. +-- * Target Unit data: Several fields documenting the target unit related to the event. +-- * Weapon data: Certain events populate weapon information. +-- * Place data: Certain events populate place information. +-- +-- --- This function is an Event Handling function that will be called when Tank1 is Dead. +-- -- EventData is an EVENTDATA structure. +-- -- We use the EventData.IniUnit to smoke the tank Green. +-- -- @param Wrapper.Unit#UNIT self +-- -- @param Core.Event#EVENTDATA EventData +-- function Tank1:OnEventDead( EventData ) +-- +-- EventData.IniUnit:SmokeGreen() +-- end +-- +-- +-- Find below an overview which events populate which information categories: +-- +-- ![Objects](..\Presentations\EVENT\Dia14.JPG) +-- +-- **IMPORTANT NOTE:** Some events can involve not just UNIT objects, but also STATIC objects!!! +-- In that case the initiator or target unit fields will refer to a STATIC object! +-- In case a STATIC object is involved, the documentation indicates which fields will and won't not be populated. +-- The fields **IniObjectCategory** and **TgtObjectCategory** contain the indicator which **kind of object is involved** in the event. +-- You can use the enumerator **Object.Category.UNIT** and **Object.Category.STATIC** to check on IniObjectCategory and TgtObjectCategory. +-- Example code snippet: +-- +-- if Event.IniObjectCategory == Object.Category.UNIT then +-- ... +-- end +-- if Event.IniObjectCategory == Object.Category.STATIC then +-- ... +-- end +-- +-- When a static object is involved in the event, the Group and Player fields won't be populated. +-- +-- === +-- +-- ### Author: **FlightControl** +-- ### Contributions: +-- +-- === +-- +-- @module Core.Event +-- @image Core_Event.JPG + + +--- @type EVENT +-- @field #EVENT.Events Events +-- @extends Core.Base#BASE + +--- The EVENT class +-- @field #EVENT +EVENT = { + ClassName = "EVENT", + ClassID = 0, + MissionEnd = false, +} + +world.event.S_EVENT_NEW_CARGO = world.event.S_EVENT_MAX + 1000 +world.event.S_EVENT_DELETE_CARGO = world.event.S_EVENT_MAX + 1001 +world.event.S_EVENT_NEW_ZONE = world.event.S_EVENT_MAX + 1002 +world.event.S_EVENT_DELETE_ZONE = world.event.S_EVENT_MAX + 1003 +world.event.S_EVENT_NEW_ZONE_GOAL = world.event.S_EVENT_MAX + 1004 +world.event.S_EVENT_DELETE_ZONE_GOAL = world.event.S_EVENT_MAX + 1005 +world.event.S_EVENT_REMOVE_UNIT = world.event.S_EVENT_MAX + 1006 +world.event.S_EVENT_PLAYER_ENTER_AIRCRAFT = world.event.S_EVENT_MAX + 1007 + + +--- The different types of events supported by MOOSE. +-- Use this structure to subscribe to events using the @{Core.Base#BASE.HandleEvent}() method. +-- @type EVENTS +EVENTS = { + Shot = world.event.S_EVENT_SHOT, + Hit = world.event.S_EVENT_HIT, + Takeoff = world.event.S_EVENT_TAKEOFF, + Land = world.event.S_EVENT_LAND, + Crash = world.event.S_EVENT_CRASH, + Ejection = world.event.S_EVENT_EJECTION, + Refueling = world.event.S_EVENT_REFUELING, + Dead = world.event.S_EVENT_DEAD, + PilotDead = world.event.S_EVENT_PILOT_DEAD, + BaseCaptured = world.event.S_EVENT_BASE_CAPTURED, + MissionStart = world.event.S_EVENT_MISSION_START, + MissionEnd = world.event.S_EVENT_MISSION_END, + TookControl = world.event.S_EVENT_TOOK_CONTROL, + RefuelingStop = world.event.S_EVENT_REFUELING_STOP, + Birth = world.event.S_EVENT_BIRTH, + HumanFailure = world.event.S_EVENT_HUMAN_FAILURE, + EngineStartup = world.event.S_EVENT_ENGINE_STARTUP, + EngineShutdown = world.event.S_EVENT_ENGINE_SHUTDOWN, + PlayerEnterUnit = world.event.S_EVENT_PLAYER_ENTER_UNIT, + PlayerLeaveUnit = world.event.S_EVENT_PLAYER_LEAVE_UNIT, + PlayerComment = world.event.S_EVENT_PLAYER_COMMENT, + ShootingStart = world.event.S_EVENT_SHOOTING_START, + ShootingEnd = world.event.S_EVENT_SHOOTING_END, + -- Added with DCS 2.5.1 + MarkAdded = world.event.S_EVENT_MARK_ADDED, + MarkChange = world.event.S_EVENT_MARK_CHANGE, + MarkRemoved = world.event.S_EVENT_MARK_REMOVED, + -- Moose Events + NewCargo = world.event.S_EVENT_NEW_CARGO, + DeleteCargo = world.event.S_EVENT_DELETE_CARGO, + NewZone = world.event.S_EVENT_NEW_ZONE, + DeleteZone = world.event.S_EVENT_DELETE_ZONE, + NewZoneGoal = world.event.S_EVENT_NEW_ZONE_GOAL, + DeleteZoneGoal = world.event.S_EVENT_DELETE_ZONE_GOAL, + RemoveUnit = world.event.S_EVENT_REMOVE_UNIT, + PlayerEnterAircraft = world.event.S_EVENT_PLAYER_ENTER_AIRCRAFT, + -- Added with DCS 2.5.6 + DetailedFailure = world.event.S_EVENT_DETAILED_FAILURE or -1, --We set this to -1 for backward compatibility to DCS 2.5.5 and earlier + Kill = world.event.S_EVENT_KILL or -1, + Score = world.event.S_EVENT_SCORE or -1, + UnitLost = world.event.S_EVENT_UNIT_LOST or -1, + LandingAfterEjection = world.event.S_EVENT_LANDING_AFTER_EJECTION or -1, + -- Added with DCS 2.7.0 + ParatrooperLanding = world.event.S_EVENT_PARATROOPER_LENDING or -1, + DiscardChairAfterEjection = world.event.S_EVENT_DISCARD_CHAIR_AFTER_EJECTION or -1, + WeaponAdd = world.event.S_EVENT_WEAPON_ADD or -1, + TriggerZone = world.event.S_EVENT_TRIGGER_ZONE or -1, + LandingQualityMark = world.event.S_EVENT_LANDING_QUALITY_MARK or -1, + BDA = world.event.S_EVENT_BDA or -1, +} + +--- The Event structure +-- Note that at the beginning of each field description, there is an indication which field will be populated depending on the object type involved in the Event: +-- +-- * A (Object.Category.)UNIT : A UNIT object type is involved in the Event. +-- * A (Object.Category.)STATIC : A STATIC object type is involved in the Event. +-- +-- @type EVENTDATA +-- @field #number id The identifier of the event. +-- +-- @field DCS#Unit initiator (UNIT/STATIC/SCENERY) The initiating @{DCS#Unit} or @{DCS#StaticObject}. +-- @field DCS#Object.Category IniObjectCategory (UNIT/STATIC/SCENERY) The initiator object category ( Object.Category.UNIT or Object.Category.STATIC ). +-- @field DCS#Unit IniDCSUnit (UNIT/STATIC) The initiating @{DCS#Unit} or @{DCS#StaticObject}. +-- @field #string IniDCSUnitName (UNIT/STATIC) The initiating Unit name. +-- @field Wrapper.Unit#UNIT IniUnit (UNIT/STATIC) The initiating MOOSE wrapper @{Wrapper.Unit#UNIT} of the initiator Unit object. +-- @field #string IniUnitName (UNIT/STATIC) The initiating UNIT name (same as IniDCSUnitName). +-- @field DCS#Group IniDCSGroup (UNIT) The initiating {DCSGroup#Group}. +-- @field #string IniDCSGroupName (UNIT) The initiating Group name. +-- @field Wrapper.Group#GROUP IniGroup (UNIT) The initiating MOOSE wrapper @{Wrapper.Group#GROUP} of the initiator Group object. +-- @field #string IniGroupName UNIT) The initiating GROUP name (same as IniDCSGroupName). +-- @field #string IniPlayerName (UNIT) The name of the initiating player in case the Unit is a client or player slot. +-- @field DCS#coalition.side IniCoalition (UNIT) The coalition of the initiator. +-- @field DCS#Unit.Category IniCategory (UNIT) The category of the initiator. +-- @field #string IniTypeName (UNIT) The type name of the initiator. +-- +-- @field DCS#Unit target (UNIT/STATIC) The target @{DCS#Unit} or @{DCS#StaticObject}. +-- @field DCS#Object.Category TgtObjectCategory (UNIT/STATIC) The target object category ( Object.Category.UNIT or Object.Category.STATIC ). +-- @field DCS#Unit TgtDCSUnit (UNIT/STATIC) The target @{DCS#Unit} or @{DCS#StaticObject}. +-- @field #string TgtDCSUnitName (UNIT/STATIC) The target Unit name. +-- @field Wrapper.Unit#UNIT TgtUnit (UNIT/STATIC) The target MOOSE wrapper @{Wrapper.Unit#UNIT} of the target Unit object. +-- @field #string TgtUnitName (UNIT/STATIC) The target UNIT name (same as TgtDCSUnitName). +-- @field DCS#Group TgtDCSGroup (UNIT) The target {DCSGroup#Group}. +-- @field #string TgtDCSGroupName (UNIT) The target Group name. +-- @field Wrapper.Group#GROUP TgtGroup (UNIT) The target MOOSE wrapper @{Wrapper.Group#GROUP} of the target Group object. +-- @field #string TgtGroupName (UNIT) The target GROUP name (same as TgtDCSGroupName). +-- @field #string TgtPlayerName (UNIT) The name of the target player in case the Unit is a client or player slot. +-- @field DCS#coalition.side TgtCoalition (UNIT) The coalition of the target. +-- @field DCS#Unit.Category TgtCategory (UNIT) The category of the target. +-- @field #string TgtTypeName (UNIT) The type name of the target. +-- +-- @field DCS#Airbase place The @{DCS#Airbase} +-- @field Wrapper.Airbase#AIRBASE Place The MOOSE airbase object. +-- @field #string PlaceName The name of the airbase. +-- +-- @field #table weapon The weapon used during the event. +-- @field #table Weapon +-- @field #string WeaponName Name of the weapon. +-- @field DCS#Unit WeaponTgtDCSUnit Target DCS unit of the weapon. +-- +-- @field Cargo.Cargo#CARGO Cargo The cargo object. +-- @field #string CargoName The name of the cargo object. +-- +-- @field Core.ZONE#ZONE Zone The zone object. +-- @field #string ZoneName The name of the zone. + + + +local _EVENTMETA = { + [world.event.S_EVENT_SHOT] = { + Order = 1, + Side = "I", + Event = "OnEventShot", + Text = "S_EVENT_SHOT" + }, + [world.event.S_EVENT_HIT] = { + Order = 1, + Side = "T", + Event = "OnEventHit", + Text = "S_EVENT_HIT" + }, + [world.event.S_EVENT_TAKEOFF] = { + Order = 1, + Side = "I", + Event = "OnEventTakeoff", + Text = "S_EVENT_TAKEOFF" + }, + [world.event.S_EVENT_LAND] = { + Order = 1, + Side = "I", + Event = "OnEventLand", + Text = "S_EVENT_LAND" + }, + [world.event.S_EVENT_CRASH] = { + Order = -1, + Side = "I", + Event = "OnEventCrash", + Text = "S_EVENT_CRASH" + }, + [world.event.S_EVENT_EJECTION] = { + Order = 1, + Side = "I", + Event = "OnEventEjection", + Text = "S_EVENT_EJECTION" + }, + [world.event.S_EVENT_REFUELING] = { + Order = 1, + Side = "I", + Event = "OnEventRefueling", + Text = "S_EVENT_REFUELING" + }, + [world.event.S_EVENT_DEAD] = { + Order = -1, + Side = "I", + Event = "OnEventDead", + Text = "S_EVENT_DEAD" + }, + [world.event.S_EVENT_PILOT_DEAD] = { + Order = 1, + Side = "I", + Event = "OnEventPilotDead", + Text = "S_EVENT_PILOT_DEAD" + }, + [world.event.S_EVENT_BASE_CAPTURED] = { + Order = 1, + Side = "I", + Event = "OnEventBaseCaptured", + Text = "S_EVENT_BASE_CAPTURED" + }, + [world.event.S_EVENT_MISSION_START] = { + Order = 1, + Side = "N", + Event = "OnEventMissionStart", + Text = "S_EVENT_MISSION_START" + }, + [world.event.S_EVENT_MISSION_END] = { + Order = 1, + Side = "N", + Event = "OnEventMissionEnd", + Text = "S_EVENT_MISSION_END" + }, + [world.event.S_EVENT_TOOK_CONTROL] = { + Order = 1, + Side = "N", + Event = "OnEventTookControl", + Text = "S_EVENT_TOOK_CONTROL" + }, + [world.event.S_EVENT_REFUELING_STOP] = { + Order = 1, + Side = "I", + Event = "OnEventRefuelingStop", + Text = "S_EVENT_REFUELING_STOP" + }, + [world.event.S_EVENT_BIRTH] = { + Order = 1, + Side = "I", + Event = "OnEventBirth", + Text = "S_EVENT_BIRTH" + }, + [world.event.S_EVENT_HUMAN_FAILURE] = { + Order = 1, + Side = "I", + Event = "OnEventHumanFailure", + Text = "S_EVENT_HUMAN_FAILURE" + }, + [world.event.S_EVENT_ENGINE_STARTUP] = { + Order = 1, + Side = "I", + Event = "OnEventEngineStartup", + Text = "S_EVENT_ENGINE_STARTUP" + }, + [world.event.S_EVENT_ENGINE_SHUTDOWN] = { + Order = 1, + Side = "I", + Event = "OnEventEngineShutdown", + Text = "S_EVENT_ENGINE_SHUTDOWN" + }, + [world.event.S_EVENT_PLAYER_ENTER_UNIT] = { + Order = 1, + Side = "I", + Event = "OnEventPlayerEnterUnit", + Text = "S_EVENT_PLAYER_ENTER_UNIT" + }, + [world.event.S_EVENT_PLAYER_LEAVE_UNIT] = { + Order = -1, + Side = "I", + Event = "OnEventPlayerLeaveUnit", + Text = "S_EVENT_PLAYER_LEAVE_UNIT" + }, + [world.event.S_EVENT_PLAYER_COMMENT] = { + Order = 1, + Side = "I", + Event = "OnEventPlayerComment", + Text = "S_EVENT_PLAYER_COMMENT" + }, + [world.event.S_EVENT_SHOOTING_START] = { + Order = 1, + Side = "I", + Event = "OnEventShootingStart", + Text = "S_EVENT_SHOOTING_START" + }, + [world.event.S_EVENT_SHOOTING_END] = { + Order = 1, + Side = "I", + Event = "OnEventShootingEnd", + Text = "S_EVENT_SHOOTING_END" + }, + [world.event.S_EVENT_MARK_ADDED] = { + Order = 1, + Side = "I", + Event = "OnEventMarkAdded", + Text = "S_EVENT_MARK_ADDED" + }, + [world.event.S_EVENT_MARK_CHANGE] = { + Order = 1, + Side = "I", + Event = "OnEventMarkChange", + Text = "S_EVENT_MARK_CHANGE" + }, + [world.event.S_EVENT_MARK_REMOVED] = { + Order = 1, + Side = "I", + Event = "OnEventMarkRemoved", + Text = "S_EVENT_MARK_REMOVED" + }, + [EVENTS.NewCargo] = { + Order = 1, + Event = "OnEventNewCargo", + Text = "S_EVENT_NEW_CARGO" + }, + [EVENTS.DeleteCargo] = { + Order = 1, + Event = "OnEventDeleteCargo", + Text = "S_EVENT_DELETE_CARGO" + }, + [EVENTS.NewZone] = { + Order = 1, + Event = "OnEventNewZone", + Text = "S_EVENT_NEW_ZONE" + }, + [EVENTS.DeleteZone] = { + Order = 1, + Event = "OnEventDeleteZone", + Text = "S_EVENT_DELETE_ZONE" + }, + [EVENTS.NewZoneGoal] = { + Order = 1, + Event = "OnEventNewZoneGoal", + Text = "S_EVENT_NEW_ZONE_GOAL" + }, + [EVENTS.DeleteZoneGoal] = { + Order = 1, + Event = "OnEventDeleteZoneGoal", + Text = "S_EVENT_DELETE_ZONE_GOAL" + }, + [EVENTS.RemoveUnit] = { + Order = -1, + Event = "OnEventRemoveUnit", + Text = "S_EVENT_REMOVE_UNIT" + }, + [EVENTS.PlayerEnterAircraft] = { + Order = 1, + Event = "OnEventPlayerEnterAircraft", + Text = "S_EVENT_PLAYER_ENTER_AIRCRAFT" + }, + -- Added with DCS 2.5.6 + [EVENTS.DetailedFailure] = { + Order = 1, + Event = "OnEventDetailedFailure", + Text = "S_EVENT_DETAILED_FAILURE" + }, + [EVENTS.Kill] = { + Order = 1, + Event = "OnEventKill", + Text = "S_EVENT_KILL" + }, + [EVENTS.Score] = { + Order = 1, + Event = "OnEventScore", + Text = "S_EVENT_SCORE" + }, + [EVENTS.UnitLost] = { + Order = 1, + Event = "OnEventUnitLost", + Text = "S_EVENT_UNIT_LOST" + }, + [EVENTS.LandingAfterEjection] = { + Order = 1, + Event = "OnEventLandingAfterEjection", + Text = "S_EVENT_LANDING_AFTER_EJECTION" + }, + -- Added with DCS 2.7.0 + [EVENTS.ParatrooperLanding] = { + Order = 1, + Event = "OnEventParatrooperLanding", + Text = "S_EVENT_PARATROOPER_LENDING" + }, + [EVENTS.DiscardChairAfterEjection] = { + Order = 1, + Event = "OnEventDiscardChairAfterEjection", + Text = "S_EVENT_DISCARD_CHAIR_AFTER_EJECTION" + }, + [EVENTS.WeaponAdd] = { + Order = 1, + Event = "OnEventWeaponAdd", + Text = "S_EVENT_WEAPON_ADD" + }, + [EVENTS.TriggerZone] = { + Order = 1, + Event = "OnEventTriggerZone", + Text = "S_EVENT_TRIGGER_ZONE" + }, + [EVENTS.LandingQualityMark] = { + Order = 1, + Event = "OnEventLandingQualityMark", + Text = "S_EVENT_LANDING_QUALITYMARK" + }, + [EVENTS.BDA] = { + Order = 1, + Event = "OnEventBDA", + Text = "S_EVENT_BDA" + }, +} + + +--- The Events structure +-- @type EVENT.Events +-- @field #number IniUnit + +--- Create new event handler. +-- @param #EVENT self +-- @return #EVENT self +function EVENT:New() + + -- Inherit base. + local self = BASE:Inherit( self, BASE:New() ) + + -- Add world event handler. + self.EventHandler = world.addEventHandler(self) + + return self +end + + +--- Initializes the Events structure for the event. +-- @param #EVENT self +-- @param DCS#world.event EventID Event ID. +-- @param Core.Base#BASE EventClass The class object for which events are handled. +-- @return #EVENT.Events +function EVENT:Init( EventID, EventClass ) + self:F3( { _EVENTMETA[EventID].Text, EventClass } ) + + if not self.Events[EventID] then + -- Create a WEAK table to ensure that the garbage collector is cleaning the event links when the object usage is cleaned. + self.Events[EventID] = {} + end + + -- Each event has a subtable of EventClasses, ordered by EventPriority. + local EventPriority = EventClass:GetEventPriority() + + if not self.Events[EventID][EventPriority] then + self.Events[EventID][EventPriority] = setmetatable( {}, { __mode = "k" } ) + end + + if not self.Events[EventID][EventPriority][EventClass] then + self.Events[EventID][EventPriority][EventClass] = {} + end + + return self.Events[EventID][EventPriority][EventClass] +end + +--- Removes a subscription +-- @param #EVENT self +-- @param Core.Base#BASE EventClass The self instance of the class for which the event is. +-- @param DCS#world.event EventID Event ID. +-- @return #EVENT self +function EVENT:RemoveEvent( EventClass, EventID ) + + -- Debug info. + self:F2( { "Removing subscription for class: ", EventClass:GetClassNameAndID() } ) + + -- Get event prio. + local EventPriority = EventClass:GetEventPriority() + + -- Events. + self.Events = self.Events or {} + self.Events[EventID] = self.Events[EventID] or {} + self.Events[EventID][EventPriority] = self.Events[EventID][EventPriority] or {} + + -- Remove + self.Events[EventID][EventPriority][EventClass] = nil + + return self +end + +--- Resets subscriptions. +-- @param #EVENT self +-- @param Core.Base#BASE EventClass The self instance of the class for which the event is. +-- @param DCS#world.event EventID Event ID. +-- @return #EVENT.Events +function EVENT:Reset( EventObject ) --R2.1 + + self:F( { "Resetting subscriptions for class: ", EventObject:GetClassNameAndID() } ) + + local EventPriority = EventObject:GetEventPriority() + + for EventID, EventData in pairs( self.Events ) do + if self.EventsDead then + if self.EventsDead[EventID] then + if self.EventsDead[EventID][EventPriority] then + if self.EventsDead[EventID][EventPriority][EventObject] then + self.Events[EventID][EventPriority][EventObject] = self.EventsDead[EventID][EventPriority][EventObject] + end + end + end + end + end +end + + +--- Clears all event subscriptions for a @{Core.Base#BASE} derived object. +-- @param #EVENT self +-- @param Core.Base#BASE EventClass The self class object for which the events are removed. +-- @return #EVENT self +function EVENT:RemoveAll(EventClass) + + local EventClassName = EventClass:GetClassNameAndID() + + -- Get Event prio. + local EventPriority = EventClass:GetEventPriority() + + for EventID, EventData in pairs( self.Events ) do + self.Events[EventID][EventPriority][EventClass] = nil + end + + return self +end + + + +--- Create an OnDead event handler for a group +-- @param #EVENT self +-- @param #table EventTemplate +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param EventClass The instance of the class for which the event is. +-- @param #function OnEventFunction +-- @return #EVENT self +function EVENT:OnEventForTemplate( EventTemplate, EventFunction, EventClass, EventID ) + self:F2( EventTemplate.name ) + + for EventUnitID, EventUnit in pairs( EventTemplate.units ) do + self:OnEventForUnit( EventUnit.name, EventFunction, EventClass, EventID ) + end + return self +end + +--- Set a new listener for an `S_EVENT_X` event independent from a unit or a weapon. +-- @param #EVENT self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Core.Base#BASE EventClass The self instance of the class for which the event is captured. When the event happens, the event process will be called in this class provided. +-- @param EventID +-- @return #EVENT +function EVENT:OnEventGeneric( EventFunction, EventClass, EventID ) + self:F2( { EventID, EventClass, EventFunction } ) + + local EventData = self:Init( EventID, EventClass ) + EventData.EventFunction = EventFunction + + return self +end + + +--- Set a new listener for an `S_EVENT_X` event for a UNIT. +-- @param #EVENT self +-- @param #string UnitName The name of the UNIT. +-- @param #function EventFunction The function to be called when the event occurs for the GROUP. +-- @param Core.Base#BASE EventClass The self instance of the class for which the event is. +-- @param EventID +-- @return #EVENT self +function EVENT:OnEventForUnit( UnitName, EventFunction, EventClass, EventID ) + self:F2( UnitName ) + + local EventData = self:Init( EventID, EventClass ) + EventData.EventUnit = true + EventData.EventFunction = EventFunction + return self +end + +--- Set a new listener for an S_EVENT_X event for a GROUP. +-- @param #EVENT self +-- @param #string GroupName The name of the GROUP. +-- @param #function EventFunction The function to be called when the event occurs for the GROUP. +-- @param Core.Base#BASE EventClass The self instance of the class for which the event is. +-- @param #number EventID Event ID. +-- @param ... Optional arguments passed to the event function. +-- @return #EVENT self +function EVENT:OnEventForGroup( GroupName, EventFunction, EventClass, EventID, ... ) + + local Event = self:Init( EventID, EventClass ) + Event.EventGroup = true + Event.EventFunction = EventFunction + Event.Params = arg + return self +end + +do -- OnBirth + + --- Create an OnBirth event handler for a group + -- @param #EVENT self + -- @param Wrapper.Group#GROUP EventGroup + -- @param #function EventFunction The function to be called when the event occurs for the unit. + -- @param EventClass The self instance of the class for which the event is. + -- @return #EVENT self + function EVENT:OnBirthForTemplate( EventTemplate, EventFunction, EventClass ) + self:F2( EventTemplate.name ) + + self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, EVENTS.Birth ) + + return self + end + +end + +do -- OnCrash + + --- Create an OnCrash event handler for a group + -- @param #EVENT self + -- @param Wrapper.Group#GROUP EventGroup + -- @param #function EventFunction The function to be called when the event occurs for the unit. + -- @param EventClass The self instance of the class for which the event is. + -- @return #EVENT + function EVENT:OnCrashForTemplate( EventTemplate, EventFunction, EventClass ) + self:F2( EventTemplate.name ) + + self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, EVENTS.Crash ) + + return self + end + +end + +do -- OnDead + + --- Create an OnDead event handler for a group + -- @param #EVENT self + -- @param Wrapper.Group#GROUP EventGroup The GROUP object. + -- @param #function EventFunction The function to be called when the event occurs for the unit. + -- @param #table EventClass The self instance of the class for which the event is. + -- @return #EVENT self + function EVENT:OnDeadForTemplate( EventTemplate, EventFunction, EventClass ) + self:F2( EventTemplate.name ) + + self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, EVENTS.Dead ) + + return self + end + +end + + +do -- OnLand + + --- Create an OnLand event handler for a group + -- @param #EVENT self + -- @param #table EventTemplate + -- @param #function EventFunction The function to be called when the event occurs for the unit. + -- @param #table EventClass The self instance of the class for which the event is. + -- @return #EVENT self + function EVENT:OnLandForTemplate( EventTemplate, EventFunction, EventClass ) + self:F2( EventTemplate.name ) + + self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, EVENTS.Land ) + + return self + end + +end + +do -- OnTakeOff + + --- Create an OnTakeOff event handler for a group + -- @param #EVENT self + -- @param #table EventTemplate Template table. + -- @param #function EventFunction The function to be called when the event occurs for the unit. + -- @param #table EventClass The self instance of the class for which the event is. + -- @return #EVENT self + function EVENT:OnTakeOffForTemplate( EventTemplate, EventFunction, EventClass ) + self:F2( EventTemplate.name ) + + self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, EVENTS.Takeoff ) + + return self + end + +end + +do -- OnEngineShutDown + + --- Create an OnDead event handler for a group + -- @param #EVENT self + -- @param #table EventTemplate + -- @param #function EventFunction The function to be called when the event occurs for the unit. + -- @param EventClass The self instance of the class for which the event is. + -- @return #EVENT + function EVENT:OnEngineShutDownForTemplate( EventTemplate, EventFunction, EventClass ) + self:F2( EventTemplate.name ) + + self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, EVENTS.EngineShutdown ) + + return self + end + +end + +do -- Event Creation + + --- Creation of a New Cargo Event. + -- @param #EVENT self + -- @param AI.AI_Cargo#AI_CARGO Cargo The Cargo created. + function EVENT:CreateEventNewCargo( Cargo ) + self:F( { Cargo } ) + + local Event = { + id = EVENTS.NewCargo, + time = timer.getTime(), + cargo = Cargo, + } + + world.onEvent( Event ) + end + + --- Creation of a Cargo Deletion Event. + -- @param #EVENT self + -- @param AI.AI_Cargo#AI_CARGO Cargo The Cargo created. + function EVENT:CreateEventDeleteCargo( Cargo ) + self:F( { Cargo } ) + + local Event = { + id = EVENTS.DeleteCargo, + time = timer.getTime(), + cargo = Cargo, + } + + world.onEvent( Event ) + end + + --- Creation of a New Zone Event. + -- @param #EVENT self + -- @param Core.Zone#ZONE_BASE Zone The Zone created. + function EVENT:CreateEventNewZone( Zone ) + self:F( { Zone } ) + + local Event = { + id = EVENTS.NewZone, + time = timer.getTime(), + zone = Zone, + } + + world.onEvent( Event ) + end + + --- Creation of a Zone Deletion Event. + -- @param #EVENT self + -- @param Core.Zone#ZONE_BASE Zone The Zone created. + function EVENT:CreateEventDeleteZone( Zone ) + self:F( { Zone } ) + + local Event = { + id = EVENTS.DeleteZone, + time = timer.getTime(), + zone = Zone, + } + + world.onEvent( Event ) + end + + --- Creation of a New ZoneGoal Event. + -- @param #EVENT self + -- @param Core.Functional#ZONE_GOAL ZoneGoal The ZoneGoal created. + function EVENT:CreateEventNewZoneGoal( ZoneGoal ) + self:F( { ZoneGoal } ) + + local Event = { + id = EVENTS.NewZoneGoal, + time = timer.getTime(), + ZoneGoal = ZoneGoal, + } + + world.onEvent( Event ) + end + + + --- Creation of a ZoneGoal Deletion Event. + -- @param #EVENT self + -- @param Core.ZoneGoal#ZONE_GOAL ZoneGoal The ZoneGoal created. + function EVENT:CreateEventDeleteZoneGoal( ZoneGoal ) + self:F( { ZoneGoal } ) + + local Event = { + id = EVENTS.DeleteZoneGoal, + time = timer.getTime(), + ZoneGoal = ZoneGoal, + } + + world.onEvent( Event ) + end + + + --- Creation of a S_EVENT_PLAYER_ENTER_UNIT Event. + -- @param #EVENT self + -- @param Wrapper.Unit#UNIT PlayerUnit. + function EVENT:CreateEventPlayerEnterUnit( PlayerUnit ) + self:F( { PlayerUnit } ) + + local Event = { + id = EVENTS.PlayerEnterUnit, + time = timer.getTime(), + initiator = PlayerUnit:GetDCSObject() + } + + world.onEvent( Event ) + end + + --- Creation of a S_EVENT_PLAYER_ENTER_AIRCRAFT event. + -- @param #EVENT self + -- @param Wrapper.Unit#UNIT PlayerUnit The aircraft unit the player entered. + function EVENT:CreateEventPlayerEnterAircraft( PlayerUnit ) + self:F( { PlayerUnit } ) + + local Event = { + id = EVENTS.PlayerEnterAircraft, + time = timer.getTime(), + initiator = PlayerUnit:GetDCSObject() + } + + world.onEvent( Event ) + end + +end + +--- Main event function. +-- @param #EVENT self +-- @param #EVENTDATA Event Event data table. +function EVENT:onEvent( Event ) + + local ErrorHandler = function( errmsg ) + + env.info( "Error in SCHEDULER function:" .. errmsg ) + if BASE.Debug ~= nil then + env.info( debug.traceback() ) + end + + return errmsg + end + + + -- Get event meta data. + local EventMeta = _EVENTMETA[Event.id] + + -- Check if this is a known event? + if EventMeta then + + if self and self.Events and self.Events[Event.id] and self.MissionEnd==false and (Event.initiator~=nil or (Event.initiator==nil and Event.id~=EVENTS.PlayerLeaveUnit)) then + + if Event.id and Event.id == EVENTS.MissionEnd then + self.MissionEnd = true + end + + if Event.initiator then + + Event.IniObjectCategory = Event.initiator:getCategory() + + if Event.IniObjectCategory == Object.Category.UNIT then + Event.IniDCSUnit = Event.initiator + Event.IniDCSUnitName = Event.IniDCSUnit:getName() + Event.IniUnitName = Event.IniDCSUnitName + Event.IniDCSGroup = Event.IniDCSUnit:getGroup() + Event.IniUnit = UNIT:FindByName( Event.IniDCSUnitName ) + if not Event.IniUnit then + -- Unit can be a CLIENT. Most likely this will be the case ... + Event.IniUnit = CLIENT:FindByName( Event.IniDCSUnitName, '', true ) + end + Event.IniDCSGroupName = "" + if Event.IniDCSGroup and Event.IniDCSGroup:isExist() then + Event.IniDCSGroupName = Event.IniDCSGroup:getName() + Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) + --if Event.IniGroup then + Event.IniGroupName = Event.IniDCSGroupName + --end + end + Event.IniPlayerName = Event.IniDCSUnit:getPlayerName() + Event.IniCoalition = Event.IniDCSUnit:getCoalition() + Event.IniTypeName = Event.IniDCSUnit:getTypeName() + Event.IniCategory = Event.IniDCSUnit:getDesc().category + end + + if Event.IniObjectCategory == Object.Category.STATIC then + + if Event.id==31 then + + -- Event.initiator is a Static object representing the pilot. But getName() errors due to DCS bug. + Event.IniDCSUnit = Event.initiator + local ID=Event.initiator.id_ + Event.IniDCSUnitName = string.format("Ejected Pilot ID %s", tostring(ID)) + Event.IniUnitName = Event.IniDCSUnitName + Event.IniCoalition = 0 + Event.IniCategory = 0 + Event.IniTypeName = "Ejected Pilot" + elseif Event.id == 33 then -- ejection seat discarded + Event.IniDCSUnit = Event.initiator + local ID=Event.initiator.id_ + Event.IniDCSUnitName = string.format("Ejection Seat ID %s", tostring(ID)) + Event.IniUnitName = Event.IniDCSUnitName + Event.IniCoalition = 0 + Event.IniCategory = 0 + Event.IniTypeName = "Ejection Seat" + else + Event.IniDCSUnit = Event.initiator + Event.IniDCSUnitName = Event.IniDCSUnit:getName() + Event.IniUnitName = Event.IniDCSUnitName + Event.IniUnit = STATIC:FindByName( Event.IniDCSUnitName, false ) + Event.IniCoalition = Event.IniDCSUnit:getCoalition() + Event.IniCategory = Event.IniDCSUnit:getDesc().category + Event.IniTypeName = Event.IniDCSUnit:getTypeName() + end + end + + if Event.IniObjectCategory == Object.Category.CARGO then + Event.IniDCSUnit = Event.initiator + Event.IniDCSUnitName = Event.IniDCSUnit:getName() + Event.IniUnitName = Event.IniDCSUnitName + Event.IniUnit = CARGO:FindByName( Event.IniDCSUnitName ) + Event.IniCoalition = Event.IniDCSUnit:getCoalition() + Event.IniCategory = Event.IniDCSUnit:getDesc().category + Event.IniTypeName = Event.IniDCSUnit:getTypeName() + end + + if Event.IniObjectCategory == Object.Category.SCENERY then + Event.IniDCSUnit = Event.initiator + Event.IniDCSUnitName = Event.IniDCSUnit:getName() + Event.IniUnitName = Event.IniDCSUnitName + Event.IniUnit = SCENERY:Register( Event.IniDCSUnitName, Event.initiator ) + Event.IniCategory = Event.IniDCSUnit:getDesc().category + Event.IniTypeName = Event.initiator:isExist() and Event.IniDCSUnit:getTypeName() or "SCENERY" -- TODO: Bug fix for 2.1! + end + + if Event.IniObjectCategory == Object.Category.BASE then + Event.IniDCSUnit = Event.initiator + Event.IniDCSUnitName = Event.IniDCSUnit:getName() + Event.IniUnitName = Event.IniDCSUnitName + Event.IniUnit = AIRBASE:FindByName(Event.IniDCSUnitName) + Event.IniCoalition = Event.IniDCSUnit:getCoalition() + Event.IniCategory = Event.IniDCSUnit:getDesc().category + Event.IniTypeName = Event.IniDCSUnit:getTypeName() + end + end + + if Event.target then + + Event.TgtObjectCategory = Event.target:getCategory() + + if Event.TgtObjectCategory == Object.Category.UNIT then + Event.TgtDCSUnit = Event.target + Event.TgtDCSGroup = Event.TgtDCSUnit:getGroup() + Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() + Event.TgtUnitName = Event.TgtDCSUnitName + Event.TgtUnit = UNIT:FindByName( Event.TgtDCSUnitName ) + Event.TgtDCSGroupName = "" + if Event.TgtDCSGroup and Event.TgtDCSGroup:isExist() then + Event.TgtDCSGroupName = Event.TgtDCSGroup:getName() + Event.TgtGroup = GROUP:FindByName( Event.TgtDCSGroupName ) + --if Event.TgtGroup then + Event.TgtGroupName = Event.TgtDCSGroupName + --end + end + Event.TgtPlayerName = Event.TgtDCSUnit:getPlayerName() + Event.TgtCoalition = Event.TgtDCSUnit:getCoalition() + Event.TgtCategory = Event.TgtDCSUnit:getDesc().category + Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() + end + + if Event.TgtObjectCategory == Object.Category.STATIC then + -- get base data + Event.TgtDCSUnit = Event.target + if Event.target:isExist() and Event.id ~= 33 then -- leave out ejected seat object + Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() + Event.TgtUnitName = Event.TgtDCSUnitName + Event.TgtUnit = STATIC:FindByName( Event.TgtDCSUnitName, false ) + Event.TgtCoalition = Event.TgtDCSUnit:getCoalition() + Event.TgtCategory = Event.TgtDCSUnit:getDesc().category + Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() + else + Event.TgtDCSUnitName = string.format("No target object for Event ID %s", tostring(Event.id)) + Event.TgtUnitName = Event.TgtDCSUnitName + Event.TgtUnit = nil + Event.TgtCoalition = 0 + Event.TgtCategory = 0 + if Event.id == 6 then + Event.TgtTypeName = "Ejected Pilot" + Event.TgtDCSUnitName = string.format("Ejected Pilot ID %s", tostring(Event.IniDCSUnitName)) + Event.TgtUnitName = Event.TgtDCSUnitName + elseif Event.id == 33 then + Event.TgtTypeName = "Ejection Seat" + Event.TgtDCSUnitName = string.format("Ejection Seat ID %s", tostring(Event.IniDCSUnitName)) + Event.TgtUnitName = Event.TgtDCSUnitName + else + Event.TgtTypeName = "Static" + end + end + end + + if Event.TgtObjectCategory == Object.Category.SCENERY then + Event.TgtDCSUnit = Event.target + Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() + Event.TgtUnitName = Event.TgtDCSUnitName + Event.TgtUnit = SCENERY:Register( Event.TgtDCSUnitName, Event.target ) + Event.TgtCategory = Event.TgtDCSUnit:getDesc().category + Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() + end + end + + if Event.weapon then + Event.Weapon = Event.weapon + Event.WeaponName = Event.Weapon:getTypeName() + Event.WeaponUNIT = CLIENT:Find( Event.Weapon, '', true ) -- Sometimes, the weapon is a player unit! + Event.WeaponPlayerName = Event.WeaponUNIT and Event.Weapon:getPlayerName() + Event.WeaponCoalition = Event.WeaponUNIT and Event.Weapon:getCoalition() + Event.WeaponCategory = Event.WeaponUNIT and Event.Weapon:getDesc().category + Event.WeaponTypeName = Event.WeaponUNIT and Event.Weapon:getTypeName() + --Event.WeaponTgtDCSUnit = Event.Weapon:getTarget() + end + + -- Place should be given for takeoff and landing events as well as base captured. It should be a DCS airbase. + if Event.place then + if Event.id==EVENTS.LandingAfterEjection then + -- Place is here the UNIT of which the pilot ejected. + --local name=Event.place:getName() -- This returns a DCS error "Airbase doesn't exit" :( + -- However, this is not a big thing, as the aircraft the pilot ejected from is usually long crashed before the ejected pilot touches the ground. + --Event.Place=UNIT:Find(Event.place) + else + Event.Place=AIRBASE:Find(Event.place) + Event.PlaceName=Event.Place:GetName() + end + end + + -- Mark points. + if Event.idx then + Event.MarkID=Event.idx + Event.MarkVec3=Event.pos + Event.MarkCoordinate=COORDINATE:NewFromVec3(Event.pos) + Event.MarkText=Event.text + Event.MarkCoalition=Event.coalition + Event.MarkGroupID = Event.groupID + end + + if Event.cargo then + Event.Cargo = Event.cargo + Event.CargoName = Event.cargo.Name + end + + if Event.zone then + Event.Zone = Event.zone + Event.ZoneName = Event.zone.ZoneName + end + + local PriorityOrder = EventMeta.Order + local PriorityBegin = PriorityOrder == -1 and 5 or 1 + local PriorityEnd = PriorityOrder == -1 and 1 or 5 + + if Event.IniObjectCategory ~= Object.Category.STATIC then + self:F( { EventMeta.Text, Event, Event.IniDCSUnitName, Event.TgtDCSUnitName, PriorityOrder } ) + end + + for EventPriority = PriorityBegin, PriorityEnd, PriorityOrder do + + if self.Events[Event.id][EventPriority] then + + -- Okay, we got the event from DCS. Now loop the SORTED self.EventSorted[] table for the received Event.id, and for each EventData registered, check if a function needs to be called. + for EventClass, EventData in pairs( self.Events[Event.id][EventPriority] ) do + + --if Event.IniObjectCategory ~= Object.Category.STATIC then + -- self:E( { "Evaluating: ", EventClass:GetClassNameAndID() } ) + --end + + Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) + Event.TgtGroup = GROUP:FindByName( Event.TgtDCSGroupName ) + + -- If the EventData is for a UNIT, the call directly the EventClass EventFunction for that UNIT. + if EventData.EventUnit then + + -- So now the EventClass must be a UNIT class!!! We check if it is still "Alive". + if EventClass:IsAlive() or + Event.id == EVENTS.PlayerEnterUnit or + Event.id == EVENTS.Crash or + Event.id == EVENTS.Dead or + Event.id == EVENTS.RemoveUnit then + + local UnitName = EventClass:GetName() + + if ( EventMeta.Side == "I" and UnitName == Event.IniDCSUnitName ) or + ( EventMeta.Side == "T" and UnitName == Event.TgtDCSUnitName ) then + + -- First test if a EventFunction is Set, otherwise search for the default function + if EventData.EventFunction then + + if Event.IniObjectCategory ~= 3 then + self:F( { "Calling EventFunction for UNIT ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName, EventPriority } ) + end + + local Result, Value = xpcall( + function() + return EventData.EventFunction( EventClass, Event ) + end, ErrorHandler ) + + else + + -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. + local EventFunction = EventClass[ EventMeta.Event ] + if EventFunction and type( EventFunction ) == "function" then + + -- Now call the default event function. + if Event.IniObjectCategory ~= 3 then + self:F( { "Calling " .. EventMeta.Event .. " for Class ", EventClass:GetClassNameAndID(), EventPriority } ) + end + + local Result, Value = xpcall( + function() + return EventFunction( EventClass, Event ) + end, ErrorHandler ) + end + end + end + else + -- The EventClass is not alive anymore, we remove it from the EventHandlers... + self:RemoveEvent( EventClass, Event.id ) + end + + else + + --- If the EventData is for a GROUP, the call directly the EventClass EventFunction for the UNIT in that GROUP. + if EventData.EventGroup then + + -- So now the EventClass must be a GROUP class!!! We check if it is still "Alive". + if EventClass:IsAlive() or + Event.id == EVENTS.PlayerEnterUnit or + Event.id == EVENTS.Crash or + Event.id == EVENTS.Dead or + Event.id == EVENTS.RemoveUnit then + + -- We can get the name of the EventClass, which is now always a GROUP object. + local GroupName = EventClass:GetName() + + if ( EventMeta.Side == "I" and GroupName == Event.IniDCSGroupName ) or + ( EventMeta.Side == "T" and GroupName == Event.TgtDCSGroupName ) then + + -- First test if a EventFunction is Set, otherwise search for the default function + if EventData.EventFunction then + + if Event.IniObjectCategory ~= 3 then + self:F( { "Calling EventFunction for GROUP ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName, EventPriority } ) + end + + local Result, Value = xpcall( + function() + return EventData.EventFunction( EventClass, Event, unpack( EventData.Params ) ) + end, ErrorHandler ) + + else + + -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. + local EventFunction = EventClass[ EventMeta.Event ] + if EventFunction and type( EventFunction ) == "function" then + + -- Now call the default event function. + if Event.IniObjectCategory ~= 3 then + self:F( { "Calling " .. EventMeta.Event .. " for GROUP ", EventClass:GetClassNameAndID(), EventPriority } ) + end + + local Result, Value = xpcall( + function() + return EventFunction( EventClass, Event, unpack( EventData.Params ) ) + end, ErrorHandler ) + end + end + end + else + -- The EventClass is not alive anymore, we remove it from the EventHandlers... + --self:RemoveEvent( EventClass, Event.id ) + end + else + + -- If the EventData is not bound to a specific unit, then call the EventClass EventFunction. + -- Note that here the EventFunction will need to implement and determine the logic for the relevant source- or target unit, or weapon. + if not EventData.EventUnit then + + -- First test if a EventFunction is Set, otherwise search for the default function + if EventData.EventFunction then + + -- There is an EventFunction defined, so call the EventFunction. + if Event.IniObjectCategory ~= 3 then + self:F2( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID(), EventPriority } ) + end + local Result, Value = xpcall( + function() + return EventData.EventFunction( EventClass, Event ) + end, ErrorHandler ) + else + + -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. + local EventFunction = EventClass[ EventMeta.Event ] + if EventFunction and type( EventFunction ) == "function" then + + -- Now call the default event function. + if Event.IniObjectCategory ~= 3 then + self:F2( { "Calling " .. EventMeta.Event .. " for Class ", EventClass:GetClassNameAndID(), EventPriority } ) + end + + local Result, Value = xpcall( + function() + local Result, Value = EventFunction( EventClass, Event ) + return Result, Value + end, ErrorHandler ) + end + end + + end + end + end + end + end + end + + -- When cargo was deleted, it may probably be because of an S_EVENT_DEAD. + -- However, in the loading logic, an S_EVENT_DEAD is also generated after a Destroy() call. + -- And this is a problem because it will remove all entries from the SET_CARGOs. + -- To prevent this from happening, the Cargo object has a flag NoDestroy. + -- When true, the SET_CARGO won't Remove the Cargo object from the set. + -- But we need to switch that flag off after the event handlers have been called. + if Event.id == EVENTS.DeleteCargo then + Event.Cargo.NoDestroy = nil + end + else + self:T( { EventMeta.Text, Event } ) + end + else + self:E(string.format("WARNING: Could not get EVENTMETA data for event ID=%d! Is this an unknown/new DCS event?", tostring(Event.id))) + end + + Event = nil +end + +--- The EVENTHANDLER structure. +-- @type EVENTHANDLER +-- @extends Core.Base#BASE +EVENTHANDLER = { + ClassName = "EVENTHANDLER", + ClassID = 0, +} + +--- The EVENTHANDLER constructor. +-- @param #EVENTHANDLER self +-- @return #EVENTHANDLER self +function EVENTHANDLER:New() + self = BASE:Inherit( self, BASE:New() ) -- #EVENTHANDLER + return self +end +--- **Core** - Manages various settings for running missions, consumed by moose classes and provides a menu system for players to tweak settings in running missions. +-- +-- === +-- +-- ## Features: +-- +-- * Provide a settings menu system to the players. +-- * Provide a player settings menu and an overall mission settings menu. +-- * Mission settings provide default settings, while player settings override mission settings. +-- * Provide a menu to select between different coordinate formats for A2G coordinates. +-- * Provide a menu to select between different coordinate formats for A2A coordinates. +-- * Provide a menu to select between different message time duration options. +-- * Provide a menu to select between different metric systems. +-- +-- === +-- +-- The documentation of the SETTINGS class can be found further in this document. +-- +-- === +-- +-- # **AUTHORS and CONTRIBUTIONS** +-- +-- ### Contributions: +-- +-- ### Authors: +-- +-- * **FlightControl**: Design & Programming +-- +-- @module Core.Settings +-- @image Core_Settings.JPG + + +--- @type SETTINGS +-- @extends Core.Base#BASE + +--- Takes care of various settings that influence the behaviour of certain functionalities and classes within the MOOSE framework. +-- +-- === +-- +-- The SETTINGS class takes care of various settings that influence the behaviour of certain functionalities and classes within the MOOSE framework. +-- SETTINGS can work on 2 levels: +-- +-- - **Default settings**: A running mission has **Default settings**. +-- - **Player settings**: For each player its own **Player settings** can be defined, overriding the **Default settings**. +-- +-- So, when there isn't any **Player setting** defined for a player for a specific setting, or, the player cannot be identified, the **Default setting** will be used instead. +-- +-- # 1) \_SETTINGS object +-- +-- MOOSE defines by default a singleton object called **\_SETTINGS**. Use this object to modify all the **Default settings** for a running mission. +-- For each player, MOOSE will automatically allocate also a **player settings** object, and will expose a radio menu to allow the player to adapt the settings to his own preferences. +-- +-- # 2) SETTINGS Menu +-- +-- Settings can be adapted by the Players and by the Mission Administrator through **radio menus, which are automatically available in the mission**. +-- These menus can be found **on level F10 under "Settings"**. There are two kinds of menus generated by the system. +-- +-- ## 2.1) Default settings menu +-- +-- A menu is created automatically per Command Center that allows to modify the **Default** settings. +-- So, when joining a CC unit, a menu will be available that allows to change the settings parameters **FOR ALL THE PLAYERS**! +-- Note that the **Default settings** will only be used when a player has not choosen its own settings. +-- +-- ## 2.2) Player settings menu +-- +-- A menu is created automatically per Player Slot (group) that allows to modify the **Player** settings. +-- So, when joining a slot, a menu wil be available that allows to change the settings parameters **FOR THE PLAYER ONLY**! +-- Note that when a player has not chosen a specific setting, the **Default settings** will be used. +-- +-- ## 2.3) Show or Hide the Player Setting menus +-- +-- Of course, it may be requried not to show any setting menus. In this case, a method is available on the **\_SETTINGS object**. +-- Use @{#SETTINGS.SetPlayerMenuOff}() to hide the player menus, and use @{#SETTINGS.SetPlayerMenuOn}() show the player menus. +-- Note that when this method is used, any player already in a slot will not have its menus visibility changed. +-- The option will only have effect when a player enters a new slot or changes a slot. +-- +-- Example: +-- +-- _SETTINGS:SetPlayerMenuOff() -- will disable the player menus. +-- _SETTINGS:SetPlayerMenuOn() -- will enable the player menus. +-- -- But only when a player exits and reenters the slot these settings will have effect! +-- +-- +-- # 3) Settings +-- +-- There are different settings that are managed and applied within the MOOSE framework. +-- See below a comprehensive description of each. +-- +-- ## 3.1) **A2G coordinates** display formatting +-- +-- ### 3.1.1) A2G coordinates setting **types** +-- +-- Will customize which display format is used to indicate A2G coordinates in text as part of the Command Center communications. +-- +-- - A2G BR: [Bearing Range](https://en.wikipedia.org/wiki/Bearing_(navigation)). +-- - A2G MGRS: The [Military Grid Reference System](https://en.wikipedia.org/wiki/Military_Grid_Reference_System). The accuracy can also be adapted. +-- - A2G LL DMS: Lattitude Longitude [Degrees Minutes Seconds](https://en.wikipedia.org/wiki/Geographic_coordinate_conversion). The accuracy can also be adapted. +-- - A2G LL DDM: Lattitude Longitude [Decimal Degrees Minutes](https://en.wikipedia.org/wiki/Decimal_degrees). The accuracy can also be adapted. +-- +-- ### 3.1.2) A2G coordinates setting **menu** +-- +-- The settings can be changed by using the **Default settings menu** on the Command Center or the **Player settings menu** on the Player Slot. +-- +-- ### 3.1.3) A2G coordinates setting **methods** +-- +-- There are different methods that can be used to change the **System settings** using the \_SETTINGS object. +-- +-- - @{#SETTINGS.SetA2G_BR}(): Enable the BR display formatting by default. +-- - @{#SETTINGS.SetA2G_MGRS}(): Enable the MGRS display formatting by default. Use @{SETTINGS.SetMGRS_Accuracy}() to adapt the accuracy of the MGRS formatting. +-- - @{#SETTINGS.SetA2G_LL_DMS}(): Enable the LL DMS display formatting by default. Use @{SETTINGS.SetLL_Accuracy}() to adapt the accuracy of the Seconds formatting. +-- - @{#SETTINGS.SetA2G_LL_DDM}(): Enable the LL DDM display formatting by default. Use @{SETTINGS.SetLL_Accuracy}() to adapt the accuracy of the Seconds formatting. +-- +-- ### 3.1.4) A2G coordinates setting - additional notes +-- +-- One additional note on BR. In a situation when a BR coordinate should be given, +-- but there isn't any player context (no player unit to reference from), the MGRS formatting will be applied! +-- +-- ## 3.2) **A2A coordinates** formatting +-- +-- ### 3.2.1) A2A coordinates setting **types** +-- +-- Will customize which display format is used to indicate A2A coordinates in text as part of the Command Center communications. +-- +-- - A2A BRAA: [Bearing Range Altitude Aspect](https://en.wikipedia.org/wiki/Bearing_(navigation)). +-- - A2A MGRS: The [Military Grid Reference System](https://en.wikipedia.org/wiki/Military_Grid_Reference_System). The accuracy can also be adapted. +-- - A2A LL DMS: Lattitude Longitude [Degrees Minutes Seconds](https://en.wikipedia.org/wiki/Geographic_coordinate_conversion). The accuracy can also be adapted. +-- - A2A LL DDM: Lattitude Longitude [Decimal Degrees and Minutes](https://en.wikipedia.org/wiki/Decimal_degrees). The accuracy can also be adapted. +-- - A2A BULLS: [Bullseye](http://falcon4.wikidot.com/concepts:bullseye). +-- +-- ### 3.2.2) A2A coordinates setting **menu** +-- +-- The settings can be changed by using the **Default settings menu** on the Command Center or the **Player settings menu** on the Player Slot. +-- +-- ### 3.2.3) A2A coordinates setting **methods** +-- +-- There are different methods that can be used to change the **System settings** using the \_SETTINGS object. +-- +-- - @{#SETTINGS.SetA2A_BRAA}(): Enable the BR display formatting by default. +-- - @{#SETTINGS.SetA2A_MGRS}(): Enable the MGRS display formatting by default. Use @{SETTINGS.SetMGRS_Accuracy}() to adapt the accuracy of the MGRS formatting. +-- - @{#SETTINGS.SetA2A_LL_DMS}(): Enable the LL DMS display formatting by default. Use @{SETTINGS.SetLL_Accuracy}() to adapt the accuracy of the Seconds formatting. +-- - @{#SETTINGS.SetA2A_LL_DDM}(): Enable the LL DDM display formatting by default. Use @{SETTINGS.SetLL_Accuracy}() to adapt the accuracy of the Seconds formatting. +-- - @{#SETTINGS.SetA2A_BULLS}(): Enable the BULLSeye display formatting by default. +-- +-- ### 3.2.4) A2A coordinates settings - additional notes +-- +-- One additional note on BRAA. In a situation when a BRAA coordinate should be given, +-- but there isn't any player context (no player unit to reference from), the MGRS formatting will be applied! +-- +-- ## 3.3) **Measurements** formatting +-- +-- ### 3.3.1) Measurements setting **types** +-- +-- Will customize the measurements system being used as part as part of the Command Center communications. +-- +-- - **Metrics** system: Applies the [Metrics system](https://en.wikipedia.org/wiki/Metric_system) ... +-- - **Imperial** system: Applies the [Imperial system](https://en.wikipedia.org/wiki/Imperial_units) ... +-- +-- ### 3.3.2) Measurements setting **menu** +-- +-- The settings can be changed by using the **Default settings menu** on the Command Center or the **Player settings menu** on the Player Slot. +-- +-- ### 3.3.3) Measurements setting **methods** +-- +-- There are different methods that can be used to change the **Default settings** using the \_SETTINGS object. +-- +-- - @{#SETTINGS.SetMetric}(): Enable the Metric system. +-- - @{#SETTINGS.SetImperial}(): Enable the Imperial system. +-- +-- ## 3.4) **Message** display times +-- +-- ### 3.4.1) Message setting **types** +-- +-- There are various **Message Types** that will influence the duration how long a message will appear as part of the Command Center communications. +-- +-- - **Update** message: A short update message. +-- - **Information** message: Provides new information **while** executing a mission. +-- - **Briefing** message: Provides a complete briefing **before** executing a mission. +-- - **Overview report**: Provides a short report overview, the summary of the report. +-- - **Detailed report**: Provides a complete report. +-- +-- ### 3.4.2) Message setting **menu** +-- +-- The settings can be changed by using the **Default settings menu** on the Command Center or the **Player settings menu** on the Player Slot. +-- +-- Each Message Type has specific timings that will be applied when the message is displayed. +-- The Settings Menu will provide for each Message Type a selection of proposed durations from which can be choosen. +-- So the player can choose its own amount of seconds how long a message should be displayed of a certain type. +-- Note that **Update** messages can be chosen not to be displayed at all! +-- +-- ### 3.4.3) Message setting **methods** +-- +-- There are different methods that can be used to change the **System settings** using the \_SETTINGS object. +-- +-- - @{#SETTINGS.SetMessageTime}(): Define for a specific @{Message.MESSAGE.MessageType} the duration to be displayed in seconds. +-- - @{#SETTINGS.GetMessageTime}(): Retrieves for a specific @{Message.MESSAGE.MessageType} the duration to be displayed in seconds. +-- +-- ## 3.5) **Era** of the battle +-- +-- The threat level metric is scaled according the era of the battle. A target that is AAA, will pose a much greather threat in WWII than on modern warfare. +-- Therefore, there are 4 era that are defined within the settings: +-- +-- - **WWII** era: Use for warfare with equipment during the world war II time. +-- - **Korea** era: Use for warfare with equipment during the Korea war time. +-- - **Cold War** era: Use for warfare with equipment during the cold war time. +-- - **Modern** era: Use for warfare with modern equipment in the 2000s. +-- +-- There are different API defined that you can use with the _SETTINGS object to configure your mission script to work in one of the 4 era: +-- @{#SETTINGS.SetEraWWII}(), @{#SETTINGS.SetEraKorea}(), @{#SETTINGS.SetEraCold}(), @{#SETTINGS.SetEraModern}() +-- +-- === +-- +-- @field #SETTINGS +SETTINGS = { + ClassName = "SETTINGS", + ShowPlayerMenu = true, + MenuShort = false, + MenuStatic = false, +} + +SETTINGS.__Enum = {} + +--- @type SETTINGS.__Enum.Era +-- @field #number WWII +-- @field #number Korea +-- @field #number Cold +-- @field #number Modern +SETTINGS.__Enum.Era = { + WWII = 1, + Korea = 2, + Cold = 3, + Modern = 4, +} + + +do -- SETTINGS + + --- SETTINGS constructor. + -- @param #SETTINGS self + -- @param #string PlayerName (Optional) Set settings for this player. + -- @return #SETTINGS + function SETTINGS:Set( PlayerName ) + + if PlayerName == nil then + local self = BASE:Inherit( self, BASE:New() ) -- #SETTINGS + self:SetMetric() -- Defaults + self:SetA2G_BR() -- Defaults + self:SetA2A_BRAA() -- Defaults + self:SetLL_Accuracy( 3 ) -- Defaults + self:SetMGRS_Accuracy( 5 ) -- Defaults + self:SetMessageTime( MESSAGE.Type.Briefing, 180 ) + self:SetMessageTime( MESSAGE.Type.Detailed, 60 ) + self:SetMessageTime( MESSAGE.Type.Information, 30 ) + self:SetMessageTime( MESSAGE.Type.Overview, 60 ) + self:SetMessageTime( MESSAGE.Type.Update, 15 ) + self:SetEraModern() + return self + else + local Settings = _DATABASE:GetPlayerSettings( PlayerName ) + if not Settings then + Settings = BASE:Inherit( self, BASE:New() ) -- #SETTINGS + _DATABASE:SetPlayerSettings( PlayerName, Settings ) + end + return Settings + end + end + + --- Set short text for menus on (*true*) or off (*false*). + -- Short text are better suited for, e.g., VR. + -- @param #SETTINGS self + -- @param #boolean onoff If *true* use short menu texts. If *false* long ones (default). + function SETTINGS:SetMenutextShort(onoff) + _SETTINGS.MenuShort = onoff + end + + --- Set menu to be static. + -- @param #SETTINGS self + -- @param #boolean onoff If *true* menu is static. If *false* menu will be updated after changes (default). + function SETTINGS:SetMenuStatic(onoff) + _SETTINGS.MenuStatic = onoff + end + + --- Sets the SETTINGS metric. + -- @param #SETTINGS self + function SETTINGS:SetMetric() + self.Metric = true + end + + --- Gets if the SETTINGS is metric. + -- @param #SETTINGS self + -- @return #boolean true if metric. + function SETTINGS:IsMetric() + return ( self.Metric ~= nil and self.Metric == true ) or ( self.Metric == nil and _SETTINGS:IsMetric() ) + end + + --- Sets the SETTINGS imperial. + -- @param #SETTINGS self + function SETTINGS:SetImperial() + self.Metric = false + end + + --- Gets if the SETTINGS is imperial. + -- @param #SETTINGS self + -- @return #boolean true if imperial. + function SETTINGS:IsImperial() + return ( self.Metric ~= nil and self.Metric == false ) or ( self.Metric == nil and _SETTINGS:IsMetric() ) + end + + --- Sets the SETTINGS LL accuracy. + -- @param #SETTINGS self + -- @param #number LL_Accuracy + -- @return #SETTINGS + function SETTINGS:SetLL_Accuracy( LL_Accuracy ) + self.LL_Accuracy = LL_Accuracy + end + + --- Gets the SETTINGS LL accuracy. + -- @param #SETTINGS self + -- @return #number + function SETTINGS:GetLL_DDM_Accuracy() + return self.LL_DDM_Accuracy or _SETTINGS:GetLL_DDM_Accuracy() + end + + --- Sets the SETTINGS MGRS accuracy. + -- @param #SETTINGS self + -- @param #number MGRS_Accuracy + -- @return #SETTINGS + function SETTINGS:SetMGRS_Accuracy( MGRS_Accuracy ) + self.MGRS_Accuracy = MGRS_Accuracy + end + + --- Gets the SETTINGS MGRS accuracy. + -- @param #SETTINGS self + -- @return #number + function SETTINGS:GetMGRS_Accuracy() + return self.MGRS_Accuracy or _SETTINGS:GetMGRS_Accuracy() + end + + --- Sets the SETTINGS Message Display Timing of a MessageType + -- @param #SETTINGS self + -- @param Core.Message#MESSAGE MessageType The type of the message. + -- @param #number MessageTime The display time duration in seconds of the MessageType. + function SETTINGS:SetMessageTime( MessageType, MessageTime ) + self.MessageTypeTimings = self.MessageTypeTimings or {} + self.MessageTypeTimings[MessageType] = MessageTime + end + + + --- Gets the SETTINGS Message Display Timing of a MessageType + -- @param #SETTINGS self + -- @param Core.Message#MESSAGE MessageType The type of the message. + -- @return #number + function SETTINGS:GetMessageTime( MessageType ) + return ( self.MessageTypeTimings and self.MessageTypeTimings[MessageType] ) or _SETTINGS:GetMessageTime( MessageType ) + end + + --- Sets A2G LL DMS + -- @param #SETTINGS self + -- @return #SETTINGS + function SETTINGS:SetA2G_LL_DMS() + self.A2GSystem = "LL DMS" + end + + --- Sets A2G LL DDM + -- @param #SETTINGS self + -- @return #SETTINGS + function SETTINGS:SetA2G_LL_DDM() + self.A2GSystem = "LL DDM" + end + + --- Is LL DMS + -- @param #SETTINGS self + -- @return #boolean true if LL DMS + function SETTINGS:IsA2G_LL_DMS() + return ( self.A2GSystem and self.A2GSystem == "LL DMS" ) or ( not self.A2GSystem and _SETTINGS:IsA2G_LL_DMS() ) + end + + --- Is LL DDM + -- @param #SETTINGS self + -- @return #boolean true if LL DDM + function SETTINGS:IsA2G_LL_DDM() + return ( self.A2GSystem and self.A2GSystem == "LL DDM" ) or ( not self.A2GSystem and _SETTINGS:IsA2G_LL_DDM() ) + end + + --- Sets A2G MGRS + -- @param #SETTINGS self + -- @return #SETTINGS + function SETTINGS:SetA2G_MGRS() + self.A2GSystem = "MGRS" + end + + --- Is MGRS + -- @param #SETTINGS self + -- @return #boolean true if MGRS + function SETTINGS:IsA2G_MGRS() + return ( self.A2GSystem and self.A2GSystem == "MGRS" ) or ( not self.A2GSystem and _SETTINGS:IsA2G_MGRS() ) + end + + --- Sets A2G BRA + -- @param #SETTINGS self + -- @return #SETTINGS + function SETTINGS:SetA2G_BR() + self.A2GSystem = "BR" + end + + --- Is BRA + -- @param #SETTINGS self + -- @return #boolean true if BRA + function SETTINGS:IsA2G_BR() + return ( self.A2GSystem and self.A2GSystem == "BR" ) or ( not self.A2GSystem and _SETTINGS:IsA2G_BR() ) + end + + --- Sets A2A BRA + -- @param #SETTINGS self + -- @return #SETTINGS + function SETTINGS:SetA2A_BRAA() + self.A2ASystem = "BRAA" + end + + --- Is BRA + -- @param #SETTINGS self + -- @return #boolean true if BRA + function SETTINGS:IsA2A_BRAA() + return ( self.A2ASystem and self.A2ASystem == "BRAA" ) or ( not self.A2ASystem and _SETTINGS:IsA2A_BRAA() ) + end + + --- Sets A2A BULLS + -- @param #SETTINGS self + -- @return #SETTINGS + function SETTINGS:SetA2A_BULLS() + self.A2ASystem = "BULLS" + end + + --- Is BULLS + -- @param #SETTINGS self + -- @return #boolean true if BULLS + function SETTINGS:IsA2A_BULLS() + return ( self.A2ASystem and self.A2ASystem == "BULLS" ) or ( not self.A2ASystem and _SETTINGS:IsA2A_BULLS() ) + end + + --- Sets A2A LL DMS + -- @param #SETTINGS self + -- @return #SETTINGS + function SETTINGS:SetA2A_LL_DMS() + self.A2ASystem = "LL DMS" + end + + --- Sets A2A LL DDM + -- @param #SETTINGS self + -- @return #SETTINGS + function SETTINGS:SetA2A_LL_DDM() + self.A2ASystem = "LL DDM" + end + + --- Is LL DMS + -- @param #SETTINGS self + -- @return #boolean true if LL DMS + function SETTINGS:IsA2A_LL_DMS() + return ( self.A2ASystem and self.A2ASystem == "LL DMS" ) or ( not self.A2ASystem and _SETTINGS:IsA2A_LL_DMS() ) + end + + --- Is LL DDM + -- @param #SETTINGS self + -- @return #boolean true if LL DDM + function SETTINGS:IsA2A_LL_DDM() + return ( self.A2ASystem and self.A2ASystem == "LL DDM" ) or ( not self.A2ASystem and _SETTINGS:IsA2A_LL_DDM() ) + end + + --- Sets A2A MGRS + -- @param #SETTINGS self + -- @return #SETTINGS + function SETTINGS:SetA2A_MGRS() + self.A2ASystem = "MGRS" + end + + --- Is MGRS + -- @param #SETTINGS self + -- @return #boolean true if MGRS + function SETTINGS:IsA2A_MGRS() + return ( self.A2ASystem and self.A2ASystem == "MGRS" ) or ( not self.A2ASystem and _SETTINGS:IsA2A_MGRS() ) + end + + --- @param #SETTINGS self + -- @param Wrapper.Group#GROUP MenuGroup Group for which to add menus. + -- @param #table RootMenu Root menu table + -- @return #SETTINGS + function SETTINGS:SetSystemMenu( MenuGroup, RootMenu ) + + local MenuText = "System Settings" + + local MenuTime = timer.getTime() + + local SettingsMenu = MENU_GROUP:New( MenuGroup, MenuText, RootMenu ):SetTime( MenuTime ) + + ------- + -- A2G Coordinate System + ------- + + local text="A2G Coordinate System" + if _SETTINGS.MenuShort then + text="A2G Coordinates" + end + local A2GCoordinateMenu = MENU_GROUP:New( MenuGroup, text, SettingsMenu ):SetTime( MenuTime ) + + -- Set LL DMS + if not self:IsA2G_LL_DMS() then + local text="Lat/Lon Degree Min Sec (LL DMS)" + if _SETTINGS.MenuShort then + text="LL DMS" + end + MENU_GROUP_COMMAND:New( MenuGroup, text, A2GCoordinateMenu, self.A2GMenuSystem, self, MenuGroup, RootMenu, "LL DMS" ):SetTime( MenuTime ) + end + + -- Set LL DDM + if not self:IsA2G_LL_DDM() then + local text="Lat/Lon Degree Dec Min (LL DDM)" + if _SETTINGS.MenuShort then + text="LL DDM" + end + MENU_GROUP_COMMAND:New( MenuGroup, "Lat/Lon Degree Dec Min (LL DDM)", A2GCoordinateMenu, self.A2GMenuSystem, self, MenuGroup, RootMenu, "LL DDM" ):SetTime( MenuTime ) + end + + -- Set LL DMS accuracy. + if self:IsA2G_LL_DDM() then + local text1="LL DDM Accuracy 1" + local text2="LL DDM Accuracy 2" + local text3="LL DDM Accuracy 3" + if _SETTINGS.MenuShort then + text1="LL DDM" + end + MENU_GROUP_COMMAND:New( MenuGroup, "LL DDM Accuracy 1", A2GCoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 1 ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "LL DDM Accuracy 2", A2GCoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 2 ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "LL DDM Accuracy 3", A2GCoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 3 ):SetTime( MenuTime ) + end + + -- Set BR. + if not self:IsA2G_BR() then + local text="Bearing, Range (BR)" + if _SETTINGS.MenuShort then + text="BR" + end + MENU_GROUP_COMMAND:New( MenuGroup, text , A2GCoordinateMenu, self.A2GMenuSystem, self, MenuGroup, RootMenu, "BR" ):SetTime( MenuTime ) + end + + -- Set MGRS. + if not self:IsA2G_MGRS() then + local text="Military Grid (MGRS)" + if _SETTINGS.MenuShort then + text="MGRS" + end + MENU_GROUP_COMMAND:New( MenuGroup, text, A2GCoordinateMenu, self.A2GMenuSystem, self, MenuGroup, RootMenu, "MGRS" ):SetTime( MenuTime ) + end + + -- Set MGRS accuracy. + if self:IsA2G_MGRS() then + MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 1", A2GCoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 1 ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 2", A2GCoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 2 ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 3", A2GCoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 3 ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 4", A2GCoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 4 ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 5", A2GCoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 5 ):SetTime( MenuTime ) + end + + ------- + -- A2A Coordinate System + ------- + + local text="A2A Coordinate System" + if _SETTINGS.MenuShort then + text="A2A Coordinates" + end + local A2ACoordinateMenu = MENU_GROUP:New( MenuGroup, text, SettingsMenu ):SetTime( MenuTime ) + + if not self:IsA2A_LL_DMS() then + local text="Lat/Lon Degree Min Sec (LL DMS)" + if _SETTINGS.MenuShort then + text="LL DMS" + end + MENU_GROUP_COMMAND:New( MenuGroup, text, A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "LL DMS" ):SetTime( MenuTime ) + end + + if not self:IsA2A_LL_DDM() then + local text="Lat/Lon Degree Dec Min (LL DDM)" + if _SETTINGS.MenuShort then + text="LL DDM" + end + MENU_GROUP_COMMAND:New( MenuGroup, text, A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "LL DDM" ):SetTime( MenuTime ) + end + + if self:IsA2A_LL_DDM() or self:IsA2A_LL_DMS() then + MENU_GROUP_COMMAND:New( MenuGroup, "LL Accuracy 0", A2ACoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 0 ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "LL Accuracy 1", A2ACoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 1 ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "LL Accuracy 2", A2ACoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 2 ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "LL Accuracy 3", A2ACoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 3 ):SetTime( MenuTime ) + end + + if not self:IsA2A_BULLS() then + local text="Bullseye (BULLS)" + if _SETTINGS.MenuShort then + text="Bulls" + end + MENU_GROUP_COMMAND:New( MenuGroup, text, A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "BULLS" ):SetTime( MenuTime ) + end + + if not self:IsA2A_BRAA() then + local text="Bearing Range Altitude Aspect (BRAA)" + if _SETTINGS.MenuShort then + text="BRAA" + end + MENU_GROUP_COMMAND:New( MenuGroup, text, A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "BRAA" ):SetTime( MenuTime ) + end + + if not self:IsA2A_MGRS() then + local text="Military Grid (MGRS)" + if _SETTINGS.MenuShort then + text="MGRS" + end + MENU_GROUP_COMMAND:New( MenuGroup, text, A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "MGRS" ):SetTime( MenuTime ) + end + + if self:IsA2A_MGRS() then + MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 1", A2ACoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 1 ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 2", A2ACoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 2 ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 3", A2ACoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 3 ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 4", A2ACoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 4 ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 5", A2ACoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 5 ):SetTime( MenuTime ) + end + + local text="Measures and Weights System" + if _SETTINGS.MenuShort then + text="Unit System" + end + local MetricsMenu = MENU_GROUP:New( MenuGroup, text, SettingsMenu ):SetTime( MenuTime ) + + if self:IsMetric() then + local text="Imperial (Miles,Feet)" + if _SETTINGS.MenuShort then + text="Imperial" + end + MENU_GROUP_COMMAND:New( MenuGroup, text, MetricsMenu, self.MenuMWSystem, self, MenuGroup, RootMenu, false ):SetTime( MenuTime ) + end + + if self:IsImperial() then + local text="Metric (Kilometers,Meters)" + if _SETTINGS.MenuShort then + text="Metric" + end + MENU_GROUP_COMMAND:New( MenuGroup, text, MetricsMenu, self.MenuMWSystem, self, MenuGroup, RootMenu, true ):SetTime( MenuTime ) + end + + local text="Messages and Reports" + if _SETTINGS.MenuShort then + text="Messages & Reports" + end + local MessagesMenu = MENU_GROUP:New( MenuGroup, text, SettingsMenu ):SetTime( MenuTime ) + + local UpdateMessagesMenu = MENU_GROUP:New( MenuGroup, "Update Messages", MessagesMenu ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "Off", UpdateMessagesMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Update, 0 ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "5 seconds", UpdateMessagesMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Update, 5 ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "10 seconds", UpdateMessagesMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Update, 10 ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "15 seconds", UpdateMessagesMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Update, 15 ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "30 seconds", UpdateMessagesMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Update, 30 ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "1 minute", UpdateMessagesMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Update, 60 ):SetTime( MenuTime ) + + local InformationMessagesMenu = MENU_GROUP:New( MenuGroup, "Information Messages", MessagesMenu ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "5 seconds", InformationMessagesMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Information, 5 ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "10 seconds", InformationMessagesMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Information, 10 ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "15 seconds", InformationMessagesMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Information, 15 ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "30 seconds", InformationMessagesMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Information, 30 ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "1 minute", InformationMessagesMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Information, 60 ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "2 minutes", InformationMessagesMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Information, 120 ):SetTime( MenuTime ) + + local BriefingReportsMenu = MENU_GROUP:New( MenuGroup, "Briefing Reports", MessagesMenu ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "15 seconds", BriefingReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Briefing, 15 ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "30 seconds", BriefingReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Briefing, 30 ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "1 minute", BriefingReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Briefing, 60 ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "2 minutes", BriefingReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Briefing, 120 ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "3 minutes", BriefingReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Briefing, 180 ):SetTime( MenuTime ) + + local OverviewReportsMenu = MENU_GROUP:New( MenuGroup, "Overview Reports", MessagesMenu ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "15 seconds", OverviewReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Overview, 15 ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "30 seconds", OverviewReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Overview, 30 ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "1 minute", OverviewReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Overview, 60 ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "2 minutes", OverviewReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Overview, 120 ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "3 minutes", OverviewReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Overview, 180 ):SetTime( MenuTime ) + + local DetailedReportsMenu = MENU_GROUP:New( MenuGroup, "Detailed Reports", MessagesMenu ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "15 seconds", DetailedReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.DetailedReportsMenu, 15 ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "30 seconds", DetailedReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.DetailedReportsMenu, 30 ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "1 minute", DetailedReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.DetailedReportsMenu, 60 ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "2 minutes", DetailedReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.DetailedReportsMenu, 120 ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "3 minutes", DetailedReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.DetailedReportsMenu, 180 ):SetTime( MenuTime ) + + + SettingsMenu:Remove( MenuTime ) + + return self + end + + --- Sets the player menus on, so that the **Player setting menus** show up for the players. + -- But only when a player exits and reenters the slot these settings will have effect! + -- It is advised to use this method at the start of the mission. + -- @param #SETTINGS self + -- @return #SETTINGS + -- @usage + -- _SETTINGS:SetPlayerMenuOn() -- will enable the player menus. + function SETTINGS:SetPlayerMenuOn() + self.ShowPlayerMenu = true + end + + --- Sets the player menus off, so that the **Player setting menus** won't show up for the players. + -- But only when a player exits and reenters the slot these settings will have effect! + -- It is advised to use this method at the start of the mission. + -- @param #SETTINGS self + -- @return #SETTINGS self + -- @usage + -- _SETTINGS:SetPlayerMenuOff() -- will disable the player menus. + function SETTINGS:SetPlayerMenuOff() + self.ShowPlayerMenu = false + end + + --- Updates the menu of the player seated in the PlayerUnit. + -- @param #SETTINGS self + -- @param Wrapper.Client#CLIENT PlayerUnit + -- @return #SETTINGS self + function SETTINGS:SetPlayerMenu( PlayerUnit ) + + if _SETTINGS.ShowPlayerMenu == true then + + local PlayerGroup = PlayerUnit:GetGroup() + local PlayerName = PlayerUnit:GetPlayerName() + local PlayerNames = PlayerGroup:GetPlayerNames() + + local PlayerMenu = MENU_GROUP:New( PlayerGroup, 'Settings "' .. PlayerName .. '"' ) + + self.PlayerMenu = PlayerMenu + + self:I(string.format("Setting menu for player %s", tostring(PlayerName))) + + local submenu = MENU_GROUP:New( PlayerGroup, "LL Accuracy", PlayerMenu ) + MENU_GROUP_COMMAND:New( PlayerGroup, "LL 0 Decimals", submenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 0 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "LL 1 Decimal", submenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 1 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "LL 2 Decimals", submenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 2 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "LL 3 Decimals", submenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 3 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "LL 4 Decimals", submenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 4 ) + + local submenu = MENU_GROUP:New( PlayerGroup, "MGRS Accuracy", PlayerMenu ) + MENU_GROUP_COMMAND:New( PlayerGroup, "MRGS Accuracy 0", submenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 0 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "MRGS Accuracy 1", submenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 1 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "MRGS Accuracy 2", submenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 2 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "MRGS Accuracy 3", submenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 3 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "MRGS Accuracy 4", submenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 4 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "MRGS Accuracy 5", submenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 5 ) + + ------ + -- A2G Coordinate System + ------ + + local text="A2G Coordinate System" + if _SETTINGS.MenuShort then + text="A2G Coordinates" + end + local A2GCoordinateMenu = MENU_GROUP:New( PlayerGroup, text, PlayerMenu ) + + if not self:IsA2G_LL_DMS() or _SETTINGS.MenuStatic then + local text="Lat/Lon Degree Min Sec (LL DMS)" + if _SETTINGS.MenuShort then + text="A2G LL DMS" + end + MENU_GROUP_COMMAND:New( PlayerGroup, text, A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "LL DMS" ) + end + + if not self:IsA2G_LL_DDM() or _SETTINGS.MenuStatic then + local text="Lat/Lon Degree Dec Min (LL DDM)" + if _SETTINGS.MenuShort then + text="A2G LL DDM" + end + MENU_GROUP_COMMAND:New( PlayerGroup, text, A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "LL DDM" ) + end + + if not self:IsA2G_BR() or _SETTINGS.MenuStatic then + local text="Bearing, Range (BR)" + if _SETTINGS.MenuShort then + text="A2G BR" + end + MENU_GROUP_COMMAND:New( PlayerGroup, text, A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "BR" ) + end + + if not self:IsA2G_MGRS() or _SETTINGS.MenuStatic then + local text="Military Grid (MGRS)" + if _SETTINGS.MenuShort then + text="A2G MGRS" + end + MENU_GROUP_COMMAND:New( PlayerGroup, text, A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "MGRS" ) + end + + ------ + -- A2A Coordinates Menu + ------ + + local text="A2A Coordinate System" + if _SETTINGS.MenuShort then + text="A2A Coordinates" + end + local A2ACoordinateMenu = MENU_GROUP:New( PlayerGroup, text, PlayerMenu ) + + + if not self:IsA2A_LL_DMS() or _SETTINGS.MenuStatic then + local text="Lat/Lon Degree Min Sec (LL DMS)" + if _SETTINGS.MenuShort then + text="A2A LL DMS" + end + MENU_GROUP_COMMAND:New( PlayerGroup, text, A2ACoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "LL DMS" ) + end + + if not self:IsA2A_LL_DDM() or _SETTINGS.MenuStatic then + local text="Lat/Lon Degree Dec Min (LL DDM)" + if _SETTINGS.MenuShort then + text="A2A LL DDM" + end + MENU_GROUP_COMMAND:New( PlayerGroup, text, A2ACoordinateMenu, self.MenuGroupA2ASystem, self, PlayerUnit, PlayerGroup, PlayerName, "LL DDM" ) + end + + if not self:IsA2A_BULLS() or _SETTINGS.MenuStatic then + local text="Bullseye (BULLS)" + if _SETTINGS.MenuShort then + text="A2A BULLS" + end + MENU_GROUP_COMMAND:New( PlayerGroup, text, A2ACoordinateMenu, self.MenuGroupA2ASystem, self, PlayerUnit, PlayerGroup, PlayerName, "BULLS" ) + end + + if not self:IsA2A_BRAA() or _SETTINGS.MenuStatic then + local text="Bearing Range Altitude Aspect (BRAA)" + if _SETTINGS.MenuShort then + text="A2A BRAA" + end + MENU_GROUP_COMMAND:New( PlayerGroup, text, A2ACoordinateMenu, self.MenuGroupA2ASystem, self, PlayerUnit, PlayerGroup, PlayerName, "BRAA" ) + end + + if not self:IsA2A_MGRS() or _SETTINGS.MenuStatic then + local text="Military Grid (MGRS)" + if _SETTINGS.MenuShort then + text="A2A MGRS" + end + MENU_GROUP_COMMAND:New( PlayerGroup, text, A2ACoordinateMenu, self.MenuGroupA2ASystem, self, PlayerUnit, PlayerGroup, PlayerName, "MGRS" ) + end + + --- + -- Unit system + --- + + local text="Measures and Weights System" + if _SETTINGS.MenuShort then + text="Unit System" + end + local MetricsMenu = MENU_GROUP:New( PlayerGroup, text, PlayerMenu ) + + if self:IsMetric() or _SETTINGS.MenuStatic then + local text="Imperial (Miles,Feet)" + if _SETTINGS.MenuShort then + text="Imperial" + end + MENU_GROUP_COMMAND:New( PlayerGroup, text, MetricsMenu, self.MenuGroupMWSystem, self, PlayerUnit, PlayerGroup, PlayerName, false ) + end + + if self:IsImperial() or _SETTINGS.MenuStatic then + local text="Metric (Kilometers,Meters)" + if _SETTINGS.MenuShort then + text="Metric" + end + MENU_GROUP_COMMAND:New( PlayerGroup, text, MetricsMenu, self.MenuGroupMWSystem, self, PlayerUnit, PlayerGroup, PlayerName, true ) + end + + --- + -- Messages and Reports + --- + + local text="Messages and Reports" + if _SETTINGS.MenuShort then + text="Messages & Reports" + end + local MessagesMenu = MENU_GROUP:New( PlayerGroup, text, PlayerMenu ) + + local UpdateMessagesMenu = MENU_GROUP:New( PlayerGroup, "Update Messages", MessagesMenu ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Updates Off", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 0 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Updates 5 sec", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 5 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Updates 10 sec", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 10 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Updates 15 sec", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 15 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Updates 30 sec", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 30 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Updates 1 min", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 60 ) + + local InformationMessagesMenu = MENU_GROUP:New( PlayerGroup, "Info Messages", MessagesMenu ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Info 5 sec", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 5 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Info 10 sec", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 10 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Info 15 sec", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 15 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Info 30 sec", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 30 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Info 1 min", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 60 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Info 2 min", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 120 ) + + local BriefingReportsMenu = MENU_GROUP:New( PlayerGroup, "Briefing Reports", MessagesMenu ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Brief 15 sec", BriefingReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Briefing, 15 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Brief 30 sec", BriefingReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Briefing, 30 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Brief 1 min", BriefingReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Briefing, 60 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Brief 2 min", BriefingReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Briefing, 120 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Brief 3 min", BriefingReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Briefing, 180 ) + + local OverviewReportsMenu = MENU_GROUP:New( PlayerGroup, "Overview Reports", MessagesMenu ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Overview 15 sec", OverviewReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Overview, 15 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Overview 30 sec", OverviewReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Overview, 30 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Overview 1 min", OverviewReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Overview, 60 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Overview 2 min", OverviewReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Overview, 120 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Overview 3 min", OverviewReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Overview, 180 ) + + local DetailedReportsMenu = MENU_GROUP:New( PlayerGroup, "Detailed Reports", MessagesMenu ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Detailed 15 sec", DetailedReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.DetailedReportsMenu, 15 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Detailed 30 sec", DetailedReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.DetailedReportsMenu, 30 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Detailed 1 min", DetailedReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.DetailedReportsMenu, 60 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Detailed 2 min", DetailedReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.DetailedReportsMenu, 120 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Detailed 3 min", DetailedReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.DetailedReportsMenu, 180 ) + + end + + return self + end + + --- Removes the player menu from the PlayerUnit. + -- @param #SETTINGS self + -- @param Wrapper.Client#CLIENT PlayerUnit + -- @return #SETTINGS self + function SETTINGS:RemovePlayerMenu( PlayerUnit ) + + if self.PlayerMenu then + self.PlayerMenu:Remove() + self.PlayerMenu = nil + end + + return self + end + + + --- @param #SETTINGS self + function SETTINGS:A2GMenuSystem( MenuGroup, RootMenu, A2GSystem ) + self.A2GSystem = A2GSystem + MESSAGE:New( string.format("Settings: Default A2G coordinate system set to %s for all players!", A2GSystem ), 5 ):ToAll() + self:SetSystemMenu( MenuGroup, RootMenu ) + end + + --- @param #SETTINGS self + function SETTINGS:A2AMenuSystem( MenuGroup, RootMenu, A2ASystem ) + self.A2ASystem = A2ASystem + MESSAGE:New( string.format("Settings: Default A2A coordinate system set to %s for all players!", A2ASystem ), 5 ):ToAll() + self:SetSystemMenu( MenuGroup, RootMenu ) + end + + --- @param #SETTINGS self + function SETTINGS:MenuLL_DDM_Accuracy( MenuGroup, RootMenu, LL_Accuracy ) + self.LL_Accuracy = LL_Accuracy + MESSAGE:New( string.format("Settings: Default LL accuracy set to %s for all players!", LL_Accuracy ), 5 ):ToAll() + self:SetSystemMenu( MenuGroup, RootMenu ) + end + + --- @param #SETTINGS self + function SETTINGS:MenuMGRS_Accuracy( MenuGroup, RootMenu, MGRS_Accuracy ) + self.MGRS_Accuracy = MGRS_Accuracy + MESSAGE:New( string.format("Settings: Default MGRS accuracy set to %s for all players!", MGRS_Accuracy ), 5 ):ToAll() + self:SetSystemMenu( MenuGroup, RootMenu ) + end + + --- @param #SETTINGS self + function SETTINGS:MenuMWSystem( MenuGroup, RootMenu, MW ) + self.Metric = MW + MESSAGE:New( string.format("Settings: Default measurement format set to %s for all players!", MW and "Metric" or "Imperial" ), 5 ):ToAll() + self:SetSystemMenu( MenuGroup, RootMenu ) + end + + --- @param #SETTINGS self + function SETTINGS:MenuMessageTimingsSystem( MenuGroup, RootMenu, MessageType, MessageTime ) + self:SetMessageTime( MessageType, MessageTime ) + MESSAGE:New( string.format( "Settings: Default message time set for %s to %d.", MessageType, MessageTime ), 5 ):ToAll() + end + + do + --- @param #SETTINGS self + function SETTINGS:MenuGroupA2GSystem( PlayerUnit, PlayerGroup, PlayerName, A2GSystem ) + BASE:E( {self, PlayerUnit:GetName(), A2GSystem} ) + self.A2GSystem = A2GSystem + MESSAGE:New( string.format( "Settings: A2G format set to %s for player %s.", A2GSystem, PlayerName ), 5 ):ToGroup( PlayerGroup ) + if _SETTINGS.MenuStatic==false then + self:RemovePlayerMenu(PlayerUnit) + self:SetPlayerMenu(PlayerUnit) + end + end + + --- @param #SETTINGS self + function SETTINGS:MenuGroupA2ASystem( PlayerUnit, PlayerGroup, PlayerName, A2ASystem ) + self.A2ASystem = A2ASystem + MESSAGE:New( string.format( "Settings: A2A format set to %s for player %s.", A2ASystem, PlayerName ), 5 ):ToGroup( PlayerGroup ) + if _SETTINGS.MenuStatic==false then + self:RemovePlayerMenu(PlayerUnit) + self:SetPlayerMenu(PlayerUnit) + end + end + + --- @param #SETTINGS self + function SETTINGS:MenuGroupLL_DDM_AccuracySystem( PlayerUnit, PlayerGroup, PlayerName, LL_Accuracy ) + self.LL_Accuracy = LL_Accuracy + MESSAGE:New( string.format( "Settings: LL format accuracy set to %d decimal places for player %s.", LL_Accuracy, PlayerName ), 5 ):ToGroup( PlayerGroup ) + if _SETTINGS.MenuStatic==false then + self:RemovePlayerMenu(PlayerUnit) + self:SetPlayerMenu(PlayerUnit) + end + end + + --- @param #SETTINGS self + function SETTINGS:MenuGroupMGRS_AccuracySystem( PlayerUnit, PlayerGroup, PlayerName, MGRS_Accuracy ) + self.MGRS_Accuracy = MGRS_Accuracy + MESSAGE:New( string.format( "Settings: MGRS format accuracy set to %d for player %s.", MGRS_Accuracy, PlayerName ), 5 ):ToGroup( PlayerGroup ) + if _SETTINGS.MenuStatic==false then + self:RemovePlayerMenu(PlayerUnit) + self:SetPlayerMenu(PlayerUnit) + end + end + + --- @param #SETTINGS self + function SETTINGS:MenuGroupMWSystem( PlayerUnit, PlayerGroup, PlayerName, MW ) + self.Metric = MW + MESSAGE:New( string.format( "Settings: Measurement format set to %s for player %s.", MW and "Metric" or "Imperial", PlayerName ), 5 ):ToGroup( PlayerGroup ) + if _SETTINGS.MenuStatic==false then + self:RemovePlayerMenu(PlayerUnit) + self:SetPlayerMenu(PlayerUnit) + end + end + + --- @param #SETTINGS self + function SETTINGS:MenuGroupMessageTimingsSystem( PlayerUnit, PlayerGroup, PlayerName, MessageType, MessageTime ) + self:SetMessageTime( MessageType, MessageTime ) + MESSAGE:New( string.format( "Settings: Default message time set for %s to %d.", MessageType, MessageTime ), 5 ):ToGroup( PlayerGroup ) + end + + end + + --- Configures the era of the mission to be WWII. + -- @param #SETTINGS self + -- @return #SETTINGS self + function SETTINGS:SetEraWWII() + + self.Era = SETTINGS.__Enum.Era.WWII + + end + + --- Configures the era of the mission to be Korea. + -- @param #SETTINGS self + -- @return #SETTINGS self + function SETTINGS:SetEraKorea() + + self.Era = SETTINGS.__Enum.Era.Korea + + end + + + --- Configures the era of the mission to be Cold war. + -- @param #SETTINGS self + -- @return #SETTINGS self + function SETTINGS:SetEraCold() + + self.Era = SETTINGS.__Enum.Era.Cold + + end + + + --- Configures the era of the mission to be Modern war. + -- @param #SETTINGS self + -- @return #SETTINGS self + function SETTINGS:SetEraModern() + + self.Era = SETTINGS.__Enum.Era.Modern + + end + + + + +end +--- **Core** - Manage hierarchical menu structures and commands for players within a mission. +-- +-- === +-- +-- ### Features: +-- +-- * Setup mission sub menus. +-- * Setup mission command menus. +-- * Setup coalition sub menus. +-- * Setup coalition command menus. +-- * Setup group sub menus. +-- * Setup group command menus. +-- * Manage menu creation intelligently, avoid double menu creation. +-- * Only create or delete menus when required, and keep existing menus persistent. +-- * Update menu structures. +-- * Refresh menu structures intelligently, based on a time stamp of updates. +-- - Delete obscolete menus. +-- - Create new one where required. +-- - Don't touch the existing ones. +-- * Provide a variable amount of parameters to menus. +-- * Update the parameters and the receiving methods, without updating the menu within DCS! +-- * Provide a great performance boost in menu management. +-- * Provide a great tool to manage menus in your code. +-- +-- DCS Menus can be managed using the MENU classes. +-- The advantage of using MENU classes is that it hides the complexity of dealing with menu management in more advanced scanerios where you need to +-- set menus and later remove them, and later set them again. You'll find while using use normal DCS scripting functions, that setting and removing +-- menus is not a easy feat if you have complex menu hierarchies defined. +-- Using the MOOSE menu classes, the removal and refreshing of menus are nicely being handled within these classes, and becomes much more easy. +-- On top, MOOSE implements **variable parameter** passing for command menus. +-- +-- There are basically two different MENU class types that you need to use: +-- +-- ### To manage **main menus**, the classes begin with **MENU_**: +-- +-- * @{Core.Menu#MENU_MISSION}: Manages main menus for whole mission file. +-- * @{Core.Menu#MENU_COALITION}: Manages main menus for whole coalition. +-- * @{Core.Menu#MENU_GROUP}: Manages main menus for GROUPs. +-- +-- ### To manage **command menus**, which are menus that allow the player to issue **functions**, the classes begin with **MENU_COMMAND_**: +-- +-- * @{Core.Menu#MENU_MISSION_COMMAND}: Manages command menus for whole mission file. +-- * @{Core.Menu#MENU_COALITION_COMMAND}: Manages command menus for whole coalition. +-- * @{Core.Menu#MENU_GROUP_COMMAND}: Manages command menus for GROUPs. +-- +-- === +--- +-- ### Author: **FlightControl** +-- ### Contributions: +-- +-- === +-- +-- @module Core.Menu +-- @image Core_Menu.JPG + + +MENU_INDEX = {} +MENU_INDEX.MenuMission = {} +MENU_INDEX.MenuMission.Menus = {} +MENU_INDEX.Coalition = {} +MENU_INDEX.Coalition[coalition.side.BLUE] = {} +MENU_INDEX.Coalition[coalition.side.BLUE].Menus = {} +MENU_INDEX.Coalition[coalition.side.RED] = {} +MENU_INDEX.Coalition[coalition.side.RED].Menus = {} +MENU_INDEX.Group = {} + + + +function MENU_INDEX:ParentPath( ParentMenu, MenuText ) + + local Path = ParentMenu and "@" .. table.concat( ParentMenu.MenuPath or {}, "@" ) or "" + if ParentMenu then + if ParentMenu:IsInstanceOf( "MENU_GROUP" ) or ParentMenu:IsInstanceOf( "MENU_GROUP_COMMAND" ) then + local GroupName = ParentMenu.Group:GetName() + if not self.Group[GroupName].Menus[Path] then + BASE:E( { Path = Path, GroupName = GroupName } ) + error( "Parent path not found in menu index for group menu" ) + return nil + end + elseif ParentMenu:IsInstanceOf( "MENU_COALITION" ) or ParentMenu:IsInstanceOf( "MENU_COALITION_COMMAND" ) then + local Coalition = ParentMenu.Coalition + if not self.Coalition[Coalition].Menus[Path] then + BASE:E( { Path = Path, Coalition = Coalition } ) + error( "Parent path not found in menu index for coalition menu" ) + return nil + end + elseif ParentMenu:IsInstanceOf( "MENU_MISSION" ) or ParentMenu:IsInstanceOf( "MENU_MISSION_COMMAND" ) then + if not self.MenuMission.Menus[Path] then + BASE:E( { Path = Path } ) + error( "Parent path not found in menu index for mission menu" ) + return nil + end + end + end + + Path = Path .. "@" .. MenuText + return Path + +end + + +function MENU_INDEX:PrepareMission() + self.MenuMission.Menus = self.MenuMission.Menus or {} +end + + +function MENU_INDEX:PrepareCoalition( CoalitionSide ) + self.Coalition[CoalitionSide] = self.Coalition[CoalitionSide] or {} + self.Coalition[CoalitionSide].Menus = self.Coalition[CoalitionSide].Menus or {} +end + +--- +-- @param Wrapper.Group#GROUP Group +function MENU_INDEX:PrepareGroup( Group ) + if Group and Group:IsAlive() ~= nil then -- something was changed here! + local GroupName = Group:GetName() + self.Group[GroupName] = self.Group[GroupName] or {} + self.Group[GroupName].Menus = self.Group[GroupName].Menus or {} + end +end + + + +function MENU_INDEX:HasMissionMenu( Path ) + + return self.MenuMission.Menus[Path] +end + +function MENU_INDEX:SetMissionMenu( Path, Menu ) + + self.MenuMission.Menus[Path] = Menu +end + +function MENU_INDEX:ClearMissionMenu( Path ) + + self.MenuMission.Menus[Path] = nil +end + + + +function MENU_INDEX:HasCoalitionMenu( Coalition, Path ) + + return self.Coalition[Coalition].Menus[Path] +end + +function MENU_INDEX:SetCoalitionMenu( Coalition, Path, Menu ) + + self.Coalition[Coalition].Menus[Path] = Menu +end + +function MENU_INDEX:ClearCoalitionMenu( Coalition, Path ) + + self.Coalition[Coalition].Menus[Path] = nil +end + + + +function MENU_INDEX:HasGroupMenu( Group, Path ) + if Group and Group:IsAlive() then + local MenuGroupName = Group:GetName() + return self.Group[MenuGroupName].Menus[Path] + end + return nil +end + +function MENU_INDEX:SetGroupMenu( Group, Path, Menu ) + + local MenuGroupName = Group:GetName() + Group:F({MenuGroupName=MenuGroupName,Path=Path}) + self.Group[MenuGroupName].Menus[Path] = Menu +end + +function MENU_INDEX:ClearGroupMenu( Group, Path ) + + local MenuGroupName = Group:GetName() + self.Group[MenuGroupName].Menus[Path] = nil +end + +function MENU_INDEX:Refresh( Group ) + + for MenuID, Menu in pairs( self.MenuMission.Menus ) do + Menu:Refresh() + end + + for MenuID, Menu in pairs( self.Coalition[coalition.side.BLUE].Menus ) do + Menu:Refresh() + end + + for MenuID, Menu in pairs( self.Coalition[coalition.side.RED].Menus ) do + Menu:Refresh() + end + + local GroupName = Group:GetName() + for MenuID, Menu in pairs( self.Group[GroupName].Menus ) do + Menu:Refresh() + end + +end + + + + + + + + +do -- MENU_BASE + + --- @type MENU_BASE + -- @extends Base#BASE + + --- Defines the main MENU class where other MENU classes are derived from. + -- This is an abstract class, so don't use it. + -- @field #MENU_BASE + MENU_BASE = { + ClassName = "MENU_BASE", + MenuPath = nil, + MenuText = "", + MenuParentPath = nil + } + + --- Consructor + -- @param #MENU_BASE + -- @return #MENU_BASE + function MENU_BASE:New( MenuText, ParentMenu ) + + local MenuParentPath = {} + if ParentMenu ~= nil then + MenuParentPath = ParentMenu.MenuPath + end + + local self = BASE:Inherit( self, BASE:New() ) + + self.MenuPath = nil + self.MenuText = MenuText + self.ParentMenu = ParentMenu + self.MenuParentPath = MenuParentPath + self.Path = ( self.ParentMenu and "@" .. table.concat( self.MenuParentPath or {}, "@" ) or "" ) .. "@" .. self.MenuText + self.Menus = {} + self.MenuCount = 0 + self.MenuStamp = timer.getTime() + self.MenuRemoveParent = false + + if self.ParentMenu then + self.ParentMenu.Menus = self.ParentMenu.Menus or {} + self.ParentMenu.Menus[MenuText] = self + end + + return self + end + + function MENU_BASE:SetParentMenu( MenuText, Menu ) + if self.ParentMenu then + self.ParentMenu.Menus = self.ParentMenu.Menus or {} + self.ParentMenu.Menus[MenuText] = Menu + self.ParentMenu.MenuCount = self.ParentMenu.MenuCount + 1 + end + end + + function MENU_BASE:ClearParentMenu( MenuText ) + if self.ParentMenu and self.ParentMenu.Menus[MenuText] then + self.ParentMenu.Menus[MenuText] = nil + self.ParentMenu.MenuCount = self.ParentMenu.MenuCount - 1 + if self.ParentMenu.MenuCount == 0 then + --self.ParentMenu:Remove() + end + end + end + + --- Sets a @{Menu} to remove automatically the parent menu when the menu removed is the last child menu of that parent @{Menu}. + -- @param #MENU_BASE self + -- @param #boolean RemoveParent If true, the parent menu is automatically removed when this menu is the last child menu of that parent @{Menu}. + -- @return #MENU_BASE + function MENU_BASE:SetRemoveParent( RemoveParent ) + --self:F( { RemoveParent } ) + self.MenuRemoveParent = RemoveParent + return self + end + + + --- Gets a @{Menu} from a parent @{Menu} + -- @param #MENU_BASE self + -- @param #string MenuText The text of the child menu. + -- @return #MENU_BASE + function MENU_BASE:GetMenu( MenuText ) + return self.Menus[MenuText] + end + + --- Sets a menu stamp for later prevention of menu removal. + -- @param #MENU_BASE self + -- @param MenuStamp + -- @return #MENU_BASE + function MENU_BASE:SetStamp( MenuStamp ) + self.MenuStamp = MenuStamp + return self + end + + + --- Gets a menu stamp for later prevention of menu removal. + -- @param #MENU_BASE self + -- @return MenuStamp + function MENU_BASE:GetStamp() + return timer.getTime() + end + + + --- Sets a time stamp for later prevention of menu removal. + -- @param #MENU_BASE self + -- @param MenuStamp + -- @return #MENU_BASE + function MENU_BASE:SetTime( MenuStamp ) + self.MenuStamp = MenuStamp + return self + end + + --- Sets a tag for later selection of menu refresh. + -- @param #MENU_BASE self + -- @param #string MenuTag A Tag or Key that will filter only menu items set with this key. + -- @return #MENU_BASE + function MENU_BASE:SetTag( MenuTag ) + self.MenuTag = MenuTag + return self + end + +end + +do -- MENU_COMMAND_BASE + + --- @type MENU_COMMAND_BASE + -- @field #function MenuCallHandler + -- @extends Core.Menu#MENU_BASE + + --- Defines the main MENU class where other MENU COMMAND_ + -- classes are derived from, in order to set commands. + -- + -- @field #MENU_COMMAND_BASE + MENU_COMMAND_BASE = { + ClassName = "MENU_COMMAND_BASE", + CommandMenuFunction = nil, + CommandMenuArgument = nil, + MenuCallHandler = nil, + } + + --- Constructor + -- @param #MENU_COMMAND_BASE + -- @return #MENU_COMMAND_BASE + function MENU_COMMAND_BASE:New( MenuText, ParentMenu, CommandMenuFunction, CommandMenuArguments ) + + local self = BASE:Inherit( self, MENU_BASE:New( MenuText, ParentMenu ) ) -- #MENU_COMMAND_BASE + + -- When a menu function goes into error, DCS displays an obscure menu message. + -- This error handler catches the menu error and displays the full call stack. + local ErrorHandler = function( errmsg ) + env.info( "MOOSE error in MENU COMMAND function: " .. errmsg ) + if BASE.Debug ~= nil then + env.info( BASE.Debug.traceback() ) + end + return errmsg + end + + self:SetCommandMenuFunction( CommandMenuFunction ) + self:SetCommandMenuArguments( CommandMenuArguments ) + self.MenuCallHandler = function() + local function MenuFunction() + return self.CommandMenuFunction( unpack( self.CommandMenuArguments ) ) + end + local Status, Result = xpcall( MenuFunction, ErrorHandler ) + end + + return self + end + + --- This sets the new command function of a menu, + -- so that if a menu is regenerated, or if command function changes, + -- that the function set for the menu is loosely coupled with the menu itself!!! + -- If the function changes, no new menu needs to be generated if the menu text is the same!!! + -- @param #MENU_COMMAND_BASE + -- @return #MENU_COMMAND_BASE + function MENU_COMMAND_BASE:SetCommandMenuFunction( CommandMenuFunction ) + self.CommandMenuFunction = CommandMenuFunction + return self + end + + --- This sets the new command arguments of a menu, + -- so that if a menu is regenerated, or if command arguments change, + -- that the arguments set for the menu are loosely coupled with the menu itself!!! + -- If the arguments change, no new menu needs to be generated if the menu text is the same!!! + -- @param #MENU_COMMAND_BASE + -- @return #MENU_COMMAND_BASE + function MENU_COMMAND_BASE:SetCommandMenuArguments( CommandMenuArguments ) + self.CommandMenuArguments = CommandMenuArguments + return self + end + +end + + +do -- MENU_MISSION + + --- @type MENU_MISSION + -- @extends Core.Menu#MENU_BASE + + --- Manages the main menus for a complete mission. + -- + -- You can add menus with the @{#MENU_MISSION.New} method, which constructs a MENU_MISSION object and returns you the object reference. + -- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_MISSION.Remove}. + -- @field #MENU_MISSION + MENU_MISSION = { + ClassName = "MENU_MISSION" + } + + --- MENU_MISSION constructor. Creates a new MENU_MISSION object and creates the menu for a complete mission file. + -- @param #MENU_MISSION self + -- @param #string MenuText The text for the menu. + -- @param #table ParentMenu The parent menu. This parameter can be ignored if you want the menu to be located at the perent menu of DCS world (under F10 other). + -- @return #MENU_MISSION + function MENU_MISSION:New( MenuText, ParentMenu ) + + MENU_INDEX:PrepareMission() + local Path = MENU_INDEX:ParentPath( ParentMenu, MenuText ) + local MissionMenu = MENU_INDEX:HasMissionMenu( Path ) + + if MissionMenu then + return MissionMenu + else + local self = BASE:Inherit( self, MENU_BASE:New( MenuText, ParentMenu ) ) + MENU_INDEX:SetMissionMenu( Path, self ) + + self.MenuPath = missionCommands.addSubMenu( self.MenuText, self.MenuParentPath ) + self:SetParentMenu( self.MenuText, self ) + return self + end + + end + + --- Refreshes a radio item for a mission + -- @param #MENU_MISSION self + -- @return #MENU_MISSION + function MENU_MISSION:Refresh() + + do + missionCommands.removeItem( self.MenuPath ) + self.MenuPath = missionCommands.addSubMenu( self.MenuText, self.MenuParentPath ) + end + + end + + --- Removes the sub menus recursively of this MENU_MISSION. Note that the main menu is kept! + -- @param #MENU_MISSION self + -- @return #MENU_MISSION + function MENU_MISSION:RemoveSubMenus() + + for MenuID, Menu in pairs( self.Menus or {} ) do + Menu:Remove() + end + + self.Menus = nil + + end + + --- Removes the main menu and the sub menus recursively of this MENU_MISSION. + -- @param #MENU_MISSION self + -- @return #nil + function MENU_MISSION:Remove( MenuStamp, MenuTag ) + + MENU_INDEX:PrepareMission() + local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText ) + local MissionMenu = MENU_INDEX:HasMissionMenu( Path ) + + if MissionMenu == self then + self:RemoveSubMenus() + if not MenuStamp or self.MenuStamp ~= MenuStamp then + if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then + self:F( { Text = self.MenuText, Path = self.MenuPath } ) + if self.MenuPath ~= nil then + missionCommands.removeItem( self.MenuPath ) + end + MENU_INDEX:ClearMissionMenu( self.Path ) + self:ClearParentMenu( self.MenuText ) + return nil + end + end + else + BASE:E( { "Cannot Remove MENU_MISSION", Path = Path, ParentMenu = self.ParentMenu, MenuText = self.MenuText } ) + end + + return self + end + + + +end + +do -- MENU_MISSION_COMMAND + + --- @type MENU_MISSION_COMMAND + -- @extends Core.Menu#MENU_COMMAND_BASE + + --- Manages the command menus for a complete mission, which allow players to execute functions during mission execution. + -- + -- You can add menus with the @{#MENU_MISSION_COMMAND.New} method, which constructs a MENU_MISSION_COMMAND object and returns you the object reference. + -- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_MISSION_COMMAND.Remove}. + -- + -- @field #MENU_MISSION_COMMAND + MENU_MISSION_COMMAND = { + ClassName = "MENU_MISSION_COMMAND" + } + + --- MENU_MISSION constructor. Creates a new radio command item for a complete mission file, which can invoke a function with parameters. + -- @param #MENU_MISSION_COMMAND self + -- @param #string MenuText The text for the menu. + -- @param Core.Menu#MENU_MISSION ParentMenu The parent menu. + -- @param CommandMenuFunction A function that is called when the menu key is pressed. + -- @param CommandMenuArgument An argument for the function. There can only be ONE argument given. So multiple arguments must be wrapped into a table. See the below example how to do this. + -- @return #MENU_MISSION_COMMAND self + function MENU_MISSION_COMMAND:New( MenuText, ParentMenu, CommandMenuFunction, ... ) + + MENU_INDEX:PrepareMission() + local Path = MENU_INDEX:ParentPath( ParentMenu, MenuText ) + local MissionMenu = MENU_INDEX:HasMissionMenu( Path ) + + if MissionMenu then + MissionMenu:SetCommandMenuFunction( CommandMenuFunction ) + MissionMenu:SetCommandMenuArguments( arg ) + return MissionMenu + else + local self = BASE:Inherit( self, MENU_COMMAND_BASE:New( MenuText, ParentMenu, CommandMenuFunction, arg ) ) + MENU_INDEX:SetMissionMenu( Path, self ) + + self.MenuPath = missionCommands.addCommand( MenuText, self.MenuParentPath, self.MenuCallHandler ) + self:SetParentMenu( self.MenuText, self ) + return self + end + end + + --- Refreshes a radio item for a mission + -- @param #MENU_MISSION_COMMAND self + -- @return #MENU_MISSION_COMMAND + function MENU_MISSION_COMMAND:Refresh() + + do + missionCommands.removeItem( self.MenuPath ) + missionCommands.addCommand( self.MenuText, self.MenuParentPath, self.MenuCallHandler ) + end + + end + + --- Removes a radio command item for a coalition + -- @param #MENU_MISSION_COMMAND self + -- @return #nil + function MENU_MISSION_COMMAND:Remove() + + MENU_INDEX:PrepareMission() + local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText ) + local MissionMenu = MENU_INDEX:HasMissionMenu( Path ) + + if MissionMenu == self then + if not MenuStamp or self.MenuStamp ~= MenuStamp then + if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then + self:F( { Text = self.MenuText, Path = self.MenuPath } ) + if self.MenuPath ~= nil then + missionCommands.removeItem( self.MenuPath ) + end + MENU_INDEX:ClearMissionMenu( self.Path ) + self:ClearParentMenu( self.MenuText ) + return nil + end + end + else + BASE:E( { "Cannot Remove MENU_MISSION_COMMAND", Path = Path, ParentMenu = self.ParentMenu, MenuText = self.MenuText } ) + end + + return self + end + +end + + + +do -- MENU_COALITION + + --- @type MENU_COALITION + -- @extends Core.Menu#MENU_BASE + + --- Manages the main menus for @{DCS.coalition}s. + -- + -- You can add menus with the @{#MENU_COALITION.New} method, which constructs a MENU_COALITION object and returns you the object reference. + -- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_COALITION.Remove}. + -- + -- + -- @usage + -- -- This demo creates a menu structure for the planes within the red coalition. + -- -- To test, join the planes, then look at the other radio menus (Option F10). + -- -- Then switch planes and check if the menu is still there. + -- + -- local Plane1 = CLIENT:FindByName( "Plane 1" ) + -- local Plane2 = CLIENT:FindByName( "Plane 2" ) + -- + -- + -- -- This would create a menu for the red coalition under the main DCS "Others" menu. + -- local MenuCoalitionRed = MENU_COALITION:New( coalition.side.RED, "Manage Menus" ) + -- + -- + -- local function ShowStatus( StatusText, Coalition ) + -- + -- MESSAGE:New( Coalition, 15 ):ToRed() + -- Plane1:Message( StatusText, 15 ) + -- Plane2:Message( StatusText, 15 ) + -- end + -- + -- local MenuStatus -- Menu#MENU_COALITION + -- local MenuStatusShow -- Menu#MENU_COALITION_COMMAND + -- + -- local function RemoveStatusMenu() + -- MenuStatus:Remove() + -- end + -- + -- local function AddStatusMenu() + -- + -- -- This would create a menu for the red coalition under the MenuCoalitionRed menu object. + -- MenuStatus = MENU_COALITION:New( coalition.side.RED, "Status for Planes" ) + -- MenuStatusShow = MENU_COALITION_COMMAND:New( coalition.side.RED, "Show Status", MenuStatus, ShowStatus, "Status of planes is ok!", "Message to Red Coalition" ) + -- end + -- + -- local MenuAdd = MENU_COALITION_COMMAND:New( coalition.side.RED, "Add Status Menu", MenuCoalitionRed, AddStatusMenu ) + -- local MenuRemove = MENU_COALITION_COMMAND:New( coalition.side.RED, "Remove Status Menu", MenuCoalitionRed, RemoveStatusMenu ) + -- + -- @field #MENU_COALITION + MENU_COALITION = { + ClassName = "MENU_COALITION" + } + + --- MENU_COALITION constructor. Creates a new MENU_COALITION object and creates the menu for a complete coalition. + -- @param #MENU_COALITION self + -- @param DCS#coalition.side Coalition The coalition owning the menu. + -- @param #string MenuText The text for the menu. + -- @param #table ParentMenu The parent menu. This parameter can be ignored if you want the menu to be located at the perent menu of DCS world (under F10 other). + -- @return #MENU_COALITION self + function MENU_COALITION:New( Coalition, MenuText, ParentMenu ) + + MENU_INDEX:PrepareCoalition( Coalition ) + local Path = MENU_INDEX:ParentPath( ParentMenu, MenuText ) + local CoalitionMenu = MENU_INDEX:HasCoalitionMenu( Coalition, Path ) + + if CoalitionMenu then + return CoalitionMenu + else + + local self = BASE:Inherit( self, MENU_BASE:New( MenuText, ParentMenu ) ) + MENU_INDEX:SetCoalitionMenu( Coalition, Path, self ) + + self.Coalition = Coalition + + self.MenuPath = missionCommands.addSubMenuForCoalition( Coalition, MenuText, self.MenuParentPath ) + self:SetParentMenu( self.MenuText, self ) + return self + end + end + + --- Refreshes a radio item for a coalition + -- @param #MENU_COALITION self + -- @return #MENU_COALITION + function MENU_COALITION:Refresh() + + do + missionCommands.removeItemForCoalition( self.Coalition, self.MenuPath ) + missionCommands.addSubMenuForCoalition( self.Coalition, self.MenuText, self.MenuParentPath ) + end + + end + + --- Removes the sub menus recursively of this MENU_COALITION. Note that the main menu is kept! + -- @param #MENU_COALITION self + -- @return #MENU_COALITION + function MENU_COALITION:RemoveSubMenus() + + for MenuID, Menu in pairs( self.Menus or {} ) do + Menu:Remove() + end + + self.Menus = nil + end + + --- Removes the main menu and the sub menus recursively of this MENU_COALITION. + -- @param #MENU_COALITION self + -- @return #nil + function MENU_COALITION:Remove( MenuStamp, MenuTag ) + + MENU_INDEX:PrepareCoalition( self.Coalition ) + local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText ) + local CoalitionMenu = MENU_INDEX:HasCoalitionMenu( self.Coalition, Path ) + + if CoalitionMenu == self then + self:RemoveSubMenus() + if not MenuStamp or self.MenuStamp ~= MenuStamp then + if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then + self:F( { Coalition = self.Coalition, Text = self.MenuText, Path = self.MenuPath } ) + if self.MenuPath ~= nil then + missionCommands.removeItemForCoalition( self.Coalition, self.MenuPath ) + end + MENU_INDEX:ClearCoalitionMenu( self.Coalition, Path ) + self:ClearParentMenu( self.MenuText ) + return nil + end + end + else + BASE:E( { "Cannot Remove MENU_COALITION", Path = Path, ParentMenu = self.ParentMenu, MenuText = self.MenuText, Coalition = self.Coalition } ) + end + + return self + end + +end + + + +do -- MENU_COALITION_COMMAND + + --- @type MENU_COALITION_COMMAND + -- @extends Core.Menu#MENU_COMMAND_BASE + + --- Manages the command menus for coalitions, which allow players to execute functions during mission execution. + -- + -- You can add menus with the @{#MENU_COALITION_COMMAND.New} method, which constructs a MENU_COALITION_COMMAND object and returns you the object reference. + -- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_COALITION_COMMAND.Remove}. + -- + -- @field #MENU_COALITION_COMMAND + MENU_COALITION_COMMAND = { + ClassName = "MENU_COALITION_COMMAND" + } + + --- MENU_COALITION constructor. Creates a new radio command item for a coalition, which can invoke a function with parameters. + -- @param #MENU_COALITION_COMMAND self + -- @param DCS#coalition.side Coalition The coalition owning the menu. + -- @param #string MenuText The text for the menu. + -- @param Core.Menu#MENU_COALITION ParentMenu The parent menu. + -- @param CommandMenuFunction A function that is called when the menu key is pressed. + -- @param CommandMenuArgument An argument for the function. There can only be ONE argument given. So multiple arguments must be wrapped into a table. See the below example how to do this. + -- @return #MENU_COALITION_COMMAND + function MENU_COALITION_COMMAND:New( Coalition, MenuText, ParentMenu, CommandMenuFunction, ... ) + + MENU_INDEX:PrepareCoalition( Coalition ) + local Path = MENU_INDEX:ParentPath( ParentMenu, MenuText ) + local CoalitionMenu = MENU_INDEX:HasCoalitionMenu( Coalition, Path ) + + if CoalitionMenu then + CoalitionMenu:SetCommandMenuFunction( CommandMenuFunction ) + CoalitionMenu:SetCommandMenuArguments( arg ) + return CoalitionMenu + else + + local self = BASE:Inherit( self, MENU_COMMAND_BASE:New( MenuText, ParentMenu, CommandMenuFunction, arg ) ) + MENU_INDEX:SetCoalitionMenu( Coalition, Path, self ) + + self.Coalition = Coalition + self.MenuPath = missionCommands.addCommandForCoalition( self.Coalition, MenuText, self.MenuParentPath, self.MenuCallHandler ) + self:SetParentMenu( self.MenuText, self ) + return self + end + + end + + + --- Refreshes a radio item for a coalition + -- @param #MENU_COALITION_COMMAND self + -- @return #MENU_COALITION_COMMAND + function MENU_COALITION_COMMAND:Refresh() + + do + missionCommands.removeItemForCoalition( self.Coalition, self.MenuPath ) + missionCommands.addCommandForCoalition( self.Coalition, self.MenuText, self.MenuParentPath, self.MenuCallHandler ) + end + + end + + --- Removes a radio command item for a coalition + -- @param #MENU_COALITION_COMMAND self + -- @return #nil + function MENU_COALITION_COMMAND:Remove( MenuStamp, MenuTag ) + + MENU_INDEX:PrepareCoalition( self.Coalition ) + local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText ) + local CoalitionMenu = MENU_INDEX:HasCoalitionMenu( self.Coalition, Path ) + + if CoalitionMenu == self then + if not MenuStamp or self.MenuStamp ~= MenuStamp then + if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then + self:F( { Coalition = self.Coalition, Text = self.MenuText, Path = self.MenuPath } ) + if self.MenuPath ~= nil then + missionCommands.removeItemForCoalition( self.Coalition, self.MenuPath ) + end + MENU_INDEX:ClearCoalitionMenu( self.Coalition, Path ) + self:ClearParentMenu( self.MenuText ) + return nil + end + end + else + BASE:E( { "Cannot Remove MENU_COALITION_COMMAND", Path = Path, ParentMenu = self.ParentMenu, MenuText = self.MenuText, Coalition = self.Coalition } ) + end + + return self + end + +end + + +--- MENU_GROUP + +do + -- This local variable is used to cache the menus registered under groups. + -- Menus don't dissapear when groups for players are destroyed and restarted. + -- So every menu for a client created must be tracked so that program logic accidentally does not create. + -- the same menus twice during initialization logic. + -- These menu classes are handling this logic with this variable. + local _MENUGROUPS = {} + + --- @type MENU_GROUP + -- @extends Core.Menu#MENU_BASE + + + --- Manages the main menus for @{Wrapper.Group}s. + -- + -- You can add menus with the @{#MENU_GROUP.New} method, which constructs a MENU_GROUP object and returns you the object reference. + -- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_GROUP.Remove}. + -- + -- @usage + -- -- This demo creates a menu structure for the two groups of planes. + -- -- Each group will receive a different menu structure. + -- -- To test, join the planes, then look at the other radio menus (Option F10). + -- -- Then switch planes and check if the menu is still there. + -- -- And play with the Add and Remove menu options. + -- + -- -- Note that in multi player, this will only work after the DCS groups bug is solved. + -- + -- local function ShowStatus( PlaneGroup, StatusText, Coalition ) + -- + -- MESSAGE:New( Coalition, 15 ):ToRed() + -- PlaneGroup:Message( StatusText, 15 ) + -- end + -- + -- local MenuStatus = {} + -- + -- local function RemoveStatusMenu( MenuGroup ) + -- local MenuGroupName = MenuGroup:GetName() + -- MenuStatus[MenuGroupName]:Remove() + -- end + -- + -- --- @param Wrapper.Group#GROUP MenuGroup + -- local function AddStatusMenu( MenuGroup ) + -- local MenuGroupName = MenuGroup:GetName() + -- -- This would create a menu for the red coalition under the MenuCoalitionRed menu object. + -- MenuStatus[MenuGroupName] = MENU_GROUP:New( MenuGroup, "Status for Planes" ) + -- MENU_GROUP_COMMAND:New( MenuGroup, "Show Status", MenuStatus[MenuGroupName], ShowStatus, MenuGroup, "Status of planes is ok!", "Message to Red Coalition" ) + -- end + -- + -- SCHEDULER:New( nil, + -- function() + -- local PlaneGroup = GROUP:FindByName( "Plane 1" ) + -- if PlaneGroup and PlaneGroup:IsAlive() then + -- local MenuManage = MENU_GROUP:New( PlaneGroup, "Manage Menus" ) + -- MENU_GROUP_COMMAND:New( PlaneGroup, "Add Status Menu Plane 1", MenuManage, AddStatusMenu, PlaneGroup ) + -- MENU_GROUP_COMMAND:New( PlaneGroup, "Remove Status Menu Plane 1", MenuManage, RemoveStatusMenu, PlaneGroup ) + -- end + -- end, {}, 10, 10 ) + -- + -- SCHEDULER:New( nil, + -- function() + -- local PlaneGroup = GROUP:FindByName( "Plane 2" ) + -- if PlaneGroup and PlaneGroup:IsAlive() then + -- local MenuManage = MENU_GROUP:New( PlaneGroup, "Manage Menus" ) + -- MENU_GROUP_COMMAND:New( PlaneGroup, "Add Status Menu Plane 2", MenuManage, AddStatusMenu, PlaneGroup ) + -- MENU_GROUP_COMMAND:New( PlaneGroup, "Remove Status Menu Plane 2", MenuManage, RemoveStatusMenu, PlaneGroup ) + -- end + -- end, {}, 10, 10 ) + -- + -- @field #MENU_GROUP + MENU_GROUP = { + ClassName = "MENU_GROUP" + } + + --- MENU_GROUP constructor. Creates a new radio menu item for a group. + -- @param #MENU_GROUP self + -- @param Wrapper.Group#GROUP Group The Group owning the menu. + -- @param #string MenuText The text for the menu. + -- @param #table ParentMenu The parent menu. + -- @return #MENU_GROUP self + function MENU_GROUP:New( Group, MenuText, ParentMenu ) + + MENU_INDEX:PrepareGroup( Group ) + local Path = MENU_INDEX:ParentPath( ParentMenu, MenuText ) + local GroupMenu = MENU_INDEX:HasGroupMenu( Group, Path ) + + if GroupMenu then + return GroupMenu + else + self = BASE:Inherit( self, MENU_BASE:New( MenuText, ParentMenu ) ) + MENU_INDEX:SetGroupMenu( Group, Path, self ) + + self.Group = Group + self.GroupID = Group:GetID() + + self.MenuPath = missionCommands.addSubMenuForGroup( self.GroupID, MenuText, self.MenuParentPath ) + + self:SetParentMenu( self.MenuText, self ) + return self + end + + end + + --- Refreshes a new radio item for a group and submenus + -- @param #MENU_GROUP self + -- @return #MENU_GROUP + function MENU_GROUP:Refresh() + + do + missionCommands.removeItemForGroup( self.GroupID, self.MenuPath ) + missionCommands.addSubMenuForGroup( self.GroupID, self.MenuText, self.MenuParentPath ) + + for MenuText, Menu in pairs( self.Menus or {} ) do + Menu:Refresh() + end + end + + end + + --- Removes the sub menus recursively of this MENU_GROUP. + -- @param #MENU_GROUP self + -- @param MenuStamp + -- @param MenuTag A Tag or Key to filter the menus to be refreshed with the Tag set. + -- @return #MENU_GROUP self + function MENU_GROUP:RemoveSubMenus( MenuStamp, MenuTag ) + + for MenuText, Menu in pairs( self.Menus or {} ) do + Menu:Remove( MenuStamp, MenuTag ) + end + + self.Menus = nil + + end + + + --- Removes the main menu and sub menus recursively of this MENU_GROUP. + -- @param #MENU_GROUP self + -- @param MenuStamp + -- @param MenuTag A Tag or Key to filter the menus to be refreshed with the Tag set. + -- @return #nil + function MENU_GROUP:Remove( MenuStamp, MenuTag ) + + MENU_INDEX:PrepareGroup( self.Group ) + local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText ) + local GroupMenu = MENU_INDEX:HasGroupMenu( self.Group, Path ) + + if GroupMenu == self then + self:RemoveSubMenus( MenuStamp, MenuTag ) + if not MenuStamp or self.MenuStamp ~= MenuStamp then + if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then + if self.MenuPath ~= nil then + self:F( { Group = self.GroupID, Text = self.MenuText, Path = self.MenuPath } ) + missionCommands.removeItemForGroup( self.GroupID, self.MenuPath ) + end + MENU_INDEX:ClearGroupMenu( self.Group, Path ) + self:ClearParentMenu( self.MenuText ) + return nil + end + end + else + BASE:E( { "Cannot Remove MENU_GROUP", Path = Path, ParentMenu = self.ParentMenu, MenuText = self.MenuText, Group = self.Group } ) + return nil + end + + return self + end + + + --- @type MENU_GROUP_COMMAND + -- @extends Core.Menu#MENU_COMMAND_BASE + + --- The @{Core.Menu#MENU_GROUP_COMMAND} class manages the command menus for coalitions, which allow players to execute functions during mission execution. + -- You can add menus with the @{#MENU_GROUP_COMMAND.New} method, which constructs a MENU_GROUP_COMMAND object and returns you the object reference. + -- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_GROUP_COMMAND.Remove}. + -- + -- @field #MENU_GROUP_COMMAND + MENU_GROUP_COMMAND = { + ClassName = "MENU_GROUP_COMMAND" + } + + --- Creates a new radio command item for a group + -- @param #MENU_GROUP_COMMAND self + -- @param Wrapper.Group#GROUP Group The Group owning the menu. + -- @param MenuText The text for the menu. + -- @param ParentMenu The parent menu. + -- @param CommandMenuFunction A function that is called when the menu key is pressed. + -- @param CommandMenuArgument An argument for the function. + -- @return #MENU_GROUP_COMMAND + function MENU_GROUP_COMMAND:New( Group, MenuText, ParentMenu, CommandMenuFunction, ... ) + + MENU_INDEX:PrepareGroup( Group ) + local Path = MENU_INDEX:ParentPath( ParentMenu, MenuText ) + local GroupMenu = MENU_INDEX:HasGroupMenu( Group, Path ) + + if GroupMenu then + GroupMenu:SetCommandMenuFunction( CommandMenuFunction ) + GroupMenu:SetCommandMenuArguments( arg ) + return GroupMenu + else + self = BASE:Inherit( self, MENU_COMMAND_BASE:New( MenuText, ParentMenu, CommandMenuFunction, arg ) ) + + MENU_INDEX:SetGroupMenu( Group, Path, self ) + + self.Group = Group + self.GroupID = Group:GetID() + + self.MenuPath = missionCommands.addCommandForGroup( self.GroupID, MenuText, self.MenuParentPath, self.MenuCallHandler ) + + self:SetParentMenu( self.MenuText, self ) + return self + end + + end + + --- Refreshes a radio item for a group + -- @param #MENU_GROUP_COMMAND self + -- @return #MENU_GROUP_COMMAND + function MENU_GROUP_COMMAND:Refresh() + + do + missionCommands.removeItemForGroup( self.GroupID, self.MenuPath ) + missionCommands.addCommandForGroup( self.GroupID, self.MenuText, self.MenuParentPath, self.MenuCallHandler ) + end + + end + + --- Removes a menu structure for a group. + -- @param #MENU_GROUP_COMMAND self + -- @param MenuStamp + -- @param MenuTag A Tag or Key to filter the menus to be refreshed with the Tag set. + -- @return #nil + function MENU_GROUP_COMMAND:Remove( MenuStamp, MenuTag ) + + MENU_INDEX:PrepareGroup( self.Group ) + local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText ) + local GroupMenu = MENU_INDEX:HasGroupMenu( self.Group, Path ) + + if GroupMenu == self then + if not MenuStamp or self.MenuStamp ~= MenuStamp then + if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then + if self.MenuPath ~= nil then + self:F( { Group = self.GroupID, Text = self.MenuText, Path = self.MenuPath } ) + missionCommands.removeItemForGroup( self.GroupID, self.MenuPath ) + end + MENU_INDEX:ClearGroupMenu( self.Group, Path ) + self:ClearParentMenu( self.MenuText ) + return nil + end + end + else + BASE:E( { "Cannot Remove MENU_GROUP_COMMAND", Path = Path, ParentMenu = self.ParentMenu, MenuText = self.MenuText, Group = self.Group } ) + end + + return self + end + +end + +--- MENU_GROUP_DELAYED + +do + + --- @type MENU_GROUP_DELAYED + -- @extends Core.Menu#MENU_BASE + + + --- The MENU_GROUP_DELAYED class manages the main menus for groups. + -- You can add menus with the @{#MENU_GROUP.New} method, which constructs a MENU_GROUP object and returns you the object reference. + -- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_GROUP.Remove}. + -- The creation of the menu item is delayed however, and must be created using the @{#MENU_GROUP.Set} method. + -- This method is most of the time called after the "old" menu items have been removed from the sub menu. + -- + -- + -- @field #MENU_GROUP_DELAYED + MENU_GROUP_DELAYED = { + ClassName = "MENU_GROUP_DELAYED" + } + + --- MENU_GROUP_DELAYED constructor. Creates a new radio menu item for a group. + -- @param #MENU_GROUP_DELAYED self + -- @param Wrapper.Group#GROUP Group The Group owning the menu. + -- @param #string MenuText The text for the menu. + -- @param #table ParentMenu The parent menu. + -- @return #MENU_GROUP_DELAYED self + function MENU_GROUP_DELAYED:New( Group, MenuText, ParentMenu ) + + MENU_INDEX:PrepareGroup( Group ) + local Path = MENU_INDEX:ParentPath( ParentMenu, MenuText ) + local GroupMenu = MENU_INDEX:HasGroupMenu( Group, Path ) + + if GroupMenu then + return GroupMenu + else + self = BASE:Inherit( self, MENU_BASE:New( MenuText, ParentMenu ) ) + MENU_INDEX:SetGroupMenu( Group, Path, self ) + + self.Group = Group + self.GroupID = Group:GetID() + + if self.MenuParentPath then + self.MenuPath = UTILS.DeepCopy( self.MenuParentPath ) + else + self.MenuPath = {} + end + table.insert( self.MenuPath, self.MenuText ) + + self:SetParentMenu( self.MenuText, self ) + return self + end + + end + + + --- Refreshes a new radio item for a group and submenus + -- @param #MENU_GROUP_DELAYED self + -- @return #MENU_GROUP_DELAYED + function MENU_GROUP_DELAYED:Set() + + do + if not self.MenuSet then + missionCommands.addSubMenuForGroup( self.GroupID, self.MenuText, self.MenuParentPath ) + self.MenuSet = true + end + + for MenuText, Menu in pairs( self.Menus or {} ) do + Menu:Set() + end + end + + end + + + --- Refreshes a new radio item for a group and submenus + -- @param #MENU_GROUP_DELAYED self + -- @return #MENU_GROUP_DELAYED + function MENU_GROUP_DELAYED:Refresh() + + do + missionCommands.removeItemForGroup( self.GroupID, self.MenuPath ) + missionCommands.addSubMenuForGroup( self.GroupID, self.MenuText, self.MenuParentPath ) + + for MenuText, Menu in pairs( self.Menus or {} ) do + Menu:Refresh() + end + end + + end + + --- Removes the sub menus recursively of this MENU_GROUP_DELAYED. + -- @param #MENU_GROUP_DELAYED self + -- @param MenuStamp + -- @param MenuTag A Tag or Key to filter the menus to be refreshed with the Tag set. + -- @return #MENU_GROUP_DELAYED self + function MENU_GROUP_DELAYED:RemoveSubMenus( MenuStamp, MenuTag ) + + for MenuText, Menu in pairs( self.Menus or {} ) do + Menu:Remove( MenuStamp, MenuTag ) + end + + self.Menus = nil + + end + + + --- Removes the main menu and sub menus recursively of this MENU_GROUP. + -- @param #MENU_GROUP_DELAYED self + -- @param MenuStamp + -- @param MenuTag A Tag or Key to filter the menus to be refreshed with the Tag set. + -- @return #nil + function MENU_GROUP_DELAYED:Remove( MenuStamp, MenuTag ) + + MENU_INDEX:PrepareGroup( self.Group ) + local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText ) + local GroupMenu = MENU_INDEX:HasGroupMenu( self.Group, Path ) + + if GroupMenu == self then + self:RemoveSubMenus( MenuStamp, MenuTag ) + if not MenuStamp or self.MenuStamp ~= MenuStamp then + if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then + if self.MenuPath ~= nil then + self:F( { Group = self.GroupID, Text = self.MenuText, Path = self.MenuPath } ) + missionCommands.removeItemForGroup( self.GroupID, self.MenuPath ) + end + MENU_INDEX:ClearGroupMenu( self.Group, Path ) + self:ClearParentMenu( self.MenuText ) + return nil + end + end + else + BASE:E( { "Cannot Remove MENU_GROUP_DELAYED", Path = Path, ParentMenu = self.ParentMenu, MenuText = self.MenuText, Group = self.Group } ) + return nil + end + + return self + end + + + --- @type MENU_GROUP_COMMAND_DELAYED + -- @extends Core.Menu#MENU_COMMAND_BASE + + --- Manages the command menus for coalitions, which allow players to execute functions during mission execution. + -- + -- You can add menus with the @{#MENU_GROUP_COMMAND_DELAYED.New} method, which constructs a MENU_GROUP_COMMAND_DELAYED object and returns you the object reference. + -- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_GROUP_COMMAND_DELAYED.Remove}. + -- + -- @field #MENU_GROUP_COMMAND_DELAYED + MENU_GROUP_COMMAND_DELAYED = { + ClassName = "MENU_GROUP_COMMAND_DELAYED" + } + + --- Creates a new radio command item for a group + -- @param #MENU_GROUP_COMMAND_DELAYED self + -- @param Wrapper.Group#GROUP Group The Group owning the menu. + -- @param MenuText The text for the menu. + -- @param ParentMenu The parent menu. + -- @param CommandMenuFunction A function that is called when the menu key is pressed. + -- @param CommandMenuArgument An argument for the function. + -- @return #MENU_GROUP_COMMAND_DELAYED + function MENU_GROUP_COMMAND_DELAYED:New( Group, MenuText, ParentMenu, CommandMenuFunction, ... ) + + MENU_INDEX:PrepareGroup( Group ) + local Path = MENU_INDEX:ParentPath( ParentMenu, MenuText ) + local GroupMenu = MENU_INDEX:HasGroupMenu( Group, Path ) + + if GroupMenu then + GroupMenu:SetCommandMenuFunction( CommandMenuFunction ) + GroupMenu:SetCommandMenuArguments( arg ) + return GroupMenu + else + self = BASE:Inherit( self, MENU_COMMAND_BASE:New( MenuText, ParentMenu, CommandMenuFunction, arg ) ) + + MENU_INDEX:SetGroupMenu( Group, Path, self ) + + self.Group = Group + self.GroupID = Group:GetID() + + if self.MenuParentPath then + self.MenuPath = UTILS.DeepCopy( self.MenuParentPath ) + else + self.MenuPath = {} + end + table.insert( self.MenuPath, self.MenuText ) + + self:SetParentMenu( self.MenuText, self ) + return self + end + + end + + --- Refreshes a radio item for a group + -- @param #MENU_GROUP_COMMAND_DELAYED self + -- @return #MENU_GROUP_COMMAND_DELAYED + function MENU_GROUP_COMMAND_DELAYED:Set() + + do + if not self.MenuSet then + self.MenuPath = missionCommands.addCommandForGroup( self.GroupID, self.MenuText, self.MenuParentPath, self.MenuCallHandler ) + self.MenuSet = true + end + end + + end + + --- Refreshes a radio item for a group + -- @param #MENU_GROUP_COMMAND_DELAYED self + -- @return #MENU_GROUP_COMMAND_DELAYED + function MENU_GROUP_COMMAND_DELAYED:Refresh() + + do + missionCommands.removeItemForGroup( self.GroupID, self.MenuPath ) + missionCommands.addCommandForGroup( self.GroupID, self.MenuText, self.MenuParentPath, self.MenuCallHandler ) + end + + end + + --- Removes a menu structure for a group. + -- @param #MENU_GROUP_COMMAND_DELAYED self + -- @param MenuStamp + -- @param MenuTag A Tag or Key to filter the menus to be refreshed with the Tag set. + -- @return #nil + function MENU_GROUP_COMMAND_DELAYED:Remove( MenuStamp, MenuTag ) + + MENU_INDEX:PrepareGroup( self.Group ) + local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText ) + local GroupMenu = MENU_INDEX:HasGroupMenu( self.Group, Path ) + + if GroupMenu == self then + if not MenuStamp or self.MenuStamp ~= MenuStamp then + if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then + if self.MenuPath ~= nil then + self:F( { Group = self.GroupID, Text = self.MenuText, Path = self.MenuPath } ) + missionCommands.removeItemForGroup( self.GroupID, self.MenuPath ) + end + MENU_INDEX:ClearGroupMenu( self.Group, Path ) + self:ClearParentMenu( self.MenuText ) + return nil + end + end + else + BASE:E( { "Cannot Remove MENU_GROUP_COMMAND_DELAYED", Path = Path, ParentMenu = self.ParentMenu, MenuText = self.MenuText, Group = self.Group } ) + end + + return self + end + +end + +--- **Core** - Define zones within your mission of various forms, with various capabilities. +-- +-- === +-- +-- ## Features: +-- +-- * Create radius zones. +-- * Create trigger zones. +-- * Create polygon zones. +-- * Create moving zones around a unit. +-- * Create moving zones around a group. +-- * Provide the zone behaviour. Some zones are static, while others are moveable. +-- * Enquiry if a coordinate is within a zone. +-- * Smoke zones. +-- * Set a zone probability to control zone selection. +-- * Get zone coordinates. +-- * Get zone properties. +-- * Get zone bounding box. +-- * Set/get zone name. +-- * Draw zones (circular and polygon) on the F10 map. +-- +-- +-- There are essentially two core functions that zones accomodate: +-- +-- * Test if an object is within the zone boundaries. +-- * Provide the zone behaviour. Some zones are static, while others are moveable. +-- +-- The object classes are using the zone classes to test the zone boundaries, which can take various forms: +-- +-- * Test if completely within the zone. +-- * Test if partly within the zone (for @{Wrapper.Group#GROUP} objects). +-- * Test if not in the zone. +-- * Distance to the nearest intersecting point of the zone. +-- * Distance to the center of the zone. +-- * ... +-- +-- Each of these ZONE classes have a zone name, and specific parameters defining the zone type: +-- +-- * @{#ZONE_BASE}: The ZONE_BASE class defining the base for all other zone classes. +-- * @{#ZONE_RADIUS}: The ZONE_RADIUS class defined by a zone name, a location and a radius. +-- * @{#ZONE}: The ZONE class, defined by the zone name as defined within the Mission Editor. +-- * @{#ZONE_UNIT}: The ZONE_UNIT class defines by a zone around a @{Wrapper.Unit#UNIT} with a radius. +-- * @{#ZONE_GROUP}: The ZONE_GROUP class defines by a zone around a @{Wrapper.Group#GROUP} with a radius. +-- * @{#ZONE_POLYGON}: The ZONE_POLYGON class defines by a sequence of @{Wrapper.Group#GROUP} waypoints within the Mission Editor, forming a polygon. +-- +-- === +-- +-- ### Author: **FlightControl** +-- ### Contributions: +-- +-- === +-- +-- @module Core.Zone +-- @image Core_Zones.JPG + + +--- @type ZONE_BASE +-- @field #string ZoneName Name of the zone. +-- @field #number ZoneProbability A value between 0 and 1. 0 = 0% and 1 = 100% probability. +-- @field #number DrawID Unique ID of the drawn zone on the F10 map. +-- @field #table Color Table with four entries, e.g. {1, 0, 0, 0.15}. First three are RGB color code. Fourth is the transparency Alpha value. +-- @extends Core.Fsm#FSM + + +--- This class is an abstract BASE class for derived classes, and is not meant to be instantiated. +-- +-- ## Each zone has a name: +-- +-- * @{#ZONE_BASE.GetName}(): Returns the name of the zone. +-- * @{#ZONE_BASE.SetName}(): Sets the name of the zone. +-- +-- +-- ## Each zone implements two polymorphic functions defined in @{Core.Zone#ZONE_BASE}: +-- +-- * @{#ZONE_BASE.IsVec2InZone}(): Returns if a 2D vector is within the zone. +-- * @{#ZONE_BASE.IsVec3InZone}(): Returns if a 3D vector is within the zone. +-- * @{#ZONE_BASE.IsPointVec2InZone}(): Returns if a 2D point vector is within the zone. +-- * @{#ZONE_BASE.IsPointVec3InZone}(): Returns if a 3D point vector is within the zone. +-- +-- ## A zone has a probability factor that can be set to randomize a selection between zones: +-- +-- * @{#ZONE_BASE.SetZoneProbability}(): Set the randomization probability of a zone to be selected, taking a value between 0 and 1 ( 0 = 0%, 1 = 100% ) +-- * @{#ZONE_BASE.GetZoneProbability}(): Get the randomization probability of a zone to be selected, passing a value between 0 and 1 ( 0 = 0%, 1 = 100% ) +-- * @{#ZONE_BASE.GetZoneMaybe}(): Get the zone taking into account the randomization probability. nil is returned if this zone is not a candidate. +-- +-- ## A zone manages vectors: +-- +-- * @{#ZONE_BASE.GetVec2}(): Returns the 2D vector coordinate of the zone. +-- * @{#ZONE_BASE.GetVec3}(): Returns the 3D vector coordinate of the zone. +-- * @{#ZONE_BASE.GetPointVec2}(): Returns the 2D point vector coordinate of the zone. +-- * @{#ZONE_BASE.GetPointVec3}(): Returns the 3D point vector coordinate of the zone. +-- * @{#ZONE_BASE.GetRandomVec2}(): Define a random 2D vector within the zone. +-- * @{#ZONE_BASE.GetRandomPointVec2}(): Define a random 2D point vector within the zone. +-- * @{#ZONE_BASE.GetRandomPointVec3}(): Define a random 3D point vector within the zone. +-- +-- ## A zone has a bounding square: +-- +-- * @{#ZONE_BASE.GetBoundingSquare}(): Get the outer most bounding square of the zone. +-- +-- ## A zone can be marked: +-- +-- * @{#ZONE_BASE.SmokeZone}(): Smokes the zone boundaries in a color. +-- * @{#ZONE_BASE.FlareZone}(): Flares the zone boundaries in a color. +-- +-- @field #ZONE_BASE +ZONE_BASE = { + ClassName = "ZONE_BASE", + ZoneName = "", + ZoneProbability = 1, + DrawID=nil, + Color={} +} + + +--- The ZONE_BASE.BoundingSquare +-- @type ZONE_BASE.BoundingSquare +-- @field DCS#Distance x1 The lower x coordinate (left down) +-- @field DCS#Distance y1 The lower y coordinate (left down) +-- @field DCS#Distance x2 The higher x coordinate (right up) +-- @field DCS#Distance y2 The higher y coordinate (right up) + + +--- ZONE_BASE constructor +-- @param #ZONE_BASE self +-- @param #string ZoneName Name of the zone. +-- @return #ZONE_BASE self +function ZONE_BASE:New( ZoneName ) + local self = BASE:Inherit( self, FSM:New() ) + self:F( ZoneName ) + + self.ZoneName = ZoneName + + return self +end + + + +--- Returns the name of the zone. +-- @param #ZONE_BASE self +-- @return #string The name of the zone. +function ZONE_BASE:GetName() + self:F2() + + return self.ZoneName +end + + +--- Sets the name of the zone. +-- @param #ZONE_BASE self +-- @param #string ZoneName The name of the zone. +-- @return #ZONE_BASE +function ZONE_BASE:SetName( ZoneName ) + self:F2() + + self.ZoneName = ZoneName +end + +--- Returns if a Vec2 is within the zone. +-- @param #ZONE_BASE self +-- @param DCS#Vec2 Vec2 The Vec2 to test. +-- @return #boolean true if the Vec2 is within the zone. +function ZONE_BASE:IsVec2InZone( Vec2 ) + self:F2( Vec2 ) + + return false +end + +--- Returns if a Vec3 is within the zone. +-- @param #ZONE_BASE self +-- @param DCS#Vec3 Vec3 The point to test. +-- @return #boolean true if the Vec3 is within the zone. +function ZONE_BASE:IsVec3InZone( Vec3 ) + local InZone = self:IsVec2InZone( { x = Vec3.x, y = Vec3.z } ) + return InZone +end + +--- Returns if a Coordinate is within the zone. +-- @param #ZONE_BASE self +-- @param Core.Point#COORDINATE Coordinate The coordinate to test. +-- @return #boolean true if the coordinate is within the zone. +function ZONE_BASE:IsCoordinateInZone( Coordinate ) + local InZone = self:IsVec2InZone( Coordinate:GetVec2() ) + return InZone +end + +--- Returns if a PointVec2 is within the zone. (Name is misleading, actually takes a #COORDINATE) +-- @param #ZONE_BASE self +-- @param Core.Point#COORDINATE PointVec2 The coordinate to test. +-- @return #boolean true if the PointVec2 is within the zone. +function ZONE_BASE:IsPointVec2InZone( Coordinate ) + local InZone = self:IsVec2InZone( Coordinate:GetVec2() ) + return InZone +end + +--- Returns if a PointVec3 is within the zone. +-- @param #ZONE_BASE self +-- @param Core.Point#POINT_VEC3 PointVec3 The PointVec3 to test. +-- @return #boolean true if the PointVec3 is within the zone. +function ZONE_BASE:IsPointVec3InZone( PointVec3 ) + local InZone = self:IsPointVec2InZone( PointVec3 ) + return InZone +end + + +--- Returns the @{DCS#Vec2} coordinate of the zone. +-- @param #ZONE_BASE self +-- @return #nil. +function ZONE_BASE:GetVec2() + return nil +end + +--- Returns a @{Core.Point#POINT_VEC2} of the zone. +-- @param #ZONE_BASE self +-- @param DCS#Distance Height The height to add to the land height where the center of the zone is located. +-- @return Core.Point#POINT_VEC2 The PointVec2 of the zone. +function ZONE_BASE:GetPointVec2() + self:F2( self.ZoneName ) + + local Vec2 = self:GetVec2() + + local PointVec2 = POINT_VEC2:NewFromVec2( Vec2 ) + + self:T2( { PointVec2 } ) + + return PointVec2 +end + + +--- Returns the @{DCS#Vec3} of the zone. +-- @param #ZONE_BASE self +-- @param DCS#Distance Height The height to add to the land height where the center of the zone is located. +-- @return DCS#Vec3 The Vec3 of the zone. +function ZONE_BASE:GetVec3( Height ) + self:F2( self.ZoneName ) + + Height = Height or 0 + + local Vec2 = self:GetVec2() + + local Vec3 = { x = Vec2.x, y = Height and Height or land.getHeight( self:GetVec2() ), z = Vec2.y } + + self:T2( { Vec3 } ) + + return Vec3 +end + +--- Returns a @{Core.Point#POINT_VEC3} of the zone. +-- @param #ZONE_BASE self +-- @param DCS#Distance Height The height to add to the land height where the center of the zone is located. +-- @return Core.Point#POINT_VEC3 The PointVec3 of the zone. +function ZONE_BASE:GetPointVec3( Height ) + self:F2( self.ZoneName ) + + local Vec3 = self:GetVec3( Height ) + + local PointVec3 = POINT_VEC3:NewFromVec3( Vec3 ) + + self:T2( { PointVec3 } ) + + return PointVec3 +end + +--- Returns a @{Core.Point#COORDINATE} of the zone. +-- @param #ZONE_BASE self +-- @param DCS#Distance Height The height to add to the land height where the center of the zone is located. +-- @return Core.Point#COORDINATE The Coordinate of the zone. +function ZONE_BASE:GetCoordinate( Height ) --R2.1 + self:F2(self.ZoneName) + + local Vec3 = self:GetVec3( Height ) + + if self.Coordinate then + + -- Update coordinates. + self.Coordinate.x=Vec3.x + self.Coordinate.y=Vec3.y + self.Coordinate.z=Vec3.z + + --env.info("FF GetCoordinate NEW for ZONE_BASE "..tostring(self.ZoneName)) + else + + -- Create a new coordinate object. + self.Coordinate=COORDINATE:NewFromVec3(Vec3) + + --env.info("FF GetCoordinate NEW for ZONE_BASE "..tostring(self.ZoneName)) + end + + return self.Coordinate +end + + +--- Define a random @{DCS#Vec2} within the zone. +-- @param #ZONE_BASE self +-- @return DCS#Vec2 The Vec2 coordinates. +function ZONE_BASE:GetRandomVec2() + return nil +end + +--- Define a random @{Core.Point#POINT_VEC2} within the zone. +-- @param #ZONE_BASE self +-- @return Core.Point#POINT_VEC2 The PointVec2 coordinates. +function ZONE_BASE:GetRandomPointVec2() + return nil +end + +--- Define a random @{Core.Point#POINT_VEC3} within the zone. +-- @param #ZONE_BASE self +-- @return Core.Point#POINT_VEC3 The PointVec3 coordinates. +function ZONE_BASE:GetRandomPointVec3() + return nil +end + +--- Get the bounding square the zone. +-- @param #ZONE_BASE self +-- @return #nil The bounding square. +function ZONE_BASE:GetBoundingSquare() + --return { x1 = 0, y1 = 0, x2 = 0, y2 = 0 } + return nil +end + +--- Bound the zone boundaries with a tires. +-- @param #ZONE_BASE self +function ZONE_BASE:BoundZone() + self:F2() + +end + + +--- Set color of zone. +-- @param #ZONE_BASE self +-- @param #table RGBcolor RGB color table. Default `{1, 0, 0}`. +-- @param #number Alpha Transparacy between 0 and 1. Default 0.15. +-- @return #ZONE_BASE self +function ZONE_BASE:SetColor(RGBcolor, Alpha) + + RGBcolor=RGBcolor or {1, 0, 0} + Alpha=Alpha or 0.15 + + self.Color={} + self.Color[1]=RGBcolor[1] + self.Color[2]=RGBcolor[2] + self.Color[3]=RGBcolor[3] + self.Color[4]=Alpha + + return self +end + +--- Get color table of the zone. +-- @param #ZONE_BASE self +-- @return #table Table with four entries, e.g. {1, 0, 0, 0.15}. First three are RGB color code. Fourth is the transparency Alpha value. +function ZONE_BASE:GetColor() + return self.Color +end + +--- Get RGB color of zone. +-- @param #ZONE_BASE self +-- @return #table Table with three entries, e.g. {1, 0, 0}, which is the RGB color code. +function ZONE_BASE:GetColorRGB() + local rgb={} + rgb[1]=self.Color[1] + rgb[2]=self.Color[2] + rgb[3]=self.Color[3] + return rgb +end + +--- Get transperency Alpha value of zone. +-- @param #ZONE_BASE self +-- @return #number Alpha value. +function ZONE_BASE:GetColorAlpha() + local alpha=self.Color[4] + return alpha +end + +--- Remove the drawing of the zone from the F10 map. +-- @param #ZONE_BASE self +-- @param #number Delay (Optional) Delay before the drawing is removed. +-- @return #ZONE_BASE self +function ZONE_BASE:UndrawZone(Delay) + if Delay and Delay>0 then + self:ScheduleOnce(Delay, ZONE_BASE.UndrawZone, self) + else + if self.DrawID then + UTILS.RemoveMark(self.DrawID) + end + end + return self +end + +--- Get ID of the zone object drawn on the F10 map. +-- The ID can be used to remove the drawn object from the F10 map view via `UTILS.RemoveMark(MarkID)`. +-- @param #ZONE_BASE self +-- @return #number Unique ID of the +function ZONE_BASE:GetDrawID() + return self.DrawID +end + + +--- Smokes the zone boundaries in a color. +-- @param #ZONE_BASE self +-- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color. +function ZONE_BASE:SmokeZone( SmokeColor ) + self:F2( SmokeColor ) + +end + +--- Set the randomization probability of a zone to be selected. +-- @param #ZONE_BASE self +-- @param #number ZoneProbability A value between 0 and 1. 0 = 0% and 1 = 100% probability. +-- @return #ZONE_BASE self +function ZONE_BASE:SetZoneProbability( ZoneProbability ) + self:F( { self:GetName(), ZoneProbability = ZoneProbability } ) + + self.ZoneProbability = ZoneProbability or 1 + return self +end + +--- Get the randomization probability of a zone to be selected. +-- @param #ZONE_BASE self +-- @return #number A value between 0 and 1. 0 = 0% and 1 = 100% probability. +function ZONE_BASE:GetZoneProbability() + self:F2() + + return self.ZoneProbability +end + +--- Get the zone taking into account the randomization probability of a zone to be selected. +-- @param #ZONE_BASE self +-- @return #ZONE_BASE The zone is selected taking into account the randomization probability factor. +-- @return #nil The zone is not selected taking into account the randomization probability factor. +-- @usage +-- +-- local ZoneArray = { ZONE:New( "Zone1" ), ZONE:New( "Zone2" ) } +-- +-- -- We set a zone probability of 70% to the first zone and 30% to the second zone. +-- ZoneArray[1]:SetZoneProbability( 0.5 ) +-- ZoneArray[2]:SetZoneProbability( 0.5 ) +-- +-- local ZoneSelected = nil +-- +-- while ZoneSelected == nil do +-- for _, Zone in pairs( ZoneArray ) do +-- ZoneSelected = Zone:GetZoneMaybe() +-- if ZoneSelected ~= nil then +-- break +-- end +-- end +-- end +-- +-- -- The result should be that Zone1 would be more probable selected than Zone2. +-- +function ZONE_BASE:GetZoneMaybe() + self:F2() + + local Randomization = math.random() + if Randomization <= self.ZoneProbability then + return self + else + return nil + end +end + + +--- The ZONE_RADIUS class, defined by a zone name, a location and a radius. +-- @type ZONE_RADIUS +-- @field DCS#Vec2 Vec2 The current location of the zone. +-- @field DCS#Distance Radius The radius of the zone. +-- @extends #ZONE_BASE + +--- The ZONE_RADIUS class defined by a zone name, a location and a radius. +-- This class implements the inherited functions from Core.Zone#ZONE_BASE taking into account the own zone format and properties. +-- +-- ## ZONE_RADIUS constructor +-- +-- * @{#ZONE_RADIUS.New}(): Constructor. +-- +-- ## Manage the radius of the zone +-- +-- * @{#ZONE_RADIUS.SetRadius}(): Sets the radius of the zone. +-- * @{#ZONE_RADIUS.GetRadius}(): Returns the radius of the zone. +-- +-- ## Manage the location of the zone +-- +-- * @{#ZONE_RADIUS.SetVec2}(): Sets the @{DCS#Vec2} of the zone. +-- * @{#ZONE_RADIUS.GetVec2}(): Returns the @{DCS#Vec2} of the zone. +-- * @{#ZONE_RADIUS.GetVec3}(): Returns the @{DCS#Vec3} of the zone, taking an additional height parameter. +-- +-- ## Zone point randomization +-- +-- Various functions exist to find random points within the zone. +-- +-- * @{#ZONE_RADIUS.GetRandomVec2}(): Gets a random 2D point in the zone. +-- * @{#ZONE_RADIUS.GetRandomPointVec2}(): Gets a @{Core.Point#POINT_VEC2} object representing a random 2D point in the zone. +-- * @{#ZONE_RADIUS.GetRandomPointVec3}(): Gets a @{Core.Point#POINT_VEC3} object representing a random 3D point in the zone. Note that the height of the point is at landheight. +-- +-- ## Draw zone +-- +-- * @{#ZONE_RADIUS.DrawZone}(): Draws the zone on the F10 map. +-- +-- @field #ZONE_RADIUS +ZONE_RADIUS = { + ClassName="ZONE_RADIUS", + } + +--- Constructor of @{#ZONE_RADIUS}, taking the zone name, the zone location and a radius. +-- @param #ZONE_RADIUS self +-- @param #string ZoneName Name of the zone. +-- @param DCS#Vec2 Vec2 The location of the zone. +-- @param DCS#Distance Radius The radius of the zone. +-- @return #ZONE_RADIUS self +function ZONE_RADIUS:New( ZoneName, Vec2, Radius ) + + -- Inherit ZONE_BASE. + local self = BASE:Inherit( self, ZONE_BASE:New( ZoneName ) ) -- #ZONE_RADIUS + self:F( { ZoneName, Vec2, Radius } ) + + self.Radius = Radius + self.Vec2 = Vec2 + + --self.Coordinate=COORDINATE:NewFromVec2(Vec2) + + return self +end + +--- Update zone from a 2D vector. +-- @param #ZONE_RADIUS self +-- @param DCS#Vec2 Vec2 The location of the zone. +-- @param DCS#Distance Radius The radius of the zone. +-- @return #ZONE_RADIUS self +function ZONE_RADIUS:UpdateFromVec2(Vec2, Radius) + + -- New center of the zone. + self.Vec2=Vec2 + + if Radius then + self.Radius=Radius + end + + return self +end + +--- Update zone from a 2D vector. +-- @param #ZONE_RADIUS self +-- @param DCS#Vec3 Vec3 The location of the zone. +-- @param DCS#Distance Radius The radius of the zone. +-- @return #ZONE_RADIUS self +function ZONE_RADIUS:UpdateFromVec3(Vec3, Radius) + + -- New center of the zone. + self.Vec2.x=Vec3.x + self.Vec2.y=Vec3.z + + if Radius then + self.Radius=Radius + end + + return self +end + +--- Mark the zone with markers on the F10 map. +-- @param #ZONE_RADIUS self +-- @param #number Points (Optional) The amount of points in the circle. Default 360. +-- @return #ZONE_RADIUS self +function ZONE_RADIUS:MarkZone(Points) + + local Point = {} + local Vec2 = self:GetVec2() + + Points = Points and Points or 360 + + local Angle + local RadialBase = math.pi*2 + + for Angle = 0, 360, (360 / Points ) do + + local Radial = Angle * RadialBase / 360 + + Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() + Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius() + + COORDINATE:NewFromVec2(Point):MarkToAll(self:GetName()) + + end + +end + +--- Draw the zone circle on the F10 map. +-- @param #ZONE_RADIUS self +-- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All. +-- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red. +-- @param #number Alpha Transparency [0,1]. Default 1. +-- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value. +-- @param #number FillAlpha Transparency [0,1]. Default 0.15. +-- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. +-- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. +-- @return #ZONE_RADIUS self +function ZONE_RADIUS:DrawZone(Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly) + + local coordinate=self:GetCoordinate() + + local Radius=self:GetRadius() + + Color=Color or self:GetColorRGB() + Alpha=Alpha or 1 + FillColor=FillColor or Color + FillAlpha=FillAlpha or self:GetColorAlpha() + + self.DrawID=coordinate:CircleToAll(Radius, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly) + + return self +end + +--- Bounds the zone with tires. +-- @param #ZONE_RADIUS self +-- @param #number Points (optional) The amount of points in the circle. Default 360. +-- @param DCS#country.id CountryID The country id of the tire objects, e.g. country.id.USA for blue or country.id.RUSSIA for red. +-- @param #boolean UnBound (Optional) If true the tyres will be destroyed. +-- @return #ZONE_RADIUS self +function ZONE_RADIUS:BoundZone( Points, CountryID, UnBound ) + + local Point = {} + local Vec2 = self:GetVec2() + + Points = Points and Points or 360 + + local Angle + local RadialBase = math.pi*2 + + -- + for Angle = 0, 360, (360 / Points ) do + local Radial = Angle * RadialBase / 360 + Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() + Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius() + + local CountryName = _DATABASE.COUNTRY_NAME[CountryID] + + local Tire = { + ["country"] = CountryName, + ["category"] = "Fortifications", + ["canCargo"] = false, + ["shape_name"] = "H-tyre_B_WF", + ["type"] = "Black_Tyre_WF", + --["unitId"] = Angle + 10000, + ["y"] = Point.y, + ["x"] = Point.x, + ["name"] = string.format( "%s-Tire #%0d", self:GetName(), Angle ), + ["heading"] = 0, + } -- end of ["group"] + + local Group = coalition.addStaticObject( CountryID, Tire ) + if UnBound and UnBound == true then + Group:destroy() + end + end + + return self +end + + +--- Smokes the zone boundaries in a color. +-- @param #ZONE_RADIUS self +-- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color. +-- @param #number Points (optional) The amount of points in the circle. +-- @param #number AddHeight (optional) The height to be added for the smoke. +-- @param #number AddOffSet (optional) The angle to be added for the smoking start position. +-- @return #ZONE_RADIUS self +function ZONE_RADIUS:SmokeZone( SmokeColor, Points, AddHeight, AngleOffset ) + self:F2( SmokeColor ) + + local Point = {} + local Vec2 = self:GetVec2() + + AddHeight = AddHeight or 0 + AngleOffset = AngleOffset or 0 + + Points = Points and Points or 360 + + local Angle + local RadialBase = math.pi*2 + + for Angle = 0, 360, 360 / Points do + local Radial = ( Angle + AngleOffset ) * RadialBase / 360 + Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() + Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius() + POINT_VEC2:New( Point.x, Point.y, AddHeight ):Smoke( SmokeColor ) + end + + return self +end + + +--- Flares the zone boundaries in a color. +-- @param #ZONE_RADIUS self +-- @param Utilities.Utils#FLARECOLOR FlareColor The flare color. +-- @param #number Points (optional) The amount of points in the circle. +-- @param DCS#Azimuth Azimuth (optional) Azimuth The azimuth of the flare. +-- @param #number AddHeight (optional) The height to be added for the smoke. +-- @return #ZONE_RADIUS self +function ZONE_RADIUS:FlareZone( FlareColor, Points, Azimuth, AddHeight ) + self:F2( { FlareColor, Azimuth } ) + + local Point = {} + local Vec2 = self:GetVec2() + + AddHeight = AddHeight or 0 + + Points = Points and Points or 360 + + local Angle + local RadialBase = math.pi*2 + + for Angle = 0, 360, 360 / Points do + local Radial = Angle * RadialBase / 360 + Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() + Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius() + POINT_VEC2:New( Point.x, Point.y, AddHeight ):Flare( FlareColor, Azimuth ) + end + + return self +end + +--- Returns the radius of the zone. +-- @param #ZONE_RADIUS self +-- @return DCS#Distance The radius of the zone. +function ZONE_RADIUS:GetRadius() + self:F2( self.ZoneName ) + + self:T2( { self.Radius } ) + + return self.Radius +end + +--- Sets the radius of the zone. +-- @param #ZONE_RADIUS self +-- @param DCS#Distance Radius The radius of the zone. +-- @return DCS#Distance The radius of the zone. +function ZONE_RADIUS:SetRadius( Radius ) + self:F2( self.ZoneName ) + + self.Radius = Radius + self:T2( { self.Radius } ) + + return self.Radius +end + +--- Returns the @{DCS#Vec2} of the zone. +-- @param #ZONE_RADIUS self +-- @return DCS#Vec2 The location of the zone. +function ZONE_RADIUS:GetVec2() + self:F2( self.ZoneName ) + + self:T2( { self.Vec2 } ) + + return self.Vec2 +end + +--- Sets the @{DCS#Vec2} of the zone. +-- @param #ZONE_RADIUS self +-- @param DCS#Vec2 Vec2 The new location of the zone. +-- @return DCS#Vec2 The new location of the zone. +function ZONE_RADIUS:SetVec2( Vec2 ) + self:F2( self.ZoneName ) + + self.Vec2 = Vec2 + + self:T2( { self.Vec2 } ) + + return self.Vec2 +end + +--- Returns the @{DCS#Vec3} of the ZONE_RADIUS. +-- @param #ZONE_RADIUS self +-- @param DCS#Distance Height The height to add to the land height where the center of the zone is located. +-- @return DCS#Vec3 The point of the zone. +function ZONE_RADIUS:GetVec3( Height ) + self:F2( { self.ZoneName, Height } ) + + Height = Height or 0 + local Vec2 = self:GetVec2() + + local Vec3 = { x = Vec2.x, y = land.getHeight( self:GetVec2() ) + Height, z = Vec2.y } + + self:T2( { Vec3 } ) + + return Vec3 +end + + + + + +--- Scan the zone for the presence of units of the given ObjectCategories. +-- Note that after a zone has been scanned, the zone can be evaluated by: +-- +-- * @{ZONE_RADIUS.IsAllInZoneOfCoalition}(): Scan the presence of units in the zone of a coalition. +-- * @{ZONE_RADIUS.IsAllInZoneOfOtherCoalition}(): Scan the presence of units in the zone of an other coalition. +-- * @{ZONE_RADIUS.IsSomeInZoneOfCoalition}(): Scan if there is some presence of units in the zone of the given coalition. +-- * @{ZONE_RADIUS.IsNoneInZoneOfCoalition}(): Scan if there isn't any presence of units in the zone of an other coalition than the given one. +-- * @{ZONE_RADIUS.IsNoneInZone}(): Scan if the zone is empty. +-- @{#ZONE_RADIUS. +-- @param #ZONE_RADIUS self +-- @param ObjectCategories An array of categories of the objects to find in the zone. +-- @param UnitCategories An array of unit categories of the objects to find in the zone. +-- @usage +-- self.Zone:Scan() +-- local IsAttacked = self.Zone:IsSomeInZoneOfCoalition( self.Coalition ) +function ZONE_RADIUS:Scan( ObjectCategories, UnitCategories ) + + self.ScanData = {} + self.ScanData.Coalitions = {} + self.ScanData.Scenery = {} + self.ScanData.Units = {} + + local ZoneCoord = self:GetCoordinate() + local ZoneRadius = self:GetRadius() + + self:F({ZoneCoord = ZoneCoord, ZoneRadius = ZoneRadius, ZoneCoordLL = ZoneCoord:ToStringLLDMS()}) + + local SphereSearch = { + id = world.VolumeType.SPHERE, + params = { + point = ZoneCoord:GetVec3(), + radius = ZoneRadius, + } + } + + local function EvaluateZone( ZoneObject ) + --if ZoneObject:isExist() then --FF: isExist always returns false for SCENERY objects since DCS 2.2 and still in DCS 2.5 + if ZoneObject then + + local ObjectCategory = ZoneObject:getCategory() + + --local name=ZoneObject:getName() + --env.info(string.format("Zone object %s", tostring(name))) + --self:E(ZoneObject) + + if ( ObjectCategory == Object.Category.UNIT and ZoneObject:isExist() and ZoneObject:isActive() ) or (ObjectCategory == Object.Category.STATIC and ZoneObject:isExist()) then + + local CoalitionDCSUnit = ZoneObject:getCoalition() + + local Include = false + if not UnitCategories then + -- Anythink found is included. + Include = true + else + -- Check if found object is in specified categories. + local CategoryDCSUnit = ZoneObject:getDesc().category + + for UnitCategoryID, UnitCategory in pairs( UnitCategories ) do + if UnitCategory == CategoryDCSUnit then + Include = true + break + end + end + + end + + if Include then + + local CoalitionDCSUnit = ZoneObject:getCoalition() + + -- This coalition is inside the zone. + self.ScanData.Coalitions[CoalitionDCSUnit] = true + + self.ScanData.Units[ZoneObject] = ZoneObject + + self:F2( { Name = ZoneObject:getName(), Coalition = CoalitionDCSUnit } ) + end + end + + if ObjectCategory == Object.Category.SCENERY then + local SceneryType = ZoneObject:getTypeName() + local SceneryName = ZoneObject:getName() + self.ScanData.Scenery[SceneryType] = self.ScanData.Scenery[SceneryType] or {} + self.ScanData.Scenery[SceneryType][SceneryName] = SCENERY:Register( SceneryName, ZoneObject ) + self:F2( { SCENERY = self.ScanData.Scenery[SceneryType][SceneryName] } ) + end + + end + + return true + end + + -- Search objects. + world.searchObjects( ObjectCategories, SphereSearch, EvaluateZone ) + +end + +--- Count the number of different coalitions inside the zone. +-- @param #ZONE_RADIUS self +-- @return #table Table of DCS units and DCS statics inside the zone. +function ZONE_RADIUS:GetScannedUnits() + + return self.ScanData.Units +end + + +--- Get a set of scanned units. +-- @param #ZONE_RADIUS self +-- @return Core.Set#SET_UNIT Set of units and statics inside the zone. +function ZONE_RADIUS:GetScannedSetUnit() + + local SetUnit = SET_UNIT:New() + + if self.ScanData then + for ObjectID, UnitObject in pairs( self.ScanData.Units ) do + local UnitObject = UnitObject -- DCS#Unit + if UnitObject:isExist() then + local FoundUnit = UNIT:FindByName( UnitObject:getName() ) + if FoundUnit then + SetUnit:AddUnit( FoundUnit ) + else + local FoundStatic = STATIC:FindByName( UnitObject:getName() ) + if FoundStatic then + SetUnit:AddUnit( FoundStatic ) + end + end + end + end + end + + return SetUnit +end + +--- Get a set of scanned units. +-- @param #ZONE_RADIUS self +-- @return Core.Set#SET_GROUP Set of groups. +function ZONE_RADIUS:GetScannedSetGroup() + + self.ScanSetGroup=self.ScanSetGroup or SET_GROUP:New() --Core.Set#SET_GROUP + + self.ScanSetGroup.Set={} + + if self.ScanData then + for ObjectID, UnitObject in pairs( self.ScanData.Units ) do + local UnitObject = UnitObject -- DCS#Unit + if UnitObject:isExist() then + + local FoundUnit=UNIT:FindByName(UnitObject:getName()) + if FoundUnit then + local group=FoundUnit:GetGroup() + self.ScanSetGroup:AddGroup(group) + end + end + end + end + + return self.ScanSetGroup +end + + +--- Count the number of different coalitions inside the zone. +-- @param #ZONE_RADIUS self +-- @return #number Counted coalitions. +function ZONE_RADIUS:CountScannedCoalitions() + + local Count = 0 + + for CoalitionID, Coalition in pairs( self.ScanData.Coalitions ) do + Count = Count + 1 + end + + return Count +end + +--- Check if a certain coalition is inside a scanned zone. +-- @param #ZONE_RADIUS self +-- @param #number Coalition The coalition id, e.g. coalition.side.BLUE. +-- @return #boolean If true, the coalition is inside the zone. +function ZONE_RADIUS:CheckScannedCoalition( Coalition ) + if Coalition then + return self.ScanData.Coalitions[Coalition] + end + return nil +end + +--- Get Coalitions of the units in the Zone, or Check if there are units of the given Coalition in the Zone. +-- Returns nil if there are none to two Coalitions in the zone! +-- Returns one Coalition if there are only Units of one Coalition in the Zone. +-- Returns the Coalition for the given Coalition if there are units of the Coalition in the Zone. +-- @param #ZONE_RADIUS self +-- @return #table +function ZONE_RADIUS:GetScannedCoalition( Coalition ) + + if Coalition then + return self.ScanData.Coalitions[Coalition] + else + local Count = 0 + local ReturnCoalition = nil + + for CoalitionID, Coalition in pairs( self.ScanData.Coalitions ) do + Count = Count + 1 + ReturnCoalition = CoalitionID + end + + if Count ~= 1 then + ReturnCoalition = nil + end + + return ReturnCoalition + end +end + + +--- Get scanned scenery type +-- @param #ZONE_RADIUS self +-- @return #table Table of DCS scenery type objects. +function ZONE_RADIUS:GetScannedSceneryType( SceneryType ) + return self.ScanData.Scenery[SceneryType] +end + + +--- Get scanned scenery table +-- @param #ZONE_RADIUS self +-- @return #table Table of DCS scenery objects. +function ZONE_RADIUS:GetScannedScenery() + return self.ScanData.Scenery +end + + +--- Is All in Zone of Coalition? +-- Check if only the specifed coalition is inside the zone and noone else. +-- @param #ZONE_RADIUS self +-- @param #number Coalition Coalition ID of the coalition which is checked to be the only one in the zone. +-- @return #boolean True, if **only** that coalition is inside the zone and no one else. +-- @usage +-- self.Zone:Scan() +-- local IsGuarded = self.Zone:IsAllInZoneOfCoalition( self.Coalition ) +function ZONE_RADIUS:IsAllInZoneOfCoalition( Coalition ) + + --self:E( { Coalitions = self.Coalitions, Count = self:CountScannedCoalitions() } ) + return self:CountScannedCoalitions() == 1 and self:GetScannedCoalition( Coalition ) == true +end + + +--- Is All in Zone of Other Coalition? +-- Check if only one coalition is inside the zone and the specified coalition is not the one. +-- You first need to use the @{#ZONE_RADIUS.Scan} method to scan the zone before it can be evaluated! +-- Note that once a zone has been scanned, multiple evaluations can be done on the scan result set. +-- @param #ZONE_RADIUS self +-- @param #number Coalition Coalition ID of the coalition which is not supposed to be in the zone. +-- @return #boolean True, if and only if only one coalition is inside the zone and the specified coalition is not it. +-- @usage +-- self.Zone:Scan() +-- local IsCaptured = self.Zone:IsAllInZoneOfOtherCoalition( self.Coalition ) +function ZONE_RADIUS:IsAllInZoneOfOtherCoalition( Coalition ) + + --self:E( { Coalitions = self.Coalitions, Count = self:CountScannedCoalitions() } ) + return self:CountScannedCoalitions() == 1 and self:GetScannedCoalition( Coalition ) == nil +end + + +--- Is Some in Zone of Coalition? +-- Check if more than one coaltion is inside the zone and the specifed coalition is one of them. +-- You first need to use the @{#ZONE_RADIUS.Scan} method to scan the zone before it can be evaluated! +-- Note that once a zone has been scanned, multiple evaluations can be done on the scan result set. +-- @param #ZONE_RADIUS self +-- @param #number Coalition ID of the coaliton which is checked to be inside the zone. +-- @return #boolean True if more than one coalition is inside the zone and the specified coalition is one of them. +-- @usage +-- self.Zone:Scan() +-- local IsAttacked = self.Zone:IsSomeInZoneOfCoalition( self.Coalition ) +function ZONE_RADIUS:IsSomeInZoneOfCoalition( Coalition ) + + return self:CountScannedCoalitions() > 1 and self:GetScannedCoalition( Coalition ) == true +end + + +--- Is None in Zone of Coalition? +-- You first need to use the @{#ZONE_RADIUS.Scan} method to scan the zone before it can be evaluated! +-- Note that once a zone has been scanned, multiple evaluations can be done on the scan result set. +-- @param #ZONE_RADIUS self +-- @param Coalition +-- @return #boolean +-- @usage +-- self.Zone:Scan() +-- local IsOccupied = self.Zone:IsNoneInZoneOfCoalition( self.Coalition ) +function ZONE_RADIUS:IsNoneInZoneOfCoalition( Coalition ) + + return self:GetScannedCoalition( Coalition ) == nil +end + + +--- Is None in Zone? +-- You first need to use the @{#ZONE_RADIUS.Scan} method to scan the zone before it can be evaluated! +-- Note that once a zone has been scanned, multiple evaluations can be done on the scan result set. +-- @param #ZONE_RADIUS self +-- @return #boolean +-- @usage +-- self.Zone:Scan() +-- local IsEmpty = self.Zone:IsNoneInZone() +function ZONE_RADIUS:IsNoneInZone() + + return self:CountScannedCoalitions() == 0 +end + + + + +--- Searches the zone +-- @param #ZONE_RADIUS self +-- @param ObjectCategories A list of categories, which are members of Object.Category +-- @param EvaluateFunction +function ZONE_RADIUS:SearchZone( EvaluateFunction, ObjectCategories ) + + local SearchZoneResult = true + + local ZoneCoord = self:GetCoordinate() + local ZoneRadius = self:GetRadius() + + self:F({ZoneCoord = ZoneCoord, ZoneRadius = ZoneRadius, ZoneCoordLL = ZoneCoord:ToStringLLDMS()}) + + local SphereSearch = { + id = world.VolumeType.SPHERE, + params = { + point = ZoneCoord:GetVec3(), + radius = ZoneRadius / 2, + } + } + + local function EvaluateZone( ZoneDCSUnit ) + + + local ZoneUnit = UNIT:Find( ZoneDCSUnit ) + + return EvaluateFunction( ZoneUnit ) + end + + world.searchObjects( Object.Category.UNIT, SphereSearch, EvaluateZone ) + +end + +--- Returns if a location is within the zone. +-- @param #ZONE_RADIUS self +-- @param DCS#Vec2 Vec2 The location to test. +-- @return #boolean true if the location is within the zone. +function ZONE_RADIUS:IsVec2InZone( Vec2 ) + self:F2( Vec2 ) + + local ZoneVec2 = self:GetVec2() + + if ZoneVec2 then + if (( Vec2.x - ZoneVec2.x )^2 + ( Vec2.y - ZoneVec2.y ) ^2 ) ^ 0.5 <= self:GetRadius() then + return true + end + end + + return false +end + +--- Returns if a point is within the zone. +-- @param #ZONE_RADIUS self +-- @param DCS#Vec3 Vec3 The point to test. +-- @return #boolean true if the point is within the zone. +function ZONE_RADIUS:IsVec3InZone( Vec3 ) + self:F2( Vec3 ) + + local InZone = self:IsVec2InZone( { x = Vec3.x, y = Vec3.z } ) + + return InZone +end + +--- Returns a random Vec2 location within the zone. +-- @param #ZONE_RADIUS self +-- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. +-- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. +-- @return DCS#Vec2 The random location within the zone. +function ZONE_RADIUS:GetRandomVec2( inner, outer ) + self:F( self.ZoneName, inner, outer ) + + local Point = {} + local Vec2 = self:GetVec2() + local _inner = inner or 0 + local _outer = outer or self:GetRadius() + + local angle = math.random() * math.pi * 2; + Point.x = Vec2.x + math.cos( angle ) * math.random(_inner, _outer); + Point.y = Vec2.y + math.sin( angle ) * math.random(_inner, _outer); + + self:T( { Point } ) + + return Point +end + +--- Returns a @{Core.Point#POINT_VEC2} object reflecting a random 2D location within the zone. +-- @param #ZONE_RADIUS self +-- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. +-- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. +-- @return Core.Point#POINT_VEC2 The @{Core.Point#POINT_VEC2} object reflecting the random 3D location within the zone. +function ZONE_RADIUS:GetRandomPointVec2( inner, outer ) + self:F( self.ZoneName, inner, outer ) + + local PointVec2 = POINT_VEC2:NewFromVec2( self:GetRandomVec2( inner, outer ) ) + + self:T3( { PointVec2 } ) + + return PointVec2 +end + +--- Returns Returns a random Vec3 location within the zone. +-- @param #ZONE_RADIUS self +-- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. +-- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. +-- @return DCS#Vec3 The random location within the zone. +function ZONE_RADIUS:GetRandomVec3( inner, outer ) + self:F( self.ZoneName, inner, outer ) + + local Vec2 = self:GetRandomVec2( inner, outer ) + + self:T3( { x = Vec2.x, y = self.y, z = Vec2.y } ) + + return { x = Vec2.x, y = self.y, z = Vec2.y } +end + + +--- Returns a @{Core.Point#POINT_VEC3} object reflecting a random 3D location within the zone. +-- @param #ZONE_RADIUS self +-- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. +-- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. +-- @return Core.Point#POINT_VEC3 The @{Core.Point#POINT_VEC3} object reflecting the random 3D location within the zone. +function ZONE_RADIUS:GetRandomPointVec3( inner, outer ) + self:F( self.ZoneName, inner, outer ) + + local PointVec3 = POINT_VEC3:NewFromVec2( self:GetRandomVec2( inner, outer ) ) + + self:T3( { PointVec3 } ) + + return PointVec3 +end + + +--- Returns a @{Core.Point#COORDINATE} object reflecting a random 3D location within the zone. +-- @param #ZONE_RADIUS self +-- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. +-- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. +-- @return Core.Point#COORDINATE +function ZONE_RADIUS:GetRandomCoordinate( inner, outer ) + self:F( self.ZoneName, inner, outer ) + + local Coordinate = COORDINATE:NewFromVec2( self:GetRandomVec2(inner, outer) ) + + self:T3( { Coordinate = Coordinate } ) + + return Coordinate +end + + + +--- @type ZONE +-- @extends #ZONE_RADIUS + + +--- The ZONE class, defined by the zone name as defined within the Mission Editor. +-- This class implements the inherited functions from @{#ZONE_RADIUS} taking into account the own zone format and properties. +-- +-- ## ZONE constructor +-- +-- * @{#ZONE.New}(): Constructor. This will search for a trigger zone with the name given, and will return for you a ZONE object. +-- +-- ## Declare a ZONE directly in the DCS mission editor! +-- +-- You can declare a ZONE using the DCS mission editor by adding a trigger zone in the mission editor. +-- +-- Then during mission startup, when loading Moose.lua, this trigger zone will be detected as a ZONE declaration. +-- Within the background, a ZONE object will be created within the @{Core.Database}. +-- The ZONE name will be the trigger zone name. +-- +-- So, you can search yourself for the ZONE object by using the @{#ZONE.FindByName}() method. +-- In this example, `local TriggerZone = ZONE:FindByName( "DefenseZone" )` would return the ZONE object +-- that was created at mission startup, and reference it into the `TriggerZone` local object. +-- +-- Refer to mission `ZON-110` for a demonstration. +-- +-- This is especially handy if you want to quickly setup a SET_ZONE... +-- So when you would declare `local SetZone = SET_ZONE:New():FilterPrefixes( "Defense" ):FilterStart()`, +-- then SetZone would contain the ZONE object `DefenseZone` as part of the zone collection, +-- without much scripting overhead!!! +-- +-- +-- @field #ZONE +ZONE = { + ClassName="ZONE", + } + + +--- Constructor of ZONE taking the zone name. +-- @param #ZONE self +-- @param #string ZoneName The name of the zone as defined within the mission editor. +-- @return #ZONE self +function ZONE:New( ZoneName ) + + -- First try to find the zone in the DB. + local zone=_DATABASE:FindZone(ZoneName) + + if zone then + --env.info("FF found zone in DB") + return zone + end + + -- Get zone from DCS trigger function. + local Zone = trigger.misc.getZone( ZoneName ) + + -- Error! + if not Zone then + error( "Zone " .. ZoneName .. " does not exist." ) + return nil + end + + -- Create a new ZONE_RADIUS. + local self=BASE:Inherit( self, ZONE_RADIUS:New(ZoneName, {x=Zone.point.x, y=Zone.point.z}, Zone.radius)) + self:F(ZoneName) + + -- Color of zone. + self.Color={1, 0, 0, 0.15} + + -- DCS zone. + self.Zone = Zone + + return self +end + +--- Find a zone in the _DATABASE using the name of the zone. +-- @param #ZONE_BASE self +-- @param #string ZoneName The name of the zone. +-- @return #ZONE_BASE self +function ZONE:FindByName( ZoneName ) + + local ZoneFound = _DATABASE:FindZone( ZoneName ) + return ZoneFound +end + + + +--- @type ZONE_UNIT +-- @field Wrapper.Unit#UNIT ZoneUNIT +-- @extends Core.Zone#ZONE_RADIUS + + +--- # ZONE_UNIT class, extends @{Zone#ZONE_RADIUS} +-- +-- The ZONE_UNIT class defined by a zone attached to a @{Wrapper.Unit#UNIT} with a radius and optional offsets. +-- This class implements the inherited functions from @{#ZONE_RADIUS} taking into account the own zone format and properties. +-- +-- @field #ZONE_UNIT +ZONE_UNIT = { + ClassName="ZONE_UNIT", + } + +--- Constructor to create a ZONE_UNIT instance, taking the zone name, a zone unit and a radius and optional offsets in X and Y directions. +-- @param #ZONE_UNIT self +-- @param #string ZoneName Name of the zone. +-- @param Wrapper.Unit#UNIT ZoneUNIT The unit as the center of the zone. +-- @param Dcs.DCSTypes#Distance Radius The radius of the zone. +-- @param #table Offset A table specifying the offset. The offset table may have the following elements: +-- dx The offset in X direction, +x is north. +-- dy The offset in Y direction, +y is east. +-- rho The distance of the zone from the unit +-- theta The azimuth of the zone relative to unit +-- relative_to_unit If true, theta is measured clockwise from unit's direction else clockwise from north. If using dx, dy setting this to true makes +x parallel to unit heading. +-- dx, dy OR rho, theta may be used, not both. +-- @return #ZONE_UNIT self +function ZONE_UNIT:New( ZoneName, ZoneUNIT, Radius, Offset) + + if Offset then + -- check if the inputs was reasonable, either (dx, dy) or (rho, theta) can be given, else raise an exception. + if (Offset.dx or Offset.dy) and (Offset.rho or Offset.theta) then + error("Cannot use (dx, dy) with (rho, theta)") + end + + self.dy = Offset.dy or 0.0 + self.dx = Offset.dx or 0.0 + self.rho = Offset.rho or 0.0 + self.theta = (Offset.theta or 0.0) * math.pi / 180.0 + self.relative_to_unit = Offset.relative_to_unit or false + end + + local self = BASE:Inherit( self, ZONE_RADIUS:New( ZoneName, ZoneUNIT:GetVec2(), Radius ) ) + + self:F( { ZoneName, ZoneUNIT:GetVec2(), Radius } ) + + self.ZoneUNIT = ZoneUNIT + self.LastVec2 = ZoneUNIT:GetVec2() + + -- Zone objects are added to the _DATABASE and SET_ZONE objects. + _EVENTDISPATCHER:CreateEventNewZone( self ) + + return self +end + + +--- Returns the current location of the @{Wrapper.Unit#UNIT}. +-- @param #ZONE_UNIT self +-- @return DCS#Vec2 The location of the zone based on the @{Wrapper.Unit#UNIT}location and the offset, if any. +function ZONE_UNIT:GetVec2() + self:F2( self.ZoneName ) + + local ZoneVec2 = self.ZoneUNIT:GetVec2() + if ZoneVec2 then + + local heading + if self.relative_to_unit then + heading = ( self.ZoneUNIT:GetHeading() or 0.0 ) * math.pi / 180.0 + else + heading = 0.0 + end + + -- update the zone position with the offsets. + if (self.dx or self.dy) then + + -- use heading to rotate offset relative to unit using rotation matrix in 2D. + -- see: https://en.wikipedia.org/wiki/Rotation_matrix + ZoneVec2.x = ZoneVec2.x + self.dx * math.cos( -heading ) + self.dy * math.sin( -heading ) + ZoneVec2.y = ZoneVec2.y - self.dx * math.sin( -heading ) + self.dy * math.cos( -heading ) + end + + -- if using the polar coordinates + if (self.rho or self.theta) then + ZoneVec2.x = ZoneVec2.x + self.rho * math.cos( self.theta + heading ) + ZoneVec2.y = ZoneVec2.y + self.rho * math.sin( self.theta + heading ) + end + + self.LastVec2 = ZoneVec2 + return ZoneVec2 + else + return self.LastVec2 + end + + self:T2( { ZoneVec2 } ) + + return nil +end + +--- Returns a random location within the zone. +-- @param #ZONE_UNIT self +-- @return DCS#Vec2 The random location within the zone. +function ZONE_UNIT:GetRandomVec2() + self:F( self.ZoneName ) + + local RandomVec2 = {} + --local Vec2 = self.ZoneUNIT:GetVec2() -- FF: This does not take care of the new offset feature! + local Vec2 = self:GetVec2() + + if not Vec2 then + Vec2 = self.LastVec2 + end + + local angle = math.random() * math.pi*2; + RandomVec2.x = Vec2.x + math.cos( angle ) * math.random() * self:GetRadius(); + RandomVec2.y = Vec2.y + math.sin( angle ) * math.random() * self:GetRadius(); + + self:T( { RandomVec2 } ) + + return RandomVec2 +end + +--- Returns the @{DCS#Vec3} of the ZONE_UNIT. +-- @param #ZONE_UNIT self +-- @param DCS#Distance Height The height to add to the land height where the center of the zone is located. +-- @return DCS#Vec3 The point of the zone. +function ZONE_UNIT:GetVec3( Height ) + self:F2( self.ZoneName ) + + Height = Height or 0 + + local Vec2 = self:GetVec2() + + local Vec3 = { x = Vec2.x, y = land.getHeight( self:GetVec2() ) + Height, z = Vec2.y } + + self:T2( { Vec3 } ) + + return Vec3 +end + +--- @type ZONE_GROUP +-- @extends #ZONE_RADIUS + + +--- The ZONE_GROUP class defines by a zone around a @{Wrapper.Group#GROUP} with a radius. The current leader of the group defines the center of the zone. +-- This class implements the inherited functions from @{Core.Zone#ZONE_RADIUS} taking into account the own zone format and properties. +-- +-- @field #ZONE_GROUP +ZONE_GROUP = { + ClassName="ZONE_GROUP", + } + +--- Constructor to create a ZONE_GROUP instance, taking the zone name, a zone @{Wrapper.Group#GROUP} and a radius. +-- @param #ZONE_GROUP self +-- @param #string ZoneName Name of the zone. +-- @param Wrapper.Group#GROUP ZoneGROUP The @{Wrapper.Group} as the center of the zone. +-- @param DCS#Distance Radius The radius of the zone. +-- @return #ZONE_GROUP self +function ZONE_GROUP:New( ZoneName, ZoneGROUP, Radius ) + local self = BASE:Inherit( self, ZONE_RADIUS:New( ZoneName, ZoneGROUP:GetVec2(), Radius ) ) + self:F( { ZoneName, ZoneGROUP:GetVec2(), Radius } ) + + self._.ZoneGROUP = ZoneGROUP + self._.ZoneVec2Cache = self._.ZoneGROUP:GetVec2() + + -- Zone objects are added to the _DATABASE and SET_ZONE objects. + _EVENTDISPATCHER:CreateEventNewZone( self ) + + return self +end + + +--- Returns the current location of the @{Wrapper.Group}. +-- @param #ZONE_GROUP self +-- @return DCS#Vec2 The location of the zone based on the @{Wrapper.Group} location. +function ZONE_GROUP:GetVec2() + self:F( self.ZoneName ) + + local ZoneVec2 = nil + + if self._.ZoneGROUP:IsAlive() then + ZoneVec2 = self._.ZoneGROUP:GetVec2() + self._.ZoneVec2Cache = ZoneVec2 + else + ZoneVec2 = self._.ZoneVec2Cache + end + + self:T( { ZoneVec2 } ) + + return ZoneVec2 +end + +--- Returns a random location within the zone of the @{Wrapper.Group}. +-- @param #ZONE_GROUP self +-- @return DCS#Vec2 The random location of the zone based on the @{Wrapper.Group} location. +function ZONE_GROUP:GetRandomVec2() + self:F( self.ZoneName ) + + local Point = {} + local Vec2 = self._.ZoneGROUP:GetVec2() + + local angle = math.random() * math.pi*2; + Point.x = Vec2.x + math.cos( angle ) * math.random() * self:GetRadius(); + Point.y = Vec2.y + math.sin( angle ) * math.random() * self:GetRadius(); + + self:T( { Point } ) + + return Point +end + +--- Returns a @{Core.Point#POINT_VEC2} object reflecting a random 2D location within the zone. +-- @param #ZONE_GROUP self +-- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. +-- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. +-- @return Core.Point#POINT_VEC2 The @{Core.Point#POINT_VEC2} object reflecting the random 3D location within the zone. +function ZONE_GROUP:GetRandomPointVec2( inner, outer ) + self:F( self.ZoneName, inner, outer ) + + local PointVec2 = POINT_VEC2:NewFromVec2( self:GetRandomVec2() ) + + self:T3( { PointVec2 } ) + + return PointVec2 +end + + +--- @type ZONE_POLYGON_BASE +-- --@field #ZONE_POLYGON_BASE.ListVec2 Polygon The polygon defined by an array of @{DCS#Vec2}. +-- @extends #ZONE_BASE + + +--- The ZONE_POLYGON_BASE class defined by a sequence of @{Wrapper.Group#GROUP} waypoints within the Mission Editor, forming a polygon. +-- This class implements the inherited functions from @{Core.Zone#ZONE_RADIUS} taking into account the own zone format and properties. +-- This class is an abstract BASE class for derived classes, and is not meant to be instantiated. +-- +-- ## Zone point randomization +-- +-- Various functions exist to find random points within the zone. +-- +-- * @{#ZONE_POLYGON_BASE.GetRandomVec2}(): Gets a random 2D point in the zone. +-- * @{#ZONE_POLYGON_BASE.GetRandomPointVec2}(): Return a @{Core.Point#POINT_VEC2} object representing a random 2D point within the zone. +-- * @{#ZONE_POLYGON_BASE.GetRandomPointVec3}(): Return a @{Core.Point#POINT_VEC3} object representing a random 3D point at landheight within the zone. +-- +-- ## Draw zone +-- +-- * @{#ZONE_POLYGON_BASE.DrawZone}(): Draws the zone on the F10 map. +-- * @{#ZONE_POLYGON_BASE.Boundary}(): Draw a frontier on the F10 map with small filled circles. +-- +-- +-- @field #ZONE_POLYGON_BASE +ZONE_POLYGON_BASE = { + ClassName="ZONE_POLYGON_BASE", + } + +--- A 2D points array. +-- @type ZONE_POLYGON_BASE.ListVec2 +-- @list Table of 2D vectors. + +--- A 3D points array. +-- @type ZONE_POLYGON_BASE.ListVec3 +-- @list Table of 3D vectors. + +--- Constructor to create a ZONE_POLYGON_BASE instance, taking the zone name and an array of @{DCS#Vec2}, forming a polygon. +-- The @{Wrapper.Group#GROUP} waypoints define the polygon corners. The first and the last point are automatically connected. +-- @param #ZONE_POLYGON_BASE self +-- @param #string ZoneName Name of the zone. +-- @param #ZONE_POLYGON_BASE.ListVec2 PointsArray An array of @{DCS#Vec2}, forming a polygon. +-- @return #ZONE_POLYGON_BASE self +function ZONE_POLYGON_BASE:New( ZoneName, PointsArray ) + + -- Inherit ZONE_BASE. + local self = BASE:Inherit( self, ZONE_BASE:New( ZoneName ) ) + self:F( { ZoneName, PointsArray } ) + + if PointsArray then + + self._.Polygon = {} + + for i = 1, #PointsArray do + self._.Polygon[i] = {} + self._.Polygon[i].x = PointsArray[i].x + self._.Polygon[i].y = PointsArray[i].y + end + + end + + return self +end + +--- Update polygon points with an array of @{DCS#Vec2}. +-- @param #ZONE_POLYGON_BASE self +-- @param #ZONE_POLYGON_BASE.ListVec2 Vec2Array An array of @{DCS#Vec2}, forming a polygon. +-- @return #ZONE_POLYGON_BASE self +function ZONE_POLYGON_BASE:UpdateFromVec2(Vec2Array) + + self._.Polygon = {} + + for i=1,#Vec2Array do + self._.Polygon[i] = {} + self._.Polygon[i].x=Vec2Array[i].x + self._.Polygon[i].y=Vec2Array[i].y + end + + return self +end + +--- Update polygon points with an array of @{DCS#Vec3}. +-- @param #ZONE_POLYGON_BASE self +-- @param #ZONE_POLYGON_BASE.ListVec3 Vec2Array An array of @{DCS#Vec3}, forming a polygon. +-- @return #ZONE_POLYGON_BASE self +function ZONE_POLYGON_BASE:UpdateFromVec3(Vec3Array) + + self._.Polygon = {} + + for i=1,#Vec3Array do + self._.Polygon[i] = {} + self._.Polygon[i].x=Vec3Array[i].x + self._.Polygon[i].y=Vec3Array[i].z + end + + return self +end + +--- Returns the center location of the polygon. +-- @param #ZONE_POLYGON_BASE self +-- @return DCS#Vec2 The location of the zone based on the @{Wrapper.Group} location. +function ZONE_POLYGON_BASE:GetVec2() + self:F( self.ZoneName ) + + local Bounds = self:GetBoundingSquare() + + return { x = ( Bounds.x2 + Bounds.x1 ) / 2, y = ( Bounds.y2 + Bounds.y1 ) / 2 } +end + +--- Get a vertex of the polygon. +-- @param #ZONE_POLYGON_BASE self +-- @param #number Index Index of the vertex. Default 1. +-- @return DCS#Vec2 Vertex of the polygon. +function ZONE_POLYGON_BASE:GetVertexVec2(Index) + return self._.Polygon[Index or 1] +end + +--- Get a vertex of the polygon. +-- @param #ZONE_POLYGON_BASE self +-- @param #number Index Index of the vertex. Default 1. +-- @return DCS#Vec3 Vertex of the polygon. +function ZONE_POLYGON_BASE:GetVertexVec3(Index) + local vec2=self:GetVertexVec2(Index) + if vec2 then + local vec3={x=vec2.x, y=land.getHeight(vec2), z=vec2.y} + return vec3 + end + return nil +end + +--- Get a vertex of the polygon. +-- @param #ZONE_POLYGON_BASE self +-- @param #number Index Index of the vertex. Default 1. +-- @return Core.Point#COORDINATE Vertex of the polygon. +function ZONE_POLYGON_BASE:GetVertexCoordinate(Index) + local vec2=self:GetVertexVec2(Index) + if vec2 then + local coord=COORDINATE:NewFromVec2(vec2) + return coord + end + return nil +end + + +--- Get a list of verticies of the polygon. +-- @param #ZONE_POLYGON_BASE self +-- @return List of DCS#Vec2 verticies defining the edges of the polygon. +function ZONE_POLYGON_BASE:GetVerticiesVec2() + return self._.Polygon +end + +--- Get a list of verticies of the polygon. +-- @param #ZONE_POLYGON_BASE self +-- @return #table List of DCS#Vec3 verticies defining the edges of the polygon. +function ZONE_POLYGON_BASE:GetVerticiesVec3() + + local coords={} + + for i,vec2 in ipairs(self._.Polygon) do + local vec3={x=vec2.x, y=land.getHeight(vec2), z=vec2.y} + table.insert(coords, vec3) + end + + return coords +end + +--- Get a list of verticies of the polygon. +-- @param #ZONE_POLYGON_BASE self +-- @return #table List of COORDINATES verticies defining the edges of the polygon. +function ZONE_POLYGON_BASE:GetVerticiesCoordinates() + + local coords={} + + for i,vec2 in ipairs(self._.Polygon) do + local coord=COORDINATE:NewFromVec2(vec2) + table.insert(coords, coord) + end + + return coords +end + +--- Flush polygon coordinates as a table in DCS.log. +-- @param #ZONE_POLYGON_BASE self +-- @return #ZONE_POLYGON_BASE self +function ZONE_POLYGON_BASE:Flush() + self:F2() + + self:F( { Polygon = self.ZoneName, Coordinates = self._.Polygon } ) + + return self +end + +--- Smokes the zone boundaries in a color. +-- @param #ZONE_POLYGON_BASE self +-- @param #boolean UnBound If true, the tyres will be destroyed. +-- @return #ZONE_POLYGON_BASE self +function ZONE_POLYGON_BASE:BoundZone( UnBound ) + + local i + local j + local Segments = 10 + + i = 1 + j = #self._.Polygon + + while i <= #self._.Polygon do + self:T( { i, j, self._.Polygon[i], self._.Polygon[j] } ) + + local DeltaX = self._.Polygon[j].x - self._.Polygon[i].x + local DeltaY = self._.Polygon[j].y - self._.Polygon[i].y + + for Segment = 0, Segments do -- We divide each line in 5 segments and smoke a point on the line. + local PointX = self._.Polygon[i].x + ( Segment * DeltaX / Segments ) + local PointY = self._.Polygon[i].y + ( Segment * DeltaY / Segments ) + local Tire = { + ["country"] = "USA", + ["category"] = "Fortifications", + ["canCargo"] = false, + ["shape_name"] = "H-tyre_B_WF", + ["type"] = "Black_Tyre_WF", + ["y"] = PointY, + ["x"] = PointX, + ["name"] = string.format( "%s-Tire #%0d", self:GetName(), ((i - 1) * Segments) + Segment ), + ["heading"] = 0, + } -- end of ["group"] + + local Group = coalition.addStaticObject( country.id.USA, Tire ) + if UnBound and UnBound == true then + Group:destroy() + end + + end + j = i + i = i + 1 + end + + return self +end + + +--- Draw the zone on the F10 map. **NOTE** Currently, only polygons with **exactly four points** are supported! +-- @param #ZONE_POLYGON_BASE self +-- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All. +-- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red. +-- @param #number Alpha Transparency [0,1]. Default 1. +-- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value. +-- @param #number FillAlpha Transparency [0,1]. Default 0.15. +-- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. +-- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. +-- @return #ZONE_POLYGON_BASE self +function ZONE_POLYGON_BASE:DrawZone(Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly) + + local coordinate=COORDINATE:NewFromVec2(self._.Polygon[1]) + + Color=Color or self:GetColorRGB() + Alpha=Alpha or 1 + FillColor=FillColor or Color + FillAlpha=FillAlpha or self:GetColorAlpha() + + + if #self._.Polygon==4 then + + local Coord2=COORDINATE:NewFromVec2(self._.Polygon[2]) + local Coord3=COORDINATE:NewFromVec2(self._.Polygon[3]) + local Coord4=COORDINATE:NewFromVec2(self._.Polygon[4]) + + self.DrawID=coordinate:QuadToAll(Coord2, Coord3, Coord4, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly) + + else + + local Coordinates=self:GetVerticiesCoordinates() + table.remove(Coordinates, 1) + + self.DrawID=coordinate:MarkupToAllFreeForm(Coordinates, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly) + + end + + + return self +end + +--- Smokes the zone boundaries in a color. +-- @param #ZONE_POLYGON_BASE self +-- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color. +-- @param #number Segments (Optional) Number of segments within boundary line. Default 10. +-- @return #ZONE_POLYGON_BASE self +function ZONE_POLYGON_BASE:SmokeZone( SmokeColor, Segments ) + self:F2( SmokeColor ) + + Segments=Segments or 10 + + local i=1 + local j=#self._.Polygon + + while i <= #self._.Polygon do + self:T( { i, j, self._.Polygon[i], self._.Polygon[j] } ) + + local DeltaX = self._.Polygon[j].x - self._.Polygon[i].x + local DeltaY = self._.Polygon[j].y - self._.Polygon[i].y + + for Segment = 0, Segments do -- We divide each line in 5 segments and smoke a point on the line. + local PointX = self._.Polygon[i].x + ( Segment * DeltaX / Segments ) + local PointY = self._.Polygon[i].y + ( Segment * DeltaY / Segments ) + POINT_VEC2:New( PointX, PointY ):Smoke( SmokeColor ) + end + j = i + i = i + 1 + end + + return self +end + + +--- Flare the zone boundaries in a color. +-- @param #ZONE_POLYGON_BASE self +-- @param Utilities.Utils#FLARECOLOR FlareColor The flare color. +-- @param #number Segments (Optional) Number of segments within boundary line. Default 10. +-- @param DCS#Azimuth Azimuth (optional) Azimuth The azimuth of the flare. +-- @param #number AddHeight (optional) The height to be added for the smoke. +-- @return #ZONE_POLYGON_BASE self +function ZONE_POLYGON_BASE:FlareZone( FlareColor, Segments, Azimuth, AddHeight ) + self:F2(FlareColor) + + Segments=Segments or 10 + + AddHeight = AddHeight or 0 + + local i=1 + local j=#self._.Polygon + + while i <= #self._.Polygon do + self:T( { i, j, self._.Polygon[i], self._.Polygon[j] } ) + + local DeltaX = self._.Polygon[j].x - self._.Polygon[i].x + local DeltaY = self._.Polygon[j].y - self._.Polygon[i].y + + for Segment = 0, Segments do -- We divide each line in 5 segments and smoke a point on the line. + local PointX = self._.Polygon[i].x + ( Segment * DeltaX / Segments ) + local PointY = self._.Polygon[i].y + ( Segment * DeltaY / Segments ) + POINT_VEC2:New( PointX, PointY, AddHeight ):Flare(FlareColor, Azimuth) + end + j = i + i = i + 1 + end + + return self +end + + + + +--- Returns if a location is within the zone. +-- Source learned and taken from: https://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html +-- @param #ZONE_POLYGON_BASE self +-- @param DCS#Vec2 Vec2 The location to test. +-- @return #boolean true if the location is within the zone. +function ZONE_POLYGON_BASE:IsVec2InZone( Vec2 ) + self:F2( Vec2 ) + + local Next + local Prev + local InPolygon = false + + Next = 1 + Prev = #self._.Polygon + + while Next <= #self._.Polygon do + self:T( { Next, Prev, self._.Polygon[Next], self._.Polygon[Prev] } ) + if ( ( ( self._.Polygon[Next].y > Vec2.y ) ~= ( self._.Polygon[Prev].y > Vec2.y ) ) and + ( Vec2.x < ( self._.Polygon[Prev].x - self._.Polygon[Next].x ) * ( Vec2.y - self._.Polygon[Next].y ) / ( self._.Polygon[Prev].y - self._.Polygon[Next].y ) + self._.Polygon[Next].x ) + ) then + InPolygon = not InPolygon + end + self:T2( { InPolygon = InPolygon } ) + Prev = Next + Next = Next + 1 + end + + self:T( { InPolygon = InPolygon } ) + return InPolygon +end + +--- Define a random @{DCS#Vec2} within the zone. +-- @param #ZONE_POLYGON_BASE self +-- @return DCS#Vec2 The Vec2 coordinate. +function ZONE_POLYGON_BASE:GetRandomVec2() + self:F2() + + --- It is a bit tricky to find a random point within a polygon. Right now i am doing it the dirty and inefficient way... + local Vec2Found = false + local Vec2 + local BS = self:GetBoundingSquare() + + self:T2( BS ) + + while Vec2Found == false do + Vec2 = { x = math.random( BS.x1, BS.x2 ), y = math.random( BS.y1, BS.y2 ) } + self:T2( Vec2 ) + if self:IsVec2InZone( Vec2 ) then + Vec2Found = true + end + end + + self:T2( Vec2 ) + + return Vec2 +end + +--- Return a @{Core.Point#POINT_VEC2} object representing a random 2D point at landheight within the zone. +-- @param #ZONE_POLYGON_BASE self +-- @return @{Core.Point#POINT_VEC2} +function ZONE_POLYGON_BASE:GetRandomPointVec2() + self:F2() + + local PointVec2 = POINT_VEC2:NewFromVec2( self:GetRandomVec2() ) + + self:T2( PointVec2 ) + + return PointVec2 +end + +--- Return a @{Core.Point#POINT_VEC3} object representing a random 3D point at landheight within the zone. +-- @param #ZONE_POLYGON_BASE self +-- @return @{Core.Point#POINT_VEC3} +function ZONE_POLYGON_BASE:GetRandomPointVec3() + self:F2() + + local PointVec3 = POINT_VEC3:NewFromVec2( self:GetRandomVec2() ) + + self:T2( PointVec3 ) + + return PointVec3 +end + + +--- Return a @{Core.Point#COORDINATE} object representing a random 3D point at landheight within the zone. +-- @param #ZONE_POLYGON_BASE self +-- @return Core.Point#COORDINATE +function ZONE_POLYGON_BASE:GetRandomCoordinate() + self:F2() + + local Coordinate = COORDINATE:NewFromVec2( self:GetRandomVec2() ) + + self:T2( Coordinate ) + + return Coordinate +end + + +--- Get the bounding square the zone. +-- @param #ZONE_POLYGON_BASE self +-- @return #ZONE_POLYGON_BASE.BoundingSquare The bounding square. +function ZONE_POLYGON_BASE:GetBoundingSquare() + + local x1 = self._.Polygon[1].x + local y1 = self._.Polygon[1].y + local x2 = self._.Polygon[1].x + local y2 = self._.Polygon[1].y + + for i = 2, #self._.Polygon do + self:T2( { self._.Polygon[i], x1, y1, x2, y2 } ) + x1 = ( x1 > self._.Polygon[i].x ) and self._.Polygon[i].x or x1 + x2 = ( x2 < self._.Polygon[i].x ) and self._.Polygon[i].x or x2 + y1 = ( y1 > self._.Polygon[i].y ) and self._.Polygon[i].y or y1 + y2 = ( y2 < self._.Polygon[i].y ) and self._.Polygon[i].y or y2 + + end + + return { x1 = x1, y1 = y1, x2 = x2, y2 = y2 } +end + +--- Draw a frontier on the F10 map with small filled circles. +-- @param #ZONE_POLYGON_BASE self +-- @param #number Coalition (Optional) Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1= All. +-- @param #table Color (Optional) RGB color table {r, g, b}, e.g. {1, 0, 0} for red. Default {1, 1, 1}= White. +-- @param #number Radius (Optional) Radius of the circles in meters. Default 1000. +-- @param #number Alpha (Optional) Alpha transparency [0,1]. Default 1. +-- @param #number Segments (Optional) Number of segments within boundary line. Default 10. +-- @param #boolean Closed (Optional) Link the last point with the first one to obtain a closed boundary. Default false +-- @return #ZONE_POLYGON_BASE self +function ZONE_POLYGON_BASE:Boundary(Coalition, Color, Radius, Alpha, Segments, Closed) + Coalition = Coalition or -1 + Color = Color or {1, 1, 1} + Radius = Radius or 1000 + Alpha = Alpha or 1 + Segments = Segments or 10 + Closed = Closed or false + local i = 1 + local j = #self._.Polygon + if (Closed) then + Limit = #self._.Polygon + 1 + else + Limit = #self._.Polygon + end + while i <= #self._.Polygon do + self:T( { i, j, self._.Polygon[i], self._.Polygon[j] } ) + if j ~= Limit then + local DeltaX = self._.Polygon[j].x - self._.Polygon[i].x + local DeltaY = self._.Polygon[j].y - self._.Polygon[i].y + for Segment = 0, Segments do + local PointX = self._.Polygon[i].x + ( Segment * DeltaX / Segments ) + local PointY = self._.Polygon[i].y + ( Segment * DeltaY / Segments ) + ZONE_RADIUS:New( "Zone", {x = PointX, y = PointY}, Radius ):DrawZone(Coalition, Color, 1, Color, Alpha, nil, true) + end + end + j = i + i = i + 1 + end + return self +end + +--- @type ZONE_POLYGON +-- @extends #ZONE_POLYGON_BASE + + +--- The ZONE_POLYGON class defined by a sequence of @{Wrapper.Group#GROUP} waypoints within the Mission Editor, forming a polygon. +-- This class implements the inherited functions from @{Core.Zone#ZONE_RADIUS} taking into account the own zone format and properties. +-- +-- ## Declare a ZONE_POLYGON directly in the DCS mission editor! +-- +-- You can declare a ZONE_POLYGON using the DCS mission editor by adding the ~ZONE_POLYGON tag in the group name. +-- +-- So, imagine you have a group declared in the mission editor, with group name `DefenseZone~ZONE_POLYGON`. +-- Then during mission startup, when loading Moose.lua, this group will be detected as a ZONE_POLYGON declaration. +-- Within the background, a ZONE_POLYGON object will be created within the @{Core.Database} using the properties of the group. +-- The ZONE_POLYGON name will be the group name without the ~ZONE_POLYGON tag. +-- +-- So, you can search yourself for the ZONE_POLYGON by using the @{#ZONE_POLYGON.FindByName}() method. +-- In this example, `local PolygonZone = ZONE_POLYGON:FindByName( "DefenseZone" )` would return the ZONE_POLYGON object +-- that was created at mission startup, and reference it into the `PolygonZone` local object. +-- +-- Mission `ZON-510` shows a demonstration of this feature or method. +-- +-- This is especially handy if you want to quickly setup a SET_ZONE... +-- So when you would declare `local SetZone = SET_ZONE:New():FilterPrefixes( "Defense" ):FilterStart()`, +-- then SetZone would contain the ZONE_POLYGON object `DefenseZone` as part of the zone collection, +-- without much scripting overhead! +-- +-- @field #ZONE_POLYGON +ZONE_POLYGON = { + ClassName="ZONE_POLYGON", + } + +--- Constructor to create a ZONE_POLYGON instance, taking the zone name and the @{Wrapper.Group#GROUP} defined within the Mission Editor. +-- The @{Wrapper.Group#GROUP} waypoints define the polygon corners. The first and the last point are automatically connected by ZONE_POLYGON. +-- @param #ZONE_POLYGON self +-- @param #string ZoneName Name of the zone. +-- @param Wrapper.Group#GROUP ZoneGroup The GROUP waypoints as defined within the Mission Editor define the polygon shape. +-- @return #ZONE_POLYGON self +function ZONE_POLYGON:New( ZoneName, ZoneGroup ) + + local GroupPoints = ZoneGroup:GetTaskRoute() + + local self = BASE:Inherit( self, ZONE_POLYGON_BASE:New( ZoneName, GroupPoints ) ) + self:F( { ZoneName, ZoneGroup, self._.Polygon } ) + + -- Zone objects are added to the _DATABASE and SET_ZONE objects. + _EVENTDISPATCHER:CreateEventNewZone( self ) + + return self +end + + +--- Constructor to create a ZONE_POLYGON instance, taking the zone name and the **name** of the @{Wrapper.Group#GROUP} defined within the Mission Editor. +-- The @{Wrapper.Group#GROUP} waypoints define the polygon corners. The first and the last point are automatically connected by ZONE_POLYGON. +-- @param #ZONE_POLYGON self +-- @param #string GroupName The group name of the GROUP defining the waypoints within the Mission Editor to define the polygon shape. +-- @return #ZONE_POLYGON self +function ZONE_POLYGON:NewFromGroupName( GroupName ) + + local ZoneGroup = GROUP:FindByName( GroupName ) + + local GroupPoints = ZoneGroup:GetTaskRoute() + + local self = BASE:Inherit( self, ZONE_POLYGON_BASE:New( GroupName, GroupPoints ) ) + self:F( { GroupName, ZoneGroup, self._.Polygon } ) + + -- Zone objects are added to the _DATABASE and SET_ZONE objects. + _EVENTDISPATCHER:CreateEventNewZone( self ) + + return self +end + + +--- Find a polygon zone in the _DATABASE using the name of the polygon zone. +-- @param #ZONE_POLYGON self +-- @param #string ZoneName The name of the polygon zone. +-- @return #ZONE_POLYGON self +function ZONE_POLYGON:FindByName( ZoneName ) + + local ZoneFound = _DATABASE:FindZone( ZoneName ) + return ZoneFound +end + +do -- ZONE_AIRBASE + + --- @type ZONE_AIRBASE + -- @extends #ZONE_RADIUS + + + --- The ZONE_AIRBASE class defines by a zone around a @{Wrapper.Airbase#AIRBASE} with a radius. + -- This class implements the inherited functions from @{Core.Zone#ZONE_RADIUS} taking into account the own zone format and properties. + -- + -- @field #ZONE_AIRBASE + ZONE_AIRBASE = { + ClassName="ZONE_AIRBASE", + } + + + + --- Constructor to create a ZONE_AIRBASE instance, taking the zone name, a zone @{Wrapper.Airbase#AIRBASE} and a radius. + -- @param #ZONE_AIRBASE self + -- @param #string AirbaseName Name of the airbase. + -- @param DCS#Distance Radius (Optional)The radius of the zone in meters. Default 4000 meters. + -- @return #ZONE_AIRBASE self + function ZONE_AIRBASE:New( AirbaseName, Radius ) + + Radius=Radius or 4000 + + local Airbase = AIRBASE:FindByName( AirbaseName ) + + local self = BASE:Inherit( self, ZONE_RADIUS:New( AirbaseName, Airbase:GetVec2(), Radius ) ) + + self._.ZoneAirbase = Airbase + self._.ZoneVec2Cache = self._.ZoneAirbase:GetVec2() + + -- Zone objects are added to the _DATABASE and SET_ZONE objects. + _EVENTDISPATCHER:CreateEventNewZone( self ) + + return self + end + + --- Get the airbase as part of the ZONE_AIRBASE object. + -- @param #ZONE_AIRBASE self + -- @return Wrapper.Airbase#AIRBASE The airbase. + function ZONE_AIRBASE:GetAirbase() + return self._.ZoneAirbase + end + + --- Returns the current location of the @{Wrapper.Group}. + -- @param #ZONE_AIRBASE self + -- @return DCS#Vec2 The location of the zone based on the @{Wrapper.Group} location. + function ZONE_AIRBASE:GetVec2() + self:F( self.ZoneName ) + + local ZoneVec2 = nil + + if self._.ZoneAirbase:IsAlive() then + ZoneVec2 = self._.ZoneAirbase:GetVec2() + self._.ZoneVec2Cache = ZoneVec2 + else + ZoneVec2 = self._.ZoneVec2Cache + end + + self:T( { ZoneVec2 } ) + + return ZoneVec2 + end + + --- Returns a random location within the zone of the @{Wrapper.Group}. + -- @param #ZONE_AIRBASE self + -- @return DCS#Vec2 The random location of the zone based on the @{Wrapper.Group} location. + function ZONE_AIRBASE:GetRandomVec2() + self:F( self.ZoneName ) + + local Point = {} + local Vec2 = self._.ZoneAirbase:GetVec2() + + local angle = math.random() * math.pi*2; + Point.x = Vec2.x + math.cos( angle ) * math.random() * self:GetRadius(); + Point.y = Vec2.y + math.sin( angle ) * math.random() * self:GetRadius(); + + self:T( { Point } ) + + return Point + end + + --- Returns a @{Core.Point#POINT_VEC2} object reflecting a random 2D location within the zone. + -- @param #ZONE_AIRBASE self + -- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. + -- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. + -- @return Core.Point#POINT_VEC2 The @{Core.Point#POINT_VEC2} object reflecting the random 3D location within the zone. + function ZONE_AIRBASE:GetRandomPointVec2( inner, outer ) + self:F( self.ZoneName, inner, outer ) + + local PointVec2 = POINT_VEC2:NewFromVec2( self:GetRandomVec2() ) + + self:T3( { PointVec2 } ) + + return PointVec2 + end + + +end + +--- The ZONE_DETECTION class, defined by a zone name, a detection object and a radius. +-- @type ZONE_DETECTION +-- @field DCS#Vec2 Vec2 The current location of the zone. +-- @field DCS#Distance Radius The radius of the zone. +-- @extends #ZONE_BASE + +--- The ZONE_DETECTION class defined by a zone name, a location and a radius. +-- This class implements the inherited functions from Core.Zone#ZONE_BASE taking into account the own zone format and properties. +-- +-- ## ZONE_DETECTION constructor +-- +-- * @{#ZONE_DETECTION.New}(): Constructor. +-- +-- @field #ZONE_DETECTION +ZONE_DETECTION = { + ClassName="ZONE_DETECTION", + } + +--- Constructor of @{#ZONE_DETECTION}, taking the zone name, the zone location and a radius. +-- @param #ZONE_DETECTION self +-- @param #string ZoneName Name of the zone. +-- @param Functional.Detection#DETECTION_BASE Detection The detection object defining the locations of the central detections. +-- @param DCS#Distance Radius The radius around the detections defining the combined zone. +-- @return #ZONE_DETECTION self +function ZONE_DETECTION:New( ZoneName, Detection, Radius ) + local self = BASE:Inherit( self, ZONE_BASE:New( ZoneName ) ) -- #ZONE_DETECTION + self:F( { ZoneName, Detection, Radius } ) + + self.Detection = Detection + self.Radius = Radius + + return self +end + +--- Bounds the zone with tires. +-- @param #ZONE_DETECTION self +-- @param #number Points (optional) The amount of points in the circle. Default 360. +-- @param DCS#country.id CountryID The country id of the tire objects, e.g. country.id.USA for blue or country.id.RUSSIA for red. +-- @param #boolean UnBound (Optional) If true the tyres will be destroyed. +-- @return #ZONE_DETECTION self +function ZONE_DETECTION:BoundZone( Points, CountryID, UnBound ) + + local Point = {} + local Vec2 = self:GetVec2() + + Points = Points and Points or 360 + + local Angle + local RadialBase = math.pi*2 + + -- + for Angle = 0, 360, (360 / Points ) do + local Radial = Angle * RadialBase / 360 + Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() + Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius() + + local CountryName = _DATABASE.COUNTRY_NAME[CountryID] + + local Tire = { + ["country"] = CountryName, + ["category"] = "Fortifications", + ["canCargo"] = false, + ["shape_name"] = "H-tyre_B_WF", + ["type"] = "Black_Tyre_WF", + --["unitId"] = Angle + 10000, + ["y"] = Point.y, + ["x"] = Point.x, + ["name"] = string.format( "%s-Tire #%0d", self:GetName(), Angle ), + ["heading"] = 0, + } -- end of ["group"] + + local Group = coalition.addStaticObject( CountryID, Tire ) + if UnBound and UnBound == true then + Group:destroy() + end + end + + return self +end + + +--- Smokes the zone boundaries in a color. +-- @param #ZONE_DETECTION self +-- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color. +-- @param #number Points (optional) The amount of points in the circle. +-- @param #number AddHeight (optional) The height to be added for the smoke. +-- @param #number AddOffSet (optional) The angle to be added for the smoking start position. +-- @return #ZONE_DETECTION self +function ZONE_DETECTION:SmokeZone( SmokeColor, Points, AddHeight, AngleOffset ) + self:F2( SmokeColor ) + + local Point = {} + local Vec2 = self:GetVec2() + + AddHeight = AddHeight or 0 + AngleOffset = AngleOffset or 0 + + Points = Points and Points or 360 + + local Angle + local RadialBase = math.pi*2 + + for Angle = 0, 360, 360 / Points do + local Radial = ( Angle + AngleOffset ) * RadialBase / 360 + Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() + Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius() + POINT_VEC2:New( Point.x, Point.y, AddHeight ):Smoke( SmokeColor ) + end + + return self +end + + +--- Flares the zone boundaries in a color. +-- @param #ZONE_DETECTION self +-- @param Utilities.Utils#FLARECOLOR FlareColor The flare color. +-- @param #number Points (optional) The amount of points in the circle. +-- @param DCS#Azimuth Azimuth (optional) Azimuth The azimuth of the flare. +-- @param #number AddHeight (optional) The height to be added for the smoke. +-- @return #ZONE_DETECTION self +function ZONE_DETECTION:FlareZone( FlareColor, Points, Azimuth, AddHeight ) + self:F2( { FlareColor, Azimuth } ) + + local Point = {} + local Vec2 = self:GetVec2() + + AddHeight = AddHeight or 0 + + Points = Points and Points or 360 + + local Angle + local RadialBase = math.pi*2 + + for Angle = 0, 360, 360 / Points do + local Radial = Angle * RadialBase / 360 + Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() + Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius() + POINT_VEC2:New( Point.x, Point.y, AddHeight ):Flare( FlareColor, Azimuth ) + end + + return self +end + +--- Returns the radius around the detected locations defining the combine zone. +-- @param #ZONE_DETECTION self +-- @return DCS#Distance The radius. +function ZONE_DETECTION:GetRadius() + self:F2( self.ZoneName ) + + self:T2( { self.Radius } ) + + return self.Radius +end + +--- Sets the radius around the detected locations defining the combine zone. +-- @param #ZONE_DETECTION self +-- @param DCS#Distance Radius The radius. +-- @return #ZONE_DETECTION self +function ZONE_DETECTION:SetRadius( Radius ) + self:F2( self.ZoneName ) + + self.Radius = Radius + self:T2( { self.Radius } ) + + return self.Radius +end + + + +--- Returns if a location is within the zone. +-- @param #ZONE_DETECTION self +-- @param DCS#Vec2 Vec2 The location to test. +-- @return #boolean true if the location is within the zone. +function ZONE_DETECTION:IsVec2InZone( Vec2 ) + self:F2( Vec2 ) + + local Coordinates = self.Detection:GetDetectedItemCoordinates() -- This returns a list of coordinates that define the (central) locations of the detections. + + for CoordinateID, Coordinate in pairs( Coordinates ) do + local ZoneVec2 = Coordinate:GetVec2() + if ZoneVec2 then + if (( Vec2.x - ZoneVec2.x )^2 + ( Vec2.y - ZoneVec2.y ) ^2 ) ^ 0.5 <= self:GetRadius() then + return true + end + end + end + + return false +end + +--- Returns if a point is within the zone. +-- @param #ZONE_DETECTION self +-- @param DCS#Vec3 Vec3 The point to test. +-- @return #boolean true if the point is within the zone. +function ZONE_DETECTION:IsVec3InZone( Vec3 ) + self:F2( Vec3 ) + + local InZone = self:IsVec2InZone( { x = Vec3.x, y = Vec3.z } ) + + return InZone +end + +--- **Core** - Manages several databases containing templates, mission objects, and mission information. +-- +-- === +-- +-- ## Features: +-- +-- * During mission startup, scan the mission environment, and create / instantiate intelligently the different objects as defined within the mission. +-- * Manage database of DCS Group templates (as modelled using the mission editor). +-- - Group templates. +-- - Unit templates. +-- - Statics templates. +-- * Manage database of @{Wrapper.Group#GROUP} objects alive in the mission. +-- * Manage database of @{Wrapper.Unit#UNIT} objects alive in the mission. +-- * Manage database of @{Wrapper.Static#STATIC} objects alive in the mission. +-- * Manage database of players. +-- * Manage database of client slots defined using the mission editor. +-- * Manage database of airbases on the map, and from FARPs and ships as defined using the mission editor. +-- * Manage database of countries. +-- * Manage database of zone names. +-- * Manage database of hits to units and statics. +-- * Manage database of destroys of units and statics. +-- * Manage database of @{Core.Zone#ZONE_BASE} objects. +-- +-- === +-- +-- ### Author: **FlightControl** +-- ### Contributions: **funkyfranky** +-- +-- === +-- +-- @module Core.Database +-- @image Core_Database.JPG + + +--- @type DATABASE +-- @field #string ClassName Name of the class. +-- @field #table Templates Templates: Units, Groups, Statics, ClientsByName, ClientsByID. +-- @field #table CLIENTS Clients. +-- @extends Core.Base#BASE + +--- Contains collections of wrapper objects defined within MOOSE that reflect objects within the simulator. +-- +-- Mission designers can use the DATABASE class to refer to: +-- +-- * STATICS +-- * UNITS +-- * GROUPS +-- * CLIENTS +-- * AIRBASES +-- * PLAYERSJOINED +-- * PLAYERS +-- * CARGOS +-- +-- On top, for internal MOOSE administration purposes, the DATBASE administers the Unit and Group TEMPLATES as defined within the Mission Editor. +-- +-- The singleton object **_DATABASE** is automatically created by MOOSE, that administers all objects within the mission. +-- Moose refers to **_DATABASE** within the framework extensively, but you can also refer to the _DATABASE object within your missions if required. +-- +-- @field #DATABASE +DATABASE = { + ClassName = "DATABASE", + Templates = { + Units = {}, + Groups = {}, + Statics = {}, + ClientsByName = {}, + ClientsByID = {}, + }, + UNITS = {}, + UNITS_Index = {}, + STATICS = {}, + GROUPS = {}, + PLAYERS = {}, + PLAYERSJOINED = {}, + PLAYERUNITS = {}, + CLIENTS = {}, + CARGOS = {}, + AIRBASES = {}, + COUNTRY_ID = {}, + COUNTRY_NAME = {}, + NavPoints = {}, + PLAYERSETTINGS = {}, + ZONENAMES = {}, + HITS = {}, + DESTROYS = {}, + ZONES = {}, + ZONES_GOAL = {}, + WAREHOUSES = {}, + FLIGHTGROUPS = {}, + FLIGHTCONTROLS = {}, +} + +local _DATABASECoalition = + { + [1] = "Red", + [2] = "Blue", + [3] = "Neutral", + } + +local _DATABASECategory = + { + ["plane"] = Unit.Category.AIRPLANE, + ["helicopter"] = Unit.Category.HELICOPTER, + ["vehicle"] = Unit.Category.GROUND_UNIT, + ["ship"] = Unit.Category.SHIP, + ["static"] = Unit.Category.STRUCTURE, + } + + +--- Creates a new DATABASE object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. +-- @param #DATABASE self +-- @return #DATABASE +-- @usage +-- -- Define a new DATABASE Object. This DBObject will contain a reference to all Group and Unit Templates defined within the ME and the DCSRTE. +-- DBObject = DATABASE:New() +function DATABASE:New() + + -- Inherits from BASE + local self = BASE:Inherit( self, BASE:New() ) -- #DATABASE + + self:SetEventPriority( 1 ) + + self:HandleEvent( EVENTS.Birth, self._EventOnBirth ) + self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) + self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) + self:HandleEvent( EVENTS.RemoveUnit, self._EventOnDeadOrCrash ) + self:HandleEvent( EVENTS.Hit, self.AccountHits ) + self:HandleEvent( EVENTS.NewCargo ) + self:HandleEvent( EVENTS.DeleteCargo ) + self:HandleEvent( EVENTS.NewZone ) + self:HandleEvent( EVENTS.DeleteZone ) + --self:HandleEvent( EVENTS.PlayerEnterUnit, self._EventOnPlayerEnterUnit ) -- This is not working anymore!, handling this through the birth event. + self:HandleEvent( EVENTS.PlayerLeaveUnit, self._EventOnPlayerLeaveUnit ) + + self:_RegisterTemplates() + self:_RegisterGroupsAndUnits() + self:_RegisterClients() + self:_RegisterStatics() + --self:_RegisterPlayers() + --self:_RegisterAirbases() + + self.UNITS_Position = 0 + + return self +end + +--- Finds a Unit based on the Unit Name. +-- @param #DATABASE self +-- @param #string UnitName +-- @return Wrapper.Unit#UNIT The found Unit. +function DATABASE:FindUnit( UnitName ) + + local UnitFound = self.UNITS[UnitName] + return UnitFound +end + + +--- Adds a Unit based on the Unit Name in the DATABASE. +-- @param #DATABASE self +-- @param #string DCSUnitName Unit name. +-- @return Wrapper.Unit#UNIT The added unit. +function DATABASE:AddUnit( DCSUnitName ) + + if not self.UNITS[DCSUnitName] then + + -- Debug info. + self:T( { "Add UNIT:", DCSUnitName } ) + + --local UnitRegister = UNIT:Register( DCSUnitName ) + + -- Register unit + self.UNITS[DCSUnitName]=UNIT:Register(DCSUnitName) + + -- This is not used anywhere in MOOSE as far as I can see so I remove it until there comes an error somewhere. + --table.insert(self.UNITS_Index, DCSUnitName ) + end + + return self.UNITS[DCSUnitName] +end + + +--- Deletes a Unit from the DATABASE based on the Unit Name. +-- @param #DATABASE self +function DATABASE:DeleteUnit( DCSUnitName ) + + self.UNITS[DCSUnitName] = nil +end + +--- Adds a Static based on the Static Name in the DATABASE. +-- @param #DATABASE self +-- @param #string DCSStaticName Name of the static. +-- @return Wrapper.Static#STATIC The static object. +function DATABASE:AddStatic( DCSStaticName ) + + if not self.STATICS[DCSStaticName] then + self.STATICS[DCSStaticName] = STATIC:Register( DCSStaticName ) + return self.STATICS[DCSStaticName] + end + + return nil +end + + +--- Deletes a Static from the DATABASE based on the Static Name. +-- @param #DATABASE self +function DATABASE:DeleteStatic( DCSStaticName ) + self.STATICS[DCSStaticName] = nil +end + +--- Finds a STATIC based on the StaticName. +-- @param #DATABASE self +-- @param #string StaticName +-- @return Wrapper.Static#STATIC The found STATIC. +function DATABASE:FindStatic( StaticName ) + + local StaticFound = self.STATICS[StaticName] + return StaticFound +end + +--- Finds a AIRBASE based on the AirbaseName. +-- @param #DATABASE self +-- @param #string AirbaseName +-- @return Wrapper.Airbase#AIRBASE The found AIRBASE. +function DATABASE:FindAirbase( AirbaseName ) + + local AirbaseFound = self.AIRBASES[AirbaseName] + return AirbaseFound +end + +--- Adds a Airbase based on the Airbase Name in the DATABASE. +-- @param #DATABASE self +-- @param #string AirbaseName The name of the airbase. +-- @return Wrapper.Airbase#AIRBASE Airbase object. +function DATABASE:AddAirbase( AirbaseName ) + + if not self.AIRBASES[AirbaseName] then + self.AIRBASES[AirbaseName] = AIRBASE:Register( AirbaseName ) + end + + return self.AIRBASES[AirbaseName] +end + + +--- Deletes a Airbase from the DATABASE based on the Airbase Name. +-- @param #DATABASE self +-- @param #string AirbaseName The name of the airbase +function DATABASE:DeleteAirbase( AirbaseName ) + + self.AIRBASES[AirbaseName] = nil +end + +--- Finds an AIRBASE based on the AirbaseName. +-- @param #DATABASE self +-- @param #string AirbaseName +-- @return Wrapper.Airbase#AIRBASE The found AIRBASE. +function DATABASE:FindAirbase( AirbaseName ) + + local AirbaseFound = self.AIRBASES[AirbaseName] + return AirbaseFound +end + + +do -- Zones + + --- Finds a @{Zone} based on the zone name. + -- @param #DATABASE self + -- @param #string ZoneName The name of the zone. + -- @return Core.Zone#ZONE_BASE The found ZONE. + function DATABASE:FindZone( ZoneName ) + + local ZoneFound = self.ZONES[ZoneName] + return ZoneFound + end + + --- Adds a @{Zone} based on the zone name in the DATABASE. + -- @param #DATABASE self + -- @param #string ZoneName The name of the zone. + -- @param Core.Zone#ZONE_BASE Zone The zone. + function DATABASE:AddZone( ZoneName, Zone ) + + if not self.ZONES[ZoneName] then + self.ZONES[ZoneName] = Zone + end + end + + + --- Deletes a @{Zone} from the DATABASE based on the zone name. + -- @param #DATABASE self + -- @param #string ZoneName The name of the zone. + function DATABASE:DeleteZone( ZoneName ) + + self.ZONES[ZoneName] = nil + end + + + --- Private method that registers new ZONE_BASE derived objects within the DATABASE Object. + -- @param #DATABASE self + -- @return #DATABASE self + function DATABASE:_RegisterZones() + + for ZoneID, ZoneData in pairs(env.mission.triggers.zones) do + local ZoneName = ZoneData.name + + -- Color + local color=ZoneData.color or {1, 0, 0, 0.15} + + -- Create new Zone + local Zone=nil --Core.Zone#ZONE_BASE + + if ZoneData.type==0 then + + --- + -- Circular zone + --- + + self:I(string.format("Register ZONE: %s (Circular)", ZoneName)) + + Zone=ZONE:New(ZoneName) + + else + + --- + -- Quad-point zone + --- + + self:I(string.format("Register ZONE: %s (Polygon, Quad)", ZoneName)) + + Zone=ZONE_POLYGON_BASE:New(ZoneName, ZoneData.verticies) + + --for i,vec2 in pairs(ZoneData.verticies) do + -- local coord=COORDINATE:NewFromVec2(vec2) + -- coord:MarkToAll(string.format("%s Point %d", ZoneName, i)) + --end + + end + + if Zone then + + -- Store color of zone. + Zone.Color=color + + -- Store in DB. + self.ZONENAMES[ZoneName] = ZoneName + + -- Add zone. + self:AddZone(ZoneName, Zone) + + end + + end + + -- Polygon zones defined by late activated groups. + for ZoneGroupName, ZoneGroup in pairs( self.GROUPS ) do + if ZoneGroupName:match("#ZONE_POLYGON") then + + local ZoneName1 = ZoneGroupName:match("(.*)#ZONE_POLYGON") + local ZoneName2 = ZoneGroupName:match(".*#ZONE_POLYGON(.*)") + local ZoneName = ZoneName1 .. ( ZoneName2 or "" ) + + -- Debug output + self:I(string.format("Register ZONE: %s (Polygon)", ZoneName)) + + -- Create a new polygon zone. + local Zone_Polygon = ZONE_POLYGON:New( ZoneName, ZoneGroup ) + + -- Set color. + Zone_Polygon:SetColor({1, 0, 0}, 0.15) + + -- Store name in DB. + self.ZONENAMES[ZoneName] = ZoneName + + -- Add zone to DB. + self:AddZone( ZoneName, Zone_Polygon ) + end + end + + end + + +end -- zone + +do -- Zone_Goal + + --- Finds a @{Zone} based on the zone name. + -- @param #DATABASE self + -- @param #string ZoneName The name of the zone. + -- @return Core.Zone#ZONE_BASE The found ZONE. + function DATABASE:FindZoneGoal( ZoneName ) + + local ZoneFound = self.ZONES_GOAL[ZoneName] + return ZoneFound + end + + --- Adds a @{Zone} based on the zone name in the DATABASE. + -- @param #DATABASE self + -- @param #string ZoneName The name of the zone. + -- @param Core.Zone#ZONE_BASE Zone The zone. + function DATABASE:AddZoneGoal( ZoneName, Zone ) + + if not self.ZONES_GOAL[ZoneName] then + self.ZONES_GOAL[ZoneName] = Zone + end + end + + + --- Deletes a @{Zone} from the DATABASE based on the zone name. + -- @param #DATABASE self + -- @param #string ZoneName The name of the zone. + function DATABASE:DeleteZoneGoal( ZoneName ) + + self.ZONES_GOAL[ZoneName] = nil + end + +end -- Zone_Goal +do -- cargo + + --- Adds a Cargo based on the Cargo Name in the DATABASE. + -- @param #DATABASE self + -- @param #string CargoName The name of the airbase + function DATABASE:AddCargo( Cargo ) + + if not self.CARGOS[Cargo.Name] then + self.CARGOS[Cargo.Name] = Cargo + end + end + + + --- Deletes a Cargo from the DATABASE based on the Cargo Name. + -- @param #DATABASE self + -- @param #string CargoName The name of the airbase + function DATABASE:DeleteCargo( CargoName ) + + self.CARGOS[CargoName] = nil + end + + --- Finds an CARGO based on the CargoName. + -- @param #DATABASE self + -- @param #string CargoName + -- @return Wrapper.Cargo#CARGO The found CARGO. + function DATABASE:FindCargo( CargoName ) + + local CargoFound = self.CARGOS[CargoName] + return CargoFound + end + + --- Checks if the Template name has a #CARGO tag. + -- If yes, the group is a cargo. + -- @param #DATABASE self + -- @param #string TemplateName + -- @return #boolean + function DATABASE:IsCargo( TemplateName ) + + TemplateName = env.getValueDictByKey( TemplateName ) + + local Cargo = TemplateName:match( "#(CARGO)" ) + + return Cargo and Cargo == "CARGO" + end + + --- Private method that registers new Static Templates within the DATABASE Object. + -- @param #DATABASE self + -- @return #DATABASE self + function DATABASE:_RegisterCargos() + + local Groups = UTILS.DeepCopy( self.GROUPS ) -- This is a very important statement. CARGO_GROUP:New creates a new _DATABASE.GROUP entry, which will confuse the loop. I searched 4 hours on this to find the bug! + + for CargoGroupName, CargoGroup in pairs( Groups ) do + if self:IsCargo( CargoGroupName ) then + local CargoInfo = CargoGroupName:match("#CARGO(.*)") + local CargoParam = CargoInfo and CargoInfo:match( "%((.*)%)") + local CargoName1 = CargoGroupName:match("(.*)#CARGO%(.*%)") + local CargoName2 = CargoGroupName:match(".*#CARGO%(.*%)(.*)") + local CargoName = CargoName1 .. ( CargoName2 or "" ) + local Type = CargoParam and CargoParam:match( "T=([%a%d ]+),?") + local Name = CargoParam and CargoParam:match( "N=([%a%d]+),?") or CargoName + local LoadRadius = CargoParam and tonumber( CargoParam:match( "RR=([%a%d]+),?") ) + local NearRadius = CargoParam and tonumber( CargoParam:match( "NR=([%a%d]+),?") ) + + self:I({"Register CargoGroup:",Type=Type,Name=Name,LoadRadius=LoadRadius,NearRadius=NearRadius}) + CARGO_GROUP:New( CargoGroup, Type, Name, LoadRadius, NearRadius ) + end + end + + for CargoStaticName, CargoStatic in pairs( self.STATICS ) do + if self:IsCargo( CargoStaticName ) then + local CargoInfo = CargoStaticName:match("#CARGO(.*)") + local CargoParam = CargoInfo and CargoInfo:match( "%((.*)%)") + local CargoName = CargoStaticName:match("(.*)#CARGO") + local Type = CargoParam and CargoParam:match( "T=([%a%d ]+),?") + local Category = CargoParam and CargoParam:match( "C=([%a%d ]+),?") + local Name = CargoParam and CargoParam:match( "N=([%a%d]+),?") or CargoName + local LoadRadius = CargoParam and tonumber( CargoParam:match( "RR=([%a%d]+),?") ) + local NearRadius = CargoParam and tonumber( CargoParam:match( "NR=([%a%d]+),?") ) + + if Category == "SLING" then + self:I({"Register CargoSlingload:",Type=Type,Name=Name,LoadRadius=LoadRadius,NearRadius=NearRadius}) + CARGO_SLINGLOAD:New( CargoStatic, Type, Name, LoadRadius, NearRadius ) + else + if Category == "CRATE" then + self:I({"Register CargoCrate:",Type=Type,Name=Name,LoadRadius=LoadRadius,NearRadius=NearRadius}) + CARGO_CRATE:New( CargoStatic, Type, Name, LoadRadius, NearRadius ) + end + end + end + end + + end + +end -- cargo + +--- Finds a CLIENT based on the ClientName. +-- @param #DATABASE self +-- @param #string ClientName +-- @return Wrapper.Client#CLIENT The found CLIENT. +function DATABASE:FindClient( ClientName ) + + local ClientFound = self.CLIENTS[ClientName] + return ClientFound +end + + +--- Adds a CLIENT based on the ClientName in the DATABASE. +-- @param #DATABASE self +-- @param #string ClientName Name of the Client unit. +-- @return Wrapper.Client#CLIENT The client object. +function DATABASE:AddClient( ClientName ) + + if not self.CLIENTS[ClientName] then + self.CLIENTS[ClientName] = CLIENT:Register( ClientName ) + end + + return self.CLIENTS[ClientName] +end + + +--- Finds a GROUP based on the GroupName. +-- @param #DATABASE self +-- @param #string GroupName +-- @return Wrapper.Group#GROUP The found GROUP. +function DATABASE:FindGroup( GroupName ) + + local GroupFound = self.GROUPS[GroupName] + return GroupFound +end + + +--- Adds a GROUP based on the GroupName in the DATABASE. +-- @param #DATABASE self +function DATABASE:AddGroup( GroupName ) + + if not self.GROUPS[GroupName] then + self:T( { "Add GROUP:", GroupName } ) + self.GROUPS[GroupName] = GROUP:Register( GroupName ) + end + + return self.GROUPS[GroupName] +end + +--- Adds a player based on the Player Name in the DATABASE. +-- @param #DATABASE self +function DATABASE:AddPlayer( UnitName, PlayerName ) + + if PlayerName then + self:T( { "Add player for unit:", UnitName, PlayerName } ) + self.PLAYERS[PlayerName] = UnitName + self.PLAYERUNITS[PlayerName] = self:FindUnit( UnitName ) + self.PLAYERSJOINED[PlayerName] = PlayerName + end +end + +--- Deletes a player from the DATABASE based on the Player Name. +-- @param #DATABASE self +function DATABASE:DeletePlayer( UnitName, PlayerName ) + + if PlayerName then + self:T( { "Clean player:", PlayerName } ) + self.PLAYERS[PlayerName] = nil + self.PLAYERUNITS[PlayerName] = nil + end +end + +--- Get the player table from the DATABASE. +-- The player table contains all unit names with the key the name of the player (PlayerName). +-- @param #DATABASE self +-- @usage +-- local Players = _DATABASE:GetPlayers() +-- for PlayerName, UnitName in pairs( Players ) do +-- .. +-- end +function DATABASE:GetPlayers() + return self.PLAYERS +end + + +--- Get the player table from the DATABASE, which contains all UNIT objects. +-- The player table contains all UNIT objects of the player with the key the name of the player (PlayerName). +-- @param #DATABASE self +-- @usage +-- local PlayerUnits = _DATABASE:GetPlayerUnits() +-- for PlayerName, PlayerUnit in pairs( PlayerUnits ) do +-- .. +-- end +function DATABASE:GetPlayerUnits() + return self.PLAYERUNITS +end + + +--- Get the player table from the DATABASE which have joined in the mission historically. +-- The player table contains all UNIT objects with the key the name of the player (PlayerName). +-- @param #DATABASE self +-- @usage +-- local PlayersJoined = _DATABASE:GetPlayersJoined() +-- for PlayerName, PlayerUnit in pairs( PlayersJoined ) do +-- .. +-- end +function DATABASE:GetPlayersJoined() + return self.PLAYERSJOINED +end + + +--- Instantiate new Groups within the DCSRTE. +-- This method expects EXACTLY the same structure as a structure within the ME, and needs 2 additional fields defined: +-- SpawnCountryID, SpawnCategoryID +-- This method is used by the SPAWN class. +-- @param #DATABASE self +-- @param #table SpawnTemplate Template of the group to spawn. +-- @return Wrapper.Group#GROUP Spawned group. +function DATABASE:Spawn( SpawnTemplate ) + self:F( SpawnTemplate.name ) + + self:T( { SpawnTemplate.SpawnCountryID, SpawnTemplate.SpawnCategoryID } ) + + -- Copy the spawn variables of the template in temporary storage, nullify, and restore the spawn variables. + local SpawnCoalitionID = SpawnTemplate.CoalitionID + local SpawnCountryID = SpawnTemplate.CountryID + local SpawnCategoryID = SpawnTemplate.CategoryID + + -- Nullify + SpawnTemplate.CoalitionID = nil + SpawnTemplate.CountryID = nil + SpawnTemplate.CategoryID = nil + + self:_RegisterGroupTemplate( SpawnTemplate, SpawnCoalitionID, SpawnCategoryID, SpawnCountryID ) + + self:T3( SpawnTemplate ) + coalition.addGroup( SpawnCountryID, SpawnCategoryID, SpawnTemplate ) + + -- Restore + SpawnTemplate.CoalitionID = SpawnCoalitionID + SpawnTemplate.CountryID = SpawnCountryID + SpawnTemplate.CategoryID = SpawnCategoryID + + -- Ensure that for the spawned group and its units, there are GROUP and UNIT objects created in the DATABASE. + local SpawnGroup = self:AddGroup( SpawnTemplate.name ) + for UnitID, UnitData in pairs( SpawnTemplate.units ) do + self:AddUnit( UnitData.name ) + end + + return SpawnGroup +end + +--- Set a status to a Group within the Database, this to check crossing events for example. +-- @param #DATABASE self +-- @param #string GroupName Group name. +-- @param #string Status Status. +function DATABASE:SetStatusGroup( GroupName, Status ) + self:F2( Status ) + + self.Templates.Groups[GroupName].Status = Status +end + +--- Get a status to a Group within the Database, this to check crossing events for example. +-- @param #DATABASE self +-- @param #string GroupName Group name. +-- @return #string Status or an empty string "". +function DATABASE:GetStatusGroup( GroupName ) + self:F2( GroupName ) + + if self.Templates.Groups[GroupName] then + return self.Templates.Groups[GroupName].Status + else + return "" + end +end + +--- Private method that registers new Group Templates within the DATABASE Object. +-- @param #DATABASE self +-- @param #table GroupTemplate +-- @param DCS#coalition.side CoalitionSide The coalition.side of the object. +-- @param DCS#Object.Category CategoryID The Object.category of the object. +-- @param DCS#country.id CountryID the country ID of the object. +-- @param #string GroupName (Optional) The name of the group. Default is `GroupTemplate.name`. +-- @return #DATABASE self +function DATABASE:_RegisterGroupTemplate( GroupTemplate, CoalitionSide, CategoryID, CountryID, GroupName ) + + local GroupTemplateName = GroupName or env.getValueDictByKey( GroupTemplate.name ) + + if not self.Templates.Groups[GroupTemplateName] then + self.Templates.Groups[GroupTemplateName] = {} + self.Templates.Groups[GroupTemplateName].Status = nil + end + + -- Delete the spans from the route, it is not needed and takes memory. + if GroupTemplate.route and GroupTemplate.route.spans then + GroupTemplate.route.spans = nil + end + + GroupTemplate.CategoryID = CategoryID + GroupTemplate.CoalitionID = CoalitionSide + GroupTemplate.CountryID = CountryID + + self.Templates.Groups[GroupTemplateName].GroupName = GroupTemplateName + self.Templates.Groups[GroupTemplateName].Template = GroupTemplate + self.Templates.Groups[GroupTemplateName].groupId = GroupTemplate.groupId + self.Templates.Groups[GroupTemplateName].UnitCount = #GroupTemplate.units + self.Templates.Groups[GroupTemplateName].Units = GroupTemplate.units + self.Templates.Groups[GroupTemplateName].CategoryID = CategoryID + self.Templates.Groups[GroupTemplateName].CoalitionID = CoalitionSide + self.Templates.Groups[GroupTemplateName].CountryID = CountryID + + local UnitNames = {} + + for unit_num, UnitTemplate in pairs( GroupTemplate.units ) do + + UnitTemplate.name = env.getValueDictByKey(UnitTemplate.name) + + self.Templates.Units[UnitTemplate.name] = {} + self.Templates.Units[UnitTemplate.name].UnitName = UnitTemplate.name + self.Templates.Units[UnitTemplate.name].Template = UnitTemplate + self.Templates.Units[UnitTemplate.name].GroupName = GroupTemplateName + self.Templates.Units[UnitTemplate.name].GroupTemplate = GroupTemplate + self.Templates.Units[UnitTemplate.name].GroupId = GroupTemplate.groupId + self.Templates.Units[UnitTemplate.name].CategoryID = CategoryID + self.Templates.Units[UnitTemplate.name].CoalitionID = CoalitionSide + self.Templates.Units[UnitTemplate.name].CountryID = CountryID + + if UnitTemplate.skill and (UnitTemplate.skill == "Client" or UnitTemplate.skill == "Player") then + self.Templates.ClientsByName[UnitTemplate.name] = UnitTemplate + self.Templates.ClientsByName[UnitTemplate.name].CategoryID = CategoryID + self.Templates.ClientsByName[UnitTemplate.name].CoalitionID = CoalitionSide + self.Templates.ClientsByName[UnitTemplate.name].CountryID = CountryID + self.Templates.ClientsByID[UnitTemplate.unitId] = UnitTemplate + end + + UnitNames[#UnitNames+1] = self.Templates.Units[UnitTemplate.name].UnitName + end + + -- Debug info. + self:T( { Group = self.Templates.Groups[GroupTemplateName].GroupName, + Coalition = self.Templates.Groups[GroupTemplateName].CoalitionID, + Category = self.Templates.Groups[GroupTemplateName].CategoryID, + Country = self.Templates.Groups[GroupTemplateName].CountryID, + Units = UnitNames + } + ) +end + +--- Get group template. +-- @param #DATABASE self +-- @param #string GroupName Group name. +-- @return #table Group template table. +function DATABASE:GetGroupTemplate( GroupName ) + local GroupTemplate = self.Templates.Groups[GroupName].Template + GroupTemplate.SpawnCoalitionID = self.Templates.Groups[GroupName].CoalitionID + GroupTemplate.SpawnCategoryID = self.Templates.Groups[GroupName].CategoryID + GroupTemplate.SpawnCountryID = self.Templates.Groups[GroupName].CountryID + return GroupTemplate +end + +--- Private method that registers new Static Templates within the DATABASE Object. +-- @param #DATABASE self +-- @param #table StaticTemplate Template table. +-- @param #number CoalitionID Coalition ID. +-- @param #number CategoryID Category ID. +-- @param #number CountryID Country ID. +-- @return #DATABASE self +function DATABASE:_RegisterStaticTemplate( StaticTemplate, CoalitionID, CategoryID, CountryID ) + + local StaticTemplate = UTILS.DeepCopy( StaticTemplate ) + + local StaticTemplateName = env.getValueDictByKey(StaticTemplate.name) + + self.Templates.Statics[StaticTemplateName] = self.Templates.Statics[StaticTemplateName] or {} + + StaticTemplate.CategoryID = CategoryID + StaticTemplate.CoalitionID = CoalitionID + StaticTemplate.CountryID = CountryID + + self.Templates.Statics[StaticTemplateName].StaticName = StaticTemplateName + self.Templates.Statics[StaticTemplateName].GroupTemplate = StaticTemplate + self.Templates.Statics[StaticTemplateName].UnitTemplate = StaticTemplate.units[1] + self.Templates.Statics[StaticTemplateName].CategoryID = CategoryID + self.Templates.Statics[StaticTemplateName].CoalitionID = CoalitionID + self.Templates.Statics[StaticTemplateName].CountryID = CountryID + + -- Debug info. + self:T( { Static = self.Templates.Statics[StaticTemplateName].StaticName, + Coalition = self.Templates.Statics[StaticTemplateName].CoalitionID, + Category = self.Templates.Statics[StaticTemplateName].CategoryID, + Country = self.Templates.Statics[StaticTemplateName].CountryID + } + ) + + self:AddStatic( StaticTemplateName ) + + return self +end + +--- Get static group template. +-- @param #DATABASE self +-- @param #string StaticName Name of the static. +-- @return #table Static template table. +function DATABASE:GetStaticGroupTemplate( StaticName ) + if self.Templates.Statics[StaticName] then + local StaticTemplate = self.Templates.Statics[StaticName].GroupTemplate + return StaticTemplate, self.Templates.Statics[StaticName].CoalitionID, self.Templates.Statics[StaticName].CategoryID, self.Templates.Statics[StaticName].CountryID + else + self:E("ERROR: Static group template does NOT exist for static "..tostring(StaticName)) + return nil + end +end + +--- Get static unit template. +-- @param #DATABASE self +-- @param #string StaticName Name of the static. +-- @return #table Static template table. +function DATABASE:GetStaticUnitTemplate( StaticName ) + if self.Templates.Statics[StaticName] then + local UnitTemplate = self.Templates.Statics[StaticName].UnitTemplate + return UnitTemplate, self.Templates.Statics[StaticName].CoalitionID, self.Templates.Statics[StaticName].CategoryID, self.Templates.Statics[StaticName].CountryID + else + self:E("ERROR: Static unit template does NOT exist for static "..tostring(StaticName)) + return nil + end +end + +--- Get group name from unit name. +-- @param #DATABASE self +-- @param #string UnitName Name of the unit. +-- @return #string Group name. +function DATABASE:GetGroupNameFromUnitName( UnitName ) + if self.Templates.Units[UnitName] then + return self.Templates.Units[UnitName].GroupName + else + self:E("ERROR: Unit template does not exist for unit "..tostring(UnitName)) + return nil + end +end + +--- Get group template from unit name. +-- @param #DATABASE self +-- @param #string UnitName Name of the unit. +-- @return #table Group template. +function DATABASE:GetGroupTemplateFromUnitName( UnitName ) + if self.Templates.Units[UnitName] then + return self.Templates.Units[UnitName].GroupTemplate + else + self:E("ERROR: Unit template does not exist for unit "..tostring(UnitName)) + return nil + end +end + +--- Get coalition ID from client name. +-- @param #DATABASE self +-- @param #string ClientName Name of the Client. +-- @return #number Coalition ID. +function DATABASE:GetCoalitionFromClientTemplate( ClientName ) + return self.Templates.ClientsByName[ClientName].CoalitionID +end + +--- Get category ID from client name. +-- @param #DATABASE self +-- @param #string ClientName Name of the Client. +-- @return #number Category ID. +function DATABASE:GetCategoryFromClientTemplate( ClientName ) + return self.Templates.ClientsByName[ClientName].CategoryID +end + +--- Get country ID from client name. +-- @param #DATABASE self +-- @param #string ClientName Name of the Client. +-- @return #number Country ID. +function DATABASE:GetCountryFromClientTemplate( ClientName ) + return self.Templates.ClientsByName[ClientName].CountryID +end + +--- Airbase + +--- Get coalition ID from airbase name. +-- @param #DATABASE self +-- @param #string AirbaseName Name of the airbase. +-- @return #number Coalition ID. +function DATABASE:GetCoalitionFromAirbase( AirbaseName ) + return self.AIRBASES[AirbaseName]:GetCoalition() +end + +--- Get category from airbase name. +-- @param #DATABASE self +-- @param #string AirbaseName Name of the airbase. +-- @return #number Category. +function DATABASE:GetCategoryFromAirbase( AirbaseName ) + return self.AIRBASES[AirbaseName]:GetCategory() +end + + + +--- Private method that registers all alive players in the mission. +-- @param #DATABASE self +-- @return #DATABASE self +function DATABASE:_RegisterPlayers() + + local CoalitionsData = { AlivePlayersRed = coalition.getPlayers( coalition.side.RED ), AlivePlayersBlue = coalition.getPlayers( coalition.side.BLUE ), AlivePlayersNeutral = coalition.getPlayers( coalition.side.NEUTRAL ) } + for CoalitionId, CoalitionData in pairs( CoalitionsData ) do + for UnitId, UnitData in pairs( CoalitionData ) do + self:T3( { "UnitData:", UnitData } ) + if UnitData and UnitData:isExist() then + local UnitName = UnitData:getName() + local PlayerName = UnitData:getPlayerName() + if not self.PLAYERS[PlayerName] then + self:I( { "Add player for unit:", UnitName, PlayerName } ) + self:AddPlayer( UnitName, PlayerName ) + end + end + end + end + + return self +end + + +--- Private method that registers all Groups and Units within in the mission. +-- @param #DATABASE self +-- @return #DATABASE self +function DATABASE:_RegisterGroupsAndUnits() + + local CoalitionsData = { GroupsRed = coalition.getGroups( coalition.side.RED ), GroupsBlue = coalition.getGroups( coalition.side.BLUE ), GroupsNeutral = coalition.getGroups( coalition.side.NEUTRAL ) } + + for CoalitionId, CoalitionData in pairs( CoalitionsData ) do + + for DCSGroupId, DCSGroup in pairs( CoalitionData ) do + + if DCSGroup:isExist() then + + -- Group name. + local DCSGroupName = DCSGroup:getName() + + -- Add group. + self:I(string.format("Register Group: %s", tostring(DCSGroupName))) + self:AddGroup( DCSGroupName ) + + -- Loop over units in group. + for DCSUnitId, DCSUnit in pairs( DCSGroup:getUnits() ) do + + -- Get unit name. + local DCSUnitName = DCSUnit:getName() + + -- Add unit. + self:I(string.format("Register Unit: %s", tostring(DCSUnitName))) + self:AddUnit( DCSUnitName ) + + end + else + self:E({"Group does not exist: ", DCSGroup}) + end + + end + end + + return self +end + +--- Private method that registers all Units of skill Client or Player within in the mission. +-- @param #DATABASE self +-- @return #DATABASE self +function DATABASE:_RegisterClients() + + for ClientName, ClientTemplate in pairs( self.Templates.ClientsByName ) do + self:I(string.format("Register Client: %s", tostring(ClientName))) + self:AddClient( ClientName ) + end + + return self +end + +--- @param #DATABASE self +function DATABASE:_RegisterStatics() + + local CoalitionsData={GroupsRed=coalition.getStaticObjects(coalition.side.RED), GroupsBlue=coalition.getStaticObjects(coalition.side.BLUE), GroupsNeutral=coalition.getStaticObjects(coalition.side.NEUTRAL)} + + for CoalitionId, CoalitionData in pairs( CoalitionsData ) do + for DCSStaticId, DCSStatic in pairs( CoalitionData ) do + + if DCSStatic:isExist() then + local DCSStaticName = DCSStatic:getName() + + self:I(string.format("Register Static: %s", tostring(DCSStaticName))) + self:AddStatic( DCSStaticName ) + else + self:E( { "Static does not exist: ", DCSStatic } ) + end + end + end + + return self +end + +--- Register all world airbases. +-- @param #DATABASE self +-- @return #DATABASE self +function DATABASE:_RegisterAirbases() + + for DCSAirbaseId, DCSAirbase in pairs(world.getAirbases()) do + + -- Get the airbase name. + local DCSAirbaseName = DCSAirbase:getName() + + -- This gave the incorrect value to be inserted into the airdromeID for DCS 2.5.6. Is fixed now. + local airbaseID=DCSAirbase:getID() + + -- Add and register airbase. + local airbase=self:AddAirbase( DCSAirbaseName ) + + -- Unique ID. + local airbaseUID=airbase:GetID(true) + + -- Debug output. + local text=string.format("Register %s: %s (ID=%d UID=%d), parking=%d [", AIRBASE.CategoryName[airbase.category], tostring(DCSAirbaseName), airbaseID, airbaseUID, airbase.NparkingTotal) + for _,terminalType in pairs(AIRBASE.TerminalType) do + if airbase.NparkingTerminal and airbase.NparkingTerminal[terminalType] then + text=text..string.format("%d=%d ", terminalType, airbase.NparkingTerminal[terminalType]) + end + end + text=text.."]" + self:I(text) + + -- Check for DCS bug IDs. + if airbaseID~=airbase:GetID() then + --self:E("WARNING: :getID does NOT match :GetID!") + end + + end + + return self +end + + +--- Events + +--- Handles the OnBirth event for the alive units set. +-- @param #DATABASE self +-- @param Core.Event#EVENTDATA Event +function DATABASE:_EventOnBirth( Event ) + self:F( { Event } ) + + if Event.IniDCSUnit then + + if Event.IniObjectCategory == 3 then + + self:AddStatic( Event.IniDCSUnitName ) + + else + + if Event.IniObjectCategory == 1 then + + self:AddUnit( Event.IniDCSUnitName ) + self:AddGroup( Event.IniDCSGroupName ) + + -- Add airbase if it was spawned later in the mission. + local DCSAirbase = Airbase.getByName(Event.IniDCSUnitName) + if DCSAirbase then + self:I(string.format("Adding airbase %s", tostring(Event.IniDCSUnitName))) + self:AddAirbase(Event.IniDCSUnitName) + end + + end + end + + if Event.IniObjectCategory == 1 then + + Event.IniUnit = self:FindUnit( Event.IniDCSUnitName ) + Event.IniGroup = self:FindGroup( Event.IniDCSGroupName ) + + -- Client + local client=self.CLIENTS[Event.IniDCSUnitName] --Wrapper.Client#CLIENT + + if client then + -- TODO: create event ClientAlive + end + + -- Get player name. + local PlayerName = Event.IniUnit:GetPlayerName() + + if PlayerName then + + -- Debug info. + self:I(string.format("Player '%s' joint unit '%s' of group '%s'", tostring(PlayerName), tostring(Event.IniDCSUnitName), tostring(Event.IniDCSGroupName))) + + -- Add client in case it does not exist already. + if not client then + client=self:AddClient(Event.IniDCSUnitName) + end + + -- Add player. + client:AddPlayer(PlayerName) + + -- Add player. + if not self.PLAYERS[PlayerName] then + self:AddPlayer( Event.IniUnitName, PlayerName ) + end + + -- Player settings. + local Settings = SETTINGS:Set( PlayerName ) + Settings:SetPlayerMenu(Event.IniUnit) + + -- Create an event. + self:CreateEventPlayerEnterAircraft(Event.IniUnit) + + end + + end + + end + +end + + +--- Handles the OnDead or OnCrash event for alive units set. +-- @param #DATABASE self +-- @param Core.Event#EVENTDATA Event +function DATABASE:_EventOnDeadOrCrash( Event ) + + if Event.IniDCSUnit then + + local name=Event.IniDCSUnitName + + if Event.IniObjectCategory == 3 then + + --- + -- STATICS + --- + + if self.STATICS[Event.IniDCSUnitName] then + self:DeleteStatic( Event.IniDCSUnitName ) + end + + else + + if Event.IniObjectCategory == 1 then + + --- + -- UNITS + --- + + -- Delete unit. + if self.UNITS[Event.IniDCSUnitName] then + self:DeleteUnit(Event.IniDCSUnitName) + end + + -- Remove client players. + local client=self.CLIENTS[name] --Wrapper.Client#CLIENT + + if client then + client:RemovePlayers() + end + + end + end + + -- Add airbase if it was spawned later in the mission. + local airbase=self.AIRBASES[Event.IniDCSUnitName] --Wrapper.Airbase#AIRBASE + if airbase and (airbase:IsHelipad() or airbase:IsShip()) then + self:DeleteAirbase(Event.IniDCSUnitName) + end + + end + + -- Account destroys. + self:AccountDestroys( Event ) +end + + +--- Handles the OnPlayerEnterUnit event to fill the active players table (with the unit filter applied). +-- @param #DATABASE self +-- @param Core.Event#EVENTDATA Event +function DATABASE:_EventOnPlayerEnterUnit( Event ) + self:F2( { Event } ) + + if Event.IniDCSUnit then + if Event.IniObjectCategory == 1 then + + -- Add unit. + self:AddUnit( Event.IniDCSUnitName ) + + -- Ini unit. + Event.IniUnit = self:FindUnit( Event.IniDCSUnitName ) + + -- Add group. + self:AddGroup( Event.IniDCSGroupName ) + + -- Get player unit. + local PlayerName = Event.IniDCSUnit:getPlayerName() + + if PlayerName then + + if not self.PLAYERS[PlayerName] then + self:AddPlayer( Event.IniDCSUnitName, PlayerName ) + end + + local Settings = SETTINGS:Set( PlayerName ) + Settings:SetPlayerMenu( Event.IniUnit ) + + else + self:E("ERROR: getPlayerName() returned nil for event PlayerEnterUnit") + end + end + end +end + + +--- Handles the OnPlayerLeaveUnit event to clean the active players table. +-- @param #DATABASE self +-- @param Core.Event#EVENTDATA Event +function DATABASE:_EventOnPlayerLeaveUnit( Event ) + self:F2( { Event } ) + + if Event.IniUnit then + + if Event.IniObjectCategory == 1 then + + -- Try to get the player name. This can be buggy for multicrew aircraft! + local PlayerName = Event.IniUnit:GetPlayerName() + + if PlayerName then --and self.PLAYERS[PlayerName] then + + -- Debug info. + self:I(string.format("Player '%s' left unit %s", tostring(PlayerName), tostring(Event.IniUnitName))) + + -- Remove player menu. + local Settings = SETTINGS:Set( PlayerName ) + Settings:RemovePlayerMenu(Event.IniUnit) + + -- Delete player. + self:DeletePlayer(Event.IniUnit, PlayerName) + + -- Client stuff. + local client=self.CLIENTS[Event.IniDCSUnitName] --Wrapper.Client#CLIENT + if client then + client:RemovePlayer(PlayerName) + end + + end + end + end +end + +--- Iterators + +--- Iterate the DATABASE and call an iterator function for the given set, providing the Object for each element within the set and optional parameters. +-- @param #DATABASE self +-- @param #function IteratorFunction The function that will be called when there is an alive player in the database. +-- @return #DATABASE self +function DATABASE:ForEach( IteratorFunction, FinalizeFunction, arg, Set ) + self:F2( arg ) + + local function CoRoutine() + local Count = 0 + for ObjectID, Object in pairs( Set ) do + self:T2( Object ) + IteratorFunction( Object, unpack( arg ) ) + Count = Count + 1 +-- if Count % 100 == 0 then +-- coroutine.yield( false ) +-- end + end + return true + end + +-- local co = coroutine.create( CoRoutine ) + local co = CoRoutine + + local function Schedule() + +-- local status, res = coroutine.resume( co ) + local status, res = co() + self:T3( { status, res } ) + + if status == false then + error( res ) + end + if res == false then + return true -- resume next time the loop + end + if FinalizeFunction then + FinalizeFunction( unpack( arg ) ) + end + return false + end + + --local Scheduler = SCHEDULER:New( self, Schedule, {}, 0.001, 0.001, 0 ) + Schedule() + + return self +end + + +--- Iterate the DATABASE and call an iterator function for each **alive** STATIC, providing the STATIC and optional parameters. +-- @param #DATABASE self +-- @param #function IteratorFunction The function that will be called for each object in the database. The function needs to accept a STATIC parameter. +-- @return #DATABASE self +function DATABASE:ForEachStatic( IteratorFunction, FinalizeFunction, ... ) --R2.1 + self:F2( arg ) + + self:ForEach( IteratorFunction, FinalizeFunction, arg, self.STATICS ) + + return self +end + + +--- Iterate the DATABASE and call an iterator function for each **alive** UNIT, providing the UNIT and optional parameters. +-- @param #DATABASE self +-- @param #function IteratorFunction The function that will be called for each object in the database. The function needs to accept a UNIT parameter. +-- @return #DATABASE self +function DATABASE:ForEachUnit( IteratorFunction, FinalizeFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, FinalizeFunction, arg, self.UNITS ) + + return self +end + + +--- Iterate the DATABASE and call an iterator function for each **alive** GROUP, providing the GROUP and optional parameters. +-- @param #DATABASE self +-- @param #function IteratorFunction The function that will be called for each object in the database. The function needs to accept a GROUP parameter. +-- @return #DATABASE self +function DATABASE:ForEachGroup( IteratorFunction, FinalizeFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, FinalizeFunction, arg, self.GROUPS ) + + return self +end + + +--- Iterate the DATABASE and call an iterator function for each **ALIVE** player, providing the player name and optional parameters. +-- @param #DATABASE self +-- @param #function IteratorFunction The function that will be called for each object in the database. The function needs to accept the player name. +-- @return #DATABASE self +function DATABASE:ForEachPlayer( IteratorFunction, FinalizeFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, FinalizeFunction, arg, self.PLAYERS ) + + return self +end + + +--- Iterate the DATABASE and call an iterator function for each player who has joined the mission, providing the Unit of the player and optional parameters. +-- @param #DATABASE self +-- @param #function IteratorFunction The function that will be called for each object in the database. The function needs to accept a UNIT parameter. +-- @return #DATABASE self +function DATABASE:ForEachPlayerJoined( IteratorFunction, FinalizeFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, FinalizeFunction, arg, self.PLAYERSJOINED ) + + return self +end + +--- Iterate the DATABASE and call an iterator function for each **ALIVE** player UNIT, providing the player UNIT and optional parameters. +-- @param #DATABASE self +-- @param #function IteratorFunction The function that will be called for each object in the database. The function needs to accept the player name. +-- @return #DATABASE self +function DATABASE:ForEachPlayerUnit( IteratorFunction, FinalizeFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, FinalizeFunction, arg, self.PLAYERUNITS ) + + return self +end + + +--- Iterate the DATABASE and call an iterator function for each CLIENT, providing the CLIENT to the function and optional parameters. +-- @param #DATABASE self +-- @param #function IteratorFunction The function that will be called object in the database. The function needs to accept a CLIENT parameter. +-- @return #DATABASE self +function DATABASE:ForEachClient( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self.CLIENTS ) + + return self +end + +--- Iterate the DATABASE and call an iterator function for each CARGO, providing the CARGO object to the function and optional parameters. +-- @param #DATABASE self +-- @param #function IteratorFunction The function that will be called for each object in the database. The function needs to accept a CLIENT parameter. +-- @return #DATABASE self +function DATABASE:ForEachCargo( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self.CARGOS ) + + return self +end + + +--- Handles the OnEventNewCargo event. +-- @param #DATABASE self +-- @param Core.Event#EVENTDATA EventData +function DATABASE:OnEventNewCargo( EventData ) + self:F2( { EventData } ) + + if EventData.Cargo then + self:AddCargo( EventData.Cargo ) + end +end + + +--- Handles the OnEventDeleteCargo. +-- @param #DATABASE self +-- @param Core.Event#EVENTDATA EventData +function DATABASE:OnEventDeleteCargo( EventData ) + self:F2( { EventData } ) + + if EventData.Cargo then + self:DeleteCargo( EventData.Cargo.Name ) + end +end + + +--- Handles the OnEventNewZone event. +-- @param #DATABASE self +-- @param Core.Event#EVENTDATA EventData +function DATABASE:OnEventNewZone( EventData ) + self:F2( { EventData } ) + + if EventData.Zone then + self:AddZone( EventData.Zone.ZoneName, EventData.Zone ) + end +end + + +--- Handles the OnEventDeleteZone. +-- @param #DATABASE self +-- @param Core.Event#EVENTDATA EventData +function DATABASE:OnEventDeleteZone( EventData ) + self:F2( { EventData } ) + + if EventData.Zone then + self:DeleteZone( EventData.Zone.ZoneName ) + end +end + + + +--- Gets the player settings +-- @param #DATABASE self +-- @param #string PlayerName +-- @return Core.Settings#SETTINGS +function DATABASE:GetPlayerSettings( PlayerName ) + self:F2( { PlayerName } ) + return self.PLAYERSETTINGS[PlayerName] +end + + +--- Sets the player settings +-- @param #DATABASE self +-- @param #string PlayerName +-- @param Core.Settings#SETTINGS Settings +-- @return Core.Settings#SETTINGS +function DATABASE:SetPlayerSettings( PlayerName, Settings ) + self:F2( { PlayerName, Settings } ) + self.PLAYERSETTINGS[PlayerName] = Settings +end + +--- Add a flight group to the data base. +-- @param #DATABASE self +-- @param Ops.FlightGroup#FLIGHTGROUP flightgroup +function DATABASE:AddFlightGroup(flightgroup) + self:T({NewFlightGroup=flightgroup.groupname}) + self.FLIGHTGROUPS[flightgroup.groupname]=flightgroup +end + +--- Get a flight group from the data base. +-- @param #DATABASE self +-- @param #string groupname Group name of the flight group. Can also be passed as GROUP object. +-- @return Ops.FlightGroup#FLIGHTGROUP Flight group object. +function DATABASE:GetFlightGroup(groupname) + + -- Get group and group name. + if type(groupname)=="string" then + else + groupname=groupname:GetName() + end + + return self.FLIGHTGROUPS[groupname] +end + +--- Add a flight control to the data base. +-- @param #DATABASE self +-- @param Ops.FlightControl#FLIGHTCONTROL flightcontrol +function DATABASE:AddFlightControl(flightcontrol) + self:F2( { flightcontrol } ) + self.FLIGHTCONTROLS[flightcontrol.airbasename]=flightcontrol +end + +--- Get a flight control object from the data base. +-- @param #DATABASE self +-- @param #string airbasename Name of the associated airbase. +-- @return Ops.FlightControl#FLIGHTCONTROL The FLIGHTCONTROL object.s +function DATABASE:GetFlightControl(airbasename) + return self.FLIGHTCONTROLS[airbasename] +end + +--- @param #DATABASE self +function DATABASE:_RegisterTemplates() + self:F2() + + self.Navpoints = {} + self.UNITS = {} + --Build routines.db.units and self.Navpoints + for CoalitionName, coa_data in pairs(env.mission.coalition) do + self:T({CoalitionName=CoalitionName}) + + if (CoalitionName == 'red' or CoalitionName == 'blue' or CoalitionName == 'neutrals') and type(coa_data) == 'table' then + --self.Units[coa_name] = {} + + local CoalitionSide = coalition.side[string.upper(CoalitionName)] + if CoalitionName=="red" then + CoalitionSide=coalition.side.RED + elseif CoalitionName=="blue" then + CoalitionSide=coalition.side.BLUE + else + CoalitionSide=coalition.side.NEUTRAL + end + + -- build nav points DB + self.Navpoints[CoalitionName] = {} + if coa_data.nav_points then --navpoints + for nav_ind, nav_data in pairs(coa_data.nav_points) do + + if type(nav_data) == 'table' then + self.Navpoints[CoalitionName][nav_ind] = routines.utils.deepCopy(nav_data) + + self.Navpoints[CoalitionName][nav_ind]['name'] = nav_data.callsignStr -- name is a little bit more self-explanatory. + self.Navpoints[CoalitionName][nav_ind]['point'] = {} -- point is used by SSE, support it. + self.Navpoints[CoalitionName][nav_ind]['point']['x'] = nav_data.x + self.Navpoints[CoalitionName][nav_ind]['point']['y'] = 0 + self.Navpoints[CoalitionName][nav_ind]['point']['z'] = nav_data.y + end + end + end + + ------------------------------------------------- + if coa_data.country then --there is a country table + for cntry_id, cntry_data in pairs(coa_data.country) do + + local CountryName = string.upper(cntry_data.name) + local CountryID = cntry_data.id + + self.COUNTRY_ID[CountryName] = CountryID + self.COUNTRY_NAME[CountryID] = CountryName + + --self.Units[coa_name][countryName] = {} + --self.Units[coa_name][countryName]["countryId"] = cntry_data.id + + if type(cntry_data) == 'table' then --just making sure + + for obj_type_name, obj_type_data in pairs(cntry_data) do + + if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then --should be an unncessary check + + local CategoryName = obj_type_name + + if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! + + --self.Units[coa_name][countryName][category] = {} + + for group_num, Template in pairs(obj_type_data.group) do + + if obj_type_name ~= "static" and Template and Template.units and type(Template.units) == 'table' then --making sure again- this is a valid group + + self:_RegisterGroupTemplate(Template, CoalitionSide, _DATABASECategory[string.lower(CategoryName)], CountryID) + + else + + self:_RegisterStaticTemplate(Template, CoalitionSide, _DATABASECategory[string.lower(CategoryName)], CountryID) + + end --if GroupTemplate and GroupTemplate.units then + end --for group_num, GroupTemplate in pairs(obj_type_data.group) do + end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then + end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then + end --for obj_type_name, obj_type_data in pairs(cntry_data) do + end --if type(cntry_data) == 'table' then + end --for cntry_id, cntry_data in pairs(coa_data.country) do + end --if coa_data.country then --there is a country table + end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then + end --for coa_name, coa_data in pairs(mission.coalition) do + + return self +end + + --- Account the Hits of the Players. + -- @param #DATABASE self + -- @param Core.Event#EVENTDATA Event + function DATABASE:AccountHits( Event ) + self:F( { Event } ) + + if Event.IniPlayerName ~= nil then -- It is a player that is hitting something + self:T( "Hitting Something" ) + + -- What is he hitting? + if Event.TgtCategory then + + -- A target got hit + self.HITS[Event.TgtUnitName] = self.HITS[Event.TgtUnitName] or {} + local Hit = self.HITS[Event.TgtUnitName] + + Hit.Players = Hit.Players or {} + Hit.Players[Event.IniPlayerName] = true + end + end + + -- It is a weapon initiated by a player, that is hitting something + -- This seems to occur only with scenery and static objects. + if Event.WeaponPlayerName ~= nil then + self:T( "Hitting Scenery" ) + + -- What is he hitting? + if Event.TgtCategory then + + if Event.WeaponCoalition then -- A coalition object was hit, probably a static. + -- A target got hit + self.HITS[Event.TgtUnitName] = self.HITS[Event.TgtUnitName] or {} + local Hit = self.HITS[Event.TgtUnitName] + + Hit.Players = Hit.Players or {} + Hit.Players[Event.WeaponPlayerName] = true + else -- A scenery object was hit. + end + end + end + end + + --- Account the destroys. + -- @param #DATABASE self + -- @param Core.Event#EVENTDATA Event + function DATABASE:AccountDestroys( Event ) + self:F( { Event } ) + + local TargetUnit = nil + local TargetGroup = nil + local TargetUnitName = "" + local TargetGroupName = "" + local TargetPlayerName = "" + local TargetCoalition = nil + local TargetCategory = nil + local TargetType = nil + local TargetUnitCoalition = nil + local TargetUnitCategory = nil + local TargetUnitType = nil + + if Event.IniDCSUnit then + + TargetUnit = Event.IniUnit + TargetUnitName = Event.IniDCSUnitName + TargetGroup = Event.IniDCSGroup + TargetGroupName = Event.IniDCSGroupName + TargetPlayerName = Event.IniPlayerName + + TargetCoalition = Event.IniCoalition + --TargetCategory = TargetUnit:getCategory() + --TargetCategory = TargetUnit:getDesc().category -- Workaround + TargetCategory = Event.IniCategory + TargetType = Event.IniTypeName + + TargetUnitType = TargetType + + self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType } ) + end + + local Destroyed = false + + -- What is the player destroying? + if self.HITS[Event.IniUnitName] then -- Was there a hit for this unit for this player before registered??? + self.DESTROYS[Event.IniUnitName] = self.DESTROYS[Event.IniUnitName] or {} + self.DESTROYS[Event.IniUnitName] = true + end + end +--- **Core** - Define collections of objects to perform bulk actions and logically group objects. +-- +-- === +-- +-- ## Features: +-- +-- * Dynamically maintain collections of objects. +-- * Manually modify the collection, by adding or removing objects. +-- * Collections of different types. +-- * Validate the presence of objects in the collection. +-- * Perform bulk actions on collection. +-- +-- === +-- +-- Group objects or data of the same type into a collection, which is either: +-- +-- * Manually managed using the **:Add...()** or **:Remove...()** methods. The initial SET can be filtered with the **@{#SET_BASE.FilterOnce}()** method. +-- * Dynamically updated when new objects are created or objects are destroyed using the **@{#SET_BASE.FilterStart}()** method. +-- +-- Various types of SET_ classes are available: +-- +-- * @{#SET_GROUP}: Defines a collection of @{Wrapper.Group}s filtered by filter criteria. +-- * @{#SET_UNIT}: Defines a colleciton of @{Wrapper.Unit}s filtered by filter criteria. +-- * @{#SET_STATIC}: Defines a collection of @{Wrapper.Static}s filtered by filter criteria. +-- * @{#SET_CLIENT}: Defines a collection of @{Client}s filterd by filter criteria. +-- * @{#SET_AIRBASE}: Defines a collection of @{Wrapper.Airbase}s filtered by filter criteria. +-- * @{#SET_CARGO}: Defines a collection of @{Cargo.Cargo}s filtered by filter criteria. +-- * @{#SET_ZONE}: Defines a collection of @{Core.Zone}s filtered by filter criteria. +-- +-- These classes are derived from @{#SET_BASE}, which contains the main methods to manage the collections. +-- +-- A multitude of other methods are available in the individual set classes that allow to: +-- +-- * Validate the presence of objects in the SET. +-- * Trigger events when objects in the SET change a zone presence. +-- +-- === +-- +-- ### Author: **FlightControl** +-- ### Contributions: **funkyfranky** +-- +-- === +-- +-- @module Core.Set +-- @image Core_Sets.JPG + + +do -- SET_BASE + + --- @type SET_BASE + -- @field #table Filter Table of filters. + -- @field #table Set Table of objects. + -- @field #table Index Table of indicies. + -- @field #table List Unused table. + -- @field Core.Scheduler#SCHEDULER CallScheduler + -- @extends Core.Base#BASE + + + --- The @{Core.Set#SET_BASE} class defines the core functions that define a collection of objects. + -- A SET provides iterators to iterate the SET, but will **temporarily** yield the ForEach interator loop at defined **"intervals"** to the mail simulator loop. + -- In this way, large loops can be done while not blocking the simulator main processing loop. + -- The default **"yield interval"** is after 10 objects processed. + -- The default **"time interval"** is after 0.001 seconds. + -- + -- ## Add or remove objects from the SET + -- + -- Some key core functions are @{Core.Set#SET_BASE.Add} and @{Core.Set#SET_BASE.Remove} to add or remove objects from the SET in your logic. + -- + -- ## Define the SET iterator **"yield interval"** and the **"time interval"** + -- + -- Modify the iterator intervals with the @{Core.Set#SET_BASE.SetInteratorIntervals} method. + -- You can set the **"yield interval"**, and the **"time interval"**. (See above). + -- + -- @field #SET_BASE SET_BASE + SET_BASE = { + ClassName = "SET_BASE", + Filter = {}, + Set = {}, + List = {}, + Index = {}, + Database = nil, + CallScheduler=nil, + TimeInterval=nil, + YieldInterval=nil, + } + + + --- Creates a new SET_BASE object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. + -- @param #SET_BASE self + -- @return #SET_BASE + -- @usage + -- -- Define a new SET_BASE Object. This DBObject will contain a reference to all Group and Unit Templates defined within the ME and the DCSRTE. + -- DBObject = SET_BASE:New() + function SET_BASE:New( Database ) + + -- Inherits from BASE + local self = BASE:Inherit( self, FSM:New() ) -- Core.Set#SET_BASE + + self.Database = Database + + self:SetStartState( "Started" ) + + --- Added Handler OnAfter for SET_BASE + -- @function [parent=#SET_BASE] OnAfterAdded + -- @param #SET_BASE self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param #string ObjectName The name of the object. + -- @param Object The object. + + + self:AddTransition( "*", "Added", "*" ) + + --- Removed Handler OnAfter for SET_BASE + -- @function [parent=#SET_BASE] OnAfterRemoved + -- @param #SET_BASE self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param #string ObjectName The name of the object. + -- @param Object The object. + + self:AddTransition( "*", "Removed", "*" ) + + self.YieldInterval = 10 + self.TimeInterval = 0.001 + + self.Set = {} + self.Index = {} + + self.CallScheduler = SCHEDULER:New( self ) + + self:SetEventPriority( 2 ) + + return self + end + + --- Clear the Objects in the Set. + -- @param #SET_BASE self + -- @return #SET_BASE self + function SET_BASE:Clear() + + for Name, Object in pairs( self.Set ) do + self:Remove( Name ) + end + + return self + end + + + + --- Finds an @{Core.Base#BASE} object based on the object Name. + -- @param #SET_BASE self + -- @param #string ObjectName + -- @return Core.Base#BASE The Object found. + function SET_BASE:_Find( ObjectName ) + + local ObjectFound = self.Set[ObjectName] + return ObjectFound + end + + + --- Gets the Set. + -- @param #SET_BASE self + -- @return #SET_BASE self + function SET_BASE:GetSet() + self:F2() + + return self.Set or {} + end + + --- Gets a list of the Names of the Objects in the Set. + -- @param #SET_BASE self + -- @return #SET_BASE self + function SET_BASE:GetSetNames() -- R2.3 + self:F2() + + local Names = {} + + for Name, Object in pairs( self.Set ) do + table.insert( Names, Name ) + end + + return Names + end + + + --- Gets a list of the Objects in the Set. + -- @param #SET_BASE self + -- @return #SET_BASE self + function SET_BASE:GetSetObjects() -- R2.3 + self:F2() + + local Objects = {} + + for Name, Object in pairs( self.Set ) do + table.insert( Objects, Object ) + end + + return Objects + end + + + --- Removes a @{Core.Base#BASE} object from the @{Core.Set#SET_BASE} and derived classes, based on the Object Name. + -- @param #SET_BASE self + -- @param #string ObjectName + -- @param NoTriggerEvent (optional) When `true`, the :Remove() method will not trigger a **Removed** event. + function SET_BASE:Remove( ObjectName, NoTriggerEvent ) + self:F2( { ObjectName = ObjectName } ) + + local Object = self.Set[ObjectName] + + if Object then + for Index, Key in ipairs( self.Index ) do + if Key == ObjectName then + table.remove( self.Index, Index ) + self.Set[ObjectName] = nil + break + end + end + -- When NoTriggerEvent is true, then no Removed event will be triggered. + if not NoTriggerEvent then + self:Removed( ObjectName, Object ) + end + end + end + + + --- Adds a @{Core.Base#BASE} object in the @{Core.Set#SET_BASE}, using a given ObjectName as the index. + -- @param #SET_BASE self + -- @param #string ObjectName The name of the object. + -- @param Core.Base#BASE Object The object itself. + -- @return Core.Base#BASE The added BASE Object. + function SET_BASE:Add( ObjectName, Object ) + self:F2( { ObjectName = ObjectName, Object = Object } ) + + -- Ensure that the existing element is removed from the Set before a new one is inserted to the Set + if self.Set[ObjectName] then + self:Remove( ObjectName, true ) + end + + -- Add object to set. + self.Set[ObjectName] = Object + + -- Add Object name to Index. + table.insert( self.Index, ObjectName ) + + -- Trigger Added event. + self:Added( ObjectName, Object ) + end + + --- Adds a @{Core.Base#BASE} object in the @{Core.Set#SET_BASE}, using the Object Name as the index. + -- @param #SET_BASE self + -- @param Wrapper.Object#OBJECT Object + -- @return Core.Base#BASE The added BASE Object. + function SET_BASE:AddObject( Object ) + self:F2( Object.ObjectName ) + + self:T( Object.UnitName ) + self:T( Object.ObjectName ) + self:Add( Object.ObjectName, Object ) + + end + + + --- Get the *union* of two sets. + -- @param #SET_BASE self + -- @param Core.Set#SET_BASE SetB Set *B*. + -- @return Core.Set#SET_BASE The union set, i.e. contains objects that are in set *A* **or** in set *B*. + function SET_BASE:GetSetUnion(SetB) + + local union=SET_BASE:New() + + for _,ObjectA in pairs(self.Set) do + union:AddObject(ObjectA) + end + + for _,ObjectB in pairs(SetB.Set) do + union:AddObject(ObjectB) + end + + return union + end + + --- Get the *intersection* of this set, called *A*, and another set. + -- @param #SET_BASE self + -- @param Core.Set#SET_BASE SetB Set other set, called *B*. + -- @return Core.Set#SET_BASE A set of objects that is included in set *A* **and** in set *B*. + function SET_BASE:GetSetIntersection(SetB) + + local intersection=SET_BASE:New() + + local union=self:GetSetUnion(SetB) + + for _,Object in pairs(union.Set) do + if self:IsIncludeObject(Object) and SetB:IsIncludeObject(Object) then + intersection:AddObject(intersection) + end + end + + return intersection + end + + --- Get the *complement* of two sets. + -- @param #SET_BASE self + -- @param Core.Set#SET_BASE SetB Set other set, called *B*. + -- @return Core.Set#SET_BASE The set of objects that are in set *B* but **not** in this set *A*. + function SET_BASE:GetSetComplement(SetB) + + local complement=SET_BASE:New() + + local union=self:GetSetUnion(SetA, SetB) + + for _,Object in pairs(union.Set) do + if SetA:IsIncludeObject(Object) and SetB:IsIncludeObject(Object) then + intersection:Add(intersection) + end + end + + return intersection + end + + + --- Compare two sets. + -- @param #SET_BASE self + -- @param Core.Set#SET_BASE SetA First set. + -- @param Core.Set#SET_BASE SetB Set to be merged into first set. + -- @return Core.Set#SET_BASE The set of objects that are included in SetA and SetB. + function SET_BASE:CompareSets(SetA, SetB) + + for _,ObjectB in pairs(SetB.Set) do + if SetA:IsIncludeObject(ObjectB) then + SetA:Add(ObjectB) + end + end + + return SetA + end + + + + + --- Gets a @{Core.Base#BASE} object from the @{Core.Set#SET_BASE} and derived classes, based on the Object Name. + -- @param #SET_BASE self + -- @param #string ObjectName + -- @return Core.Base#BASE + function SET_BASE:Get( ObjectName ) + self:F( ObjectName ) + + local Object = self.Set[ObjectName] + + self:T3( { ObjectName, Object } ) + return Object + end + + --- Gets the first object from the @{Core.Set#SET_BASE} and derived classes. + -- @param #SET_BASE self + -- @return Core.Base#BASE + function SET_BASE:GetFirst() + + local ObjectName = self.Index[1] + local FirstObject = self.Set[ObjectName] + self:T3( { FirstObject } ) + return FirstObject + end + + --- Gets the last object from the @{Core.Set#SET_BASE} and derived classes. + -- @param #SET_BASE self + -- @return Core.Base#BASE + function SET_BASE:GetLast() + + local ObjectName = self.Index[#self.Index] + local LastObject = self.Set[ObjectName] + self:T3( { LastObject } ) + return LastObject + end + + --- Gets a random object from the @{Core.Set#SET_BASE} and derived classes. + -- @param #SET_BASE self + -- @return Core.Base#BASE + function SET_BASE:GetRandom() + + local RandomItem = self.Set[self.Index[math.random(#self.Index)]] + self:T3( { RandomItem } ) + return RandomItem + end + + + --- Retrieves the amount of objects in the @{Core.Set#SET_BASE} and derived classes. + -- @param #SET_BASE self + -- @return #number Count + function SET_BASE:Count() + + return self.Index and #self.Index or 0 + end + + + --- Copies the Filter criteria from a given Set (for rebuilding a new Set based on an existing Set). + -- @param #SET_BASE self + -- @param #SET_BASE BaseSet + -- @return #SET_BASE + function SET_BASE:SetDatabase( BaseSet ) + + -- Copy the filter criteria of the BaseSet + local OtherFilter = routines.utils.deepCopy( BaseSet.Filter ) + self.Filter = OtherFilter + + -- Now base the new Set on the BaseSet + self.Database = BaseSet:GetSet() + return self + end + + + + --- Define the SET iterator **"yield interval"** and the **"time interval"**. + -- @param #SET_BASE self + -- @param #number YieldInterval Sets the frequency when the iterator loop will yield after the number of objects processed. The default frequency is 10 objects processed. + -- @param #number TimeInterval Sets the time in seconds when the main logic will resume the iterator loop. The default time is 0.001 seconds. + -- @return #SET_BASE self + function SET_BASE:SetIteratorIntervals( YieldInterval, TimeInterval ) + + self.YieldInterval = YieldInterval + self.TimeInterval = TimeInterval + + return self + end + + --- Define the SET iterator **"limit"**. + -- @param #SET_BASE self + -- @param #number Limit Defines how many objects are evaluated of the set as part of the Some iterators. The default is 1. + -- @return #SET_BASE self + function SET_BASE:SetSomeIteratorLimit( Limit ) + + self.SomeIteratorLimit = Limit or 1 + + return self + end + + --- Get the SET iterator **"limit"**. + -- @param #SET_BASE self + -- @return #number Defines how many objects are evaluated of the set as part of the Some iterators. + function SET_BASE:GetSomeIteratorLimit() + + return self.SomeIteratorLimit or self:Count() + end + + + --- Filters for the defined collection. + -- @param #SET_BASE self + -- @return #SET_BASE self + function SET_BASE:FilterOnce() + + for ObjectName, Object in pairs( self.Database ) do + + if self:IsIncludeObject( Object ) then + self:Add( ObjectName, Object ) + end + end + + return self + end + + --- Starts the filtering for the defined collection. + -- @param #SET_BASE self + -- @return #SET_BASE self + function SET_BASE:_FilterStart() + + for ObjectName, Object in pairs( self.Database ) do + + if self:IsIncludeObject( Object ) then + self:Add( ObjectName, Object ) + end + end + + -- Follow alive players and clients + --self:HandleEvent( EVENTS.PlayerEnterUnit, self._EventOnPlayerEnterUnit ) + --self:HandleEvent( EVENTS.PlayerLeaveUnit, self._EventOnPlayerLeaveUnit ) + + + return self + end + + --- Starts the filtering of the Dead events for the collection. + -- @param #SET_BASE self + -- @return #SET_BASE self + function SET_BASE:FilterDeads() --R2.1 allow deads to be filtered to automatically handle deads in the collection. + + self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) + + return self + end + + --- Starts the filtering of the Crash events for the collection. + -- @param #SET_BASE self + -- @return #SET_BASE self + function SET_BASE:FilterCrashes() --R2.1 allow crashes to be filtered to automatically handle crashes in the collection. + + self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) + + return self + end + + --- Stops the filtering for the defined collection. + -- @param #SET_BASE self + -- @return #SET_BASE self + function SET_BASE:FilterStop() + + self:UnHandleEvent( EVENTS.Birth ) + self:UnHandleEvent( EVENTS.Dead ) + self:UnHandleEvent( EVENTS.Crash ) + + return self + end + + --- Iterate the SET_BASE while identifying the nearest object from a @{Core.Point#POINT_VEC2}. + -- @param #SET_BASE self + -- @param Core.Point#POINT_VEC2 PointVec2 A @{Core.Point#POINT_VEC2} object from where to evaluate the closest object in the set. + -- @return Core.Base#BASE The closest object. + function SET_BASE:FindNearestObjectFromPointVec2( PointVec2 ) + self:F2( PointVec2 ) + + local NearestObject = nil + local ClosestDistance = nil + + for ObjectID, ObjectData in pairs( self.Set ) do + if NearestObject == nil then + NearestObject = ObjectData + ClosestDistance = PointVec2:DistanceFromPointVec2( ObjectData:GetCoordinate() ) + else + local Distance = PointVec2:DistanceFromPointVec2( ObjectData:GetCoordinate() ) + if Distance < ClosestDistance then + NearestObject = ObjectData + ClosestDistance = Distance + end + end + end + + return NearestObject + end + + + + ----- Private method that registers all alive players in the mission. + ---- @param #SET_BASE self + ---- @return #SET_BASE self + --function SET_BASE:_RegisterPlayers() + -- + -- local CoalitionsData = { AlivePlayersRed = coalition.getPlayers( coalition.side.RED ), AlivePlayersBlue = coalition.getPlayers( coalition.side.BLUE ) } + -- for CoalitionId, CoalitionData in pairs( CoalitionsData ) do + -- for UnitId, UnitData in pairs( CoalitionData ) do + -- self:T3( { "UnitData:", UnitData } ) + -- if UnitData and UnitData:isExist() then + -- local UnitName = UnitData:getName() + -- if not self.PlayersAlive[UnitName] then + -- self:E( { "Add player for unit:", UnitName, UnitData:getPlayerName() } ) + -- self.PlayersAlive[UnitName] = UnitData:getPlayerName() + -- end + -- end + -- end + -- end + -- + -- return self + --end + + --- Events + + --- Handles the OnBirth event for the Set. + -- @param #SET_BASE self + -- @param Core.Event#EVENTDATA Event + function SET_BASE:_EventOnBirth( Event ) + self:F3( { Event } ) + + if Event.IniDCSUnit then + local ObjectName, Object = self:AddInDatabase( Event ) + self:T3( ObjectName, Object ) + if Object and self:IsIncludeObject( Object ) then + self:Add( ObjectName, Object ) + --self:_EventOnPlayerEnterUnit( Event ) + end + end + end + + --- Handles the OnDead or OnCrash event for alive units set. + -- @param #SET_BASE self + -- @param Core.Event#EVENTDATA Event + function SET_BASE:_EventOnDeadOrCrash( Event ) + self:F( { Event } ) + + if Event.IniDCSUnit then + local ObjectName, Object = self:FindInDatabase( Event ) + if ObjectName then + self:Remove( ObjectName ) + end + end + end + + --- Handles the OnPlayerEnterUnit event to fill the active players table (with the unit filter applied). + -- @param #SET_BASE self + -- @param Core.Event#EVENTDATA Event + --function SET_BASE:_EventOnPlayerEnterUnit( Event ) + -- self:F3( { Event } ) + -- + -- if Event.IniDCSUnit then + -- local ObjectName, Object = self:AddInDatabase( Event ) + -- self:T3( ObjectName, Object ) + -- if self:IsIncludeObject( Object ) then + -- self:Add( ObjectName, Object ) + -- --self:_EventOnPlayerEnterUnit( Event ) + -- end + -- end + --end + + --- Handles the OnPlayerLeaveUnit event to clean the active players table. + -- @param #SET_BASE self + -- @param Core.Event#EVENTDATA Event + --function SET_BASE:_EventOnPlayerLeaveUnit( Event ) + -- self:F3( { Event } ) + -- + -- local ObjectName = Event.IniDCSUnit + -- if Event.IniDCSUnit then + -- if Event.IniDCSGroup then + -- local GroupUnits = Event.IniDCSGroup:getUnits() + -- local PlayerCount = 0 + -- for _, DCSUnit in pairs( GroupUnits ) do + -- if DCSUnit ~= Event.IniDCSUnit then + -- if DCSUnit:getPlayerName() ~= nil then + -- PlayerCount = PlayerCount + 1 + -- end + -- end + -- end + -- self:E(PlayerCount) + -- if PlayerCount == 0 then + -- self:Remove( Event.IniDCSGroupName ) + -- end + -- end + -- end + --end + + -- Iterators + + --- Iterate the SET_BASE and derived classes and call an iterator function for the given SET_BASE, providing the Object for each element within the set and optional parameters. + -- @param #SET_BASE self + -- @param #function IteratorFunction The function that will be called. + -- @param #table arg Arguments of the IteratorFunction. + -- @param #SET_BASE Set (Optional) The set to use. Default self:GetSet(). + -- @param #function Function (Optional) A function returning a #boolean true/false. Only if true, the IteratorFunction is called. + -- @param #table FunctionArguments (Optional) Function arguments. + -- @return #SET_BASE self + function SET_BASE:ForEach( IteratorFunction, arg, Set, Function, FunctionArguments ) + self:F3( arg ) + + Set = Set or self:GetSet() + arg = arg or {} + + local function CoRoutine() + local Count = 0 + for ObjectID, ObjectData in pairs( Set ) do + local Object = ObjectData + self:T3( Object ) + if Function then + if Function( unpack( FunctionArguments or {} ), Object ) == true then + IteratorFunction( Object, unpack( arg ) ) + end + else + IteratorFunction( Object, unpack( arg ) ) + end + Count = Count + 1 + -- if Count % self.YieldInterval == 0 then + -- coroutine.yield( false ) + -- end + end + return true + end + + -- local co = coroutine.create( CoRoutine ) + local co = CoRoutine + + local function Schedule() + + -- local status, res = coroutine.resume( co ) + local status, res = co() + self:T3( { status, res } ) + + if status == false then + error( res ) + end + if res == false then + return true -- resume next time the loop + end + + return false + end + + --self.CallScheduler:Schedule( self, Schedule, {}, self.TimeInterval, self.TimeInterval, 0 ) + Schedule() + + return self + end + + --- Iterate the SET_BASE and derived classes and call an iterator function for the given SET_BASE, providing the Object for each element within the set and optional parameters. + -- @param #SET_BASE self + -- @param #function IteratorFunction The function that will be called. + -- @return #SET_BASE self + function SET_BASE:ForSome( IteratorFunction, arg, Set, Function, FunctionArguments ) + self:F3( arg ) + + Set = Set or self:GetSet() + arg = arg or {} + + local Limit = self:GetSomeIteratorLimit() + + local function CoRoutine() + local Count = 0 + for ObjectID, ObjectData in pairs( Set ) do + local Object = ObjectData + self:T3( Object ) + if Function then + if Function( unpack( FunctionArguments ), Object ) == true then + IteratorFunction( Object, unpack( arg ) ) + end + else + IteratorFunction( Object, unpack( arg ) ) + end + Count = Count + 1 + if Count >= Limit then + break + end + -- if Count % self.YieldInterval == 0 then + -- coroutine.yield( false ) + -- end + end + return true + end + + -- local co = coroutine.create( CoRoutine ) + local co = CoRoutine + + local function Schedule() + + -- local status, res = coroutine.resume( co ) + local status, res = co() + self:T3( { status, res } ) + + if status == false then + error( res ) + end + if res == false then + return true -- resume next time the loop + end + + return false + end + + --self.CallScheduler:Schedule( self, Schedule, {}, self.TimeInterval, self.TimeInterval, 0 ) + Schedule() + + return self + end + + + ----- Iterate the SET_BASE and call an interator function for each **alive** unit, providing the Unit and optional parameters. + ---- @param #SET_BASE self + ---- @param #function IteratorFunction The function that will be called when there is an alive unit in the SET_BASE. The function needs to accept a UNIT parameter. + ---- @return #SET_BASE self + --function SET_BASE:ForEachDCSUnitAlive( IteratorFunction, ... ) + -- self:F3( arg ) + -- + -- self:ForEach( IteratorFunction, arg, self.DCSUnitsAlive ) + -- + -- return self + --end + -- + ----- Iterate the SET_BASE and call an interator function for each **alive** player, providing the Unit of the player and optional parameters. + ---- @param #SET_BASE self + ---- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_BASE. The function needs to accept a UNIT parameter. + ---- @return #SET_BASE self + --function SET_BASE:ForEachPlayer( IteratorFunction, ... ) + -- self:F3( arg ) + -- + -- self:ForEach( IteratorFunction, arg, self.PlayersAlive ) + -- + -- return self + --end + -- + -- + ----- Iterate the SET_BASE and call an interator function for each client, providing the Client to the function and optional parameters. + ---- @param #SET_BASE self + ---- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_BASE. The function needs to accept a CLIENT parameter. + ---- @return #SET_BASE self + --function SET_BASE:ForEachClient( IteratorFunction, ... ) + -- self:F3( arg ) + -- + -- self:ForEach( IteratorFunction, arg, self.Clients ) + -- + -- return self + --end + + + --- Decides whether to include the Object. + -- @param #SET_BASE self + -- @param #table Object + -- @return #SET_BASE self + function SET_BASE:IsIncludeObject( Object ) + self:F3( Object ) + + return true + end + + --- Decides whether to include the Object. + -- @param #SET_BASE self + -- @param #table Object + -- @return #SET_BASE self + function SET_BASE:IsInSet(ObjectName) + self:F3( Object ) + + return true + end + + --- Gets a string with all the object names. + -- @param #SET_BASE self + -- @return #string A string with the names of the objects. + function SET_BASE:GetObjectNames() + self:F3() + + local ObjectNames = "" + for ObjectName, Object in pairs( self.Set ) do + ObjectNames = ObjectNames .. ObjectName .. ", " + end + + return ObjectNames + end + + --- Flushes the current SET_BASE contents in the log ... (for debugging reasons). + -- @param #SET_BASE self + -- @param Core.Base#BASE MasterObject (optional) The master object as a reference. + -- @return #string A string with the names of the objects. + function SET_BASE:Flush( MasterObject ) + self:F3() + + local ObjectNames = "" + for ObjectName, Object in pairs( self.Set ) do + ObjectNames = ObjectNames .. ObjectName .. ", " + end + self:F( { MasterObject = MasterObject and MasterObject:GetClassNameAndID(), "Objects in Set:", ObjectNames } ) + + return ObjectNames + end + +end + + +do -- SET_GROUP + + --- @type SET_GROUP + -- @extends Core.Set#SET_BASE + + --- Mission designers can use the @{Core.Set#SET_GROUP} class to build sets of groups belonging to certain: + -- + -- * Coalitions + -- * Categories + -- * Countries + -- * Starting with certain prefix strings. + -- + -- ## SET_GROUP constructor + -- + -- Create a new SET_GROUP object with the @{#SET_GROUP.New} method: + -- + -- * @{#SET_GROUP.New}: Creates a new SET_GROUP object. + -- + -- ## Add or Remove GROUP(s) from SET_GROUP + -- + -- GROUPS can be added and removed using the @{Core.Set#SET_GROUP.AddGroupsByName} and @{Core.Set#SET_GROUP.RemoveGroupsByName} respectively. + -- These methods take a single GROUP name or an array of GROUP names to be added or removed from SET_GROUP. + -- + -- ## SET_GROUP filter criteria + -- + -- You can set filter criteria to define the set of groups within the SET_GROUP. + -- Filter criteria are defined by: + -- + -- * @{#SET_GROUP.FilterCoalitions}: Builds the SET_GROUP with the groups belonging to the coalition(s). + -- * @{#SET_GROUP.FilterCategories}: Builds the SET_GROUP with the groups belonging to the category(ies). + -- * @{#SET_GROUP.FilterCountries}: Builds the SET_GROUP with the groups belonging to the country(ies). + -- * @{#SET_GROUP.FilterPrefixes}: Builds the SET_GROUP with the groups *containing* the given string in the group name. **Attention!** Bad naming convention, as this not really filtering *prefixes*. + -- * @{#SET_GROUP.FilterActive}: Builds the SET_GROUP with the groups that are only active. Groups that are inactive (late activation) won't be included in the set! + -- + -- For the Category Filter, extra methods have been added: + -- + -- * @{#SET_GROUP.FilterCategoryAirplane}: Builds the SET_GROUP from airplanes. + -- * @{#SET_GROUP.FilterCategoryHelicopter}: Builds the SET_GROUP from helicopters. + -- * @{#SET_GROUP.FilterCategoryGround}: Builds the SET_GROUP from ground vehicles or infantry. + -- * @{#SET_GROUP.FilterCategoryShip}: Builds the SET_GROUP from ships. + -- * @{#SET_GROUP.FilterCategoryStructure}: Builds the SET_GROUP from structures. + -- + -- + -- Once the filter criteria have been set for the SET_GROUP, you can start filtering using: + -- + -- * @{#SET_GROUP.FilterStart}: Starts the filtering of the groups within the SET_GROUP and add or remove GROUP objects **dynamically**. + -- * @{#SET_GROUP.FilterOnce}: Filters of the groups **once**. + -- + -- Planned filter criteria within development are (so these are not yet available): + -- + -- * @{#SET_GROUP.FilterZones}: Builds the SET_GROUP with the groups within a @{Core.Zone#ZONE}. + -- + -- ## SET_GROUP iterators + -- + -- Once the filters have been defined and the SET_GROUP has been built, you can iterate the SET_GROUP with the available iterator methods. + -- The iterator methods will walk the SET_GROUP set, and call for each element within the set a function that you provide. + -- The following iterator methods are currently available within the SET_GROUP: + -- + -- * @{#SET_GROUP.ForEachGroup}: Calls a function for each alive group it finds within the SET_GROUP. + -- * @{#SET_GROUP.ForEachGroupCompletelyInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence completely in a @{Zone}, providing the GROUP and optional parameters to the called function. + -- * @{#SET_GROUP.ForEachGroupPartlyInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence partly in a @{Zone}, providing the GROUP and optional parameters to the called function. + -- * @{#SET_GROUP.ForEachGroupNotInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence not in a @{Zone}, providing the GROUP and optional parameters to the called function. + -- + -- + -- ## SET_GROUP trigger events on the GROUP objects. + -- + -- The SET is derived from the FSM class, which provides extra capabilities to track the contents of the GROUP objects in the SET_GROUP. + -- + -- ### When a GROUP object crashes or is dead, the SET_GROUP will trigger a **Dead** event. + -- + -- You can handle the event using the OnBefore and OnAfter event handlers. + -- The event handlers need to have the paramters From, Event, To, GroupObject. + -- The GroupObject is the GROUP object that is dead and within the SET_GROUP, and is passed as a parameter to the event handler. + -- See the following example: + -- + -- -- Create the SetCarrier SET_GROUP collection. + -- + -- local SetHelicopter = SET_GROUP:New():FilterPrefixes( "Helicopter" ):FilterStart() + -- + -- -- Put a Dead event handler on SetCarrier, to ensure that when a carrier is destroyed, that all internal parameters are reset. + -- + -- function SetHelicopter:OnAfterDead( From, Event, To, GroupObject ) + -- self:F( { GroupObject = GroupObject:GetName() } ) + -- end + -- + -- While this is a good example, there is a catch. + -- Imageine you want to execute the code above, the the self would need to be from the object declared outside (above) the OnAfterDead method. + -- So, the self would need to contain another object. Fortunately, this can be done, but you must use then the **`.`** notation for the method. + -- See the modified example: + -- + -- -- Now we have a constructor of the class AI_CARGO_DISPATCHER, that receives the SetHelicopter as a parameter. + -- -- Within that constructor, we want to set an enclosed event handler OnAfterDead for SetHelicopter. + -- -- But within the OnAfterDead method, we want to refer to the self variable of the AI_CARGO_DISPATCHER. + -- + -- function AI_CARGO_DISPATCHER:New( SetCarrier, SetCargo, SetDeployZones ) + -- + -- local self = BASE:Inherit( self, FSM:New() ) -- #AI_CARGO_DISPATCHER + -- + -- -- Put a Dead event handler on SetCarrier, to ensure that when a carrier is destroyed, that all internal parameters are reset. + -- -- Note the "." notation, and the explicit declaration of SetHelicopter, which would be using the ":" notation the implicit self variable declaration. + -- + -- function SetHelicopter.OnAfterDead( SetHelicopter, From, Event, To, GroupObject ) + -- SetHelicopter:F( { GroupObject = GroupObject:GetName() } ) + -- self.PickupCargo[GroupObject] = nil -- So here I clear the PickupCargo table entry of the self object AI_CARGO_DISPATCHER. + -- self.CarrierHome[GroupObject] = nil + -- end + -- + -- end + -- + -- === + -- @field #SET_GROUP SET_GROUP + SET_GROUP = { + ClassName = "SET_GROUP", + Filter = { + Coalitions = nil, + Categories = nil, + Countries = nil, + GroupPrefixes = nil, + }, + FilterMeta = { + Coalitions = { + red = coalition.side.RED, + blue = coalition.side.BLUE, + neutral = coalition.side.NEUTRAL, + }, + Categories = { + plane = Group.Category.AIRPLANE, + helicopter = Group.Category.HELICOPTER, + ground = Group.Category.GROUND, -- R2.2 + ship = Group.Category.SHIP, + structure = Group.Category.STRUCTURE, + }, + }, + } + + + --- Creates a new SET_GROUP object, building a set of groups belonging to a coalitions, categories, countries, types or with defined prefix names. + -- @param #SET_GROUP self + -- @return #SET_GROUP + -- @usage + -- -- Define a new SET_GROUP Object. This DBObject will contain a reference to all alive GROUPS. + -- DBObject = SET_GROUP:New() + function SET_GROUP:New() + + -- Inherits from BASE + local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.GROUPS ) ) -- #SET_GROUP + + self:FilterActive( false ) + + return self + end + + --- Gets the Set. + -- @param #SET_GROUP self + -- @return #SET_GROUP self + function SET_GROUP:GetAliveSet() + self:F2() + + local AliveSet = SET_GROUP:New() + + -- Clean the Set before returning with only the alive Groups. + for GroupName, GroupObject in pairs( self.Set ) do + local GroupObject=GroupObject --Wrapper.Group#GROUP + if GroupObject then + if GroupObject:IsAlive() then + AliveSet:Add( GroupName, GroupObject ) + end + end + end + + return AliveSet.Set or {} + end + + --- Returns a report of of unit types. + -- @param #SET_GROUP self + -- @return Core.Report#REPORT A report of the unit types found. The key is the UnitTypeName and the value is the amount of unit types found. + function SET_GROUP:GetUnitTypeNames() + self:F2() + + local MT = {} -- Message Text + local UnitTypes = {} + + local ReportUnitTypes = REPORT:New() + + for GroupID, GroupData in pairs( self:GetSet() ) do + local Units = GroupData:GetUnits() + for UnitID, UnitData in pairs( Units ) do + if UnitData:IsAlive() then + local UnitType = UnitData:GetTypeName() + + if not UnitTypes[UnitType] then + UnitTypes[UnitType] = 1 + else + UnitTypes[UnitType] = UnitTypes[UnitType] + 1 + end + end + end + end + + for UnitTypeID, UnitType in pairs( UnitTypes ) do + ReportUnitTypes:Add( UnitType .. " of " .. UnitTypeID ) + end + + return ReportUnitTypes + end + + --- Add a GROUP to SET_GROUP. + -- Note that for each unit in the group that is set, a default cargo bay limit is initialized. + -- @param Core.Set#SET_GROUP self + -- @param Wrapper.Group#GROUP group The group which should be added to the set. + -- @return Core.Set#SET_GROUP self + function SET_GROUP:AddGroup( group ) + + self:Add( group:GetName(), group ) + + -- I set the default cargo bay weight limit each time a new group is added to the set. + for UnitID, UnitData in pairs( group:GetUnits() ) do + UnitData:SetCargoBayWeightLimit() + end + + return self + end + + --- Add GROUP(s) to SET_GROUP. + -- @param Core.Set#SET_GROUP self + -- @param #string AddGroupNames A single name or an array of GROUP names. + -- @return Core.Set#SET_GROUP self + function SET_GROUP:AddGroupsByName( AddGroupNames ) + + local AddGroupNamesArray = ( type( AddGroupNames ) == "table" ) and AddGroupNames or { AddGroupNames } + + for AddGroupID, AddGroupName in pairs( AddGroupNamesArray ) do + self:Add( AddGroupName, GROUP:FindByName( AddGroupName ) ) + end + + return self + end + + --- Remove GROUP(s) from SET_GROUP. + -- @param Core.Set#SET_GROUP self + -- @param Wrapper.Group#GROUP RemoveGroupNames A single name or an array of GROUP names. + -- @return Core.Set#SET_GROUP self + function SET_GROUP:RemoveGroupsByName( RemoveGroupNames ) + + local RemoveGroupNamesArray = ( type( RemoveGroupNames ) == "table" ) and RemoveGroupNames or { RemoveGroupNames } + + for RemoveGroupID, RemoveGroupName in pairs( RemoveGroupNamesArray ) do + self:Remove( RemoveGroupName ) + end + + return self + end + + + + + --- Finds a Group based on the Group Name. + -- @param #SET_GROUP self + -- @param #string GroupName + -- @return Wrapper.Group#GROUP The found Group. + function SET_GROUP:FindGroup( GroupName ) + + local GroupFound = self.Set[GroupName] + return GroupFound + end + + --- Iterate the SET_GROUP while identifying the nearest object from a @{Core.Point#POINT_VEC2}. + -- @param #SET_GROUP self + -- @param Core.Point#POINT_VEC2 PointVec2 A @{Core.Point#POINT_VEC2} object from where to evaluate the closest object in the set. + -- @return Wrapper.Group#GROUP The closest group. + function SET_GROUP:FindNearestGroupFromPointVec2( PointVec2 ) + self:F2( PointVec2 ) + + local NearestGroup = nil --Wrapper.Group#GROUP + local ClosestDistance = nil + + for ObjectID, ObjectData in pairs( self.Set ) do + if NearestGroup == nil then + NearestGroup = ObjectData + ClosestDistance = PointVec2:DistanceFromPointVec2( ObjectData:GetCoordinate() ) + else + local Distance = PointVec2:DistanceFromPointVec2( ObjectData:GetCoordinate() ) + if Distance < ClosestDistance then + NearestGroup = ObjectData + ClosestDistance = Distance + end + end + end + + return NearestGroup + end + + + --- Builds a set of groups of coalitions. + -- Possible current coalitions are red, blue and neutral. + -- @param #SET_GROUP self + -- @param #string Coalitions Can take the following values: "red", "blue", "neutral". + -- @return #SET_GROUP self + function SET_GROUP:FilterCoalitions( Coalitions ) + if not self.Filter.Coalitions then + self.Filter.Coalitions = {} + end + if type( Coalitions ) ~= "table" then + Coalitions = { Coalitions } + end + for CoalitionID, Coalition in pairs( Coalitions ) do + self.Filter.Coalitions[Coalition] = Coalition + end + return self + end + + + --- Builds a set of groups out of categories. + -- Possible current categories are plane, helicopter, ground, ship. + -- @param #SET_GROUP self + -- @param #string Categories Can take the following values: "plane", "helicopter", "ground", "ship". + -- @return #SET_GROUP self + function SET_GROUP:FilterCategories( Categories ) + if not self.Filter.Categories then + self.Filter.Categories = {} + end + if type( Categories ) ~= "table" then + Categories = { Categories } + end + for CategoryID, Category in pairs( Categories ) do + self.Filter.Categories[Category] = Category + end + return self + end + + --- Builds a set of groups out of ground category. + -- @param #SET_GROUP self + -- @return #SET_GROUP self + function SET_GROUP:FilterCategoryGround() + self:FilterCategories( "ground" ) + return self + end + + --- Builds a set of groups out of airplane category. + -- @param #SET_GROUP self + -- @return #SET_GROUP self + function SET_GROUP:FilterCategoryAirplane() + self:FilterCategories( "plane" ) + return self + end + + --- Builds a set of groups out of helicopter category. + -- @param #SET_GROUP self + -- @return #SET_GROUP self + function SET_GROUP:FilterCategoryHelicopter() + self:FilterCategories( "helicopter" ) + return self + end + + --- Builds a set of groups out of ship category. + -- @param #SET_GROUP self + -- @return #SET_GROUP self + function SET_GROUP:FilterCategoryShip() + self:FilterCategories( "ship" ) + return self + end + + --- Builds a set of groups out of structure category. + -- @param #SET_GROUP self + -- @return #SET_GROUP self + function SET_GROUP:FilterCategoryStructure() + self:FilterCategories( "structure" ) + return self + end + + + + --- Builds a set of groups of defined countries. + -- Possible current countries are those known within DCS world. + -- @param #SET_GROUP self + -- @param #string Countries Can take those country strings known within DCS world. + -- @return #SET_GROUP self + function SET_GROUP:FilterCountries( Countries ) + if not self.Filter.Countries then + self.Filter.Countries = {} + end + if type( Countries ) ~= "table" then + Countries = { Countries } + end + for CountryID, Country in pairs( Countries ) do + self.Filter.Countries[Country] = Country + end + return self + end + + + --- Builds a set of groups that contain the given string in their group name. + -- **Attention!** Bad naming convention as this **does not** filter only **prefixes** but all groups that **contain** the string. + -- @param #SET_GROUP self + -- @param #string Prefixes The string pattern(s) that needs to be contained in the group name. Can also be passed as a `#table` of strings. + -- @return #SET_GROUP self + function SET_GROUP:FilterPrefixes( Prefixes ) + if not self.Filter.GroupPrefixes then + self.Filter.GroupPrefixes = {} + end + if type( Prefixes ) ~= "table" then + Prefixes = { Prefixes } + end + for PrefixID, Prefix in pairs( Prefixes ) do + self.Filter.GroupPrefixes[Prefix] = Prefix + end + return self + end + + --- Builds a set of groups that are only active. + -- Only the groups that are active will be included within the set. + -- @param #SET_GROUP self + -- @param #boolean Active (optional) Include only active groups to the set. + -- Include inactive groups if you provide false. + -- @return #SET_GROUP self + -- @usage + -- + -- -- Include only active groups to the set. + -- GroupSet = SET_GROUP:New():FilterActive():FilterStart() + -- + -- -- Include only active groups to the set of the blue coalition, and filter one time. + -- GroupSet = SET_GROUP:New():FilterActive():FilterCoalition( "blue" ):FilterOnce() + -- + -- -- Include only active groups to the set of the blue coalition, and filter one time. + -- -- Later, reset to include back inactive groups to the set. + -- GroupSet = SET_GROUP:New():FilterActive():FilterCoalition( "blue" ):FilterOnce() + -- ... logic ... + -- GroupSet = SET_GROUP:New():FilterActive( false ):FilterCoalition( "blue" ):FilterOnce() + -- + function SET_GROUP:FilterActive( Active ) + Active = Active or not ( Active == false ) + self.Filter.Active = Active + return self + end + + + --- Starts the filtering. + -- @param #SET_GROUP self + -- @return #SET_GROUP self + function SET_GROUP:FilterStart() + + if _DATABASE then + self:_FilterStart() + self:HandleEvent( EVENTS.Birth, self._EventOnBirth ) + self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) + self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) + self:HandleEvent( EVENTS.RemoveUnit, self._EventOnDeadOrCrash ) + end + + + + return self + end + + --- Handles the OnDead or OnCrash event for alive groups set. + -- Note: The GROUP object in the SET_GROUP collection will only be removed if the last unit is destroyed of the GROUP. + -- @param #SET_GROUP self + -- @param Core.Event#EVENTDATA Event + function SET_GROUP:_EventOnDeadOrCrash( Event ) + self:F( { Event } ) + + if Event.IniDCSUnit then + local ObjectName, Object = self:FindInDatabase( Event ) + if ObjectName then + if Event.IniDCSGroup:getSize() == 1 then -- Only remove if the last unit of the group was destroyed. + self:Remove( ObjectName ) + end + end + end + end + + --- Handles the Database to check on an event (birth) that the Object was added in the Database. + -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! + -- @param #SET_GROUP self + -- @param Core.Event#EVENTDATA Event + -- @return #string The name of the GROUP + -- @return #table The GROUP + function SET_GROUP:AddInDatabase( Event ) + self:F3( { Event } ) + + if Event.IniObjectCategory == 1 then + if not self.Database[Event.IniDCSGroupName] then + self.Database[Event.IniDCSGroupName] = GROUP:Register( Event.IniDCSGroupName ) + self:T3( self.Database[Event.IniDCSGroupName] ) + end + end + + return Event.IniDCSGroupName, self.Database[Event.IniDCSGroupName] + end + + --- Handles the Database to check on any event that Object exists in the Database. + -- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! + -- @param #SET_GROUP self + -- @param Core.Event#EVENTDATA Event + -- @return #string The name of the GROUP + -- @return #table The GROUP + function SET_GROUP:FindInDatabase( Event ) + self:F3( { Event } ) + + return Event.IniDCSGroupName, self.Database[Event.IniDCSGroupName] + end + + --- Iterate the SET_GROUP and call an iterator function for each GROUP object, providing the GROUP and optional parameters. + -- @param #SET_GROUP self + -- @param #function IteratorFunction The function that will be called for all GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. + -- @return #SET_GROUP self + function SET_GROUP:ForEachGroup( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self:GetSet() ) + + return self + end + + --- Iterate the SET_GROUP and call an iterator function for some GROUP objects, providing the GROUP and optional parameters. + -- @param #SET_GROUP self + -- @param #function IteratorFunction The function that will be called for some GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. + -- @return #SET_GROUP self + function SET_GROUP:ForSomeGroup( IteratorFunction, ... ) + self:F2( arg ) + + self:ForSome( IteratorFunction, arg, self:GetSet() ) + + return self + end + + --- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP object, providing the GROUP and optional parameters. + -- @param #SET_GROUP self + -- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. + -- @return #SET_GROUP self + function SET_GROUP:ForEachGroupAlive( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self:GetAliveSet() ) + + return self + end + + --- Iterate the SET_GROUP and call an iterator function for some **alive** GROUP objects, providing the GROUP and optional parameters. + -- @param #SET_GROUP self + -- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. + -- @return #SET_GROUP self + function SET_GROUP:ForSomeGroupAlive( IteratorFunction, ... ) + self:F2( arg ) + + self:ForSome( IteratorFunction, arg, self:GetAliveSet() ) + + return self + end + + --- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence completely in a @{Zone}, providing the GROUP and optional parameters to the called function. + -- @param #SET_GROUP self + -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. + -- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. + -- @return #SET_GROUP self + function SET_GROUP:ForEachGroupCompletelyInZone( ZoneObject, IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self:GetSet(), + --- @param Core.Zone#ZONE_BASE ZoneObject + -- @param Wrapper.Group#GROUP GroupObject + function( ZoneObject, GroupObject ) + if GroupObject:IsCompletelyInZone( ZoneObject ) then + return true + else + return false + end + end, { ZoneObject } ) + + return self + end + + --- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence partly in a @{Zone}, providing the GROUP and optional parameters to the called function. + -- @param #SET_GROUP self + -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. + -- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. + -- @return #SET_GROUP self + function SET_GROUP:ForEachGroupPartlyInZone( ZoneObject, IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self:GetSet(), + --- @param Core.Zone#ZONE_BASE ZoneObject + -- @param Wrapper.Group#GROUP GroupObject + function( ZoneObject, GroupObject ) + if GroupObject:IsPartlyInZone( ZoneObject ) then + return true + else + return false + end + end, { ZoneObject } ) + + return self + end + + --- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence not in a @{Zone}, providing the GROUP and optional parameters to the called function. + -- @param #SET_GROUP self + -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. + -- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. + -- @return #SET_GROUP self + function SET_GROUP:ForEachGroupNotInZone( ZoneObject, IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self:GetSet(), + --- @param Core.Zone#ZONE_BASE ZoneObject + -- @param Wrapper.Group#GROUP GroupObject + function( ZoneObject, GroupObject ) + if GroupObject:IsNotInZone( ZoneObject ) then + return true + else + return false + end + end, { ZoneObject } ) + + return self + end + + --- Iterate the SET_GROUP and return true if all the @{Wrapper.Group#GROUP} are completely in the @{Core.Zone#ZONE} + -- @param #SET_GROUP self + -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. + -- @return #boolean true if all the @{Wrapper.Group#GROUP} are completly in the @{Core.Zone#ZONE}, false otherwise + -- @usage + -- local MyZone = ZONE:New("Zone1") + -- local MySetGroup = SET_GROUP:New() + -- MySetGroup:AddGroupsByName({"Group1", "Group2"}) + -- + -- if MySetGroup:AllCompletelyInZone(MyZone) then + -- MESSAGE:New("All the SET's GROUP are in zone !", 10):ToAll() + -- else + -- MESSAGE:New("Some or all SET's GROUP are outside zone !", 10):ToAll() + -- end + function SET_GROUP:AllCompletelyInZone(Zone) + self:F2(Zone) + local Set = self:GetSet() + for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP + if not GroupData:IsCompletelyInZone(Zone) then + return false + end + end + return true + end + + --- Iterate the SET_GROUP and call an iterator function for each alive GROUP that has any unit in the @{Core.Zone}, providing the GROUP and optional parameters to the called function. + -- @param #SET_GROUP self + -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. + -- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. + -- @return #SET_GROUP self + function SET_GROUP:ForEachGroupAnyInZone( ZoneObject, IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self:GetSet(), + --- @param Core.Zone#ZONE_BASE ZoneObject + -- @param Wrapper.Group#GROUP GroupObject + function( ZoneObject, GroupObject ) + if GroupObject:IsAnyInZone( ZoneObject ) then + return true + else + return false + end + end, { ZoneObject } ) + + return self + end + + + --- Iterate the SET_GROUP and return true if at least one of the @{Wrapper.Group#GROUP} is completely inside the @{Core.Zone#ZONE} + -- @param #SET_GROUP self + -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. + -- @return #boolean true if at least one of the @{Wrapper.Group#GROUP} is completly inside the @{Core.Zone#ZONE}, false otherwise. + -- @usage + -- local MyZone = ZONE:New("Zone1") + -- local MySetGroup = SET_GROUP:New() + -- MySetGroup:AddGroupsByName({"Group1", "Group2"}) + -- + -- if MySetGroup:AnyCompletelyInZone(MyZone) then + -- MESSAGE:New("At least one GROUP is completely in zone !", 10):ToAll() + -- else + -- MESSAGE:New("No GROUP is completely in zone !", 10):ToAll() + -- end + function SET_GROUP:AnyCompletelyInZone(Zone) + self:F2(Zone) + local Set = self:GetSet() + for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP + if GroupData:IsCompletelyInZone(Zone) then + return true + end + end + return false + end + + --- Iterate the SET_GROUP and return true if at least one @{#UNIT} of one @{GROUP} of the @{SET_GROUP} is in @{ZONE} + -- @param #SET_GROUP self + -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. + -- @return #boolean true if at least one of the @{Wrapper.Group#GROUP} is partly or completly inside the @{Core.Zone#ZONE}, false otherwise. + -- @usage + -- local MyZone = ZONE:New("Zone1") + -- local MySetGroup = SET_GROUP:New() + -- MySetGroup:AddGroupsByName({"Group1", "Group2"}) + -- + -- if MySetGroup:AnyPartlyInZone(MyZone) then + -- MESSAGE:New("At least one GROUP has at least one UNIT in zone !", 10):ToAll() + -- else + -- MESSAGE:New("No UNIT of any GROUP is in zone !", 10):ToAll() + -- end + function SET_GROUP:AnyInZone(Zone) + self:F2(Zone) + local Set = self:GetSet() + for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP + if GroupData:IsPartlyInZone(Zone) or GroupData:IsCompletelyInZone(Zone) then + return true + end + end + return false + end + + --- Iterate the SET_GROUP and return true if at least one @{GROUP} of the @{SET_GROUP} is partly in @{ZONE}. + -- Will return false if a @{GROUP} is fully in the @{ZONE} + -- @param #SET_GROUP self + -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. + -- @return #boolean true if at least one of the @{Wrapper.Group#GROUP} is partly or completly inside the @{Core.Zone#ZONE}, false otherwise. + -- @usage + -- local MyZone = ZONE:New("Zone1") + -- local MySetGroup = SET_GROUP:New() + -- MySetGroup:AddGroupsByName({"Group1", "Group2"}) + -- + -- if MySetGroup:AnyPartlyInZone(MyZone) then + -- MESSAGE:New("At least one GROUP is partially in the zone, but none are fully in it !", 10):ToAll() + -- else + -- MESSAGE:New("No GROUP are in zone, or one (or more) GROUP is completely in it !", 10):ToAll() + -- end + function SET_GROUP:AnyPartlyInZone(Zone) + self:F2(Zone) + local IsPartlyInZone = false + local Set = self:GetSet() + for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP + if GroupData:IsCompletelyInZone(Zone) then + return false + elseif GroupData:IsPartlyInZone(Zone) then + IsPartlyInZone = true -- at least one GROUP is partly in zone + end + end + + if IsPartlyInZone then + return true + else + return false + end + end + + --- Iterate the SET_GROUP and return true if no @{GROUP} of the @{SET_GROUP} is in @{ZONE} + -- This could also be achieved with `not SET_GROUP:AnyPartlyInZone(Zone)`, but it's easier for the + -- mission designer to add a dedicated method + -- @param #SET_GROUP self + -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. + -- @return #boolean true if no @{Wrapper.Group#GROUP} is inside the @{Core.Zone#ZONE} in any way, false otherwise. + -- @usage + -- local MyZone = ZONE:New("Zone1") + -- local MySetGroup = SET_GROUP:New() + -- MySetGroup:AddGroupsByName({"Group1", "Group2"}) + -- + -- if MySetGroup:NoneInZone(MyZone) then + -- MESSAGE:New("No GROUP is completely in zone !", 10):ToAll() + -- else + -- MESSAGE:New("No UNIT of any GROUP is in zone !", 10):ToAll() + -- end + function SET_GROUP:NoneInZone(Zone) + self:F2(Zone) + local Set = self:GetSet() + for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP + if not GroupData:IsNotInZone(Zone) then -- If the GROUP is in Zone in any way + return false + end + end + return true + end + + --- Iterate the SET_GROUP and count how many GROUPs are completely in the Zone + -- That could easily be done with SET_GROUP:ForEachGroupCompletelyInZone(), but this function + -- provides an easy to use shortcut... + -- @param #SET_GROUP self + -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. + -- @return #number the number of GROUPs completely in the Zone + -- @usage + -- local MyZone = ZONE:New("Zone1") + -- local MySetGroup = SET_GROUP:New() + -- MySetGroup:AddGroupsByName({"Group1", "Group2"}) + -- + -- MESSAGE:New("There are " .. MySetGroup:CountInZone(MyZone) .. " GROUPs in the Zone !", 10):ToAll() + function SET_GROUP:CountInZone(Zone) + self:F2(Zone) + local Count = 0 + local Set = self:GetSet() + for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP + if GroupData:IsCompletelyInZone(Zone) then + Count = Count + 1 + end + end + return Count + end + + --- Iterate the SET_GROUP and count how many UNITs are completely in the Zone + -- @param #SET_GROUP self + -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. + -- @return #number the number of GROUPs completely in the Zone + -- @usage + -- local MyZone = ZONE:New("Zone1") + -- local MySetGroup = SET_GROUP:New() + -- MySetGroup:AddGroupsByName({"Group1", "Group2"}) + -- + -- MESSAGE:New("There are " .. MySetGroup:CountUnitInZone(MyZone) .. " UNITs in the Zone !", 10):ToAll() + function SET_GROUP:CountUnitInZone(Zone) + self:F2(Zone) + local Count = 0 + local Set = self:GetSet() + for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP + Count = Count + GroupData:CountInZone(Zone) + end + return Count + end + + --- Iterate the SET_GROUP and count how many GROUPs and UNITs are alive. + -- @param #SET_GROUP self + -- @return #number The number of GROUPs alive. + -- @return #number The number of UNITs alive. + function SET_GROUP:CountAlive() + local CountG = 0 + local CountU = 0 + + local Set = self:GetSet() + + for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP + if GroupData and GroupData:IsAlive() then + + CountG = CountG + 1 + + --Count Units. + for _,_unit in pairs(GroupData:GetUnits()) do + local unit=_unit --Wrapper.Unit#UNIT + if unit and unit:IsAlive() then + CountU=CountU+1 + end + end + end + + end + + return CountG,CountU + end + + ----- Iterate the SET_GROUP and call an interator function for each **alive** player, providing the Group of the player and optional parameters. + ---- @param #SET_GROUP self + ---- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_GROUP. The function needs to accept a GROUP parameter. + ---- @return #SET_GROUP self + --function SET_GROUP:ForEachPlayer( IteratorFunction, ... ) + -- self:F2( arg ) + -- + -- self:ForEach( IteratorFunction, arg, self.PlayersAlive ) + -- + -- return self + --end + -- + -- + ----- Iterate the SET_GROUP and call an interator function for each client, providing the Client to the function and optional parameters. + ---- @param #SET_GROUP self + ---- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_GROUP. The function needs to accept a CLIENT parameter. + ---- @return #SET_GROUP self + --function SET_GROUP:ForEachClient( IteratorFunction, ... ) + -- self:F2( arg ) + -- + -- self:ForEach( IteratorFunction, arg, self.Clients ) + -- + -- return self + --end + + + --- + -- @param #SET_GROUP self + -- @param Wrapper.Group#GROUP MGroup The group that is checked for inclusion. + -- @return #SET_GROUP self + function SET_GROUP:IsIncludeObject( MGroup ) + self:F2( MGroup ) + local MGroupInclude = true + + if self.Filter.Active ~= nil then + local MGroupActive = false + self:F( { Active = self.Filter.Active } ) + if self.Filter.Active == false or ( self.Filter.Active == true and MGroup:IsActive() == true ) then + MGroupActive = true + end + MGroupInclude = MGroupInclude and MGroupActive + end + + if self.Filter.Coalitions then + local MGroupCoalition = false + for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do + self:T3( { "Coalition:", MGroup:GetCoalition(), self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) + if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == MGroup:GetCoalition() then + MGroupCoalition = true + end + end + MGroupInclude = MGroupInclude and MGroupCoalition + end + + if self.Filter.Categories then + local MGroupCategory = false + for CategoryID, CategoryName in pairs( self.Filter.Categories ) do + self:T3( { "Category:", MGroup:GetCategory(), self.FilterMeta.Categories[CategoryName], CategoryName } ) + if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == MGroup:GetCategory() then + MGroupCategory = true + end + end + MGroupInclude = MGroupInclude and MGroupCategory + end + + if self.Filter.Countries then + local MGroupCountry = false + for CountryID, CountryName in pairs( self.Filter.Countries ) do + self:T3( { "Country:", MGroup:GetCountry(), CountryName } ) + if country.id[CountryName] == MGroup:GetCountry() then + MGroupCountry = true + end + end + MGroupInclude = MGroupInclude and MGroupCountry + end + + if self.Filter.GroupPrefixes then + local MGroupPrefix = false + for GroupPrefixId, GroupPrefix in pairs( self.Filter.GroupPrefixes ) do + self:T3( { "Prefix:", string.find( MGroup:GetName(), GroupPrefix, 1 ), GroupPrefix } ) + if string.find( MGroup:GetName(), GroupPrefix:gsub ("-", "%%-"), 1 ) then + MGroupPrefix = true + end + end + MGroupInclude = MGroupInclude and MGroupPrefix + end + + self:T2( MGroupInclude ) + return MGroupInclude + end + + + --- Iterate the SET_GROUP and set for each unit the default cargo bay weight limit. + -- Because within a group, the type of carriers can differ, each cargo bay weight limit is set on @{Wrapper.Unit} level. + -- @param #SET_GROUP self + -- @usage + -- -- Set the default cargo bay weight limits of the carrier units. + -- local MySetGroup = SET_GROUP:New() + -- MySetGroup:SetCargoBayWeightLimit() + function SET_GROUP:SetCargoBayWeightLimit() + local Set = self:GetSet() + for GroupID, GroupData in pairs( Set ) do -- For each GROUP in SET_GROUP + for UnitName, UnitData in pairs( GroupData:GetUnits() ) do + --local UnitData = UnitData -- Wrapper.Unit#UNIT + UnitData:SetCargoBayWeightLimit() + end + end + end + +end + + +do -- SET_UNIT + + --- @type SET_UNIT + -- @extends Core.Set#SET_BASE + + --- Mission designers can use the SET_UNIT class to build sets of units belonging to certain: + -- + -- * Coalitions + -- * Categories + -- * Countries + -- * Unit types + -- * Starting with certain prefix strings. + -- + -- ## 1) SET_UNIT constructor + -- + -- Create a new SET_UNIT object with the @{#SET_UNIT.New} method: + -- + -- * @{#SET_UNIT.New}: Creates a new SET_UNIT object. + -- + -- ## 2) Add or Remove UNIT(s) from SET_UNIT + -- + -- UNITs can be added and removed using the @{Core.Set#SET_UNIT.AddUnitsByName} and @{Core.Set#SET_UNIT.RemoveUnitsByName} respectively. + -- These methods take a single UNIT name or an array of UNIT names to be added or removed from SET_UNIT. + -- + -- ## 3) SET_UNIT filter criteria + -- + -- You can set filter criteria to define the set of units within the SET_UNIT. + -- Filter criteria are defined by: + -- + -- * @{#SET_UNIT.FilterCoalitions}: Builds the SET_UNIT with the units belonging to the coalition(s). + -- * @{#SET_UNIT.FilterCategories}: Builds the SET_UNIT with the units belonging to the category(ies). + -- * @{#SET_UNIT.FilterTypes}: Builds the SET_UNIT with the units belonging to the unit type(s). + -- * @{#SET_UNIT.FilterCountries}: Builds the SET_UNIT with the units belonging to the country(ies). + -- * @{#SET_UNIT.FilterPrefixes}: Builds the SET_UNIT with the units sharing the same string(s) in their name. **ATTENTION!** Bad naming convention as this *does not* only filter *prefixes*. + -- * @{#SET_UNIT.FilterActive}: Builds the SET_UNIT with the units that are only active. Units that are inactive (late activation) won't be included in the set! + -- + -- Once the filter criteria have been set for the SET_UNIT, you can start filtering using: + -- + -- * @{#SET_UNIT.FilterStart}: Starts the filtering of the units **dynamically**. + -- * @{#SET_UNIT.FilterOnce}: Filters of the units **once**. + -- + -- Planned filter criteria within development are (so these are not yet available): + -- + -- * @{#SET_UNIT.FilterZones}: Builds the SET_UNIT with the units within a @{Core.Zone#ZONE}. + -- + -- ## 4) SET_UNIT iterators + -- + -- Once the filters have been defined and the SET_UNIT has been built, you can iterate the SET_UNIT with the available iterator methods. + -- The iterator methods will walk the SET_UNIT set, and call for each element within the set a function that you provide. + -- The following iterator methods are currently available within the SET_UNIT: + -- + -- * @{#SET_UNIT.ForEachUnit}: Calls a function for each alive unit it finds within the SET_UNIT. + -- * @{#SET_UNIT.ForEachUnitInZone}: Iterate the SET_UNIT and call an iterator function for each **alive** UNIT object presence completely in a @{Zone}, providing the UNIT object and optional parameters to the called function. + -- * @{#SET_UNIT.ForEachUnitNotInZone}: Iterate the SET_UNIT and call an iterator function for each **alive** UNIT object presence not in a @{Zone}, providing the UNIT object and optional parameters to the called function. + -- + -- Planned iterators methods in development are (so these are not yet available): + -- + -- * @{#SET_UNIT.ForEachUnitInUnit}: Calls a function for each unit contained within the SET_UNIT. + -- * @{#SET_UNIT.ForEachUnitCompletelyInZone}: Iterate and call an iterator function for each **alive** UNIT presence completely in a @{Zone}, providing the UNIT and optional parameters to the called function. + -- * @{#SET_UNIT.ForEachUnitNotInZone}: Iterate and call an iterator function for each **alive** UNIT presence not in a @{Zone}, providing the UNIT and optional parameters to the called function. + -- + -- ## 5) SET_UNIT atomic methods + -- + -- Various methods exist for a SET_UNIT to perform actions or calculations and retrieve results from the SET_UNIT: + -- + -- * @{#SET_UNIT.GetTypeNames}(): Retrieve the type names of the @{Wrapper.Unit}s in the SET, delimited by a comma. + -- + -- ## 6) SET_UNIT trigger events on the UNIT objects. + -- + -- The SET is derived from the FSM class, which provides extra capabilities to track the contents of the UNIT objects in the SET_UNIT. + -- + -- ### 6.1) When a UNIT object crashes or is dead, the SET_UNIT will trigger a **Dead** event. + -- + -- You can handle the event using the OnBefore and OnAfter event handlers. + -- The event handlers need to have the paramters From, Event, To, GroupObject. + -- The GroupObject is the UNIT object that is dead and within the SET_UNIT, and is passed as a parameter to the event handler. + -- See the following example: + -- + -- -- Create the SetCarrier SET_UNIT collection. + -- + -- local SetHelicopter = SET_UNIT:New():FilterPrefixes( "Helicopter" ):FilterStart() + -- + -- -- Put a Dead event handler on SetCarrier, to ensure that when a carrier unit is destroyed, that all internal parameters are reset. + -- + -- function SetHelicopter:OnAfterDead( From, Event, To, UnitObject ) + -- self:F( { UnitObject = UnitObject:GetName() } ) + -- end + -- + -- While this is a good example, there is a catch. + -- Imageine you want to execute the code above, the the self would need to be from the object declared outside (above) the OnAfterDead method. + -- So, the self would need to contain another object. Fortunately, this can be done, but you must use then the **`.`** notation for the method. + -- See the modified example: + -- + -- -- Now we have a constructor of the class AI_CARGO_DISPATCHER, that receives the SetHelicopter as a parameter. + -- -- Within that constructor, we want to set an enclosed event handler OnAfterDead for SetHelicopter. + -- -- But within the OnAfterDead method, we want to refer to the self variable of the AI_CARGO_DISPATCHER. + -- + -- function ACLASS:New( SetCarrier, SetCargo, SetDeployZones ) + -- + -- local self = BASE:Inherit( self, FSM:New() ) -- #AI_CARGO_DISPATCHER + -- + -- -- Put a Dead event handler on SetCarrier, to ensure that when a carrier is destroyed, that all internal parameters are reset. + -- -- Note the "." notation, and the explicit declaration of SetHelicopter, which would be using the ":" notation the implicit self variable declaration. + -- + -- function SetHelicopter.OnAfterDead( SetHelicopter, From, Event, To, UnitObject ) + -- SetHelicopter:F( { UnitObject = UnitObject:GetName() } ) + -- self.array[UnitObject] = nil -- So here I clear the array table entry of the self object ACLASS. + -- end + -- + -- end + -- === + -- @field #SET_UNIT SET_UNIT + SET_UNIT = { + ClassName = "SET_UNIT", + Units = {}, + Filter = { + Coalitions = nil, + Categories = nil, + Types = nil, + Countries = nil, + UnitPrefixes = nil, + }, + FilterMeta = { + Coalitions = { + red = coalition.side.RED, + blue = coalition.side.BLUE, + neutral = coalition.side.NEUTRAL, + }, + Categories = { + plane = Unit.Category.AIRPLANE, + helicopter = Unit.Category.HELICOPTER, + ground = Unit.Category.GROUND_UNIT, + ship = Unit.Category.SHIP, + structure = Unit.Category.STRUCTURE, + }, + }, + } + + + --- Get the first unit from the set. + -- @function [parent=#SET_UNIT] GetFirst + -- @param #SET_UNIT self + -- @return Wrapper.Unit#UNIT The UNIT object. + + --- Creates a new SET_UNIT object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. + -- @param #SET_UNIT self + -- @return #SET_UNIT + -- @usage + -- -- Define a new SET_UNIT Object. This DBObject will contain a reference to all alive Units. + -- DBObject = SET_UNIT:New() + function SET_UNIT:New() + + -- Inherits from BASE + local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.UNITS ) ) -- #SET_UNIT + + self:FilterActive( false ) + + return self + end + + --- Add UNIT(s) to SET_UNIT. + -- @param #SET_UNIT self + -- @param Wrapper.Unit#UNIT Unit A single UNIT. + -- @return #SET_UNIT self + function SET_UNIT:AddUnit( Unit ) + self:F2( Unit:GetName() ) + + self:Add( Unit:GetName(), Unit ) + + -- Set the default cargo bay limit each time a new unit is added to the set. + Unit:SetCargoBayWeightLimit() + + return self + end + + + --- Add UNIT(s) to SET_UNIT. + -- @param #SET_UNIT self + -- @param #string AddUnitNames A single name or an array of UNIT names. + -- @return #SET_UNIT self + function SET_UNIT:AddUnitsByName( AddUnitNames ) + + local AddUnitNamesArray = ( type( AddUnitNames ) == "table" ) and AddUnitNames or { AddUnitNames } + + self:T( AddUnitNamesArray ) + for AddUnitID, AddUnitName in pairs( AddUnitNamesArray ) do + self:Add( AddUnitName, UNIT:FindByName( AddUnitName ) ) + end + + return self + end + + --- Remove UNIT(s) from SET_UNIT. + -- @param Core.Set#SET_UNIT self + -- @param #table RemoveUnitNames A single name or an array of UNIT names. + -- @return Core.Set#SET_UNIT self + function SET_UNIT:RemoveUnitsByName( RemoveUnitNames ) + + local RemoveUnitNamesArray = ( type( RemoveUnitNames ) == "table" ) and RemoveUnitNames or { RemoveUnitNames } + + for RemoveUnitID, RemoveUnitName in pairs( RemoveUnitNamesArray ) do + self:Remove( RemoveUnitName ) + end + + return self + end + + + --- Finds a Unit based on the Unit Name. + -- @param #SET_UNIT self + -- @param #string UnitName + -- @return Wrapper.Unit#UNIT The found Unit. + function SET_UNIT:FindUnit( UnitName ) + + local UnitFound = self.Set[UnitName] + return UnitFound + end + + + + --- Builds a set of units of coalitions. + -- Possible current coalitions are red, blue and neutral. + -- @param #SET_UNIT self + -- @param #string Coalitions Can take the following values: "red", "blue", "neutral". + -- @return #SET_UNIT self + function SET_UNIT:FilterCoalitions( Coalitions ) + + self.Filter.Coalitions = {} + if type( Coalitions ) ~= "table" then + Coalitions = { Coalitions } + end + for CoalitionID, Coalition in pairs( Coalitions ) do + self.Filter.Coalitions[Coalition] = Coalition + end + return self + end + + + --- Builds a set of units out of categories. + -- Possible current categories are plane, helicopter, ground, ship. + -- @param #SET_UNIT self + -- @param #string Categories Can take the following values: "plane", "helicopter", "ground", "ship". + -- @return #SET_UNIT self + function SET_UNIT:FilterCategories( Categories ) + if not self.Filter.Categories then + self.Filter.Categories = {} + end + if type( Categories ) ~= "table" then + Categories = { Categories } + end + for CategoryID, Category in pairs( Categories ) do + self.Filter.Categories[Category] = Category + end + return self + end + + + --- Builds a set of units of defined unit types. + -- Possible current types are those types known within DCS world. + -- @param #SET_UNIT self + -- @param #string Types Can take those type strings known within DCS world. + -- @return #SET_UNIT self + function SET_UNIT:FilterTypes( Types ) + if not self.Filter.Types then + self.Filter.Types = {} + end + if type( Types ) ~= "table" then + Types = { Types } + end + for TypeID, Type in pairs( Types ) do + self.Filter.Types[Type] = Type + end + return self + end + + + --- Builds a set of units of defined countries. + -- Possible current countries are those known within DCS world. + -- @param #SET_UNIT self + -- @param #string Countries Can take those country strings known within DCS world. + -- @return #SET_UNIT self + function SET_UNIT:FilterCountries( Countries ) + if not self.Filter.Countries then + self.Filter.Countries = {} + end + if type( Countries ) ~= "table" then + Countries = { Countries } + end + for CountryID, Country in pairs( Countries ) do + self.Filter.Countries[Country] = Country + end + return self + end + + + --- Builds a set of UNITs that contain a given string in their unit name. + -- **Attention!** Bad naming convention as this **does not** filter only **prefixes** but all units that **contain** the string. + -- @param #SET_UNIT self + -- @param #string Prefixes The string pattern(s) that needs to be contained in the unit name. Can also be passed as a `#table` of strings. + -- @return #SET_UNIT self + function SET_UNIT:FilterPrefixes( Prefixes ) + if not self.Filter.UnitPrefixes then + self.Filter.UnitPrefixes = {} + end + if type( Prefixes ) ~= "table" then + Prefixes = { Prefixes } + end + for PrefixID, Prefix in pairs( Prefixes ) do + self.Filter.UnitPrefixes[Prefix] = Prefix + end + return self + end + + --- Builds a set of units that are only active. + -- Only the units that are active will be included within the set. + -- @param #SET_UNIT self + -- @param #boolean Active (optional) Include only active units to the set. + -- Include inactive units if you provide false. + -- @return #SET_UNIT self + -- @usage + -- + -- -- Include only active units to the set. + -- UnitSet = SET_UNIT:New():FilterActive():FilterStart() + -- + -- -- Include only active units to the set of the blue coalition, and filter one time. + -- UnitSet = SET_UNIT:New():FilterActive():FilterCoalition( "blue" ):FilterOnce() + -- + -- -- Include only active units to the set of the blue coalition, and filter one time. + -- -- Later, reset to include back inactive units to the set. + -- UnitSet = SET_UNIT:New():FilterActive():FilterCoalition( "blue" ):FilterOnce() + -- ... logic ... + -- UnitSet = SET_UNIT:New():FilterActive( false ):FilterCoalition( "blue" ):FilterOnce() + -- + function SET_UNIT:FilterActive( Active ) + Active = Active or not ( Active == false ) + self.Filter.Active = Active + return self + end + + --- Builds a set of units having a radar of give types. + -- All the units having a radar of a given type will be included within the set. + -- @param #SET_UNIT self + -- @param #table RadarTypes The radar types. + -- @return #SET_UNIT self + function SET_UNIT:FilterHasRadar( RadarTypes ) + + self.Filter.RadarTypes = self.Filter.RadarTypes or {} + if type( RadarTypes ) ~= "table" then + RadarTypes = { RadarTypes } + end + for RadarTypeID, RadarType in pairs( RadarTypes ) do + self.Filter.RadarTypes[RadarType] = RadarType + end + return self + end + + --- Builds a set of SEADable units. + -- @param #SET_UNIT self + -- @return #SET_UNIT self + function SET_UNIT:FilterHasSEAD() + + self.Filter.SEAD = true + return self + end + + --- Iterate the SET_UNIT and count how many UNITs are alive. + -- @param #SET_UNIT self + -- @return #number The number of UNITs alive. + function SET_UNIT:CountAlive() + + local Set = self:GetSet() + + local CountU = 0 + for UnitID, UnitData in pairs(Set) do -- For each GROUP in SET_GROUP + if UnitData and UnitData:IsAlive() then + CountU = CountU + 1 + end + + end + + return CountU + end + + --- Starts the filtering. + -- @param #SET_UNIT self + -- @return #SET_UNIT self + function SET_UNIT:FilterStart() + + if _DATABASE then + self:_FilterStart() + self:HandleEvent( EVENTS.Birth, self._EventOnBirth ) + self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) + self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) + self:HandleEvent( EVENTS.RemoveUnit, self._EventOnDeadOrCrash ) + end + + return self + end + + + + --- Handles the Database to check on an event (birth) that the Object was added in the Database. + -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! + -- @param #SET_UNIT self + -- @param Core.Event#EVENTDATA Event + -- @return #string The name of the UNIT + -- @return #table The UNIT + function SET_UNIT:AddInDatabase( Event ) + self:F3( { Event } ) + + if Event.IniObjectCategory == 1 then + if not self.Database[Event.IniDCSUnitName] then + self.Database[Event.IniDCSUnitName] = UNIT:Register( Event.IniDCSUnitName ) + self:T3( self.Database[Event.IniDCSUnitName] ) + end + end + + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] + end + + --- Handles the Database to check on any event that Object exists in the Database. + -- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! + -- @param #SET_UNIT self + -- @param Core.Event#EVENTDATA Event + -- @return #string The name of the UNIT + -- @return #table The UNIT + function SET_UNIT:FindInDatabase( Event ) + self:F2( { Event.IniDCSUnitName, self.Set[Event.IniDCSUnitName], Event } ) + + + return Event.IniDCSUnitName, self.Set[Event.IniDCSUnitName] + end + + + do -- Is Zone methods + + --- Check if minimal one element of the SET_UNIT is in the Zone. + -- @param #SET_UNIT self + -- @param Core.Zone#ZONE ZoneTest The Zone to be tested for. + -- @return #boolean + function SET_UNIT:IsPartiallyInZone( ZoneTest ) + + local IsPartiallyInZone = false + + local function EvaluateZone( ZoneUnit ) + + local ZoneUnitName = ZoneUnit:GetName() + self:F( { ZoneUnitName = ZoneUnitName } ) + if self:FindUnit( ZoneUnitName ) then + IsPartiallyInZone = true + self:F( { Found = true } ) + return false + end + + return true + end + + ZoneTest:SearchZone( EvaluateZone ) + + return IsPartiallyInZone + end + + + --- Check if no element of the SET_UNIT is in the Zone. + -- @param #SET_UNIT self + -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. + -- @return #boolean + function SET_UNIT:IsNotInZone( Zone ) + + local IsNotInZone = true + + local function EvaluateZone( ZoneUnit ) + + local ZoneUnitName = ZoneUnit:GetName() + if self:FindUnit( ZoneUnitName ) then + IsNotInZone = false + return false + end + + return true + end + + Zone:SearchZone( EvaluateZone ) + + return IsNotInZone + end + + end + + + --- Iterate the SET_UNIT and call an interator function for each **alive** UNIT, providing the UNIT and optional parameters. + -- @param #SET_UNIT self + -- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the SET_UNIT. The function needs to accept a UNIT parameter. + -- @return #SET_UNIT self + function SET_UNIT:ForEachUnit( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self:GetSet() ) + + return self + end + + + --- Get the SET of the SET_UNIT **sorted per Threat Level**. + -- + -- @param #SET_UNIT self + -- @param #number FromThreatLevel The TreatLevel to start the evaluation **From** (this must be a value between 0 and 10). + -- @param #number ToThreatLevel The TreatLevel to stop the evaluation **To** (this must be a value between 0 and 10). + -- @return #SET_UNIT self + -- @usage + -- + -- + function SET_UNIT:GetSetPerThreatLevel( FromThreatLevel, ToThreatLevel ) + self:F2( arg ) + + local ThreatLevelSet = {} + + if self:Count() ~= 0 then + for UnitName, UnitObject in pairs( self.Set ) do + local Unit = UnitObject -- Wrapper.Unit#UNIT + + local ThreatLevel = Unit:GetThreatLevel() + ThreatLevelSet[ThreatLevel] = ThreatLevelSet[ThreatLevel] or {} + ThreatLevelSet[ThreatLevel].Set = ThreatLevelSet[ThreatLevel].Set or {} + ThreatLevelSet[ThreatLevel].Set[UnitName] = UnitObject + self:F( { ThreatLevel = ThreatLevel, ThreatLevelSet = ThreatLevelSet[ThreatLevel].Set } ) + end + + + local OrderedPerThreatLevelSet = {} + + local ThreatLevelIncrement = FromThreatLevel <= ToThreatLevel and 1 or -1 + + + for ThreatLevel = FromThreatLevel, ToThreatLevel, ThreatLevelIncrement do + self:F( { ThreatLevel = ThreatLevel } ) + local ThreatLevelItem = ThreatLevelSet[ThreatLevel] + if ThreatLevelItem then + for UnitName, UnitObject in pairs( ThreatLevelItem.Set ) do + table.insert( OrderedPerThreatLevelSet, UnitObject ) + end + end + end + + return OrderedPerThreatLevelSet + end + + end + + + --- Iterate the SET_UNIT **sorted *per Threat Level** and call an interator function for each **alive** UNIT, providing the UNIT and optional parameters. + -- + -- @param #SET_UNIT self + -- @param #number FromThreatLevel The TreatLevel to start the evaluation **From** (this must be a value between 0 and 10). + -- @param #number ToThreatLevel The TreatLevel to stop the evaluation **To** (this must be a value between 0 and 10). + -- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the SET_UNIT. The function needs to accept a UNIT parameter. + -- @return #SET_UNIT self + -- @usage + -- + -- UnitSet:ForEachUnitPerThreatLevel( 10, 0, + -- -- @param Wrapper.Unit#UNIT UnitObject The UNIT object in the UnitSet, that will be passed to the local function for evaluation. + -- function( UnitObject ) + -- .. logic .. + -- end + -- ) + -- + function SET_UNIT:ForEachUnitPerThreatLevel( FromThreatLevel, ToThreatLevel, IteratorFunction, ... ) --R2.1 Threat Level implementation + self:F2( arg ) + + local ThreatLevelSet = {} + + if self:Count() ~= 0 then + for UnitName, UnitObject in pairs( self.Set ) do + local Unit = UnitObject -- Wrapper.Unit#UNIT + + local ThreatLevel = Unit:GetThreatLevel() + ThreatLevelSet[ThreatLevel] = ThreatLevelSet[ThreatLevel] or {} + ThreatLevelSet[ThreatLevel].Set = ThreatLevelSet[ThreatLevel].Set or {} + ThreatLevelSet[ThreatLevel].Set[UnitName] = UnitObject + self:F( { ThreatLevel = ThreatLevel, ThreatLevelSet = ThreatLevelSet[ThreatLevel].Set } ) + end + + local ThreatLevelIncrement = FromThreatLevel <= ToThreatLevel and 1 or -1 + + for ThreatLevel = FromThreatLevel, ToThreatLevel, ThreatLevelIncrement do + self:F( { ThreatLevel = ThreatLevel } ) + local ThreatLevelItem = ThreatLevelSet[ThreatLevel] + if ThreatLevelItem then + self:ForEach( IteratorFunction, arg, ThreatLevelItem.Set ) + end + end + end + + return self + end + + + + --- Iterate the SET_UNIT and call an iterator function for each **alive** UNIT presence completely in a @{Zone}, providing the UNIT and optional parameters to the called function. + -- @param #SET_UNIT self + -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. + -- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the SET_UNIT. The function needs to accept a UNIT parameter. + -- @return #SET_UNIT self + function SET_UNIT:ForEachUnitCompletelyInZone( ZoneObject, IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self:GetSet(), + --- @param Core.Zone#ZONE_BASE ZoneObject + -- @param Wrapper.Unit#UNIT UnitObject + function( ZoneObject, UnitObject ) + if UnitObject:IsInZone( ZoneObject ) then + return true + else + return false + end + end, { ZoneObject } ) + + return self + end + + --- Iterate the SET_UNIT and call an iterator function for each **alive** UNIT presence not in a @{Zone}, providing the UNIT and optional parameters to the called function. + -- @param #SET_UNIT self + -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. + -- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the SET_UNIT. The function needs to accept a UNIT parameter. + -- @return #SET_UNIT self + function SET_UNIT:ForEachUnitNotInZone( ZoneObject, IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self:GetSet(), + --- @param Core.Zone#ZONE_BASE ZoneObject + -- @param Wrapper.Unit#UNIT UnitObject + function( ZoneObject, UnitObject ) + if UnitObject:IsNotInZone( ZoneObject ) then + return true + else + return false + end + end, { ZoneObject } ) + + return self + end + + --- Returns map of unit types. + -- @param #SET_UNIT self + -- @return #map<#string,#number> A map of the unit types found. The key is the UnitTypeName and the value is the amount of unit types found. + function SET_UNIT:GetUnitTypes() + self:F2() + + local MT = {} -- Message Text + local UnitTypes = {} + + for UnitID, UnitData in pairs( self:GetSet() ) do + local TextUnit = UnitData -- Wrapper.Unit#UNIT + if TextUnit:IsAlive() then + local UnitType = TextUnit:GetTypeName() + + if not UnitTypes[UnitType] then + UnitTypes[UnitType] = 1 + else + UnitTypes[UnitType] = UnitTypes[UnitType] + 1 + end + end + end + + for UnitTypeID, UnitType in pairs( UnitTypes ) do + MT[#MT+1] = UnitType .. " of " .. UnitTypeID + end + + return UnitTypes + end + + + --- Returns a comma separated string of the unit types with a count in the @{Set}. + -- @param #SET_UNIT self + -- @return #string The unit types string + function SET_UNIT:GetUnitTypesText() + self:F2() + + local MT = {} -- Message Text + local UnitTypes = self:GetUnitTypes() + + for UnitTypeID, UnitType in pairs( UnitTypes ) do + MT[#MT+1] = UnitType .. " of " .. UnitTypeID + end + + return table.concat( MT, ", " ) + end + + --- Returns map of unit threat levels. + -- @param #SET_UNIT self + -- @return #table. + function SET_UNIT:GetUnitThreatLevels() + self:F2() + + local UnitThreatLevels = {} + + for UnitID, UnitData in pairs( self:GetSet() ) do + local ThreatUnit = UnitData -- Wrapper.Unit#UNIT + if ThreatUnit:IsAlive() then + local UnitThreatLevel, UnitThreatLevelText = ThreatUnit:GetThreatLevel() + local ThreatUnitName = ThreatUnit:GetName() + + UnitThreatLevels[UnitThreatLevel] = UnitThreatLevels[UnitThreatLevel] or {} + UnitThreatLevels[UnitThreatLevel].UnitThreatLevelText = UnitThreatLevelText + UnitThreatLevels[UnitThreatLevel].Units = UnitThreatLevels[UnitThreatLevel].Units or {} + UnitThreatLevels[UnitThreatLevel].Units[ThreatUnitName] = ThreatUnit + end + end + + return UnitThreatLevels + end + + --- Calculate the maxium A2G threat level of the SET_UNIT. + -- @param #SET_UNIT self + -- @return #number The maximum threatlevel + function SET_UNIT:CalculateThreatLevelA2G() + + local MaxThreatLevelA2G = 0 + local MaxThreatText = "" + for UnitName, UnitData in pairs( self:GetSet() ) do + local ThreatUnit = UnitData -- Wrapper.Unit#UNIT + local ThreatLevelA2G, ThreatText = ThreatUnit:GetThreatLevel() + if ThreatLevelA2G > MaxThreatLevelA2G then + MaxThreatLevelA2G = ThreatLevelA2G + MaxThreatText = ThreatText + end + end + + self:F( { MaxThreatLevelA2G = MaxThreatLevelA2G, MaxThreatText = MaxThreatText } ) + return MaxThreatLevelA2G, MaxThreatText + + end + + --- Get the center coordinate of the SET_UNIT. + -- @param #SET_UNIT self + -- @return Core.Point#COORDINATE The center coordinate of all the units in the set, including heading in degrees and speed in mps in case of moving units. + function SET_UNIT:GetCoordinate() + + local Coordinate = self:GetFirst():GetCoordinate() + + local x1 = Coordinate.x + local x2 = Coordinate.x + local y1 = Coordinate.y + local y2 = Coordinate.y + local z1 = Coordinate.z + local z2 = Coordinate.z + local MaxVelocity = 0 + local AvgHeading = nil + local MovingCount = 0 + + for UnitName, UnitData in pairs( self:GetSet() ) do + + local Unit = UnitData -- Wrapper.Unit#UNIT + local Coordinate = Unit:GetCoordinate() + + x1 = ( Coordinate.x < x1 ) and Coordinate.x or x1 + x2 = ( Coordinate.x > x2 ) and Coordinate.x or x2 + y1 = ( Coordinate.y < y1 ) and Coordinate.y or y1 + y2 = ( Coordinate.y > y2 ) and Coordinate.y or y2 + z1 = ( Coordinate.y < z1 ) and Coordinate.z or z1 + z2 = ( Coordinate.y > z2 ) and Coordinate.z or z2 + + local Velocity = Coordinate:GetVelocity() + if Velocity ~= 0 then + MaxVelocity = ( MaxVelocity < Velocity ) and Velocity or MaxVelocity + local Heading = Coordinate:GetHeading() + AvgHeading = AvgHeading and ( AvgHeading + Heading ) or Heading + MovingCount = MovingCount + 1 + end + end + + AvgHeading = AvgHeading and ( AvgHeading / MovingCount ) + + Coordinate.x = ( x2 - x1 ) / 2 + x1 + Coordinate.y = ( y2 - y1 ) / 2 + y1 + Coordinate.z = ( z2 - z1 ) / 2 + z1 + Coordinate:SetHeading( AvgHeading ) + Coordinate:SetVelocity( MaxVelocity ) + + self:F( { Coordinate = Coordinate } ) + return Coordinate + + end + + --- Get the maximum velocity of the SET_UNIT. + -- @param #SET_UNIT self + -- @return #number The speed in mps in case of moving units. + function SET_UNIT:GetVelocity() + + local Coordinate = self:GetFirst():GetCoordinate() + + local MaxVelocity = 0 + + for UnitName, UnitData in pairs( self:GetSet() ) do + + local Unit = UnitData -- Wrapper.Unit#UNIT + local Coordinate = Unit:GetCoordinate() + + local Velocity = Coordinate:GetVelocity() + if Velocity ~= 0 then + MaxVelocity = ( MaxVelocity < Velocity ) and Velocity or MaxVelocity + end + end + + self:F( { MaxVelocity = MaxVelocity } ) + return MaxVelocity + + end + + --- Get the average heading of the SET_UNIT. + -- @param #SET_UNIT self + -- @return #number Heading Heading in degrees and speed in mps in case of moving units. + function SET_UNIT:GetHeading() + + local HeadingSet = nil + local MovingCount = 0 + + for UnitName, UnitData in pairs( self:GetSet() ) do + + local Unit = UnitData -- Wrapper.Unit#UNIT + local Coordinate = Unit:GetCoordinate() + + local Velocity = Coordinate:GetVelocity() + if Velocity ~= 0 then + local Heading = Coordinate:GetHeading() + if HeadingSet == nil then + HeadingSet = Heading + else + local HeadingDiff = ( HeadingSet - Heading + 180 + 360 ) % 360 - 180 + HeadingDiff = math.abs( HeadingDiff ) + if HeadingDiff > 5 then + HeadingSet = nil + break + end + end + end + end + + return HeadingSet + + end + + + + --- Returns if the @{Set} has targets having a radar (of a given type). + -- @param #SET_UNIT self + -- @param DCS#Unit.RadarType RadarType + -- @return #number The amount of radars in the Set with the given type + function SET_UNIT:HasRadar( RadarType ) + self:F2( RadarType ) + + local RadarCount = 0 + for UnitID, UnitData in pairs( self:GetSet()) do + local UnitSensorTest = UnitData -- Wrapper.Unit#UNIT + local HasSensors + if RadarType then + HasSensors = UnitSensorTest:HasSensors( Unit.SensorType.RADAR, RadarType ) + else + HasSensors = UnitSensorTest:HasSensors( Unit.SensorType.RADAR ) + end + self:T3(HasSensors) + if HasSensors then + RadarCount = RadarCount + 1 + end + end + + return RadarCount + end + + --- Returns if the @{Set} has targets that can be SEADed. + -- @param #SET_UNIT self + -- @return #number The amount of SEADable units in the Set + function SET_UNIT:HasSEAD() + self:F2() + + local SEADCount = 0 + for UnitID, UnitData in pairs( self:GetSet()) do + local UnitSEAD = UnitData -- Wrapper.Unit#UNIT + if UnitSEAD:IsAlive() then + local UnitSEADAttributes = UnitSEAD:GetDesc().attributes + + local HasSEAD = UnitSEAD:HasSEAD() + + self:T3(HasSEAD) + if HasSEAD then + SEADCount = SEADCount + 1 + end + end + end + + return SEADCount + end + + --- Returns if the @{Set} has ground targets. + -- @param #SET_UNIT self + -- @return #number The amount of ground targets in the Set. + function SET_UNIT:HasGroundUnits() + self:F2() + + local GroundUnitCount = 0 + for UnitID, UnitData in pairs( self:GetSet()) do + local UnitTest = UnitData -- Wrapper.Unit#UNIT + if UnitTest:IsGround() then + GroundUnitCount = GroundUnitCount + 1 + end + end + + return GroundUnitCount + end + + --- Returns if the @{Set} has air targets. + -- @param #SET_UNIT self + -- @return #number The amount of air targets in the Set. + function SET_UNIT:HasAirUnits() + self:F2() + + local AirUnitCount = 0 + for UnitID, UnitData in pairs( self:GetSet() ) do + local UnitTest = UnitData -- Wrapper.Unit#UNIT + if UnitTest:IsAir() then + AirUnitCount = AirUnitCount + 1 + end + end + + return AirUnitCount + end + + --- Returns if the @{Set} has friendly ground units. + -- @param #SET_UNIT self + -- @return #number The amount of ground targets in the Set. + function SET_UNIT:HasFriendlyUnits( FriendlyCoalition ) + self:F2() + + local FriendlyUnitCount = 0 + for UnitID, UnitData in pairs( self:GetSet()) do + local UnitTest = UnitData -- Wrapper.Unit#UNIT + if UnitTest:IsFriendly( FriendlyCoalition ) then + FriendlyUnitCount = FriendlyUnitCount + 1 + end + end + + return FriendlyUnitCount + end + + + + ----- Iterate the SET_UNIT and call an interator function for each **alive** player, providing the Unit of the player and optional parameters. + ---- @param #SET_UNIT self + ---- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_UNIT. The function needs to accept a UNIT parameter. + ---- @return #SET_UNIT self + --function SET_UNIT:ForEachPlayer( IteratorFunction, ... ) + -- self:F2( arg ) + -- + -- self:ForEach( IteratorFunction, arg, self.PlayersAlive ) + -- + -- return self + --end + -- + -- + ----- Iterate the SET_UNIT and call an interator function for each client, providing the Client to the function and optional parameters. + ---- @param #SET_UNIT self + ---- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_UNIT. The function needs to accept a CLIENT parameter. + ---- @return #SET_UNIT self + --function SET_UNIT:ForEachClient( IteratorFunction, ... ) + -- self:F2( arg ) + -- + -- self:ForEach( IteratorFunction, arg, self.Clients ) + -- + -- return self + --end + + + --- + -- @param #SET_UNIT self + -- @param Wrapper.Unit#UNIT MUnit + -- @return #SET_UNIT self + function SET_UNIT:IsIncludeObject( MUnit ) + self:F2( MUnit ) + + local MUnitInclude = false + + if MUnit:IsAlive() ~= nil then + + MUnitInclude = true + + if self.Filter.Active ~= nil then + local MUnitActive = false + if self.Filter.Active == false or ( self.Filter.Active == true and MUnit:IsActive() == true ) then + MUnitActive = true + end + MUnitInclude = MUnitInclude and MUnitActive + end + + if self.Filter.Coalitions then + local MUnitCoalition = false + for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do + self:F( { "Coalition:", MUnit:GetCoalition(), self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) + if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == MUnit:GetCoalition() then + MUnitCoalition = true + end + end + MUnitInclude = MUnitInclude and MUnitCoalition + end + + if self.Filter.Categories then + local MUnitCategory = false + for CategoryID, CategoryName in pairs( self.Filter.Categories ) do + self:T3( { "Category:", MUnit:GetDesc().category, self.FilterMeta.Categories[CategoryName], CategoryName } ) + if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == MUnit:GetDesc().category then + MUnitCategory = true + end + end + MUnitInclude = MUnitInclude and MUnitCategory + end + + if self.Filter.Types then + local MUnitType = false + for TypeID, TypeName in pairs( self.Filter.Types ) do + self:T3( { "Type:", MUnit:GetTypeName(), TypeName } ) + if TypeName == MUnit:GetTypeName() then + MUnitType = true + end + end + MUnitInclude = MUnitInclude and MUnitType + end + + if self.Filter.Countries then + local MUnitCountry = false + for CountryID, CountryName in pairs( self.Filter.Countries ) do + self:T3( { "Country:", MUnit:GetCountry(), CountryName } ) + if country.id[CountryName] == MUnit:GetCountry() then + MUnitCountry = true + end + end + MUnitInclude = MUnitInclude and MUnitCountry + end + + if self.Filter.UnitPrefixes then + local MUnitPrefix = false + for UnitPrefixId, UnitPrefix in pairs( self.Filter.UnitPrefixes ) do + self:T3( { "Prefix:", string.find( MUnit:GetName(), UnitPrefix, 1 ), UnitPrefix } ) + if string.find( MUnit:GetName(), UnitPrefix, 1 ) then + MUnitPrefix = true + end + end + MUnitInclude = MUnitInclude and MUnitPrefix + end + + if self.Filter.RadarTypes then + local MUnitRadar = false + for RadarTypeID, RadarType in pairs( self.Filter.RadarTypes ) do + self:T3( { "Radar:", RadarType } ) + if MUnit:HasSensors( Unit.SensorType.RADAR, RadarType ) == true then + if MUnit:GetRadar() == true then -- This call is necessary to evaluate the SEAD capability. + self:T3( "RADAR Found" ) + end + MUnitRadar = true + end + end + MUnitInclude = MUnitInclude and MUnitRadar + end + + if self.Filter.SEAD then + local MUnitSEAD = false + if MUnit:HasSEAD() == true then + self:T3( "SEAD Found" ) + MUnitSEAD = true + end + MUnitInclude = MUnitInclude and MUnitSEAD + end + end + + self:T2( MUnitInclude ) + return MUnitInclude + end + + + --- Retrieve the type names of the @{Wrapper.Unit}s in the SET, delimited by an optional delimiter. + -- @param #SET_UNIT self + -- @param #string Delimiter (optional) The delimiter, which is default a comma. + -- @return #string The types of the @{Wrapper.Unit}s delimited. + function SET_UNIT:GetTypeNames( Delimiter ) + + Delimiter = Delimiter or ", " + local TypeReport = REPORT:New() + local Types = {} + + for UnitName, UnitData in pairs( self:GetSet() ) do + + local Unit = UnitData -- Wrapper.Unit#UNIT + local UnitTypeName = Unit:GetTypeName() + + if not Types[UnitTypeName] then + Types[UnitTypeName] = UnitTypeName + TypeReport:Add( UnitTypeName ) + end + end + + return TypeReport:Text( Delimiter ) + end + + --- Iterate the SET_UNIT and set for each unit the default cargo bay weight limit. + -- @param #SET_UNIT self + -- @usage + -- -- Set the default cargo bay weight limits of the carrier units. + -- local MySetUnit = SET_UNIT:New() + -- MySetUnit:SetCargoBayWeightLimit() + function SET_UNIT:SetCargoBayWeightLimit() + local Set = self:GetSet() + for UnitID, UnitData in pairs( Set ) do -- For each UNIT in SET_UNIT + --local UnitData = UnitData -- Wrapper.Unit#UNIT + UnitData:SetCargoBayWeightLimit() + end + end + + + +end + + +do -- SET_STATIC + + --- @type SET_STATIC + -- @extends Core.Set#SET_BASE + + --- Mission designers can use the SET_STATIC class to build sets of Statics belonging to certain: + -- + -- * Coalitions + -- * Categories + -- * Countries + -- * Static types + -- * Starting with certain prefix strings. + -- + -- ## SET_STATIC constructor + -- + -- Create a new SET_STATIC object with the @{#SET_STATIC.New} method: + -- + -- * @{#SET_STATIC.New}: Creates a new SET_STATIC object. + -- + -- ## Add or Remove STATIC(s) from SET_STATIC + -- + -- STATICs can be added and removed using the @{Core.Set#SET_STATIC.AddStaticsByName} and @{Core.Set#SET_STATIC.RemoveStaticsByName} respectively. + -- These methods take a single STATIC name or an array of STATIC names to be added or removed from SET_STATIC. + -- + -- ## SET_STATIC filter criteria + -- + -- You can set filter criteria to define the set of units within the SET_STATIC. + -- Filter criteria are defined by: + -- + -- * @{#SET_STATIC.FilterCoalitions}: Builds the SET_STATIC with the units belonging to the coalition(s). + -- * @{#SET_STATIC.FilterCategories}: Builds the SET_STATIC with the units belonging to the category(ies). + -- * @{#SET_STATIC.FilterTypes}: Builds the SET_STATIC with the units belonging to the unit type(s). + -- * @{#SET_STATIC.FilterCountries}: Builds the SET_STATIC with the units belonging to the country(ies). + -- * @{#SET_STATIC.FilterPrefixes}: Builds the SET_STATIC with the units containing the same string(s) in their name. **ATTENTION** bad naming convention as this *does not** only filter *prefixes*. + -- + -- Once the filter criteria have been set for the SET_STATIC, you can start filtering using: + -- + -- * @{#SET_STATIC.FilterStart}: Starts the filtering of the units within the SET_STATIC. + -- + -- Planned filter criteria within development are (so these are not yet available): + -- + -- * @{#SET_STATIC.FilterZones}: Builds the SET_STATIC with the units within a @{Core.Zone#ZONE}. + -- + -- ## SET_STATIC iterators + -- + -- Once the filters have been defined and the SET_STATIC has been built, you can iterate the SET_STATIC with the available iterator methods. + -- The iterator methods will walk the SET_STATIC set, and call for each element within the set a function that you provide. + -- The following iterator methods are currently available within the SET_STATIC: + -- + -- * @{#SET_STATIC.ForEachStatic}: Calls a function for each alive unit it finds within the SET_STATIC. + -- * @{#SET_GROUP.ForEachGroupCompletelyInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence completely in a @{Zone}, providing the GROUP and optional parameters to the called function. + -- * @{#SET_GROUP.ForEachGroupNotInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence not in a @{Zone}, providing the GROUP and optional parameters to the called function. + -- + -- Planned iterators methods in development are (so these are not yet available): + -- + -- * @{#SET_STATIC.ForEachStaticInZone}: Calls a function for each unit contained within the SET_STATIC. + -- * @{#SET_STATIC.ForEachStaticCompletelyInZone}: Iterate and call an iterator function for each **alive** STATIC presence completely in a @{Zone}, providing the STATIC and optional parameters to the called function. + -- * @{#SET_STATIC.ForEachStaticNotInZone}: Iterate and call an iterator function for each **alive** STATIC presence not in a @{Zone}, providing the STATIC and optional parameters to the called function. + -- + -- ## SET_STATIC atomic methods + -- + -- Various methods exist for a SET_STATIC to perform actions or calculations and retrieve results from the SET_STATIC: + -- + -- * @{#SET_STATIC.GetTypeNames}(): Retrieve the type names of the @{Static}s in the SET, delimited by a comma. + -- + -- === + -- @field #SET_STATIC SET_STATIC + SET_STATIC = { + ClassName = "SET_STATIC", + Statics = {}, + Filter = { + Coalitions = nil, + Categories = nil, + Types = nil, + Countries = nil, + StaticPrefixes = nil, + }, + FilterMeta = { + Coalitions = { + red = coalition.side.RED, + blue = coalition.side.BLUE, + neutral = coalition.side.NEUTRAL, + }, + Categories = { + plane = Unit.Category.AIRPLANE, + helicopter = Unit.Category.HELICOPTER, + ground = Unit.Category.GROUND_STATIC, + ship = Unit.Category.SHIP, + structure = Unit.Category.STRUCTURE, + }, + }, + } + + + --- Get the first unit from the set. + -- @function [parent=#SET_STATIC] GetFirst + -- @param #SET_STATIC self + -- @return Wrapper.Static#STATIC The STATIC object. + + --- Creates a new SET_STATIC object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. + -- @param #SET_STATIC self + -- @return #SET_STATIC + -- @usage + -- -- Define a new SET_STATIC Object. This DBObject will contain a reference to all alive Statics. + -- DBObject = SET_STATIC:New() + function SET_STATIC:New() + + -- Inherits from BASE + local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.STATICS ) ) -- Core.Set#SET_STATIC + + return self + end + + --- Add STATIC(s) to SET_STATIC. + -- @param #SET_STATIC self + -- @param #string AddStatic A single STATIC. + -- @return #SET_STATIC self + function SET_STATIC:AddStatic( AddStatic ) + self:F2( AddStatic:GetName() ) + + self:Add( AddStatic:GetName(), AddStatic ) + + return self + end + + + --- Add STATIC(s) to SET_STATIC. + -- @param #SET_STATIC self + -- @param #string AddStaticNames A single name or an array of STATIC names. + -- @return #SET_STATIC self + function SET_STATIC:AddStaticsByName( AddStaticNames ) + + local AddStaticNamesArray = ( type( AddStaticNames ) == "table" ) and AddStaticNames or { AddStaticNames } + + self:T( AddStaticNamesArray ) + for AddStaticID, AddStaticName in pairs( AddStaticNamesArray ) do + self:Add( AddStaticName, STATIC:FindByName( AddStaticName ) ) + end + + return self + end + + --- Remove STATIC(s) from SET_STATIC. + -- @param Core.Set#SET_STATIC self + -- @param Wrapper.Static#STATIC RemoveStaticNames A single name or an array of STATIC names. + -- @return self + function SET_STATIC:RemoveStaticsByName( RemoveStaticNames ) + + local RemoveStaticNamesArray = ( type( RemoveStaticNames ) == "table" ) and RemoveStaticNames or { RemoveStaticNames } + + for RemoveStaticID, RemoveStaticName in pairs( RemoveStaticNamesArray ) do + self:Remove( RemoveStaticName ) + end + + return self + end + + + --- Finds a Static based on the Static Name. + -- @param #SET_STATIC self + -- @param #string StaticName + -- @return Wrapper.Static#STATIC The found Static. + function SET_STATIC:FindStatic( StaticName ) + + local StaticFound = self.Set[StaticName] + return StaticFound + end + + + + --- Builds a set of units of coalitions. + -- Possible current coalitions are red, blue and neutral. + -- @param #SET_STATIC self + -- @param #string Coalitions Can take the following values: "red", "blue", "neutral". + -- @return #SET_STATIC self + function SET_STATIC:FilterCoalitions( Coalitions ) + if not self.Filter.Coalitions then + self.Filter.Coalitions = {} + end + if type( Coalitions ) ~= "table" then + Coalitions = { Coalitions } + end + for CoalitionID, Coalition in pairs( Coalitions ) do + self.Filter.Coalitions[Coalition] = Coalition + end + return self + end + + + --- Builds a set of units out of categories. + -- Possible current categories are plane, helicopter, ground, ship. + -- @param #SET_STATIC self + -- @param #string Categories Can take the following values: "plane", "helicopter", "ground", "ship". + -- @return #SET_STATIC self + function SET_STATIC:FilterCategories( Categories ) + if not self.Filter.Categories then + self.Filter.Categories = {} + end + if type( Categories ) ~= "table" then + Categories = { Categories } + end + for CategoryID, Category in pairs( Categories ) do + self.Filter.Categories[Category] = Category + end + return self + end + + + --- Builds a set of units of defined unit types. + -- Possible current types are those types known within DCS world. + -- @param #SET_STATIC self + -- @param #string Types Can take those type strings known within DCS world. + -- @return #SET_STATIC self + function SET_STATIC:FilterTypes( Types ) + if not self.Filter.Types then + self.Filter.Types = {} + end + if type( Types ) ~= "table" then + Types = { Types } + end + for TypeID, Type in pairs( Types ) do + self.Filter.Types[Type] = Type + end + return self + end + + + --- Builds a set of units of defined countries. + -- Possible current countries are those known within DCS world. + -- @param #SET_STATIC self + -- @param #string Countries Can take those country strings known within DCS world. + -- @return #SET_STATIC self + function SET_STATIC:FilterCountries( Countries ) + if not self.Filter.Countries then + self.Filter.Countries = {} + end + if type( Countries ) ~= "table" then + Countries = { Countries } + end + for CountryID, Country in pairs( Countries ) do + self.Filter.Countries[Country] = Country + end + return self + end + + + --- Builds a set of STATICs that contain the given string in their name. + -- **Attention!** Bad naming convention as this **does not** filter only **prefixes** but all statics that **contain** the string. + -- @param #SET_STATIC self + -- @param #string Prefixes The string pattern(s) that need to be contained in the static name. Can also be passed as a `#table` of strings. + -- @return #SET_STATIC self + function SET_STATIC:FilterPrefixes( Prefixes ) + if not self.Filter.StaticPrefixes then + self.Filter.StaticPrefixes = {} + end + if type( Prefixes ) ~= "table" then + Prefixes = { Prefixes } + end + for PrefixID, Prefix in pairs( Prefixes ) do + self.Filter.StaticPrefixes[Prefix] = Prefix + end + return self + end + + + --- Starts the filtering. + -- @param #SET_STATIC self + -- @return #SET_STATIC self + function SET_STATIC:FilterStart() + + if _DATABASE then + self:_FilterStart() + self:HandleEvent( EVENTS.Birth, self._EventOnBirth ) + self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) + self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) + end + + return self + end + + --- Iterate the SET_STATIC and count how many STATICSs are alive. + -- @param #SET_STATIC self + -- @return #number The number of UNITs alive. + function SET_STATIC:CountAlive() + + local Set = self:GetSet() + + local CountU = 0 + for UnitID, UnitData in pairs(Set) do + if UnitData and UnitData:IsAlive() then + CountU = CountU + 1 + end + + end + + return CountU + end + + --- Handles the Database to check on an event (birth) that the Object was added in the Database. + -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! + -- @param #SET_STATIC self + -- @param Core.Event#EVENTDATA Event + -- @return #string The name of the STATIC + -- @return #table The STATIC + function SET_STATIC:AddInDatabase( Event ) + self:F3( { Event } ) + + if Event.IniObjectCategory == Object.Category.STATIC then + if not self.Database[Event.IniDCSUnitName] then + self.Database[Event.IniDCSUnitName] = STATIC:Register( Event.IniDCSUnitName ) + self:T3( self.Database[Event.IniDCSUnitName] ) + end + end + + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] + end + + --- Handles the Database to check on any event that Object exists in the Database. + -- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! + -- @param #SET_STATIC self + -- @param Core.Event#EVENTDATA Event + -- @return #string The name of the STATIC + -- @return #table The STATIC + function SET_STATIC:FindInDatabase( Event ) + self:F2( { Event.IniDCSUnitName, self.Set[Event.IniDCSUnitName], Event } ) + + + return Event.IniDCSUnitName, self.Set[Event.IniDCSUnitName] + end + + + do -- Is Zone methods + + --- Check if minimal one element of the SET_STATIC is in the Zone. + -- @param #SET_STATIC self + -- @param Core.Zone#ZONE Zone The Zone to be tested for. + -- @return #boolean + function SET_STATIC:IsPartiallyInZone( Zone ) + + local IsPartiallyInZone = false + + local function EvaluateZone( ZoneStatic ) + + local ZoneStaticName = ZoneStatic:GetName() + if self:FindStatic( ZoneStaticName ) then + IsPartiallyInZone = true + return false + end + + return true + end + + return IsPartiallyInZone + end + + + --- Check if no element of the SET_STATIC is in the Zone. + -- @param #SET_STATIC self + -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. + -- @return #boolean + function SET_STATIC:IsNotInZone( Zone ) + + local IsNotInZone = true + + local function EvaluateZone( ZoneStatic ) + + local ZoneStaticName = ZoneStatic:GetName() + if self:FindStatic( ZoneStaticName ) then + IsNotInZone = false + return false + end + + return true + end + + Zone:Search( EvaluateZone ) + + return IsNotInZone + end + + + --- Check if minimal one element of the SET_STATIC is in the Zone. + -- @param #SET_STATIC self + -- @param #function IteratorFunction The function that will be called when there is an alive STATIC in the SET_STATIC. The function needs to accept a STATIC parameter. + -- @return #SET_STATIC self + function SET_STATIC:ForEachStaticInZone( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self:GetSet() ) + + return self + end + + + end + + + --- Iterate the SET_STATIC and call an interator function for each **alive** STATIC, providing the STATIC and optional parameters. + -- @param #SET_STATIC self + -- @param #function IteratorFunction The function that will be called when there is an alive STATIC in the SET_STATIC. The function needs to accept a STATIC parameter. + -- @return #SET_STATIC self + function SET_STATIC:ForEachStatic( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self:GetSet() ) + + return self + end + + + --- Iterate the SET_STATIC and call an iterator function for each **alive** STATIC presence completely in a @{Zone}, providing the STATIC and optional parameters to the called function. + -- @param #SET_STATIC self + -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. + -- @param #function IteratorFunction The function that will be called when there is an alive STATIC in the SET_STATIC. The function needs to accept a STATIC parameter. + -- @return #SET_STATIC self + function SET_STATIC:ForEachStaticCompletelyInZone( ZoneObject, IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self:GetSet(), + --- @param Core.Zone#ZONE_BASE ZoneObject + -- @param Wrapper.Static#STATIC StaticObject + function( ZoneObject, StaticObject ) + if StaticObject:IsInZone( ZoneObject ) then + return true + else + return false + end + end, { ZoneObject } ) + + return self + end + + --- Iterate the SET_STATIC and call an iterator function for each **alive** STATIC presence not in a @{Zone}, providing the STATIC and optional parameters to the called function. + -- @param #SET_STATIC self + -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. + -- @param #function IteratorFunction The function that will be called when there is an alive STATIC in the SET_STATIC. The function needs to accept a STATIC parameter. + -- @return #SET_STATIC self + function SET_STATIC:ForEachStaticNotInZone( ZoneObject, IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self:GetSet(), + --- @param Core.Zone#ZONE_BASE ZoneObject + -- @param Wrapper.Static#STATIC StaticObject + function( ZoneObject, StaticObject ) + if StaticObject:IsNotInZone( ZoneObject ) then + return true + else + return false + end + end, { ZoneObject } ) + + return self + end + + --- Returns map of unit types. + -- @param #SET_STATIC self + -- @return #map<#string,#number> A map of the unit types found. The key is the StaticTypeName and the value is the amount of unit types found. + function SET_STATIC:GetStaticTypes() + self:F2() + + local MT = {} -- Message Text + local StaticTypes = {} + + for StaticID, StaticData in pairs( self:GetSet() ) do + local TextStatic = StaticData -- Wrapper.Static#STATIC + if TextStatic:IsAlive() then + local StaticType = TextStatic:GetTypeName() + + if not StaticTypes[StaticType] then + StaticTypes[StaticType] = 1 + else + StaticTypes[StaticType] = StaticTypes[StaticType] + 1 + end + end + end + + for StaticTypeID, StaticType in pairs( StaticTypes ) do + MT[#MT+1] = StaticType .. " of " .. StaticTypeID + end + + return StaticTypes + end + + + --- Returns a comma separated string of the unit types with a count in the @{Set}. + -- @param #SET_STATIC self + -- @return #string The unit types string + function SET_STATIC:GetStaticTypesText() + self:F2() + + local MT = {} -- Message Text + local StaticTypes = self:GetStaticTypes() + + for StaticTypeID, StaticType in pairs( StaticTypes ) do + MT[#MT+1] = StaticType .. " of " .. StaticTypeID + end + + return table.concat( MT, ", " ) + end + + --- Get the center coordinate of the SET_STATIC. + -- @param #SET_STATIC self + -- @return Core.Point#COORDINATE The center coordinate of all the units in the set, including heading in degrees and speed in mps in case of moving units. + function SET_STATIC:GetCoordinate() + + local Coordinate = self:GetFirst():GetCoordinate() + + local x1 = Coordinate.x + local x2 = Coordinate.x + local y1 = Coordinate.y + local y2 = Coordinate.y + local z1 = Coordinate.z + local z2 = Coordinate.z + local MaxVelocity = 0 + local AvgHeading = nil + local MovingCount = 0 + + for StaticName, StaticData in pairs( self:GetSet() ) do + + local Static = StaticData -- Wrapper.Static#STATIC + local Coordinate = Static:GetCoordinate() + + x1 = ( Coordinate.x < x1 ) and Coordinate.x or x1 + x2 = ( Coordinate.x > x2 ) and Coordinate.x or x2 + y1 = ( Coordinate.y < y1 ) and Coordinate.y or y1 + y2 = ( Coordinate.y > y2 ) and Coordinate.y or y2 + z1 = ( Coordinate.y < z1 ) and Coordinate.z or z1 + z2 = ( Coordinate.y > z2 ) and Coordinate.z or z2 + + local Velocity = Coordinate:GetVelocity() + if Velocity ~= 0 then + MaxVelocity = ( MaxVelocity < Velocity ) and Velocity or MaxVelocity + local Heading = Coordinate:GetHeading() + AvgHeading = AvgHeading and ( AvgHeading + Heading ) or Heading + MovingCount = MovingCount + 1 + end + end + + AvgHeading = AvgHeading and ( AvgHeading / MovingCount ) + + Coordinate.x = ( x2 - x1 ) / 2 + x1 + Coordinate.y = ( y2 - y1 ) / 2 + y1 + Coordinate.z = ( z2 - z1 ) / 2 + z1 + Coordinate:SetHeading( AvgHeading ) + Coordinate:SetVelocity( MaxVelocity ) + + self:F( { Coordinate = Coordinate } ) + return Coordinate + + end + + --- Get the maximum velocity of the SET_STATIC. + -- @param #SET_STATIC self + -- @return #number The speed in mps in case of moving units. + function SET_STATIC:GetVelocity() + + return 0 + + end + + --- Get the average heading of the SET_STATIC. + -- @param #SET_STATIC self + -- @return #number Heading Heading in degrees and speed in mps in case of moving units. + function SET_STATIC:GetHeading() + + local HeadingSet = nil + local MovingCount = 0 + + for StaticName, StaticData in pairs( self:GetSet() ) do + + local Static = StaticData -- Wrapper.Static#STATIC + local Coordinate = Static:GetCoordinate() + + local Velocity = Coordinate:GetVelocity() + if Velocity ~= 0 then + local Heading = Coordinate:GetHeading() + if HeadingSet == nil then + HeadingSet = Heading + else + local HeadingDiff = ( HeadingSet - Heading + 180 + 360 ) % 360 - 180 + HeadingDiff = math.abs( HeadingDiff ) + if HeadingDiff > 5 then + HeadingSet = nil + break + end + end + end + end + + return HeadingSet + + end + + --- Calculate the maxium A2G threat level of the SET_STATIC. + -- @param #SET_STATIC self + -- @return #number The maximum threatlevel + function SET_STATIC:CalculateThreatLevelA2G() + + local MaxThreatLevelA2G = 0 + local MaxThreatText = "" + for StaticName, StaticData in pairs( self:GetSet() ) do + local ThreatStatic = StaticData -- Wrapper.Static#STATIC + local ThreatLevelA2G, ThreatText = ThreatStatic:GetThreatLevel() + if ThreatLevelA2G > MaxThreatLevelA2G then + MaxThreatLevelA2G = ThreatLevelA2G + MaxThreatText = ThreatText + end + end + + self:F( { MaxThreatLevelA2G = MaxThreatLevelA2G, MaxThreatText = MaxThreatText } ) + return MaxThreatLevelA2G, MaxThreatText + + end + + --- + -- @param #SET_STATIC self + -- @param Wrapper.Static#STATIC MStatic + -- @return #SET_STATIC self + function SET_STATIC:IsIncludeObject( MStatic ) + self:F2( MStatic ) + local MStaticInclude = true + + if self.Filter.Coalitions then + local MStaticCoalition = false + for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do + self:T3( { "Coalition:", MStatic:GetCoalition(), self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) + if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == MStatic:GetCoalition() then + MStaticCoalition = true + end + end + MStaticInclude = MStaticInclude and MStaticCoalition + end + + if self.Filter.Categories then + local MStaticCategory = false + for CategoryID, CategoryName in pairs( self.Filter.Categories ) do + self:T3( { "Category:", MStatic:GetDesc().category, self.FilterMeta.Categories[CategoryName], CategoryName } ) + if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == MStatic:GetDesc().category then + MStaticCategory = true + end + end + MStaticInclude = MStaticInclude and MStaticCategory + end + + if self.Filter.Types then + local MStaticType = false + for TypeID, TypeName in pairs( self.Filter.Types ) do + self:T3( { "Type:", MStatic:GetTypeName(), TypeName } ) + if TypeName == MStatic:GetTypeName() then + MStaticType = true + end + end + MStaticInclude = MStaticInclude and MStaticType + end + + if self.Filter.Countries then + local MStaticCountry = false + for CountryID, CountryName in pairs( self.Filter.Countries ) do + self:T3( { "Country:", MStatic:GetCountry(), CountryName } ) + if country.id[CountryName] == MStatic:GetCountry() then + MStaticCountry = true + end + end + MStaticInclude = MStaticInclude and MStaticCountry + end + + if self.Filter.StaticPrefixes then + local MStaticPrefix = false + for StaticPrefixId, StaticPrefix in pairs( self.Filter.StaticPrefixes ) do + self:T3( { "Prefix:", string.find( MStatic:GetName(), StaticPrefix, 1 ), StaticPrefix } ) + if string.find( MStatic:GetName(), StaticPrefix, 1 ) then + MStaticPrefix = true + end + end + MStaticInclude = MStaticInclude and MStaticPrefix + end + + self:T2( MStaticInclude ) + return MStaticInclude + end + + + --- Retrieve the type names of the @{Static}s in the SET, delimited by an optional delimiter. + -- @param #SET_STATIC self + -- @param #string Delimiter (optional) The delimiter, which is default a comma. + -- @return #string The types of the @{Static}s delimited. + function SET_STATIC:GetTypeNames( Delimiter ) + + Delimiter = Delimiter or ", " + local TypeReport = REPORT:New() + local Types = {} + + for StaticName, StaticData in pairs( self:GetSet() ) do + + local Static = StaticData -- Wrapper.Static#STATIC + local StaticTypeName = Static:GetTypeName() + + if not Types[StaticTypeName] then + Types[StaticTypeName] = StaticTypeName + TypeReport:Add( StaticTypeName ) + end + end + + return TypeReport:Text( Delimiter ) + end + +end + + +do -- SET_CLIENT + + + --- @type SET_CLIENT + -- @extends Core.Set#SET_BASE + + + + --- Mission designers can use the @{Core.Set#SET_CLIENT} class to build sets of units belonging to certain: + -- + -- * Coalitions + -- * Categories + -- * Countries + -- * Client types + -- * Starting with certain prefix strings. + -- + -- ## 1) SET_CLIENT constructor + -- + -- Create a new SET_CLIENT object with the @{#SET_CLIENT.New} method: + -- + -- * @{#SET_CLIENT.New}: Creates a new SET_CLIENT object. + -- + -- ## 2) Add or Remove CLIENT(s) from SET_CLIENT + -- + -- CLIENTs can be added and removed using the @{Core.Set#SET_CLIENT.AddClientsByName} and @{Core.Set#SET_CLIENT.RemoveClientsByName} respectively. + -- These methods take a single CLIENT name or an array of CLIENT names to be added or removed from SET_CLIENT. + -- + -- ## 3) SET_CLIENT filter criteria + -- + -- You can set filter criteria to define the set of clients within the SET_CLIENT. + -- Filter criteria are defined by: + -- + -- * @{#SET_CLIENT.FilterCoalitions}: Builds the SET_CLIENT with the clients belonging to the coalition(s). + -- * @{#SET_CLIENT.FilterCategories}: Builds the SET_CLIENT with the clients belonging to the category(ies). + -- * @{#SET_CLIENT.FilterTypes}: Builds the SET_CLIENT with the clients belonging to the client type(s). + -- * @{#SET_CLIENT.FilterCountries}: Builds the SET_CLIENT with the clients belonging to the country(ies). + -- * @{#SET_CLIENT.FilterPrefixes}: Builds the SET_CLIENT with the clients containing the same string(s) in their unit/pilot name. **ATTENTION!** Bad naming convention as this *does not* only filter *prefixes*. + -- * @{#SET_CLIENT.FilterActive}: Builds the SET_CLIENT with the units that are only active. Units that are inactive (late activation) won't be included in the set! + -- + -- Once the filter criteria have been set for the SET_CLIENT, you can start filtering using: + -- + -- * @{#SET_CLIENT.FilterStart}: Starts the filtering of the clients **dynamically**. + -- * @{#SET_CLIENT.FilterOnce}: Filters the clients **once**. + -- + -- Planned filter criteria within development are (so these are not yet available): + -- + -- * @{#SET_CLIENT.FilterZones}: Builds the SET_CLIENT with the clients within a @{Core.Zone#ZONE}. + -- + -- ## 4) SET_CLIENT iterators + -- + -- Once the filters have been defined and the SET_CLIENT has been built, you can iterate the SET_CLIENT with the available iterator methods. + -- The iterator methods will walk the SET_CLIENT set, and call for each element within the set a function that you provide. + -- The following iterator methods are currently available within the SET_CLIENT: + -- + -- * @{#SET_CLIENT.ForEachClient}: Calls a function for each alive client it finds within the SET_CLIENT. + -- + -- === + -- @field #SET_CLIENT SET_CLIENT + SET_CLIENT = { + ClassName = "SET_CLIENT", + Clients = {}, + Filter = { + Coalitions = nil, + Categories = nil, + Types = nil, + Countries = nil, + ClientPrefixes = nil, + }, + FilterMeta = { + Coalitions = { + red = coalition.side.RED, + blue = coalition.side.BLUE, + neutral = coalition.side.NEUTRAL, + }, + Categories = { + plane = Unit.Category.AIRPLANE, + helicopter = Unit.Category.HELICOPTER, + ground = Unit.Category.GROUND_UNIT, + ship = Unit.Category.SHIP, + structure = Unit.Category.STRUCTURE, + }, + }, + } + + + --- Creates a new SET_CLIENT object, building a set of clients belonging to a coalitions, categories, countries, types or with defined prefix names. + -- @param #SET_CLIENT self + -- @return #SET_CLIENT + -- @usage + -- -- Define a new SET_CLIENT Object. This DBObject will contain a reference to all Clients. + -- DBObject = SET_CLIENT:New() + function SET_CLIENT:New() + -- Inherits from BASE + local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.CLIENTS ) ) -- #SET_CLIENT + + self:FilterActive( false ) + + return self + end + + --- Add CLIENT(s) to SET_CLIENT. + -- @param Core.Set#SET_CLIENT self + -- @param #string AddClientNames A single name or an array of CLIENT names. + -- @return self + function SET_CLIENT:AddClientsByName( AddClientNames ) + + local AddClientNamesArray = ( type( AddClientNames ) == "table" ) and AddClientNames or { AddClientNames } + + for AddClientID, AddClientName in pairs( AddClientNamesArray ) do + self:Add( AddClientName, CLIENT:FindByName( AddClientName ) ) + end + + return self + end + + --- Remove CLIENT(s) from SET_CLIENT. + -- @param Core.Set#SET_CLIENT self + -- @param Wrapper.Client#CLIENT RemoveClientNames A single name or an array of CLIENT names. + -- @return self + function SET_CLIENT:RemoveClientsByName( RemoveClientNames ) + + local RemoveClientNamesArray = ( type( RemoveClientNames ) == "table" ) and RemoveClientNames or { RemoveClientNames } + + for RemoveClientID, RemoveClientName in pairs( RemoveClientNamesArray ) do + self:Remove( RemoveClientName.ClientName ) + end + + return self + end + + + --- Finds a Client based on the Client Name. + -- @param #SET_CLIENT self + -- @param #string ClientName + -- @return Wrapper.Client#CLIENT The found Client. + function SET_CLIENT:FindClient( ClientName ) + + local ClientFound = self.Set[ClientName] + return ClientFound + end + + + + --- Builds a set of clients of coalitions. + -- Possible current coalitions are red, blue and neutral. + -- @param #SET_CLIENT self + -- @param #string Coalitions Can take the following values: "red", "blue", "neutral". + -- @return #SET_CLIENT self + function SET_CLIENT:FilterCoalitions( Coalitions ) + if not self.Filter.Coalitions then + self.Filter.Coalitions = {} + end + if type( Coalitions ) ~= "table" then + Coalitions = { Coalitions } + end + for CoalitionID, Coalition in pairs( Coalitions ) do + self.Filter.Coalitions[Coalition] = Coalition + end + return self + end + + + --- Builds a set of clients out of categories. + -- Possible current categories are plane, helicopter, ground, ship. + -- @param #SET_CLIENT self + -- @param #string Categories Can take the following values: "plane", "helicopter", "ground", "ship". + -- @return #SET_CLIENT self + function SET_CLIENT:FilterCategories( Categories ) + if not self.Filter.Categories then + self.Filter.Categories = {} + end + if type( Categories ) ~= "table" then + Categories = { Categories } + end + for CategoryID, Category in pairs( Categories ) do + self.Filter.Categories[Category] = Category + end + return self + end + + + --- Builds a set of clients of defined client types. + -- Possible current types are those types known within DCS world. + -- @param #SET_CLIENT self + -- @param #string Types Can take those type strings known within DCS world. + -- @return #SET_CLIENT self + function SET_CLIENT:FilterTypes( Types ) + if not self.Filter.Types then + self.Filter.Types = {} + end + if type( Types ) ~= "table" then + Types = { Types } + end + for TypeID, Type in pairs( Types ) do + self.Filter.Types[Type] = Type + end + return self + end + + + --- Builds a set of clients of defined countries. + -- Possible current countries are those known within DCS world. + -- @param #SET_CLIENT self + -- @param #string Countries Can take those country strings known within DCS world. + -- @return #SET_CLIENT self + function SET_CLIENT:FilterCountries( Countries ) + if not self.Filter.Countries then + self.Filter.Countries = {} + end + if type( Countries ) ~= "table" then + Countries = { Countries } + end + for CountryID, Country in pairs( Countries ) do + self.Filter.Countries[Country] = Country + end + return self + end + + + --- Builds a set of CLIENTs that contain the given string in their unit/pilot name. + -- **Attention!** Bad naming convention as this **does not** filter only **prefixes** but all clients that **contain** the string. + -- @param #SET_CLIENT self + -- @param #string Prefixes The string pattern(s) that needs to be contained in the unit/pilot name. Can also be passed as a `#table` of strings. + -- @return #SET_CLIENT self + function SET_CLIENT:FilterPrefixes( Prefixes ) + if not self.Filter.ClientPrefixes then + self.Filter.ClientPrefixes = {} + end + if type( Prefixes ) ~= "table" then + Prefixes = { Prefixes } + end + for PrefixID, Prefix in pairs( Prefixes ) do + self.Filter.ClientPrefixes[Prefix] = Prefix + end + return self + end + + --- Builds a set of clients that are only active. + -- Only the clients that are active will be included within the set. + -- @param #SET_CLIENT self + -- @param #boolean Active (optional) Include only active clients to the set. + -- Include inactive clients if you provide false. + -- @return #SET_CLIENT self + -- @usage + -- + -- -- Include only active clients to the set. + -- ClientSet = SET_CLIENT:New():FilterActive():FilterStart() + -- + -- -- Include only active clients to the set of the blue coalition, and filter one time. + -- ClientSet = SET_CLIENT:New():FilterActive():FilterCoalition( "blue" ):FilterOnce() + -- + -- -- Include only active clients to the set of the blue coalition, and filter one time. + -- -- Later, reset to include back inactive clients to the set. + -- ClientSet = SET_CLIENT:New():FilterActive():FilterCoalition( "blue" ):FilterOnce() + -- ... logic ... + -- ClientSet = SET_CLIENT:New():FilterActive( false ):FilterCoalition( "blue" ):FilterOnce() + -- + function SET_CLIENT:FilterActive( Active ) + Active = Active or not ( Active == false ) + self.Filter.Active = Active + return self + end + + + + --- Starts the filtering. + -- @param #SET_CLIENT self + -- @return #SET_CLIENT self + function SET_CLIENT:FilterStart() + + if _DATABASE then + self:_FilterStart() + self:HandleEvent( EVENTS.Birth, self._EventOnBirth ) + self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) + self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) + end + + return self + end + + --- Handles the Database to check on an event (birth) that the Object was added in the Database. + -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! + -- @param #SET_CLIENT self + -- @param Core.Event#EVENTDATA Event + -- @return #string The name of the CLIENT + -- @return #table The CLIENT + function SET_CLIENT:AddInDatabase( Event ) + self:F3( { Event } ) + + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] + end + + --- Handles the Database to check on any event that Object exists in the Database. + -- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! + -- @param #SET_CLIENT self + -- @param Core.Event#EVENTDATA Event + -- @return #string The name of the CLIENT + -- @return #table The CLIENT + function SET_CLIENT:FindInDatabase( Event ) + self:F3( { Event } ) + + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] + end + + --- Iterate the SET_CLIENT and call an interator function for each **alive** CLIENT, providing the CLIENT and optional parameters. + -- @param #SET_CLIENT self + -- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_CLIENT. The function needs to accept a CLIENT parameter. + -- @return #SET_CLIENT self + function SET_CLIENT:ForEachClient( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self:GetSet() ) + + return self + end + + --- Iterate the SET_CLIENT and call an iterator function for each **alive** CLIENT presence completely in a @{Zone}, providing the CLIENT and optional parameters to the called function. + -- @param #SET_CLIENT self + -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. + -- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_CLIENT. The function needs to accept a CLIENT parameter. + -- @return #SET_CLIENT self + function SET_CLIENT:ForEachClientInZone( ZoneObject, IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self:GetSet(), + --- @param Core.Zone#ZONE_BASE ZoneObject + -- @param Wrapper.Client#CLIENT ClientObject + function( ZoneObject, ClientObject ) + if ClientObject:IsInZone( ZoneObject ) then + return true + else + return false + end + end, { ZoneObject } ) + + return self + end + + --- Iterate the SET_CLIENT and call an iterator function for each **alive** CLIENT presence not in a @{Zone}, providing the CLIENT and optional parameters to the called function. + -- @param #SET_CLIENT self + -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. + -- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_CLIENT. The function needs to accept a CLIENT parameter. + -- @return #SET_CLIENT self + function SET_CLIENT:ForEachClientNotInZone( ZoneObject, IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self:GetSet(), + --- @param Core.Zone#ZONE_BASE ZoneObject + -- @param Wrapper.Client#CLIENT ClientObject + function( ZoneObject, ClientObject ) + if ClientObject:IsNotInZone( ZoneObject ) then + return true + else + return false + end + end, { ZoneObject } ) + + return self + end + + --- Iterate the SET_CLIENT and count alive units. + -- @param #SET_CLIENT self + -- @return #number count + function SET_CLIENT:CountAlive() + + local Set = self:GetSet() + + local CountU = 0 + for UnitID, UnitData in pairs(Set) do -- For each GROUP in SET_GROUP + if UnitData and UnitData:IsAlive() then + CountU = CountU + 1 + end + + end + + return CountU + end + + --- + -- @param #SET_CLIENT self + -- @param Wrapper.Client#CLIENT MClient + -- @return #SET_CLIENT self + function SET_CLIENT:IsIncludeObject( MClient ) + self:F2( MClient ) + + local MClientInclude = true + + if MClient then + local MClientName = MClient.UnitName + + if self.Filter.Active ~= nil then + local MClientActive = false + if self.Filter.Active == false or ( self.Filter.Active == true and MClient:IsActive() == true ) then + MClientActive = true + end + MClientInclude = MClientInclude and MClientActive + end + + if self.Filter.Coalitions then + local MClientCoalition = false + for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do + local ClientCoalitionID = _DATABASE:GetCoalitionFromClientTemplate( MClientName ) + self:T3( { "Coalition:", ClientCoalitionID, self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) + if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == ClientCoalitionID then + MClientCoalition = true + end + end + self:T( { "Evaluated Coalition", MClientCoalition } ) + MClientInclude = MClientInclude and MClientCoalition + end + + if self.Filter.Categories then + local MClientCategory = false + for CategoryID, CategoryName in pairs( self.Filter.Categories ) do + local ClientCategoryID = _DATABASE:GetCategoryFromClientTemplate( MClientName ) + self:T3( { "Category:", ClientCategoryID, self.FilterMeta.Categories[CategoryName], CategoryName } ) + if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == ClientCategoryID then + MClientCategory = true + end + end + self:T( { "Evaluated Category", MClientCategory } ) + MClientInclude = MClientInclude and MClientCategory + end + + if self.Filter.Types then + local MClientType = false + for TypeID, TypeName in pairs( self.Filter.Types ) do + self:T3( { "Type:", MClient:GetTypeName(), TypeName } ) + if TypeName == MClient:GetTypeName() then + MClientType = true + end + end + self:T( { "Evaluated Type", MClientType } ) + MClientInclude = MClientInclude and MClientType + end + + if self.Filter.Countries then + local MClientCountry = false + for CountryID, CountryName in pairs( self.Filter.Countries ) do + local ClientCountryID = _DATABASE:GetCountryFromClientTemplate(MClientName) + self:T3( { "Country:", ClientCountryID, country.id[CountryName], CountryName } ) + if country.id[CountryName] and country.id[CountryName] == ClientCountryID then + MClientCountry = true + end + end + self:T( { "Evaluated Country", MClientCountry } ) + MClientInclude = MClientInclude and MClientCountry + end + + if self.Filter.ClientPrefixes then + local MClientPrefix = false + for ClientPrefixId, ClientPrefix in pairs( self.Filter.ClientPrefixes ) do + self:T3( { "Prefix:", string.find( MClient.UnitName, ClientPrefix, 1 ), ClientPrefix } ) + if string.find( MClient.UnitName, ClientPrefix, 1 ) then + MClientPrefix = true + end + end + self:T( { "Evaluated Prefix", MClientPrefix } ) + MClientInclude = MClientInclude and MClientPrefix + end + end + + self:T2( MClientInclude ) + return MClientInclude + end + +end + + +do -- SET_PLAYER + + --- @type SET_PLAYER + -- @extends Core.Set#SET_BASE + + + + --- Mission designers can use the @{Core.Set#SET_PLAYER} class to build sets of units belonging to alive players: + -- + -- ## SET_PLAYER constructor + -- + -- Create a new SET_PLAYER object with the @{#SET_PLAYER.New} method: + -- + -- * @{#SET_PLAYER.New}: Creates a new SET_PLAYER object. + -- + -- ## SET_PLAYER filter criteria + -- + -- You can set filter criteria to define the set of clients within the SET_PLAYER. + -- Filter criteria are defined by: + -- + -- * @{#SET_PLAYER.FilterCoalitions}: Builds the SET_PLAYER with the clients belonging to the coalition(s). + -- * @{#SET_PLAYER.FilterCategories}: Builds the SET_PLAYER with the clients belonging to the category(ies). + -- * @{#SET_PLAYER.FilterTypes}: Builds the SET_PLAYER with the clients belonging to the client type(s). + -- * @{#SET_PLAYER.FilterCountries}: Builds the SET_PLAYER with the clients belonging to the country(ies). + -- * @{#SET_PLAYER.FilterPrefixes}: Builds the SET_PLAYER with the clients sharing the same string(s) in their unit/pilot name. **ATTENTION** Bad naming convention as this *does not* only filter prefixes. + -- + -- Once the filter criteria have been set for the SET_PLAYER, you can start filtering using: + -- + -- * @{#SET_PLAYER.FilterStart}: Starts the filtering of the clients within the SET_PLAYER. + -- + -- Planned filter criteria within development are (so these are not yet available): + -- + -- * @{#SET_PLAYER.FilterZones}: Builds the SET_PLAYER with the clients within a @{Core.Zone#ZONE}. + -- + -- ## SET_PLAYER iterators + -- + -- Once the filters have been defined and the SET_PLAYER has been built, you can iterate the SET_PLAYER with the available iterator methods. + -- The iterator methods will walk the SET_PLAYER set, and call for each element within the set a function that you provide. + -- The following iterator methods are currently available within the SET_PLAYER: + -- + -- * @{#SET_PLAYER.ForEachClient}: Calls a function for each alive client it finds within the SET_PLAYER. + -- + -- === + -- @field #SET_PLAYER SET_PLAYER + SET_PLAYER = { + ClassName = "SET_PLAYER", + Clients = {}, + Filter = { + Coalitions = nil, + Categories = nil, + Types = nil, + Countries = nil, + ClientPrefixes = nil, + }, + FilterMeta = { + Coalitions = { + red = coalition.side.RED, + blue = coalition.side.BLUE, + neutral = coalition.side.NEUTRAL, + }, + Categories = { + plane = Unit.Category.AIRPLANE, + helicopter = Unit.Category.HELICOPTER, + ground = Unit.Category.GROUND_UNIT, + ship = Unit.Category.SHIP, + structure = Unit.Category.STRUCTURE, + }, + }, + } + + + --- Creates a new SET_PLAYER object, building a set of clients belonging to a coalitions, categories, countries, types or with defined prefix names. + -- @param #SET_PLAYER self + -- @return #SET_PLAYER + -- @usage + -- -- Define a new SET_PLAYER Object. This DBObject will contain a reference to all Clients. + -- DBObject = SET_PLAYER:New() + function SET_PLAYER:New() + -- Inherits from BASE + local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.PLAYERS ) ) + + return self + end + + --- Add CLIENT(s) to SET_PLAYER. + -- @param Core.Set#SET_PLAYER self + -- @param #string AddClientNames A single name or an array of CLIENT names. + -- @return self + function SET_PLAYER:AddClientsByName( AddClientNames ) + + local AddClientNamesArray = ( type( AddClientNames ) == "table" ) and AddClientNames or { AddClientNames } + + for AddClientID, AddClientName in pairs( AddClientNamesArray ) do + self:Add( AddClientName, CLIENT:FindByName( AddClientName ) ) + end + + return self + end + + --- Remove CLIENT(s) from SET_PLAYER. + -- @param Core.Set#SET_PLAYER self + -- @param Wrapper.Client#CLIENT RemoveClientNames A single name or an array of CLIENT names. + -- @return self + function SET_PLAYER:RemoveClientsByName( RemoveClientNames ) + + local RemoveClientNamesArray = ( type( RemoveClientNames ) == "table" ) and RemoveClientNames or { RemoveClientNames } + + for RemoveClientID, RemoveClientName in pairs( RemoveClientNamesArray ) do + self:Remove( RemoveClientName.ClientName ) + end + + return self + end + + + --- Finds a Client based on the Player Name. + -- @param #SET_PLAYER self + -- @param #string PlayerName + -- @return Wrapper.Client#CLIENT The found Client. + function SET_PLAYER:FindClient( PlayerName ) + + local ClientFound = self.Set[PlayerName] + return ClientFound + end + + + + --- Builds a set of clients of coalitions joined by specific players. + -- Possible current coalitions are red, blue and neutral. + -- @param #SET_PLAYER self + -- @param #string Coalitions Can take the following values: "red", "blue", "neutral". + -- @return #SET_PLAYER self + function SET_PLAYER:FilterCoalitions( Coalitions ) + if not self.Filter.Coalitions then + self.Filter.Coalitions = {} + end + if type( Coalitions ) ~= "table" then + Coalitions = { Coalitions } + end + for CoalitionID, Coalition in pairs( Coalitions ) do + self.Filter.Coalitions[Coalition] = Coalition + end + return self + end + + + --- Builds a set of clients out of categories joined by players. + -- Possible current categories are plane, helicopter, ground, ship. + -- @param #SET_PLAYER self + -- @param #string Categories Can take the following values: "plane", "helicopter", "ground", "ship". + -- @return #SET_PLAYER self + function SET_PLAYER:FilterCategories( Categories ) + if not self.Filter.Categories then + self.Filter.Categories = {} + end + if type( Categories ) ~= "table" then + Categories = { Categories } + end + for CategoryID, Category in pairs( Categories ) do + self.Filter.Categories[Category] = Category + end + return self + end + + + --- Builds a set of clients of defined client types joined by players. + -- Possible current types are those types known within DCS world. + -- @param #SET_PLAYER self + -- @param #string Types Can take those type strings known within DCS world. + -- @return #SET_PLAYER self + function SET_PLAYER:FilterTypes( Types ) + if not self.Filter.Types then + self.Filter.Types = {} + end + if type( Types ) ~= "table" then + Types = { Types } + end + for TypeID, Type in pairs( Types ) do + self.Filter.Types[Type] = Type + end + return self + end + + + --- Builds a set of clients of defined countries. + -- Possible current countries are those known within DCS world. + -- @param #SET_PLAYER self + -- @param #string Countries Can take those country strings known within DCS world. + -- @return #SET_PLAYER self + function SET_PLAYER:FilterCountries( Countries ) + if not self.Filter.Countries then + self.Filter.Countries = {} + end + if type( Countries ) ~= "table" then + Countries = { Countries } + end + for CountryID, Country in pairs( Countries ) do + self.Filter.Countries[Country] = Country + end + return self + end + + + --- Builds a set of PLAYERs that contain the given string in their unit/pilot name. + -- **Attention!** Bad naming convention as this **does not** filter only **prefixes** but all player clients that **contain** the string. + -- @param #SET_PLAYER self + -- @param #string Prefixes The string pattern(s) that needs to be contained in the unit/pilot name. Can also be passed as a `#table` of strings. + -- @return #SET_PLAYER self + function SET_PLAYER:FilterPrefixes( Prefixes ) + if not self.Filter.ClientPrefixes then + self.Filter.ClientPrefixes = {} + end + if type( Prefixes ) ~= "table" then + Prefixes = { Prefixes } + end + for PrefixID, Prefix in pairs( Prefixes ) do + self.Filter.ClientPrefixes[Prefix] = Prefix + end + return self + end + + + + + --- Starts the filtering. + -- @param #SET_PLAYER self + -- @return #SET_PLAYER self + function SET_PLAYER:FilterStart() + + if _DATABASE then + self:_FilterStart() + self:HandleEvent( EVENTS.Birth, self._EventOnBirth ) + self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) + self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) + end + + return self + end + + --- Handles the Database to check on an event (birth) that the Object was added in the Database. + -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! + -- @param #SET_PLAYER self + -- @param Core.Event#EVENTDATA Event + -- @return #string The name of the CLIENT + -- @return #table The CLIENT + function SET_PLAYER:AddInDatabase( Event ) + self:F3( { Event } ) + + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] + end + + --- Handles the Database to check on any event that Object exists in the Database. + -- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! + -- @param #SET_PLAYER self + -- @param Core.Event#EVENTDATA Event + -- @return #string The name of the CLIENT + -- @return #table The CLIENT + function SET_PLAYER:FindInDatabase( Event ) + self:F3( { Event } ) + + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] + end + + --- Iterate the SET_PLAYER and call an interator function for each **alive** CLIENT, providing the CLIENT and optional parameters. + -- @param #SET_PLAYER self + -- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_PLAYER. The function needs to accept a CLIENT parameter. + -- @return #SET_PLAYER self + function SET_PLAYER:ForEachPlayer( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self:GetSet() ) + + return self + end + + --- Iterate the SET_PLAYER and call an iterator function for each **alive** CLIENT presence completely in a @{Zone}, providing the CLIENT and optional parameters to the called function. + -- @param #SET_PLAYER self + -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. + -- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_PLAYER. The function needs to accept a CLIENT parameter. + -- @return #SET_PLAYER self + function SET_PLAYER:ForEachPlayerInZone( ZoneObject, IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self:GetSet(), + --- @param Core.Zone#ZONE_BASE ZoneObject + -- @param Wrapper.Client#CLIENT ClientObject + function( ZoneObject, ClientObject ) + if ClientObject:IsInZone( ZoneObject ) then + return true + else + return false + end + end, { ZoneObject } ) + + return self + end + + --- Iterate the SET_PLAYER and call an iterator function for each **alive** CLIENT presence not in a @{Zone}, providing the CLIENT and optional parameters to the called function. + -- @param #SET_PLAYER self + -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. + -- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_PLAYER. The function needs to accept a CLIENT parameter. + -- @return #SET_PLAYER self + function SET_PLAYER:ForEachPlayerNotInZone( ZoneObject, IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self:GetSet(), + --- @param Core.Zone#ZONE_BASE ZoneObject + -- @param Wrapper.Client#CLIENT ClientObject + function( ZoneObject, ClientObject ) + if ClientObject:IsNotInZone( ZoneObject ) then + return true + else + return false + end + end, { ZoneObject } ) + + return self + end + + --- + -- @param #SET_PLAYER self + -- @param Wrapper.Client#CLIENT MClient + -- @return #SET_PLAYER self + function SET_PLAYER:IsIncludeObject( MClient ) + self:F2( MClient ) + + local MClientInclude = true + + if MClient then + local MClientName = MClient.UnitName + + if self.Filter.Coalitions then + local MClientCoalition = false + for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do + local ClientCoalitionID = _DATABASE:GetCoalitionFromClientTemplate( MClientName ) + self:T3( { "Coalition:", ClientCoalitionID, self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) + if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == ClientCoalitionID then + MClientCoalition = true + end + end + self:T( { "Evaluated Coalition", MClientCoalition } ) + MClientInclude = MClientInclude and MClientCoalition + end + + if self.Filter.Categories then + local MClientCategory = false + for CategoryID, CategoryName in pairs( self.Filter.Categories ) do + local ClientCategoryID = _DATABASE:GetCategoryFromClientTemplate( MClientName ) + self:T3( { "Category:", ClientCategoryID, self.FilterMeta.Categories[CategoryName], CategoryName } ) + if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == ClientCategoryID then + MClientCategory = true + end + end + self:T( { "Evaluated Category", MClientCategory } ) + MClientInclude = MClientInclude and MClientCategory + end + + if self.Filter.Types then + local MClientType = false + for TypeID, TypeName in pairs( self.Filter.Types ) do + self:T3( { "Type:", MClient:GetTypeName(), TypeName } ) + if TypeName == MClient:GetTypeName() then + MClientType = true + end + end + self:T( { "Evaluated Type", MClientType } ) + MClientInclude = MClientInclude and MClientType + end + + if self.Filter.Countries then + local MClientCountry = false + for CountryID, CountryName in pairs( self.Filter.Countries ) do + local ClientCountryID = _DATABASE:GetCountryFromClientTemplate(MClientName) + self:T3( { "Country:", ClientCountryID, country.id[CountryName], CountryName } ) + if country.id[CountryName] and country.id[CountryName] == ClientCountryID then + MClientCountry = true + end + end + self:T( { "Evaluated Country", MClientCountry } ) + MClientInclude = MClientInclude and MClientCountry + end + + if self.Filter.ClientPrefixes then + local MClientPrefix = false + for ClientPrefixId, ClientPrefix in pairs( self.Filter.ClientPrefixes ) do + self:T3( { "Prefix:", string.find( MClient.UnitName, ClientPrefix, 1 ), ClientPrefix } ) + if string.find( MClient.UnitName, ClientPrefix, 1 ) then + MClientPrefix = true + end + end + self:T( { "Evaluated Prefix", MClientPrefix } ) + MClientInclude = MClientInclude and MClientPrefix + end + end + + self:T2( MClientInclude ) + return MClientInclude + end + +end + + +do -- SET_AIRBASE + + --- @type SET_AIRBASE + -- @extends Core.Set#SET_BASE + + --- Mission designers can use the @{Core.Set#SET_AIRBASE} class to build sets of airbases optionally belonging to certain: + -- + -- * Coalitions + -- + -- ## SET_AIRBASE constructor + -- + -- Create a new SET_AIRBASE object with the @{#SET_AIRBASE.New} method: + -- + -- * @{#SET_AIRBASE.New}: Creates a new SET_AIRBASE object. + -- + -- ## Add or Remove AIRBASEs from SET_AIRBASE + -- + -- AIRBASEs can be added and removed using the @{Core.Set#SET_AIRBASE.AddAirbasesByName} and @{Core.Set#SET_AIRBASE.RemoveAirbasesByName} respectively. + -- These methods take a single AIRBASE name or an array of AIRBASE names to be added or removed from SET_AIRBASE. + -- + -- ## SET_AIRBASE filter criteria + -- + -- You can set filter criteria to define the set of clients within the SET_AIRBASE. + -- Filter criteria are defined by: + -- + -- * @{#SET_AIRBASE.FilterCoalitions}: Builds the SET_AIRBASE with the airbases belonging to the coalition(s). + -- + -- Once the filter criteria have been set for the SET_AIRBASE, you can start filtering using: + -- + -- * @{#SET_AIRBASE.FilterStart}: Starts the filtering of the airbases within the SET_AIRBASE. + -- + -- ## SET_AIRBASE iterators + -- + -- Once the filters have been defined and the SET_AIRBASE has been built, you can iterate the SET_AIRBASE with the available iterator methods. + -- The iterator methods will walk the SET_AIRBASE set, and call for each airbase within the set a function that you provide. + -- The following iterator methods are currently available within the SET_AIRBASE: + -- + -- * @{#SET_AIRBASE.ForEachAirbase}: Calls a function for each airbase it finds within the SET_AIRBASE. + -- + -- === + -- @field #SET_AIRBASE SET_AIRBASE + SET_AIRBASE = { + ClassName = "SET_AIRBASE", + Airbases = {}, + Filter = { + Coalitions = nil, + }, + FilterMeta = { + Coalitions = { + red = coalition.side.RED, + blue = coalition.side.BLUE, + neutral = coalition.side.NEUTRAL, + }, + Categories = { + airdrome = Airbase.Category.AIRDROME, + helipad = Airbase.Category.HELIPAD, + ship = Airbase.Category.SHIP, + }, + }, + } + + + --- Creates a new SET_AIRBASE object, building a set of airbases belonging to a coalitions and categories. + -- @param #SET_AIRBASE self + -- @return #SET_AIRBASE self + -- @usage + -- -- Define a new SET_AIRBASE Object. The DatabaseSet will contain a reference to all Airbases. + -- DatabaseSet = SET_AIRBASE:New() + function SET_AIRBASE:New() + -- Inherits from BASE + local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.AIRBASES ) ) + + return self + end + + --- Add an AIRBASE object to SET_AIRBASE. + -- @param Core.Set#SET_AIRBASE self + -- @param Wrapper.Airbase#AIRBASE airbase Airbase that should be added to the set. + -- @return self + function SET_AIRBASE:AddAirbase( airbase ) + + self:Add( airbase:GetName(), airbase ) + + return self + end + + --- Add AIRBASEs to SET_AIRBASE. + -- @param Core.Set#SET_AIRBASE self + -- @param #string AddAirbaseNames A single name or an array of AIRBASE names. + -- @return self + function SET_AIRBASE:AddAirbasesByName( AddAirbaseNames ) + + local AddAirbaseNamesArray = ( type( AddAirbaseNames ) == "table" ) and AddAirbaseNames or { AddAirbaseNames } + + for AddAirbaseID, AddAirbaseName in pairs( AddAirbaseNamesArray ) do + self:Add( AddAirbaseName, AIRBASE:FindByName( AddAirbaseName ) ) + end + + return self + end + + --- Remove AIRBASEs from SET_AIRBASE. + -- @param Core.Set#SET_AIRBASE self + -- @param Wrapper.Airbase#AIRBASE RemoveAirbaseNames A single name or an array of AIRBASE names. + -- @return self + function SET_AIRBASE:RemoveAirbasesByName( RemoveAirbaseNames ) + + local RemoveAirbaseNamesArray = ( type( RemoveAirbaseNames ) == "table" ) and RemoveAirbaseNames or { RemoveAirbaseNames } + + for RemoveAirbaseID, RemoveAirbaseName in pairs( RemoveAirbaseNamesArray ) do + self:Remove( RemoveAirbaseName ) + end + + return self + end + + + --- Finds a Airbase based on the Airbase Name. + -- @param #SET_AIRBASE self + -- @param #string AirbaseName + -- @return Wrapper.Airbase#AIRBASE The found Airbase. + function SET_AIRBASE:FindAirbase( AirbaseName ) + + local AirbaseFound = self.Set[AirbaseName] + return AirbaseFound + end + + + --- Finds an Airbase in range of a coordinate. + -- @param #SET_AIRBASE self + -- @param Core.Point#COORDINATE Coordinate + -- @param #number Range + -- @return Wrapper.Airbase#AIRBASE The found Airbase. + function SET_AIRBASE:FindAirbaseInRange( Coordinate, Range ) + + local AirbaseFound = nil + + for AirbaseName, AirbaseObject in pairs( self.Set ) do + + local AirbaseCoordinate = AirbaseObject:GetCoordinate() + local Distance = Coordinate:Get2DDistance( AirbaseCoordinate ) + + self:F({Distance=Distance}) + + if Distance <= Range then + AirbaseFound = AirbaseObject + break + end + + end + + return AirbaseFound + end + + + --- Finds a random Airbase in the set. + -- @param #SET_AIRBASE self + -- @return Wrapper.Airbase#AIRBASE The found Airbase. + function SET_AIRBASE:GetRandomAirbase() + + local RandomAirbase = self:GetRandom() + self:F( { RandomAirbase = RandomAirbase:GetName() } ) + + return RandomAirbase + end + + + + --- Builds a set of airbases of coalitions. + -- Possible current coalitions are red, blue and neutral. + -- @param #SET_AIRBASE self + -- @param #string Coalitions Can take the following values: "red", "blue", "neutral". + -- @return #SET_AIRBASE self + function SET_AIRBASE:FilterCoalitions( Coalitions ) + if not self.Filter.Coalitions then + self.Filter.Coalitions = {} + end + if type( Coalitions ) ~= "table" then + Coalitions = { Coalitions } + end + for CoalitionID, Coalition in pairs( Coalitions ) do + self.Filter.Coalitions[Coalition] = Coalition + end + return self + end + + + --- Builds a set of airbases out of categories. + -- Possible current categories are plane, helicopter, ground, ship. + -- @param #SET_AIRBASE self + -- @param #string Categories Can take the following values: "airdrome", "helipad", "ship". + -- @return #SET_AIRBASE self + function SET_AIRBASE:FilterCategories( Categories ) + if not self.Filter.Categories then + self.Filter.Categories = {} + end + if type( Categories ) ~= "table" then + Categories = { Categories } + end + for CategoryID, Category in pairs( Categories ) do + self.Filter.Categories[Category] = Category + end + return self + end + + --- Starts the filtering. + -- @param #SET_AIRBASE self + -- @return #SET_AIRBASE self + function SET_AIRBASE:FilterStart() + + if _DATABASE then + + -- We use the BaseCaptured event, which is generated by DCS when a base got captured. + self:HandleEvent(EVENTS.BaseCaptured) + self:HandleEvent(EVENTS.Dead) + + -- We initialize the first set. + for ObjectName, Object in pairs( self.Database ) do + if self:IsIncludeObject( Object ) then + self:Add( ObjectName, Object ) + else + self:RemoveAirbasesByName( ObjectName ) + end + end + end + + return self + end + + --- Base capturing event. + -- @param #SET_AIRBASE self + -- @param Core.Event#EVENT EventData + function SET_AIRBASE:OnEventBaseCaptured(EventData) + + -- When a base got captured, we reevaluate the set. + for ObjectName, Object in pairs( self.Database ) do + if self:IsIncludeObject( Object ) then + -- We add captured bases on yet in the set. + self:Add( ObjectName, Object ) + else + -- We remove captured bases that are not anymore part of the set. + self:RemoveAirbasesByName( ObjectName ) + end + end + + end + + --- Dead event. + -- @param #SET_AIRBASE self + -- @param Core.Event#EVENT EventData + function SET_AIRBASE:OnEventDead(EventData) + + local airbaseName, airbase=self:FindInDatabase(EventData) + + if airbase and (airbase:IsShip() or airbase:IsHelipad()) then + self:RemoveAirbasesByName(airbaseName) + end + + end + + + --- Handles the Database to check on an event (birth) that the Object was added in the Database. + -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! + -- @param #SET_AIRBASE self + -- @param Core.Event#EVENTDATA Event Event data. + -- @return #string The name of the AIRBASE. + -- @return Wrapper.Airbase#AIRBASE The AIRBASE object. + function SET_AIRBASE:AddInDatabase( Event ) + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] + end + + --- Handles the Database to check on any event that Object exists in the Database. + -- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! + -- @param #SET_AIRBASE self + -- @param Core.Event#EVENTDATA Event Event data. + -- @return #string The name of the AIRBASE. + -- @return Wrapper.Airbase#AIRBASE The AIRBASE object. + function SET_AIRBASE:FindInDatabase( Event ) + self:F3( { Event } ) + + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] + end + + --- Iterate the SET_AIRBASE and call an interator function for each AIRBASE, providing the AIRBASE and optional parameters. + -- @param #SET_AIRBASE self + -- @param #function IteratorFunction The function that will be called when there is an alive AIRBASE in the SET_AIRBASE. The function needs to accept a AIRBASE parameter. + -- @return #SET_AIRBASE self + function SET_AIRBASE:ForEachAirbase( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self:GetSet() ) + + return self + end + + --- Iterate the SET_AIRBASE while identifying the nearest @{Wrapper.Airbase#AIRBASE} from a @{Core.Point#POINT_VEC2}. + -- @param #SET_AIRBASE self + -- @param Core.Point#POINT_VEC2 PointVec2 A @{Core.Point#POINT_VEC2} object from where to evaluate the closest @{Wrapper.Airbase#AIRBASE}. + -- @return Wrapper.Airbase#AIRBASE The closest @{Wrapper.Airbase#AIRBASE}. + function SET_AIRBASE:FindNearestAirbaseFromPointVec2( PointVec2 ) + self:F2( PointVec2 ) + + local NearestAirbase = self:FindNearestObjectFromPointVec2( PointVec2 ) + return NearestAirbase + end + + + + --- + -- @param #SET_AIRBASE self + -- @param Wrapper.Airbase#AIRBASE MAirbase + -- @return #SET_AIRBASE self + function SET_AIRBASE:IsIncludeObject( MAirbase ) + self:F2( MAirbase ) + + local MAirbaseInclude = true + + if MAirbase then + local MAirbaseName = MAirbase:GetName() + + if self.Filter.Coalitions then + local MAirbaseCoalition = false + for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do + local AirbaseCoalitionID = _DATABASE:GetCoalitionFromAirbase( MAirbaseName ) + self:T3( { "Coalition:", AirbaseCoalitionID, self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) + if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == AirbaseCoalitionID then + MAirbaseCoalition = true + end + end + self:T( { "Evaluated Coalition", MAirbaseCoalition } ) + MAirbaseInclude = MAirbaseInclude and MAirbaseCoalition + end + + if self.Filter.Categories then + local MAirbaseCategory = false + for CategoryID, CategoryName in pairs( self.Filter.Categories ) do + local AirbaseCategoryID = _DATABASE:GetCategoryFromAirbase( MAirbaseName ) + self:T3( { "Category:", AirbaseCategoryID, self.FilterMeta.Categories[CategoryName], CategoryName } ) + if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == AirbaseCategoryID then + MAirbaseCategory = true + end + end + self:T( { "Evaluated Category", MAirbaseCategory } ) + MAirbaseInclude = MAirbaseInclude and MAirbaseCategory + end + end + + self:T2( MAirbaseInclude ) + return MAirbaseInclude + end + +end + + +do -- SET_CARGO + + --- @type SET_CARGO + -- @extends Core.Set#SET_BASE + + --- Mission designers can use the @{Core.Set#SET_CARGO} class to build sets of cargos optionally belonging to certain: + -- + -- * Coalitions + -- * Types + -- * Name or Prefix + -- + -- ## SET_CARGO constructor + -- + -- Create a new SET_CARGO object with the @{#SET_CARGO.New} method: + -- + -- * @{#SET_CARGO.New}: Creates a new SET_CARGO object. + -- + -- ## Add or Remove CARGOs from SET_CARGO + -- + -- CARGOs can be added and removed using the @{Core.Set#SET_CARGO.AddCargosByName} and @{Core.Set#SET_CARGO.RemoveCargosByName} respectively. + -- These methods take a single CARGO name or an array of CARGO names to be added or removed from SET_CARGO. + -- + -- ## SET_CARGO filter criteria + -- + -- You can set filter criteria to automatically maintain the SET_CARGO contents. + -- Filter criteria are defined by: + -- + -- * @{#SET_CARGO.FilterCoalitions}: Builds the SET_CARGO with the cargos belonging to the coalition(s). + -- * @{#SET_CARGO.FilterPrefixes}: Builds the SET_CARGO with the cargos containing the same string(s). **ATTENTION** Bad naming convention as this *does not* only filter *prefixes*. + -- * @{#SET_CARGO.FilterTypes}: Builds the SET_CARGO with the cargos belonging to the cargo type(s). + -- * @{#SET_CARGO.FilterCountries}: Builds the SET_CARGO with the cargos belonging to the country(ies). + -- + -- Once the filter criteria have been set for the SET_CARGO, you can start filtering using: + -- + -- * @{#SET_CARGO.FilterStart}: Starts the filtering of the cargos within the SET_CARGO. + -- + -- ## SET_CARGO iterators + -- + -- Once the filters have been defined and the SET_CARGO has been built, you can iterate the SET_CARGO with the available iterator methods. + -- The iterator methods will walk the SET_CARGO set, and call for each cargo within the set a function that you provide. + -- The following iterator methods are currently available within the SET_CARGO: + -- + -- * @{#SET_CARGO.ForEachCargo}: Calls a function for each cargo it finds within the SET_CARGO. + -- + -- @field #SET_CARGO SET_CARGO + -- + SET_CARGO = { + ClassName = "SET_CARGO", + Cargos = {}, + Filter = { + Coalitions = nil, + Types = nil, + Countries = nil, + ClientPrefixes = nil, + }, + FilterMeta = { + Coalitions = { + red = coalition.side.RED, + blue = coalition.side.BLUE, + neutral = coalition.side.NEUTRAL, + }, + }, + } + + + --- Creates a new SET_CARGO object, building a set of cargos belonging to a coalitions and categories. + -- @param #SET_CARGO self + -- @return #SET_CARGO + -- @usage + -- -- Define a new SET_CARGO Object. The DatabaseSet will contain a reference to all Cargos. + -- DatabaseSet = SET_CARGO:New() + function SET_CARGO:New() --R2.1 + -- Inherits from BASE + local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.CARGOS ) ) -- #SET_CARGO + + return self + end + + + --- (R2.1) Add CARGO to SET_CARGO. + -- @param Core.Set#SET_CARGO self + -- @param Cargo.Cargo#CARGO Cargo A single cargo. + -- @return Core.Set#SET_CARGO self + function SET_CARGO:AddCargo( Cargo ) --R2.4 + + self:Add( Cargo:GetName(), Cargo ) + + return self + end + + + --- (R2.1) Add CARGOs to SET_CARGO. + -- @param Core.Set#SET_CARGO self + -- @param #string AddCargoNames A single name or an array of CARGO names. + -- @return Core.Set#SET_CARGO self + function SET_CARGO:AddCargosByName( AddCargoNames ) --R2.1 + + local AddCargoNamesArray = ( type( AddCargoNames ) == "table" ) and AddCargoNames or { AddCargoNames } + + for AddCargoID, AddCargoName in pairs( AddCargoNamesArray ) do + self:Add( AddCargoName, CARGO:FindByName( AddCargoName ) ) + end + + return self + end + + --- (R2.1) Remove CARGOs from SET_CARGO. + -- @param Core.Set#SET_CARGO self + -- @param Wrapper.Cargo#CARGO RemoveCargoNames A single name or an array of CARGO names. + -- @return Core.Set#SET_CARGO self + function SET_CARGO:RemoveCargosByName( RemoveCargoNames ) --R2.1 + + local RemoveCargoNamesArray = ( type( RemoveCargoNames ) == "table" ) and RemoveCargoNames or { RemoveCargoNames } + + for RemoveCargoID, RemoveCargoName in pairs( RemoveCargoNamesArray ) do + self:Remove( RemoveCargoName.CargoName ) + end + + return self + end + + + --- (R2.1) Finds a Cargo based on the Cargo Name. + -- @param #SET_CARGO self + -- @param #string CargoName + -- @return Wrapper.Cargo#CARGO The found Cargo. + function SET_CARGO:FindCargo( CargoName ) --R2.1 + + local CargoFound = self.Set[CargoName] + return CargoFound + end + + + + --- (R2.1) Builds a set of cargos of coalitions. + -- Possible current coalitions are red, blue and neutral. + -- @param #SET_CARGO self + -- @param #string Coalitions Can take the following values: "red", "blue", "neutral". + -- @return #SET_CARGO self + function SET_CARGO:FilterCoalitions( Coalitions ) --R2.1 + if not self.Filter.Coalitions then + self.Filter.Coalitions = {} + end + if type( Coalitions ) ~= "table" then + Coalitions = { Coalitions } + end + for CoalitionID, Coalition in pairs( Coalitions ) do + self.Filter.Coalitions[Coalition] = Coalition + end + return self + end + + --- (R2.1) Builds a set of cargos of defined cargo types. + -- Possible current types are those types known within DCS world. + -- @param #SET_CARGO self + -- @param #string Types Can take those type strings known within DCS world. + -- @return #SET_CARGO self + function SET_CARGO:FilterTypes( Types ) --R2.1 + if not self.Filter.Types then + self.Filter.Types = {} + end + if type( Types ) ~= "table" then + Types = { Types } + end + for TypeID, Type in pairs( Types ) do + self.Filter.Types[Type] = Type + end + return self + end + + + --- (R2.1) Builds a set of cargos of defined countries. + -- Possible current countries are those known within DCS world. + -- @param #SET_CARGO self + -- @param #string Countries Can take those country strings known within DCS world. + -- @return #SET_CARGO self + function SET_CARGO:FilterCountries( Countries ) --R2.1 + if not self.Filter.Countries then + self.Filter.Countries = {} + end + if type( Countries ) ~= "table" then + Countries = { Countries } + end + for CountryID, Country in pairs( Countries ) do + self.Filter.Countries[Country] = Country + end + return self + end + + + --- Builds a set of CARGOs that contain a given string in their name. + -- **Attention!** Bad naming convention as this **does not** filter only **prefixes** but all cargos that **contain** the string. + -- @param #SET_CARGO self + -- @param #string Prefixes The string pattern(s) that need to be in the cargo name. Can also be passed as a `#table` of strings. + -- @return #SET_CARGO self + function SET_CARGO:FilterPrefixes( Prefixes ) --R2.1 + if not self.Filter.CargoPrefixes then + self.Filter.CargoPrefixes = {} + end + if type( Prefixes ) ~= "table" then + Prefixes = { Prefixes } + end + for PrefixID, Prefix in pairs( Prefixes ) do + self.Filter.CargoPrefixes[Prefix] = Prefix + end + return self + end + + + + --- (R2.1) Starts the filtering. + -- @param #SET_CARGO self + -- @return #SET_CARGO self + function SET_CARGO:FilterStart() --R2.1 + + if _DATABASE then + self:_FilterStart() + self:HandleEvent( EVENTS.NewCargo ) + self:HandleEvent( EVENTS.DeleteCargo ) + end + + return self + end + + --- Stops the filtering for the defined collection. + -- @param #SET_CARGO self + -- @return #SET_CARGO self + function SET_CARGO:FilterStop() + + self:UnHandleEvent( EVENTS.NewCargo ) + self:UnHandleEvent( EVENTS.DeleteCargo ) + + return self + end + + + --- (R2.1) Handles the Database to check on an event (birth) that the Object was added in the Database. + -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! + -- @param #SET_CARGO self + -- @param Core.Event#EVENTDATA Event + -- @return #string The name of the CARGO + -- @return #table The CARGO + function SET_CARGO:AddInDatabase( Event ) --R2.1 + self:F3( { Event } ) + + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] + end + + --- (R2.1) Handles the Database to check on any event that Object exists in the Database. + -- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! + -- @param #SET_CARGO self + -- @param Core.Event#EVENTDATA Event + -- @return #string The name of the CARGO + -- @return #table The CARGO + function SET_CARGO:FindInDatabase( Event ) --R2.1 + self:F3( { Event } ) + + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] + end + + --- (R2.1) Iterate the SET_CARGO and call an interator function for each CARGO, providing the CARGO and optional parameters. + -- @param #SET_CARGO self + -- @param #function IteratorFunction The function that will be called when there is an alive CARGO in the SET_CARGO. The function needs to accept a CARGO parameter. + -- @return #SET_CARGO self + function SET_CARGO:ForEachCargo( IteratorFunction, ... ) --R2.1 + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self:GetSet() ) + + return self + end + + --- (R2.1) Iterate the SET_CARGO while identifying the nearest @{Cargo.Cargo#CARGO} from a @{Core.Point#POINT_VEC2}. + -- @param #SET_CARGO self + -- @param Core.Point#POINT_VEC2 PointVec2 A @{Core.Point#POINT_VEC2} object from where to evaluate the closest @{Cargo.Cargo#CARGO}. + -- @return Wrapper.Cargo#CARGO The closest @{Cargo.Cargo#CARGO}. + function SET_CARGO:FindNearestCargoFromPointVec2( PointVec2 ) --R2.1 + self:F2( PointVec2 ) + + local NearestCargo = self:FindNearestObjectFromPointVec2( PointVec2 ) + return NearestCargo + end + + function SET_CARGO:FirstCargoWithState( State ) + + local FirstCargo = nil + + for CargoName, Cargo in pairs( self.Set ) do + if Cargo:Is( State ) then + FirstCargo = Cargo + break + end + end + + return FirstCargo + end + + function SET_CARGO:FirstCargoWithStateAndNotDeployed( State ) + + local FirstCargo = nil + + for CargoName, Cargo in pairs( self.Set ) do + if Cargo:Is( State ) and not Cargo:IsDeployed() then + FirstCargo = Cargo + break + end + end + + return FirstCargo + end + + + --- Iterate the SET_CARGO while identifying the first @{Cargo.Cargo#CARGO} that is UnLoaded. + -- @param #SET_CARGO self + -- @return Cargo.Cargo#CARGO The first @{Cargo.Cargo#CARGO}. + function SET_CARGO:FirstCargoUnLoaded() + local FirstCargo = self:FirstCargoWithState( "UnLoaded" ) + return FirstCargo + end + + + --- Iterate the SET_CARGO while identifying the first @{Cargo.Cargo#CARGO} that is UnLoaded and not Deployed. + -- @param #SET_CARGO self + -- @return Cargo.Cargo#CARGO The first @{Cargo.Cargo#CARGO}. + function SET_CARGO:FirstCargoUnLoadedAndNotDeployed() + local FirstCargo = self:FirstCargoWithStateAndNotDeployed( "UnLoaded" ) + return FirstCargo + end + + + --- Iterate the SET_CARGO while identifying the first @{Cargo.Cargo#CARGO} that is Loaded. + -- @param #SET_CARGO self + -- @return Cargo.Cargo#CARGO The first @{Cargo.Cargo#CARGO}. + function SET_CARGO:FirstCargoLoaded() + local FirstCargo = self:FirstCargoWithState( "Loaded" ) + return FirstCargo + end + + + --- Iterate the SET_CARGO while identifying the first @{Cargo.Cargo#CARGO} that is Deployed. + -- @param #SET_CARGO self + -- @return Cargo.Cargo#CARGO The first @{Cargo.Cargo#CARGO}. + function SET_CARGO:FirstCargoDeployed() + local FirstCargo = self:FirstCargoWithState( "Deployed" ) + return FirstCargo + end + + + + + --- (R2.1) + -- @param #SET_CARGO self + -- @param AI.AI_Cargo#AI_CARGO MCargo + -- @return #SET_CARGO self + function SET_CARGO:IsIncludeObject( MCargo ) --R2.1 + self:F2( MCargo ) + + local MCargoInclude = true + + if MCargo then + local MCargoName = MCargo:GetName() + + if self.Filter.Coalitions then + local MCargoCoalition = false + for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do + local CargoCoalitionID = MCargo:GetCoalition() + self:T3( { "Coalition:", CargoCoalitionID, self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) + if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == CargoCoalitionID then + MCargoCoalition = true + end + end + self:F( { "Evaluated Coalition", MCargoCoalition } ) + MCargoInclude = MCargoInclude and MCargoCoalition + end + + if self.Filter.Types then + local MCargoType = false + for TypeID, TypeName in pairs( self.Filter.Types ) do + self:T3( { "Type:", MCargo:GetType(), TypeName } ) + if TypeName == MCargo:GetType() then + MCargoType = true + end + end + self:F( { "Evaluated Type", MCargoType } ) + MCargoInclude = MCargoInclude and MCargoType + end + + if self.Filter.CargoPrefixes then + local MCargoPrefix = false + for CargoPrefixId, CargoPrefix in pairs( self.Filter.CargoPrefixes ) do + self:T3( { "Prefix:", string.find( MCargo.Name, CargoPrefix, 1 ), CargoPrefix } ) + if string.find( MCargo.Name, CargoPrefix, 1 ) then + MCargoPrefix = true + end + end + self:F( { "Evaluated Prefix", MCargoPrefix } ) + MCargoInclude = MCargoInclude and MCargoPrefix + end + end + + self:T2( MCargoInclude ) + return MCargoInclude + end + + --- (R2.1) Handles the OnEventNewCargo event for the Set. + -- @param #SET_CARGO self + -- @param Core.Event#EVENTDATA EventData + function SET_CARGO:OnEventNewCargo( EventData ) --R2.1 + + self:F( { "New Cargo", EventData } ) + + if EventData.Cargo then + if EventData.Cargo and self:IsIncludeObject( EventData.Cargo ) then + self:Add( EventData.Cargo.Name , EventData.Cargo ) + end + end + end + + --- (R2.1) Handles the OnDead or OnCrash event for alive units set. + -- @param #SET_CARGO self + -- @param Core.Event#EVENTDATA EventData + function SET_CARGO:OnEventDeleteCargo( EventData ) --R2.1 + self:F3( { EventData } ) + + if EventData.Cargo then + local Cargo = _DATABASE:FindCargo( EventData.Cargo.Name ) + if Cargo and Cargo.Name then + + -- When cargo was deleted, it may probably be because of an S_EVENT_DEAD. + -- However, in the loading logic, an S_EVENT_DEAD is also generated after a Destroy() call. + -- And this is a problem because it will remove all entries from the SET_CARGOs. + -- To prevent this from happening, the Cargo object has a flag NoDestroy. + -- When true, the SET_CARGO won't Remove the Cargo object from the set. + -- This flag is switched off after the event handlers have been called in the EVENT class. + self:F( { CargoNoDestroy=Cargo.NoDestroy } ) + if Cargo.NoDestroy then + else + self:Remove( Cargo.Name ) + end + end + end + end + +end + + +do -- SET_ZONE + + --- @type SET_ZONE + -- @extends Core.Set#SET_BASE + + --- Mission designers can use the @{Core.Set#SET_ZONE} class to build sets of zones of various types. + -- + -- ## SET_ZONE constructor + -- + -- Create a new SET_ZONE object with the @{#SET_ZONE.New} method: + -- + -- * @{#SET_ZONE.New}: Creates a new SET_ZONE object. + -- + -- ## Add or Remove ZONEs from SET_ZONE + -- + -- ZONEs can be added and removed using the @{Core.Set#SET_ZONE.AddZonesByName} and @{Core.Set#SET_ZONE.RemoveZonesByName} respectively. + -- These methods take a single ZONE name or an array of ZONE names to be added or removed from SET_ZONE. + -- + -- ## SET_ZONE filter criteria + -- + -- You can set filter criteria to build the collection of zones in SET_ZONE. + -- Filter criteria are defined by: + -- + -- * @{#SET_ZONE.FilterPrefixes}: Builds the SET_ZONE with the zones having a certain text pattern in their name. **ATTENTION!** Bad naming convention as this *does not* only filter *prefixes*. + -- + -- Once the filter criteria have been set for the SET_ZONE, you can start filtering using: + -- + -- * @{#SET_ZONE.FilterStart}: Starts the filtering of the zones within the SET_ZONE. + -- + -- ## SET_ZONE iterators + -- + -- Once the filters have been defined and the SET_ZONE has been built, you can iterate the SET_ZONE with the available iterator methods. + -- The iterator methods will walk the SET_ZONE set, and call for each airbase within the set a function that you provide. + -- The following iterator methods are currently available within the SET_ZONE: + -- + -- * @{#SET_ZONE.ForEachZone}: Calls a function for each zone it finds within the SET_ZONE. + -- + -- === + -- @field #SET_ZONE SET_ZONE + SET_ZONE = { + ClassName = "SET_ZONE", + Zones = {}, + Filter = { + Prefixes = nil, + }, + FilterMeta = { + }, + } + + + --- Creates a new SET_ZONE object, building a set of zones. + -- @param #SET_ZONE self + -- @return #SET_ZONE self + -- @usage + -- -- Define a new SET_ZONE Object. The DatabaseSet will contain a reference to all Zones. + -- DatabaseSet = SET_ZONE:New() + function SET_ZONE:New() + -- Inherits from BASE + local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.ZONES ) ) + + return self + end + + --- Add ZONEs by a search name to SET_ZONE. + -- @param Core.Set#SET_ZONE self + -- @param #string AddZoneNames A single name or an array of ZONE_BASE names. + -- @return self + function SET_ZONE:AddZonesByName( AddZoneNames ) + + local AddZoneNamesArray = ( type( AddZoneNames ) == "table" ) and AddZoneNames or { AddZoneNames } + + for AddAirbaseID, AddZoneName in pairs( AddZoneNamesArray ) do + self:Add( AddZoneName, ZONE:FindByName( AddZoneName ) ) + end + + return self + end + + --- Add ZONEs to SET_ZONE. + -- @param Core.Set#SET_ZONE self + -- @param Core.Zone#ZONE_BASE Zone A ZONE_BASE object. + -- @return self + function SET_ZONE:AddZone( Zone ) + + self:Add( Zone:GetName(), Zone ) + + return self + end + + + --- Remove ZONEs from SET_ZONE. + -- @param Core.Set#SET_ZONE self + -- @param Core.Zone#ZONE_BASE RemoveZoneNames A single name or an array of ZONE_BASE names. + -- @return self + function SET_ZONE:RemoveZonesByName( RemoveZoneNames ) + + local RemoveZoneNamesArray = ( type( RemoveZoneNames ) == "table" ) and RemoveZoneNames or { RemoveZoneNames } + + for RemoveZoneID, RemoveZoneName in pairs( RemoveZoneNamesArray ) do + self:Remove( RemoveZoneName ) + end + + return self + end + + + --- Finds a Zone based on the Zone Name. + -- @param #SET_ZONE self + -- @param #string ZoneName + -- @return Core.Zone#ZONE_BASE The found Zone. + function SET_ZONE:FindZone( ZoneName ) + + local ZoneFound = self.Set[ZoneName] + return ZoneFound + end + + + --- Get a random zone from the set. + -- @param #SET_ZONE self + -- @param #number margin Number of tries to find a zone + -- @return Core.Zone#ZONE_BASE The random Zone. + -- @return #nil if no zone in the collection. + function SET_ZONE:GetRandomZone(margin) + + local margin = margin or 100 + if self:Count() ~= 0 then + + local Index = self.Index + local ZoneFound = nil -- Core.Zone#ZONE_BASE + + -- Loop until a zone has been found. + -- The :GetZoneMaybe() call will evaluate the probability for the zone to be selected. + -- If the zone is not selected, then nil is returned by :GetZoneMaybe() and the loop continues! + local counter = 0 + while (not ZoneFound) or (counter < margin) do + local ZoneRandom = math.random( 1, #Index ) + ZoneFound = self.Set[Index[ZoneRandom]]:GetZoneMaybe() + counter = counter + 1 + end + + return ZoneFound + end + + return nil + end + + + --- Set a zone probability. + -- @param #SET_ZONE self + -- @param #string ZoneName The name of the zone. + function SET_ZONE:SetZoneProbability( ZoneName, ZoneProbability ) + local Zone = self:FindZone( ZoneName ) + Zone:SetZoneProbability( ZoneProbability ) + end + + + + + --- Builds a set of ZONEs that contain the given string in their name. + -- **ATTENTION!** Bad naming convention as this **does not** filter only **prefixes** but all zones that **contain** the string. + -- @param #SET_ZONE self + -- @param #string Prefixes The string pattern(s) that need to be contained in the zone name. Can also be passed as a `#table` of strings. + -- @return #SET_ZONE self + function SET_ZONE:FilterPrefixes( Prefixes ) + if not self.Filter.Prefixes then + self.Filter.Prefixes = {} + end + if type( Prefixes ) ~= "table" then + Prefixes = { Prefixes } + end + for PrefixID, Prefix in pairs( Prefixes ) do + self.Filter.Prefixes[Prefix] = Prefix + end + return self + end + + + --- Starts the filtering. + -- @param #SET_ZONE self + -- @return #SET_ZONE self + function SET_ZONE:FilterStart() + + if _DATABASE then + + -- We initialize the first set. + for ObjectName, Object in pairs( self.Database ) do + if self:IsIncludeObject( Object ) then + self:Add( ObjectName, Object ) + else + self:RemoveZonesByName( ObjectName ) + end + end + end + + self:HandleEvent( EVENTS.NewZone ) + self:HandleEvent( EVENTS.DeleteZone ) + + return self + end + + --- Stops the filtering for the defined collection. + -- @param #SET_ZONE self + -- @return #SET_ZONE self + function SET_ZONE:FilterStop() + + self:UnHandleEvent( EVENTS.NewZone ) + self:UnHandleEvent( EVENTS.DeleteZone ) + + return self + end + + --- Handles the Database to check on an event (birth) that the Object was added in the Database. + -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! + -- @param #SET_ZONE self + -- @param Core.Event#EVENTDATA Event + -- @return #string The name of the AIRBASE + -- @return #table The AIRBASE + function SET_ZONE:AddInDatabase( Event ) + self:F3( { Event } ) + + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] + end + + --- Handles the Database to check on any event that Object exists in the Database. + -- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! + -- @param #SET_ZONE self + -- @param Core.Event#EVENTDATA Event + -- @return #string The name of the AIRBASE + -- @return #table The AIRBASE + function SET_ZONE:FindInDatabase( Event ) + self:F3( { Event } ) + + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] + end + + --- Iterate the SET_ZONE and call an interator function for each ZONE, providing the ZONE and optional parameters. + -- @param #SET_ZONE self + -- @param #function IteratorFunction The function that will be called when there is an alive ZONE in the SET_ZONE. The function needs to accept a AIRBASE parameter. + -- @return #SET_ZONE self + function SET_ZONE:ForEachZone( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self:GetSet() ) + + return self + end + + + --- + -- @param #SET_ZONE self + -- @param Core.Zone#ZONE_BASE MZone + -- @return #SET_ZONE self + function SET_ZONE:IsIncludeObject( MZone ) + self:F2( MZone ) + + local MZoneInclude = true + + if MZone then + local MZoneName = MZone:GetName() + + if self.Filter.Prefixes then + local MZonePrefix = false + for ZonePrefixId, ZonePrefix in pairs( self.Filter.Prefixes ) do + self:T3( { "Prefix:", string.find( MZoneName, ZonePrefix, 1 ), ZonePrefix } ) + if string.find( MZoneName, ZonePrefix, 1 ) then + MZonePrefix = true + end + end + self:T( { "Evaluated Prefix", MZonePrefix } ) + MZoneInclude = MZoneInclude and MZonePrefix + end + end + + self:T2( MZoneInclude ) + return MZoneInclude + end + + --- Handles the OnEventNewZone event for the Set. + -- @param #SET_ZONE self + -- @param Core.Event#EVENTDATA EventData + function SET_ZONE:OnEventNewZone( EventData ) --R2.1 + + self:F( { "New Zone", EventData } ) + + if EventData.Zone then + if EventData.Zone and self:IsIncludeObject( EventData.Zone ) then + self:Add( EventData.Zone.ZoneName , EventData.Zone ) + end + end + end + + --- Handles the OnDead or OnCrash event for alive units set. + -- @param #SET_ZONE self + -- @param Core.Event#EVENTDATA EventData + function SET_ZONE:OnEventDeleteZone( EventData ) --R2.1 + self:F3( { EventData } ) + + if EventData.Zone then + local Zone = _DATABASE:FindZone( EventData.Zone.ZoneName ) + if Zone and Zone.ZoneName then + + -- When cargo was deleted, it may probably be because of an S_EVENT_DEAD. + -- However, in the loading logic, an S_EVENT_DEAD is also generated after a Destroy() call. + -- And this is a problem because it will remove all entries from the SET_ZONEs. + -- To prevent this from happening, the Zone object has a flag NoDestroy. + -- When true, the SET_ZONE won't Remove the Zone object from the set. + -- This flag is switched off after the event handlers have been called in the EVENT class. + self:F( { ZoneNoDestroy=Zone.NoDestroy } ) + if Zone.NoDestroy then + else + self:Remove( Zone.ZoneName ) + end + end + end + end + + --- Validate if a coordinate is in one of the zones in the set. + -- Returns the ZONE object where the coordiante is located. + -- If zones overlap, the first zone that validates the test is returned. + -- @param #SET_ZONE self + -- @param Core.Point#COORDINATE Coordinate The coordinate to be searched. + -- @return Core.Zone#ZONE_BASE The zone that validates the coordinate location. + -- @return #nil No zone has been found. + function SET_ZONE:IsCoordinateInZone( Coordinate ) + + for _, Zone in pairs( self:GetSet() ) do + local Zone = Zone -- Core.Zone#ZONE_BASE + if Zone:IsCoordinateInZone( Coordinate ) then + return Zone + end + end + + return nil + end + +end + +do -- SET_ZONE_GOAL + + --- @type SET_ZONE_GOAL + -- @extends Core.Set#SET_BASE + + --- Mission designers can use the @{Core.Set#SET_ZONE_GOAL} class to build sets of zones of various types. + -- + -- ## SET_ZONE_GOAL constructor + -- + -- Create a new SET_ZONE_GOAL object with the @{#SET_ZONE_GOAL.New} method: + -- + -- * @{#SET_ZONE_GOAL.New}: Creates a new SET_ZONE_GOAL object. + -- + -- ## Add or Remove ZONEs from SET_ZONE_GOAL + -- + -- ZONEs can be added and removed using the @{Core.Set#SET_ZONE_GOAL.AddZonesByName} and @{Core.Set#SET_ZONE_GOAL.RemoveZonesByName} respectively. + -- These methods take a single ZONE name or an array of ZONE names to be added or removed from SET_ZONE_GOAL. + -- + -- ## SET_ZONE_GOAL filter criteria + -- + -- You can set filter criteria to build the collection of zones in SET_ZONE_GOAL. + -- Filter criteria are defined by: + -- + -- * @{#SET_ZONE_GOAL.FilterPrefixes}: Builds the SET_ZONE_GOAL with the zones having a certain text pattern in their name. **ATTENTION!** Bad naming convention as this *does not* only filter *prefixes*. + -- + -- Once the filter criteria have been set for the SET_ZONE_GOAL, you can start filtering using: + -- + -- * @{#SET_ZONE_GOAL.FilterStart}: Starts the filtering of the zones within the SET_ZONE_GOAL. + -- + -- ## SET_ZONE_GOAL iterators + -- + -- Once the filters have been defined and the SET_ZONE_GOAL has been built, you can iterate the SET_ZONE_GOAL with the available iterator methods. + -- The iterator methods will walk the SET_ZONE_GOAL set, and call for each airbase within the set a function that you provide. + -- The following iterator methods are currently available within the SET_ZONE_GOAL: + -- + -- * @{#SET_ZONE_GOAL.ForEachZone}: Calls a function for each zone it finds within the SET_ZONE_GOAL. + -- + -- === + -- @field #SET_ZONE_GOAL SET_ZONE_GOAL + SET_ZONE_GOAL = { + ClassName = "SET_ZONE_GOAL", + Zones = {}, + Filter = { + Prefixes = nil, + }, + FilterMeta = { + }, + } + + + --- Creates a new SET_ZONE_GOAL object, building a set of zones. + -- @param #SET_ZONE_GOAL self + -- @return #SET_ZONE_GOAL self + -- @usage + -- -- Define a new SET_ZONE_GOAL Object. The DatabaseSet will contain a reference to all Zones. + -- DatabaseSet = SET_ZONE_GOAL:New() + function SET_ZONE_GOAL:New() + -- Inherits from BASE + local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.ZONES_GOAL ) ) + + return self + end + + --- Add ZONEs to SET_ZONE_GOAL. + -- @param Core.Set#SET_ZONE_GOAL self + -- @param Core.Zone#ZONE_BASE Zone A ZONE_BASE object. + -- @return self + function SET_ZONE_GOAL:AddZone( Zone ) + + self:Add( Zone:GetName(), Zone ) + + return self + end + + + --- Remove ZONEs from SET_ZONE_GOAL. + -- @param Core.Set#SET_ZONE_GOAL self + -- @param Core.Zone#ZONE_BASE RemoveZoneNames A single name or an array of ZONE_BASE names. + -- @return self + function SET_ZONE_GOAL:RemoveZonesByName( RemoveZoneNames ) + + local RemoveZoneNamesArray = ( type( RemoveZoneNames ) == "table" ) and RemoveZoneNames or { RemoveZoneNames } + + for RemoveZoneID, RemoveZoneName in pairs( RemoveZoneNamesArray ) do + self:Remove( RemoveZoneName ) + end + + return self + end + + + --- Finds a Zone based on the Zone Name. + -- @param #SET_ZONE_GOAL self + -- @param #string ZoneName + -- @return Core.Zone#ZONE_BASE The found Zone. + function SET_ZONE_GOAL:FindZone( ZoneName ) + + local ZoneFound = self.Set[ZoneName] + return ZoneFound + end + + + --- Get a random zone from the set. + -- @param #SET_ZONE_GOAL self + -- @return Core.Zone#ZONE_BASE The random Zone. + -- @return #nil if no zone in the collection. + function SET_ZONE_GOAL:GetRandomZone() + + if self:Count() ~= 0 then + + local Index = self.Index + local ZoneFound = nil -- Core.Zone#ZONE_BASE + + -- Loop until a zone has been found. + -- The :GetZoneMaybe() call will evaluate the probability for the zone to be selected. + -- If the zone is not selected, then nil is returned by :GetZoneMaybe() and the loop continues! + while not ZoneFound do + local ZoneRandom = math.random( 1, #Index ) + ZoneFound = self.Set[Index[ZoneRandom]]:GetZoneMaybe() + end + + return ZoneFound + end + + return nil + end + + + --- Set a zone probability. + -- @param #SET_ZONE_GOAL self + -- @param #string ZoneName The name of the zone. + function SET_ZONE_GOAL:SetZoneProbability( ZoneName, ZoneProbability ) + local Zone = self:FindZone( ZoneName ) + Zone:SetZoneProbability( ZoneProbability ) + end + + + + + --- Builds a set of ZONE_GOALs that contain the given string in their name. + -- **ATTENTION!** Bad naming convention as this **does not** filter only **prefixes** but all zones that **contain** the string. + -- @param #SET_ZONE_GOAL self + -- @param #string Prefixes The string pattern(s) that needs to be contained in the zone name. Can also be passed as a `#table` of strings. + -- @return #SET_ZONE_GOAL self + function SET_ZONE_GOAL:FilterPrefixes( Prefixes ) + if not self.Filter.Prefixes then + self.Filter.Prefixes = {} + end + if type( Prefixes ) ~= "table" then + Prefixes = { Prefixes } + end + for PrefixID, Prefix in pairs( Prefixes ) do + self.Filter.Prefixes[Prefix] = Prefix + end + return self + end + + + --- Starts the filtering. + -- @param #SET_ZONE_GOAL self + -- @return #SET_ZONE_GOAL self + function SET_ZONE_GOAL:FilterStart() + + if _DATABASE then + + -- We initialize the first set. + for ObjectName, Object in pairs( self.Database ) do + if self:IsIncludeObject( Object ) then + self:Add( ObjectName, Object ) + else + self:RemoveZonesByName( ObjectName ) + end + end + end + + self:HandleEvent( EVENTS.NewZoneGoal ) + self:HandleEvent( EVENTS.DeleteZoneGoal ) + + return self + end + + --- Stops the filtering for the defined collection. + -- @param #SET_ZONE_GOAL self + -- @return #SET_ZONE_GOAL self + function SET_ZONE_GOAL:FilterStop() + + self:UnHandleEvent( EVENTS.NewZoneGoal ) + self:UnHandleEvent( EVENTS.DeleteZoneGoal ) + + return self + end + + --- Handles the Database to check on an event (birth) that the Object was added in the Database. + -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! + -- @param #SET_ZONE_GOAL self + -- @param Core.Event#EVENTDATA Event + -- @return #string The name of the AIRBASE + -- @return #table The AIRBASE + function SET_ZONE_GOAL:AddInDatabase( Event ) + self:F3( { Event } ) + + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] + end + + --- Handles the Database to check on any event that Object exists in the Database. + -- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! + -- @param #SET_ZONE_GOAL self + -- @param Core.Event#EVENTDATA Event + -- @return #string The name of the AIRBASE + -- @return #table The AIRBASE + function SET_ZONE_GOAL:FindInDatabase( Event ) + self:F3( { Event } ) + + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] + end + + --- Iterate the SET_ZONE_GOAL and call an interator function for each ZONE, providing the ZONE and optional parameters. + -- @param #SET_ZONE_GOAL self + -- @param #function IteratorFunction The function that will be called when there is an alive ZONE in the SET_ZONE_GOAL. The function needs to accept a AIRBASE parameter. + -- @return #SET_ZONE_GOAL self + function SET_ZONE_GOAL:ForEachZone( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self:GetSet() ) + + return self + end + + + --- + -- @param #SET_ZONE_GOAL self + -- @param Core.Zone#ZONE_BASE MZone + -- @return #SET_ZONE_GOAL self + function SET_ZONE_GOAL:IsIncludeObject( MZone ) + self:F2( MZone ) + + local MZoneInclude = true + + if MZone then + local MZoneName = MZone:GetName() + + if self.Filter.Prefixes then + local MZonePrefix = false + for ZonePrefixId, ZonePrefix in pairs( self.Filter.Prefixes ) do + self:T3( { "Prefix:", string.find( MZoneName, ZonePrefix, 1 ), ZonePrefix } ) + if string.find( MZoneName, ZonePrefix, 1 ) then + MZonePrefix = true + end + end + self:T( { "Evaluated Prefix", MZonePrefix } ) + MZoneInclude = MZoneInclude and MZonePrefix + end + end + + self:T2( MZoneInclude ) + return MZoneInclude + end + + --- Handles the OnEventNewZone event for the Set. + -- @param #SET_ZONE_GOAL self + -- @param Core.Event#EVENTDATA EventData + function SET_ZONE_GOAL:OnEventNewZoneGoal( EventData ) + + self:I( { "New Zone Capture Coalition", EventData } ) + self:I( { "Zone Capture Coalition", EventData.ZoneGoal } ) + + if EventData.ZoneGoal then + if EventData.ZoneGoal and self:IsIncludeObject( EventData.ZoneGoal ) then + self:I( { "Adding Zone Capture Coalition", EventData.ZoneGoal.ZoneName, EventData.ZoneGoal } ) + self:Add( EventData.ZoneGoal.ZoneName , EventData.ZoneGoal ) + end + end + end + + --- Handles the OnDead or OnCrash event for alive units set. + -- @param #SET_ZONE_GOAL self + -- @param Core.Event#EVENTDATA EventData + function SET_ZONE_GOAL:OnEventDeleteZoneGoal( EventData ) --R2.1 + self:F3( { EventData } ) + + if EventData.ZoneGoal then + local Zone = _DATABASE:FindZone( EventData.ZoneGoal.ZoneName ) + if Zone and Zone.ZoneName then + + -- When cargo was deleted, it may probably be because of an S_EVENT_DEAD. + -- However, in the loading logic, an S_EVENT_DEAD is also generated after a Destroy() call. + -- And this is a problem because it will remove all entries from the SET_ZONE_GOALs. + -- To prevent this from happening, the Zone object has a flag NoDestroy. + -- When true, the SET_ZONE_GOAL won't Remove the Zone object from the set. + -- This flag is switched off after the event handlers have been called in the EVENT class. + self:F( { ZoneNoDestroy=Zone.NoDestroy } ) + if Zone.NoDestroy then + else + self:Remove( Zone.ZoneName ) + end + end + end + end + + --- Validate if a coordinate is in one of the zones in the set. + -- Returns the ZONE object where the coordiante is located. + -- If zones overlap, the first zone that validates the test is returned. + -- @param #SET_ZONE_GOAL self + -- @param Core.Point#COORDINATE Coordinate The coordinate to be searched. + -- @return Core.Zone#ZONE_BASE The zone that validates the coordinate location. + -- @return #nil No zone has been found. + function SET_ZONE_GOAL:IsCoordinateInZone( Coordinate ) + + for _, Zone in pairs( self:GetSet() ) do + local Zone = Zone -- Core.Zone#ZONE_BASE + if Zone:IsCoordinateInZone( Coordinate ) then + return Zone + end + end + + return nil + end + +end +--- **Core** - Defines an extensive API to manage 3D points in the DCS World 3D simulation space. +-- +-- ## Features: +-- +-- * Provides a COORDINATE class, which allows to manage points in 3D space and perform various operations on it. +-- * Provides a POINT\_VEC2 class, which is derived from COORDINATE, and allows to manage points in 3D space, but from a Lat/Lon and Altitude perspective. +-- * Provides a POINT\_VEC3 class, which is derived from COORDINATE, and allows to manage points in 3D space, but from a X, Z and Y vector perspective. +-- +-- === +-- +-- # Demo Missions +-- +-- ### [POINT_VEC Demo Missions source code]() +-- +-- ### [POINT_VEC Demo Missions, only for beta testers]() +-- +-- ### [ALL Demo Missions pack of the last release](https://github.com/FlightControl-Master/MOOSE_MISSIONS/releases) +-- +-- === +-- +-- # YouTube Channel +-- +-- ### [POINT_VEC YouTube Channel]() +-- +-- === +-- +-- ### Authors: +-- +-- * FlightControl : Design & Programming +-- +-- ### Contributions: +-- +-- @module Core.Point +-- @image Core_Coordinate.JPG + + + + +do -- COORDINATE + + --- @type COORDINATE + -- @extends Core.Base#BASE + + + --- Defines a 3D point in the simulator and with its methods, you can use or manipulate the point in 3D space. + -- + -- # 1) Create a COORDINATE object. + -- + -- A new COORDINATE object can be created with 3 various methods: + -- + -- * @{#COORDINATE.New}(): from a 3D point. + -- * @{#COORDINATE.NewFromVec2}(): from a @{DCS#Vec2} and possible altitude. + -- * @{#COORDINATE.NewFromVec3}(): from a @{DCS#Vec3}. + -- + -- + -- # 2) Smoke, flare, explode, illuminate at the coordinate. + -- + -- At the point a smoke, flare, explosion and illumination bomb can be triggered. Use the following methods: + -- + -- ## 2.1) Smoke + -- + -- * @{#COORDINATE.Smoke}(): To smoke the point in a certain color. + -- * @{#COORDINATE.SmokeBlue}(): To smoke the point in blue. + -- * @{#COORDINATE.SmokeRed}(): To smoke the point in red. + -- * @{#COORDINATE.SmokeOrange}(): To smoke the point in orange. + -- * @{#COORDINATE.SmokeWhite}(): To smoke the point in white. + -- * @{#COORDINATE.SmokeGreen}(): To smoke the point in green. + -- + -- ## 2.2) Flare + -- + -- * @{#COORDINATE.Flare}(): To flare the point in a certain color. + -- * @{#COORDINATE.FlareRed}(): To flare the point in red. + -- * @{#COORDINATE.FlareYellow}(): To flare the point in yellow. + -- * @{#COORDINATE.FlareWhite}(): To flare the point in white. + -- * @{#COORDINATE.FlareGreen}(): To flare the point in green. + -- + -- ## 2.3) Explode + -- + -- * @{#COORDINATE.Explosion}(): To explode the point with a certain intensity. + -- + -- ## 2.4) Illuminate + -- + -- * @{#COORDINATE.IlluminationBomb}(): To illuminate the point. + -- + -- + -- # 3) Create markings on the map. + -- + -- Place markers (text boxes with clarifications for briefings, target locations or any other reference point) + -- on the map for all players, coalitions or specific groups: + -- + -- * @{#COORDINATE.MarkToAll}(): Place a mark to all players. + -- * @{#COORDINATE.MarkToCoalition}(): Place a mark to a coalition. + -- * @{#COORDINATE.MarkToCoalitionRed}(): Place a mark to the red coalition. + -- * @{#COORDINATE.MarkToCoalitionBlue}(): Place a mark to the blue coalition. + -- * @{#COORDINATE.MarkToGroup}(): Place a mark to a group (needs to have a client in it or a CA group (CA group is bugged)). + -- * @{#COORDINATE.RemoveMark}(): Removes a mark from the map. + -- + -- # 4) Coordinate calculation methods. + -- + -- Various calculation methods exist to use or manipulate 3D space. Find below a short description of each method: + -- + -- ## 4.1) Get the distance between 2 points. + -- + -- * @{#COORDINATE.Get3DDistance}(): Obtain the distance from the current 3D point to the provided 3D point in 3D space. + -- * @{#COORDINATE.Get2DDistance}(): Obtain the distance from the current 3D point to the provided 3D point in 2D space. + -- + -- ## 4.2) Get the angle. + -- + -- * @{#COORDINATE.GetAngleDegrees}(): Obtain the angle in degrees from the current 3D point with the provided 3D direction vector. + -- * @{#COORDINATE.GetAngleRadians}(): Obtain the angle in radians from the current 3D point with the provided 3D direction vector. + -- * @{#COORDINATE.GetDirectionVec3}(): Obtain the 3D direction vector from the current 3D point to the provided 3D point. + -- + -- ## 4.3) Coordinate translation. + -- + -- * @{#COORDINATE.Translate}(): Translate the current 3D point towards an other 3D point using the given Distance and Angle. + -- + -- ## 4.4) Get the North correction of the current location. + -- + -- * @{#COORDINATE.GetNorthCorrection}(): Obtains the north correction at the current 3D point. + -- + -- ## 4.5) Point Randomization + -- + -- Various methods exist to calculate random locations around a given 3D point. + -- + -- * @{#COORDINATE.GetRandomVec2InRadius}(): Provides a random 2D vector around the current 3D point, in the given inner to outer band. + -- * @{#COORDINATE.GetRandomVec3InRadius}(): Provides a random 3D vector around the current 3D point, in the given inner to outer band. + -- + -- ## 4.6) LOS between coordinates. + -- + -- Calculate if the coordinate has Line of Sight (LOS) with the other given coordinate. + -- Mountains, trees and other objects can be positioned between the two 3D points, preventing visibilty in a straight continuous line. + -- The method @{#COORDINATE.IsLOS}() returns if the two coodinates have LOS. + -- + -- ## 4.7) Check the coordinate position. + -- + -- Various methods are available that allow to check if a coordinate is: + -- + -- * @{#COORDINATE.IsInRadius}(): in a give radius. + -- * @{#COORDINATE.IsInSphere}(): is in a given sphere. + -- * @{#COORDINATE.IsAtCoordinate2D}(): is in a given coordinate within a specific precision. + -- + -- + -- + -- # 5) Measure the simulation environment at the coordinate. + -- + -- ## 5.1) Weather specific. + -- + -- Within the DCS simulator, a coordinate has specific environmental properties, like wind, temperature, humidity etc. + -- + -- * @{#COORDINATE.GetWind}(): Retrieve the wind at the specific coordinate within the DCS simulator. + -- * @{#COORDINATE.GetTemperature}(): Retrieve the temperature at the specific height within the DCS simulator. + -- * @{#COORDINATE.GetPressure}(): Retrieve the pressure at the specific height within the DCS simulator. + -- + -- ## 5.2) Surface specific. + -- + -- Within the DCS simulator, the surface can have various objects placed at the coordinate, and the surface height will vary. + -- + -- * @{#COORDINATE.GetLandHeight}(): Retrieve the height of the surface (on the ground) within the DCS simulator. + -- * @{#COORDINATE.GetSurfaceType}(): Retrieve the surface type (on the ground) within the DCS simulator. + -- + -- # 6) Create waypoints for routes. + -- + -- A COORDINATE can prepare waypoints for Ground and Air groups to be embedded into a Route. + -- + -- * @{#COORDINATE.WaypointAir}(): Build an air route point. + -- * @{#COORDINATE.WaypointGround}(): Build a ground route point. + -- * @{#COORDINATE.WaypointNaval}(): Build a naval route point. + -- + -- Route points can be used in the Route methods of the @{Wrapper.Group#GROUP} class. + -- + -- ## 7) Manage the roads. + -- + -- Important for ground vehicle transportation and movement, the method @{#COORDINATE.GetClosestPointToRoad}() will calculate + -- the closest point on the nearest road. + -- + -- In order to use the most optimal road system to transport vehicles, the method @{#COORDINATE.GetPathOnRoad}() will calculate + -- the most optimal path following the road between two coordinates. + -- + -- ## 8) Metric or imperial system + -- + -- * @{#COORDINATE.IsMetric}(): Returns if the 3D point is Metric or Nautical Miles. + -- * @{#COORDINATE.SetMetric}(): Sets the 3D point to Metric or Nautical Miles. + -- + -- + -- ## 9) Coordinate text generation + -- + -- * @{#COORDINATE.ToStringBR}(): Generates a Bearing & Range text in the format of DDD for DI where DDD is degrees and DI is distance. + -- * @{#COORDINATE.ToStringLL}(): Generates a Latutude & Longutude text. + -- + -- ## 10) Drawings on F10 map + -- + -- * @{#COORDINATE.CircleToAll}(): Draw a circle on the F10 map. + -- * @{#COORDINATE.LineToAll}(): Draw a line on the F10 map. + -- * @{#COORDINATE.RectToAll}(): Draw a rectangle on the F10 map. + -- * @{#COORDINATE.QuadToAll}(): Draw a shape with four points on the F10 map. + -- * @{#COORDINATE.TextToAll}(): Write some text on the F10 map. + -- * @{#COORDINATE.ArrowToAll}(): Draw an arrow on the F10 map. + -- + -- @field #COORDINATE + COORDINATE = { + ClassName = "COORDINATE", + } + + --- @field COORDINATE.WaypointAltType + COORDINATE.WaypointAltType = { + BARO = "BARO", + RADIO = "RADIO", + } + + --- @field COORDINATE.WaypointAction + COORDINATE.WaypointAction = { + TurningPoint = "Turning Point", + FlyoverPoint = "Fly Over Point", + FromParkingArea = "From Parking Area", + FromParkingAreaHot = "From Parking Area Hot", + FromRunway = "From Runway", + Landing = "Landing", + LandingReFuAr = "LandingReFuAr", + } + + --- @field COORDINATE.WaypointType + COORDINATE.WaypointType = { + TakeOffParking = "TakeOffParking", + TakeOffParkingHot = "TakeOffParkingHot", + TakeOff = "TakeOffParkingHot", + TurningPoint = "Turning Point", + Land = "Land", + LandingReFuAr = "LandingReFuAr", + } + + + --- COORDINATE constructor. + -- @param #COORDINATE self + -- @param DCS#Distance x The x coordinate of the Vec3 point, pointing to the North. + -- @param DCS#Distance y The y coordinate of the Vec3 point, pointing to the Right. + -- @param DCS#Distance z The z coordinate of the Vec3 point, pointing to the Right. + -- @return #COORDINATE + function COORDINATE:New( x, y, z ) + + --env.info("FF COORDINATE New") + local self = BASE:Inherit( self, BASE:New() ) -- #COORDINATE + self.x = x + self.y = y + self.z = z + + return self + end + + --- COORDINATE constructor. + -- @param #COORDINATE self + -- @param #COORDINATE Coordinate. + -- @return #COORDINATE + function COORDINATE:NewFromCoordinate( Coordinate ) + + local self = BASE:Inherit( self, BASE:New() ) -- #COORDINATE + self.x = Coordinate.x + self.y = Coordinate.y + self.z = Coordinate.z + + return self + end + + --- Create a new COORDINATE object from Vec2 coordinates. + -- @param #COORDINATE self + -- @param DCS#Vec2 Vec2 The Vec2 point. + -- @param DCS#Distance LandHeightAdd (optional) The default height if required to be evaluated will be the land height of the x, y coordinate. You can specify an extra height to be added to the land height. + -- @return #COORDINATE + function COORDINATE:NewFromVec2( Vec2, LandHeightAdd ) + + local LandHeight = land.getHeight( Vec2 ) + + LandHeightAdd = LandHeightAdd or 0 + LandHeight = LandHeight + LandHeightAdd + + local self = self:New( Vec2.x, LandHeight, Vec2.y ) -- #COORDINATE + + self:F2( self ) + + return self + + end + + --- Create a new COORDINATE object from Vec3 coordinates. + -- @param #COORDINATE self + -- @param DCS#Vec3 Vec3 The Vec3 point. + -- @return #COORDINATE + function COORDINATE:NewFromVec3( Vec3 ) + + local self = self:New( Vec3.x, Vec3.y, Vec3.z ) -- #COORDINATE + + self:F2( self ) + + return self + end + + --- Return the coordinates itself. Sounds stupid but can be useful for compatibility. + -- @param #COORDINATE self + -- @return #COORDINATE self + function COORDINATE:GetCoordinate() + return self + end + + --- Return the coordinates of the COORDINATE in Vec3 format. + -- @param #COORDINATE self + -- @return DCS#Vec3 The Vec3 format coordinate. + function COORDINATE:GetVec3() + return { x = self.x, y = self.y, z = self.z } + end + + + --- Return the coordinates of the COORDINATE in Vec2 format. + -- @param #COORDINATE self + -- @return DCS#Vec2 The Vec2 format coordinate. + function COORDINATE:GetVec2() + return { x = self.x, y = self.z } + end + + --- Update x,y,z coordinates from a given 3D vector. + -- @param #COORDINATE self + -- @param DCS#Vec3 Vec3 The 3D vector with x,y,z components. + -- @return #COORDINATE The modified COORDINATE itself. + function COORDINATE:UpdateFromVec3(Vec3) + + self.x=Vec3.x + self.y=Vec3.y + self.z=Vec3.z + + return self + end + + --- Update x,y,z coordinates from another given COORDINATE. + -- @param #COORDINATE self + -- @param #COORDINATE Coordinate The coordinate with the new x,y,z positions. + -- @return #COORDINATE The modified COORDINATE itself. + function COORDINATE:UpdateFromCoordinate(Coordinate) + + self.x=Coordinate.x + self.y=Coordinate.y + self.z=Coordinate.z + + return self + end + + --- Update x and z coordinates from a given 2D vector. + -- @param #COORDINATE self + -- @param DCS#Vec2 Vec2 The 2D vector with x,y components. x is overwriting COORDINATE.x while y is overwriting COORDINATE.z. + -- @return #COORDINATE The modified COORDINATE itself. + function COORDINATE:UpdateFromVec2(Vec2) + + self.x=Vec2.x + self.z=Vec2.y + + return self + end + + + --- Returns the coordinate from the latitude and longitude given in decimal degrees. + -- @param #COORDINATE self + -- @param #number latitude Latitude in decimal degrees. + -- @param #number longitude Longitude in decimal degrees. + -- @param #number altitude (Optional) Altitude in meters. Default is the land height at the coordinate. + -- @return #COORDINATE + function COORDINATE:NewFromLLDD( latitude, longitude, altitude) + + -- Returns a point from latitude and longitude in the vec3 format. + local vec3=coord.LLtoLO(latitude, longitude) + + -- Convert vec3 to coordinate object. + local _coord=self:NewFromVec3(vec3) + + -- Adjust height + if altitude==nil then + _coord.y=self:GetLandHeight() + else + _coord.y=altitude + end + + return _coord + end + + + --- Returns if the 2 coordinates are at the same 2D position. + -- @param #COORDINATE self + -- @param #COORDINATE Coordinate + -- @param #number Precision + -- @return #boolean true if at the same position. + function COORDINATE:IsAtCoordinate2D( Coordinate, Precision ) + + self:F( { Coordinate = Coordinate:GetVec2() } ) + self:F( { self = self:GetVec2() } ) + + local x = Coordinate.x + local z = Coordinate.z + + return x - Precision <= self.x and x + Precision >= self.x and z - Precision <= self.z and z + Precision >= self.z + end + + --- Scan/find objects (units, statics, scenery) within a certain radius around the coordinate using the world.searchObjects() DCS API function. + -- @param #COORDINATE self + -- @param #number radius (Optional) Scan radius in meters. Default 100 m. + -- @param #boolean scanunits (Optional) If true scan for units. Default true. + -- @param #boolean scanstatics (Optional) If true scan for static objects. Default true. + -- @param #boolean scanscenery (Optional) If true scan for scenery objects. Default false. + -- @return #boolean True if units were found. + -- @return #boolean True if statics were found. + -- @return #boolean True if scenery objects were found. + -- @return #table Table of MOOSE @{Wrapper.Unit#UNIT} objects found. + -- @return #table Table of DCS static objects found. + -- @return #table Table of DCS scenery objects found. + function COORDINATE:ScanObjects(radius, scanunits, scanstatics, scanscenery) + self:F(string.format("Scanning in radius %.1f m.", radius or 100)) + + local SphereSearch = { + id = world.VolumeType.SPHERE, + params = { + point = self:GetVec3(), + radius = radius, + } + } + + -- Defaults + radius=radius or 100 + if scanunits==nil then + scanunits=true + end + if scanstatics==nil then + scanstatics=true + end + if scanscenery==nil then + scanscenery=false + end + + --{Object.Category.UNIT, Object.Category.STATIC, Object.Category.SCENERY} + local scanobjects={} + if scanunits then + table.insert(scanobjects, Object.Category.UNIT) + end + if scanstatics then + table.insert(scanobjects, Object.Category.STATIC) + end + if scanscenery then + table.insert(scanobjects, Object.Category.SCENERY) + end + + -- Found stuff. + local Units = {} + local Statics = {} + local Scenery = {} + local gotstatics=false + local gotunits=false + local gotscenery=false + + local function EvaluateZone(ZoneObject) + + if ZoneObject then + + -- Get category of scanned object. + local ObjectCategory = ZoneObject:getCategory() + + -- Check for unit or static objects + if ObjectCategory==Object.Category.UNIT and ZoneObject:isExist() then + + table.insert(Units, UNIT:Find(ZoneObject)) + gotunits=true + + elseif ObjectCategory==Object.Category.STATIC and ZoneObject:isExist() then + + table.insert(Statics, ZoneObject) + gotstatics=true + + elseif ObjectCategory==Object.Category.SCENERY then + + table.insert(Scenery, ZoneObject) + gotscenery=true + + end + + end + + return true + end + + -- Search the world. + world.searchObjects(scanobjects, SphereSearch, EvaluateZone) + + for _,unit in pairs(Units) do + self:T(string.format("Scan found unit %s", unit:GetName())) + end + for _,static in pairs(Statics) do + self:T(string.format("Scan found static %s", static:getName())) + _DATABASE:AddStatic(static:getName()) + end + for _,scenery in pairs(Scenery) do + self:T(string.format("Scan found scenery %s typename=%s", scenery:getName(), scenery:getTypeName())) + --SCENERY:Register(scenery:getName(), scenery) + end + + return gotunits, gotstatics, gotscenery, Units, Statics, Scenery + end + + --- Scan/find UNITS within a certain radius around the coordinate using the world.searchObjects() DCS API function. + -- @param #COORDINATE self + -- @param #number radius (Optional) Scan radius in meters. Default 100 m. + -- @return Core.Set#SET_UNIT Set of units. + function COORDINATE:ScanUnits(radius) + + local _,_,_,units=self:ScanObjects(radius, true, false, false) + + local set=SET_UNIT:New() + + for _,unit in pairs(units) do + set:AddUnit(unit) + end + + return set + end + + --- Find the closest unit to the COORDINATE within a certain radius. + -- @param #COORDINATE self + -- @param #number radius Scan radius in meters. Default 100 m. + -- @return Wrapper.Unit#UNIT The closest unit or #nil if no unit is inside the given radius. + function COORDINATE:FindClosestUnit(radius) + + local units=self:ScanUnits(radius) + + local umin=nil --Wrapper.Unit#UNIT + local dmin=math.huge + for _,_unit in pairs(units.Set) do + local unit=_unit --Wrapper.Unit#UNIT + local coordinate=unit:GetCoordinate() + local d=self:Get2DDistance(coordinate) + if d 180 then + direction = direction-180 + else + direction = direction+180 + end + local strength=math.sqrt((wind.x)^2+(wind.z)^2) + -- Return wind direction and strength km/h. + return direction, strength + end + + --- Returns the wind direction (from) and strength. + -- @param #COORDINATE self + -- @param height (Optional) parameter specifying the height ASL. The minimum height will be always be the land height since the wind is zero below the ground. + -- @return Direction the wind is blowing from in degrees. + function COORDINATE:GetWindWithTurbulenceVec3(height) + + -- AGL height if + local landheight=self:GetLandHeight()+0.1 -- we at 0.1 meters to be sure to be above ground since wind is zero below ground level. + + -- Point at which the wind is evaluated. + local point={x=self.x, y=math.max(height or self.y, landheight), z=self.z} + + -- Get wind velocity vector including turbulences. + local vec3 = atmosphere.getWindWithTurbulence(point) + + return vec3 + end + + + --- Returns a text documenting the wind direction (from) and strength according the measurement system @{Settings}. + -- The text will reflect the wind like this: + -- + -- - For Russian and European aircraft using the metric system - Wind direction in degrees (°) and wind speed in meters per second (mps). + -- - For Americain aircraft we link to the imperial system - Wind direction in degrees (°) and wind speed in knots per second (kps). + -- + -- A text containing a pressure will look like this: + -- + -- - `Wind: %n ° at n.d mps` + -- - `Wind: %n ° at n.d kps` + -- + -- @param #COORDINATE self + -- @param height (Optional) parameter specifying the height ASL. The minimum height will be always be the land height since the wind is zero below the ground. + -- @return #string Wind direction and strength according the measurement system @{Settings}. + function COORDINATE:GetWindText( height, Settings ) + + local Direction, Strength = self:GetWind( height ) + + local Settings = Settings or _SETTINGS + + if Direction and Strength then + if Settings:IsMetric() then + return string.format( " %d ° at %3.2f mps", Direction, UTILS.MpsToKmph( Strength ) ) + else + return string.format( " %d ° at %3.2f kps", Direction, UTILS.MpsToKnots( Strength ) ) + end + else + return " no wind" + end + + return nil + end + + --- Return the 3D distance in meters between the target COORDINATE and the COORDINATE. + -- @param #COORDINATE self + -- @param #COORDINATE TargetCoordinate The target COORDINATE. + -- @return DCS#Distance Distance The distance in meters. + function COORDINATE:Get3DDistance( TargetCoordinate ) + local TargetVec3 = TargetCoordinate:GetVec3() + local SourceVec3 = self:GetVec3() + return ( ( TargetVec3.x - SourceVec3.x ) ^ 2 + ( TargetVec3.y - SourceVec3.y ) ^ 2 + ( TargetVec3.z - SourceVec3.z ) ^ 2 ) ^ 0.5 + end + + + --- Provides a bearing text in degrees. + -- @param #COORDINATE self + -- @param #number AngleRadians The angle in randians. + -- @param #number Precision The precision. + -- @param Core.Settings#SETTINGS Settings + -- @return #string The bearing text in degrees. + function COORDINATE:GetBearingText( AngleRadians, Precision, Settings, Language ) + + local Settings = Settings or _SETTINGS -- Core.Settings#SETTINGS + + local AngleDegrees = UTILS.Round( UTILS.ToDegree( AngleRadians ), Precision ) + + local s = string.format( '%03d°', AngleDegrees ) + + return s + end + + --- Provides a distance text expressed in the units of measurement. + -- @param #COORDINATE self + -- @param #number Distance The distance in meters. + -- @param Core.Settings#SETTINGS Settings + -- @return #string The distance text expressed in the units of measurement. + function COORDINATE:GetDistanceText( Distance, Settings, Language ) + + local Settings = Settings or _SETTINGS -- Core.Settings#SETTINGS + local Language = Language or "EN" + + local DistanceText + + if Settings:IsMetric() then + if Language == "EN" then + DistanceText = " for " .. UTILS.Round( Distance / 1000, 2 ) .. " km" + elseif Language == "RU" then + DistanceText = " за " .. UTILS.Round( Distance / 1000, 2 ) .. " километров" + end + else + if Language == "EN" then + DistanceText = " for " .. UTILS.Round( UTILS.MetersToNM( Distance ), 2 ) .. " miles" + elseif Language == "RU" then + DistanceText = " за " .. UTILS.Round( UTILS.MetersToNM( Distance ), 2 ) .. " миль" + end + end + + return DistanceText + end + + --- Return the altitude text of the COORDINATE. + -- @param #COORDINATE self + -- @return #string Altitude text. + function COORDINATE:GetAltitudeText( Settings, Language ) + local Altitude = self.y + local Settings = Settings or _SETTINGS + local Language = Language or "EN" + + if Altitude ~= 0 then + if Settings:IsMetric() then + if Language == "EN" then + return " at " .. UTILS.Round( self.y, -3 ) .. " meters" + elseif Language == "RU" then + return " в " .. UTILS.Round( self.y, -3 ) .. " метры" + end + else + if Language == "EN" then + return " at " .. UTILS.Round( UTILS.MetersToFeet( self.y ), -3 ) .. " feet" + elseif Language == "RU" then + return " в " .. UTILS.Round( self.y, -3 ) .. " ноги" + end + end + else + return "" + end + end + + + + --- Return the velocity text of the COORDINATE. + -- @param #COORDINATE self + -- @return #string Velocity text. + function COORDINATE:GetVelocityText( Settings ) + local Velocity = self:GetVelocity() + local Settings = Settings or _SETTINGS + if Velocity then + if Settings:IsMetric() then + return string.format( " moving at %d km/h", UTILS.MpsToKmph( Velocity ) ) + else + return string.format( " moving at %d mi/h", UTILS.MpsToKmph( Velocity ) / 1.852 ) + end + else + return " stationary" + end + end + + + --- Return the heading text of the COORDINATE. + -- @param #COORDINATE self + -- @return #string Heading text. + function COORDINATE:GetHeadingText( Settings ) + local Heading = self:GetHeading() + if Heading then + return string.format( " bearing %3d°", Heading ) + else + return " bearing unknown" + end + end + + + --- Provides a Bearing / Range string + -- @param #COORDINATE self + -- @param #number AngleRadians The angle in randians + -- @param #number Distance The distance + -- @param Core.Settings#SETTINGS Settings + -- @return #string The BR Text + function COORDINATE:GetBRText( AngleRadians, Distance, Settings, Language ) + + local Settings = Settings or _SETTINGS -- Core.Settings#SETTINGS + + local BearingText = self:GetBearingText( AngleRadians, 0, Settings, Language ) + local DistanceText = self:GetDistanceText( Distance, Settings, Language ) + + local BRText = BearingText .. DistanceText + + return BRText + end + + --- Provides a Bearing / Range / Altitude string + -- @param #COORDINATE self + -- @param #number AngleRadians The angle in randians + -- @param #number Distance The distance + -- @param Core.Settings#SETTINGS Settings + -- @return #string The BRA Text + function COORDINATE:GetBRAText( AngleRadians, Distance, Settings, Language ) + + local Settings = Settings or _SETTINGS -- Core.Settings#SETTINGS + + local BearingText = self:GetBearingText( AngleRadians, 0, Settings, Language ) + local DistanceText = self:GetDistanceText( Distance, Settings, Language ) + local AltitudeText = self:GetAltitudeText( Settings, Language ) + + local BRAText = BearingText .. DistanceText .. AltitudeText -- When the POINT is a VEC2, there will be no altitude shown. + + return BRAText + end + + + --- Set altitude. + -- @param #COORDINATE self + -- @param #number altitude New altitude in meters. + -- @param #boolean asl Altitude above sea level. Default is above ground level. + -- @return #COORDINATE The COORDINATE with adjusted altitude. + function COORDINATE:SetAltitude(altitude, asl) + local alt=altitude + if asl then + alt=altitude + else + alt=self:GetLandHeight()+altitude + end + self.y=alt + return self + end + + --- Build an air type route point. + -- @param #COORDINATE self + -- @param #COORDINATE.WaypointAltType AltType The altitude type. + -- @param #COORDINATE.WaypointType Type The route point type. + -- @param #COORDINATE.WaypointAction Action The route point action. + -- @param DCS#Speed Speed Airspeed in km/h. Default is 500 km/h. + -- @param #boolean SpeedLocked true means the speed is locked. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase for takeoff and landing points. + -- @param #table DCSTasks A table of @{DCS#Task} items which are executed at the waypoint. + -- @param #string description A text description of the waypoint, which will be shown on the F10 map. + -- @param #number timeReFuAr Time in minutes the aircraft stays at the airport for ReFueling and ReArming. + -- @return #table The route point. + function COORDINATE:WaypointAir( AltType, Type, Action, Speed, SpeedLocked, airbase, DCSTasks, description, timeReFuAr ) + self:F2( { AltType, Type, Action, Speed, SpeedLocked } ) + + -- Set alttype or "RADIO" which is AGL. + AltType=AltType or "RADIO" + + -- Speedlocked by default + if SpeedLocked==nil then + SpeedLocked=true + end + + -- Speed or default 500 km/h. + Speed=Speed or 500 + + -- Waypoint array. + local RoutePoint = {} + + -- Coordinates. + RoutePoint.x = self.x + RoutePoint.y = self.z + + -- Altitude. + RoutePoint.alt = self.y + RoutePoint.alt_type = AltType + + -- Waypoint type. + RoutePoint.type = Type or nil + RoutePoint.action = Action or nil + + -- Speed. + RoutePoint.speed = Speed/3.6 + RoutePoint.speed_locked = SpeedLocked + + -- ETA. + RoutePoint.ETA=0 + RoutePoint.ETA_locked=false + + -- Waypoint description. + RoutePoint.name=description + + -- Airbase parameters for takeoff and landing points. + if airbase then + local AirbaseID = airbase:GetID() + local AirbaseCategory = airbase:GetAirbaseCategory() + if AirbaseCategory == Airbase.Category.SHIP or AirbaseCategory == Airbase.Category.HELIPAD then + RoutePoint.linkUnit = AirbaseID + RoutePoint.helipadId = AirbaseID + elseif AirbaseCategory == Airbase.Category.AIRDROME then + RoutePoint.airdromeId = AirbaseID + else + self:E("ERROR: Unknown airbase category in COORDINATE:WaypointAir()!") + end + end + + -- Time in minutes to stay at the airbase before resuming route. + if Type==COORDINATE.WaypointType.LandingReFuAr then + RoutePoint.timeReFuAr=timeReFuAr or 10 + end + + -- Waypoint tasks. + RoutePoint.task = {} + RoutePoint.task.id = "ComboTask" + RoutePoint.task.params = {} + RoutePoint.task.params.tasks = DCSTasks or {} + + --RoutePoint.properties={} + --RoutePoint.properties.addopt={} + + --RoutePoint.formation_template="" + + -- Debug. + self:T({RoutePoint=RoutePoint}) + + -- Return waypoint. + return RoutePoint + end + + + --- Build a Waypoint Air "Turning Point". + -- @param #COORDINATE self + -- @param #COORDINATE.WaypointAltType AltType The altitude type. + -- @param DCS#Speed Speed Airspeed in km/h. + -- @param #table DCSTasks (Optional) A table of @{DCS#Task} items which are executed at the waypoint. + -- @param #string description (Optional) A text description of the waypoint, which will be shown on the F10 map. + -- @return #table The route point. + function COORDINATE:WaypointAirTurningPoint( AltType, Speed, DCSTasks, description ) + return self:WaypointAir( AltType, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, Speed, true, nil, DCSTasks, description ) + end + + + --- Build a Waypoint Air "Fly Over Point". + -- @param #COORDINATE self + -- @param #COORDINATE.WaypointAltType AltType The altitude type. + -- @param DCS#Speed Speed Airspeed in km/h. + -- @return #table The route point. + function COORDINATE:WaypointAirFlyOverPoint( AltType, Speed ) + return self:WaypointAir( AltType, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.FlyoverPoint, Speed ) + end + + + --- Build a Waypoint Air "Take Off Parking Hot". + -- @param #COORDINATE self + -- @param #COORDINATE.WaypointAltType AltType The altitude type. + -- @param DCS#Speed Speed Airspeed in km/h. + -- @return #table The route point. + function COORDINATE:WaypointAirTakeOffParkingHot( AltType, Speed ) + return self:WaypointAir( AltType, COORDINATE.WaypointType.TakeOffParkingHot, COORDINATE.WaypointAction.FromParkingAreaHot, Speed ) + end + + + --- Build a Waypoint Air "Take Off Parking". + -- @param #COORDINATE self + -- @param #COORDINATE.WaypointAltType AltType The altitude type. + -- @param DCS#Speed Speed Airspeed in km/h. + -- @return #table The route point. + function COORDINATE:WaypointAirTakeOffParking( AltType, Speed ) + return self:WaypointAir( AltType, COORDINATE.WaypointType.TakeOffParking, COORDINATE.WaypointAction.FromParkingArea, Speed ) + end + + + --- Build a Waypoint Air "Take Off Runway". + -- @param #COORDINATE self + -- @param #COORDINATE.WaypointAltType AltType The altitude type. + -- @param DCS#Speed Speed Airspeed in km/h. + -- @return #table The route point. + function COORDINATE:WaypointAirTakeOffRunway( AltType, Speed ) + return self:WaypointAir( AltType, COORDINATE.WaypointType.TakeOff, COORDINATE.WaypointAction.FromRunway, Speed ) + end + + + --- Build a Waypoint Air "Landing". + -- @param #COORDINATE self + -- @param DCS#Speed Speed Airspeed in km/h. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase for takeoff and landing points. + -- @param #table DCSTasks A table of @{DCS#Task} items which are executed at the waypoint. + -- @param #string description A text description of the waypoint, which will be shown on the F10 map. + -- @return #table The route point. + -- @usage + -- + -- LandingZone = ZONE:New( "LandingZone" ) + -- LandingCoord = LandingZone:GetCoordinate() + -- LandingWaypoint = LandingCoord:WaypointAirLanding( 60 ) + -- HeliGroup:Route( { LandWaypoint }, 1 ) -- Start landing the helicopter in one second. + -- + function COORDINATE:WaypointAirLanding( Speed, airbase, DCSTasks, description ) + return self:WaypointAir(nil, COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, Speed, false, airbase, DCSTasks, description) + end + + --- Build a Waypoint Air "LandingReFuAr". Mimics the aircraft ReFueling and ReArming. + -- @param #COORDINATE self + -- @param DCS#Speed Speed Airspeed in km/h. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase for takeoff and landing points. + -- @param #number timeReFuAr Time in minutes, the aircraft stays at the airbase. Default 10 min. + -- @param #table DCSTasks A table of @{DCS#Task} items which are executed at the waypoint. + -- @param #string description A text description of the waypoint, which will be shown on the F10 map. + -- @return #table The route point. + function COORDINATE:WaypointAirLandingReFu( Speed, airbase, timeReFuAr, DCSTasks, description ) + return self:WaypointAir(nil, COORDINATE.WaypointType.LandingReFuAr, COORDINATE.WaypointAction.LandingReFuAr, Speed, false, airbase, DCSTasks, description, timeReFuAr or 10) + end + + + --- Build an ground type route point. + -- @param #COORDINATE self + -- @param #number Speed (Optional) Speed in km/h. The default speed is 20 km/h. + -- @param #string Formation (Optional) The route point Formation, which is a text string that specifies exactly the Text in the Type of the route point, like "Vee", "Echelon Right". + -- @param #table DCSTasks (Optional) A table of DCS tasks that are executed at the waypoints. Mind the curly brackets {}! + -- @return #table The route point. + function COORDINATE:WaypointGround( Speed, Formation, DCSTasks ) + self:F2( { Speed, Formation, DCSTasks } ) + + local RoutePoint = {} + + RoutePoint.x = self.x + RoutePoint.y = self.z + + RoutePoint.alt = self:GetLandHeight()+1 + RoutePoint.alt_type = COORDINATE.WaypointAltType.BARO + + RoutePoint.type = "Turning Point" + + RoutePoint.action = Formation or "Off Road" + RoutePoint.formation_template="" + + RoutePoint.ETA=0 + RoutePoint.ETA_locked=false + + RoutePoint.speed = ( Speed or 20 ) / 3.6 + RoutePoint.speed_locked = true + + RoutePoint.task = {} + RoutePoint.task.id = "ComboTask" + RoutePoint.task.params = {} + RoutePoint.task.params.tasks = DCSTasks or {} + + return RoutePoint + end + + --- Build route waypoint point for Naval units. + -- @param #COORDINATE self + -- @param #number Speed (Optional) Speed in km/h. The default speed is 20 km/h. + -- @param #string Depth (Optional) Dive depth in meters. Only for submarines. Default is COORDINATE.y component. + -- @param #table DCSTasks (Optional) A table of DCS tasks that are executed at the waypoints. Mind the curly brackets {}! + -- @return #table The route point. + function COORDINATE:WaypointNaval( Speed, Depth, DCSTasks ) + self:F2( { Speed, Depth, DCSTasks } ) + + local RoutePoint = {} + + RoutePoint.x = self.x + RoutePoint.y = self.z + + RoutePoint.alt = Depth or self.y -- Depth is for submarines only. Ships should have alt=0. + RoutePoint.alt_type = "BARO" + + RoutePoint.type = "Turning Point" + RoutePoint.action = "Turning Point" + RoutePoint.formation_template = "" + + RoutePoint.ETA=0 + RoutePoint.ETA_locked=false + + RoutePoint.speed = ( Speed or 20 ) / 3.6 + RoutePoint.speed_locked = true + + RoutePoint.task = {} + RoutePoint.task.id = "ComboTask" + RoutePoint.task.params = {} + RoutePoint.task.params.tasks = DCSTasks or {} + + return RoutePoint + end + + --- Gets the nearest airbase with respect to the current coordinates. + -- @param #COORDINATE self + -- @param #number Category (Optional) Category of the airbase. Enumerator of @{Wrapper.Airbase#AIRBASE.Category}. + -- @param #number Coalition (Optional) Coalition of the airbase. + -- @return Wrapper.Airbase#AIRBASE Closest Airbase to the given coordinate. + -- @return #number Distance to the closest airbase in meters. + function COORDINATE:GetClosestAirbase2(Category, Coalition) + + -- Get all airbases of the map. + local airbases=AIRBASE.GetAllAirbases(Coalition) + + local closest=nil + local distmin=nil + -- Loop over all airbases. + for _,_airbase in pairs(airbases) do + local airbase=_airbase --Wrapper.Airbase#AIRBASE + if airbase then + local category=airbase:GetAirbaseCategory() + if Category and Category==category or Category==nil then + + -- Distance to airbase. + local dist=self:Get2DDistance(airbase:GetCoordinate()) + + if closest==nil then + distmin=dist + closest=airbase + else + if dist=2 then + for i=1,#Path-1 do + Way=Way+Path[i+1]:Get2DDistance(Path[i]) + end + else + -- There are cases where no path on road can be found. + return nil,nil,false + end + + return Path, Way, GotPath + end + + --- Gets the surface type at the coordinate. + -- @param #COORDINATE self + -- @return DCS#SurfaceType Surface type. + function COORDINATE:GetSurfaceType() + local vec2=self:GetVec2() + local surface=land.getSurfaceType(vec2) + return surface + end + + --- Checks if the surface type is on land. + -- @param #COORDINATE self + -- @return #boolean If true, the surface type at the coordinate is land. + function COORDINATE:IsSurfaceTypeLand() + return self:GetSurfaceType()==land.SurfaceType.LAND + end + + --- Checks if the surface type is road. + -- @param #COORDINATE self + -- @return #boolean If true, the surface type at the coordinate is land. + function COORDINATE:IsSurfaceTypeLand() + return self:GetSurfaceType()==land.SurfaceType.LAND + end + + + --- Checks if the surface type is road. + -- @param #COORDINATE self + -- @return #boolean If true, the surface type at the coordinate is a road. + function COORDINATE:IsSurfaceTypeRoad() + return self:GetSurfaceType()==land.SurfaceType.ROAD + end + + --- Checks if the surface type is runway. + -- @param #COORDINATE self + -- @return #boolean If true, the surface type at the coordinate is a runway or taxi way. + function COORDINATE:IsSurfaceTypeRunway() + return self:GetSurfaceType()==land.SurfaceType.RUNWAY + end + + --- Checks if the surface type is shallow water. + -- @param #COORDINATE self + -- @return #boolean If true, the surface type at the coordinate is a shallow water. + function COORDINATE:IsSurfaceTypeShallowWater() + return self:GetSurfaceType()==land.SurfaceType.SHALLOW_WATER + end + + --- Checks if the surface type is water. + -- @param #COORDINATE self + -- @return #boolean If true, the surface type at the coordinate is a deep water. + function COORDINATE:IsSurfaceTypeWater() + return self:GetSurfaceType()==land.SurfaceType.WATER + end + + + --- Creates an explosion at the point of a certain intensity. + -- @param #COORDINATE self + -- @param #number ExplosionIntensity Intensity of the explosion in kg TNT. Default 100 kg. + -- @param #number Delay Delay before explosion in seconds. + -- @return #COORDINATE self + function COORDINATE:Explosion( ExplosionIntensity, Delay ) + self:F2( { ExplosionIntensity } ) + ExplosionIntensity=ExplosionIntensity or 100 + if Delay and Delay>0 then + self:ScheduleOnce(Delay, self.Explosion, self, ExplosionIntensity) + else + trigger.action.explosion(self:GetVec3(), ExplosionIntensity) + end + return self + end + + --- Creates an illumination bomb at the point. + -- @param #COORDINATE self + -- @param #number power Power of illumination bomb in Candela. + -- @return #COORDINATE self + function COORDINATE:IlluminationBomb(power) + self:F2() + trigger.action.illuminationBomb( self:GetVec3(), power ) + end + + + --- Smokes the point in a color. + -- @param #COORDINATE self + -- @param Utilities.Utils#SMOKECOLOR SmokeColor + function COORDINATE:Smoke( SmokeColor ) + self:F2( { SmokeColor } ) + trigger.action.smoke( self:GetVec3(), SmokeColor ) + end + + --- Smoke the COORDINATE Green. + -- @param #COORDINATE self + function COORDINATE:SmokeGreen() + self:F2() + self:Smoke( SMOKECOLOR.Green ) + end + + --- Smoke the COORDINATE Red. + -- @param #COORDINATE self + function COORDINATE:SmokeRed() + self:F2() + self:Smoke( SMOKECOLOR.Red ) + end + + --- Smoke the COORDINATE White. + -- @param #COORDINATE self + function COORDINATE:SmokeWhite() + self:F2() + self:Smoke( SMOKECOLOR.White ) + end + + --- Smoke the COORDINATE Orange. + -- @param #COORDINATE self + function COORDINATE:SmokeOrange() + self:F2() + self:Smoke( SMOKECOLOR.Orange ) + end + + --- Smoke the COORDINATE Blue. + -- @param #COORDINATE self + function COORDINATE:SmokeBlue() + self:F2() + self:Smoke( SMOKECOLOR.Blue ) + end + + --- Big smoke and fire at the coordinate. + -- @param #COORDINATE self + -- @param Utilities.Utils#BIGSMOKEPRESET preset Smoke preset (1=small smoke and fire, 2=medium smoke and fire, 3=large smoke and fire, 4=huge smoke and fire, 5=small smoke, 6=medium smoke, 7=large smoke, 8=huge smoke). + -- @param #number density (Optional) Smoke density. Number in [0,...,1]. Default 0.5. + function COORDINATE:BigSmokeAndFire( preset, density ) + self:F2( { preset=preset, density=density } ) + density=density or 0.5 + trigger.action.effectSmokeBig( self:GetVec3(), preset, density ) + end + + --- Small smoke and fire at the coordinate. + -- @param #COORDINATE self + -- @number density (Optional) Smoke density. Number between 0 and 1. Default 0.5. + function COORDINATE:BigSmokeAndFireSmall( density ) + self:F2( { density=density } ) + density=density or 0.5 + self:BigSmokeAndFire(BIGSMOKEPRESET.SmallSmokeAndFire, density) + end + + --- Medium smoke and fire at the coordinate. + -- @param #COORDINATE self + -- @number density (Optional) Smoke density. Number between 0 and 1. Default 0.5. + function COORDINATE:BigSmokeAndFireMedium( density ) + self:F2( { density=density } ) + density=density or 0.5 + self:BigSmokeAndFire(BIGSMOKEPRESET.MediumSmokeAndFire, density) + end + + --- Large smoke and fire at the coordinate. + -- @param #COORDINATE self + -- @number density (Optional) Smoke density. Number between 0 and 1. Default 0.5. + function COORDINATE:BigSmokeAndFireLarge( density ) + self:F2( { density=density } ) + density=density or 0.5 + self:BigSmokeAndFire(BIGSMOKEPRESET.LargeSmokeAndFire, density) + end + + --- Huge smoke and fire at the coordinate. + -- @param #COORDINATE self + -- @number density (Optional) Smoke density. Number between 0 and 1. Default 0.5. + function COORDINATE:BigSmokeAndFireHuge( density ) + self:F2( { density=density } ) + density=density or 0.5 + self:BigSmokeAndFire(BIGSMOKEPRESET.HugeSmokeAndFire, density) + end + + --- Small smoke at the coordinate. + -- @param #COORDINATE self + -- @number density (Optional) Smoke density. Number between 0 and 1. Default 0.5. + function COORDINATE:BigSmokeSmall( density ) + self:F2( { density=density } ) + density=density or 0.5 + self:BigSmokeAndFire(BIGSMOKEPRESET.SmallSmoke, density) + end + + --- Medium smoke at the coordinate. + -- @param #COORDINATE self + -- @number density (Optional) Smoke density. Number between 0 and 1. Default 0.5. + function COORDINATE:BigSmokeMedium( density ) + self:F2( { density=density } ) + density=density or 0.5 + self:BigSmokeAndFire(BIGSMOKEPRESET.MediumSmoke, density) + end + + --- Large smoke at the coordinate. + -- @param #COORDINATE self + -- @number density (Optional) Smoke density. Number between 0 and 1. Default 0.5. + function COORDINATE:BigSmokeLarge( density ) + self:F2( { density=density } ) + density=density or 0.5 + self:BigSmokeAndFire(BIGSMOKEPRESET.LargeSmoke, density) + end + + --- Huge smoke at the coordinate. + -- @param #COORDINATE self + -- @number density (Optional) Smoke density. Number between 0 and 1. Default 0.5. + function COORDINATE:BigSmokeHuge( density ) + self:F2( { density=density } ) + density=density or 0.5 + self:BigSmokeAndFire(BIGSMOKEPRESET.HugeSmoke, density) + end + + --- Flares the point in a color. + -- @param #COORDINATE self + -- @param Utilities.Utils#FLARECOLOR FlareColor + -- @param DCS#Azimuth Azimuth (optional) The azimuth of the flare direction. The default azimuth is 0. + function COORDINATE:Flare( FlareColor, Azimuth ) + self:F2( { FlareColor } ) + trigger.action.signalFlare( self:GetVec3(), FlareColor, Azimuth and Azimuth or 0 ) + end + + --- Flare the COORDINATE White. + -- @param #COORDINATE self + -- @param DCS#Azimuth Azimuth (optional) The azimuth of the flare direction. The default azimuth is 0. + function COORDINATE:FlareWhite( Azimuth ) + self:F2( Azimuth ) + self:Flare( FLARECOLOR.White, Azimuth ) + end + + --- Flare the COORDINATE Yellow. + -- @param #COORDINATE self + -- @param DCS#Azimuth Azimuth (optional) The azimuth of the flare direction. The default azimuth is 0. + function COORDINATE:FlareYellow( Azimuth ) + self:F2( Azimuth ) + self:Flare( FLARECOLOR.Yellow, Azimuth ) + end + + --- Flare the COORDINATE Green. + -- @param #COORDINATE self + -- @param DCS#Azimuth Azimuth (optional) The azimuth of the flare direction. The default azimuth is 0. + function COORDINATE:FlareGreen( Azimuth ) + self:F2( Azimuth ) + self:Flare( FLARECOLOR.Green, Azimuth ) + end + + --- Flare the COORDINATE Red. + -- @param #COORDINATE self + function COORDINATE:FlareRed( Azimuth ) + self:F2( Azimuth ) + self:Flare( FLARECOLOR.Red, Azimuth ) + end + + do -- Markings + + --- Mark to All + -- @param #COORDINATE self + -- @param #string MarkText Free format text that shows the marking clarification. + -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. + -- @param #string Text (Optional) Text displayed when mark is added. Default none. + -- @return #number The resulting Mark ID which is a number. + -- @usage + -- local TargetCoord = TargetGroup:GetCoordinate() + -- local MarkID = TargetCoord:MarkToAll( "This is a target for all players" ) + function COORDINATE:MarkToAll( MarkText, ReadOnly, Text ) + local MarkID = UTILS.GetMarkID() + if ReadOnly==nil then + ReadOnly=false + end + local text=Text or "" + trigger.action.markToAll( MarkID, MarkText, self:GetVec3(), ReadOnly, text) + return MarkID + end + + --- Mark to Coalition + -- @param #COORDINATE self + -- @param #string MarkText Free format text that shows the marking clarification. + -- @param Coalition + -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. + -- @param #string Text (Optional) Text displayed when mark is added. Default none. + -- @return #number The resulting Mark ID which is a number. + -- @usage + -- local TargetCoord = TargetGroup:GetCoordinate() + -- local MarkID = TargetCoord:MarkToCoalition( "This is a target for the red coalition", coalition.side.RED ) + function COORDINATE:MarkToCoalition( MarkText, Coalition, ReadOnly, Text ) + local MarkID = UTILS.GetMarkID() + if ReadOnly==nil then + ReadOnly=false + end + local text=Text or "" + trigger.action.markToCoalition( MarkID, MarkText, self:GetVec3(), Coalition, ReadOnly, text ) + return MarkID + end + + --- Mark to Red Coalition + -- @param #COORDINATE self + -- @param #string MarkText Free format text that shows the marking clarification. + -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. + -- @param #string Text (Optional) Text displayed when mark is added. Default none. + -- @return #number The resulting Mark ID which is a number. + -- @usage + -- local TargetCoord = TargetGroup:GetCoordinate() + -- local MarkID = TargetCoord:MarkToCoalitionRed( "This is a target for the red coalition" ) + function COORDINATE:MarkToCoalitionRed( MarkText, ReadOnly, Text ) + return self:MarkToCoalition( MarkText, coalition.side.RED, ReadOnly, Text ) + end + + --- Mark to Blue Coalition + -- @param #COORDINATE self + -- @param #string MarkText Free format text that shows the marking clarification. + -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. + -- @param #string Text (Optional) Text displayed when mark is added. Default none. + -- @return #number The resulting Mark ID which is a number. + -- @usage + -- local TargetCoord = TargetGroup:GetCoordinate() + -- local MarkID = TargetCoord:MarkToCoalitionBlue( "This is a target for the blue coalition" ) + function COORDINATE:MarkToCoalitionBlue( MarkText, ReadOnly, Text ) + return self:MarkToCoalition( MarkText, coalition.side.BLUE, ReadOnly, Text ) + end + + --- Mark to Group + -- @param #COORDINATE self + -- @param #string MarkText Free format text that shows the marking clarification. + -- @param Wrapper.Group#GROUP MarkGroup The @{Wrapper.Group} that receives the mark. + -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. + -- @param #string Text (Optional) Text displayed when mark is added. Default none. + -- @return #number The resulting Mark ID which is a number. + -- @usage + -- local TargetCoord = TargetGroup:GetCoordinate() + -- local MarkGroup = GROUP:FindByName( "AttackGroup" ) + -- local MarkID = TargetCoord:MarkToGroup( "This is a target for the attack group", AttackGroup ) + function COORDINATE:MarkToGroup( MarkText, MarkGroup, ReadOnly, Text ) + local MarkID = UTILS.GetMarkID() + if ReadOnly==nil then + ReadOnly=false + end + local text=Text or "" + trigger.action.markToGroup( MarkID, MarkText, self:GetVec3(), MarkGroup:GetID(), ReadOnly, text ) + return MarkID + end + + --- Remove a mark + -- @param #COORDINATE self + -- @param #number MarkID The ID of the mark to be removed. + -- @usage + -- local TargetCoord = TargetGroup:GetCoordinate() + -- local MarkGroup = GROUP:FindByName( "AttackGroup" ) + -- local MarkID = TargetCoord:MarkToGroup( "This is a target for the attack group", AttackGroup ) + -- <<< logic >>> + -- RemoveMark( MarkID ) -- The mark is now removed + function COORDINATE:RemoveMark( MarkID ) + trigger.action.removeMark( MarkID ) + end + + --- Line to all. + -- Creates a line on the F10 map from one point to another. + -- @param #COORDINATE self + -- @param #COORDINATE Endpoint COORDIANTE to where the line is drawn. + -- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All. + -- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red (default). + -- @param #number Alpha Transparency [0,1]. Default 1. + -- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. + -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. + -- @param #string Text (Optional) Text displayed when mark is added. Default none. + -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again. + function COORDINATE:LineToAll(Endpoint, Coalition, Color, Alpha, LineType, ReadOnly, Text) + local MarkID = UTILS.GetMarkID() + if ReadOnly==nil then + ReadOnly=false + end + local vec3=Endpoint:GetVec3() + Coalition=Coalition or -1 + Color=Color or {1,0,0} + Color[4]=Alpha or 1.0 + LineType=LineType or 1 + trigger.action.lineToAll(Coalition, MarkID, self:GetVec3(), vec3, Color, LineType, ReadOnly, Text or "") + return MarkID + end + + --- Circle to all. + -- Creates a circle on the map with a given radius, color, fill color, and outline. + -- @param #COORDINATE self + -- @param #numberr Radius Radius in meters. Default 1000 m. + -- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All. + -- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red (default). + -- @param #number Alpha Transparency [0,1]. Default 1. + -- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value. + -- @param #number FillAlpha Transparency [0,1]. Default 0.15. + -- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. + -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. + -- @param #string Text (Optional) Text displayed when mark is added. Default none. + -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again. + function COORDINATE:CircleToAll(Radius, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly, Text) + local MarkID = UTILS.GetMarkID() + if ReadOnly==nil then + ReadOnly=false + end + local vec3=self:GetVec3() + Radius=Radius or 1000 + Coalition=Coalition or -1 + Color=Color or {1,0,0} + Color[4]=Alpha or 1.0 + LineType=LineType or 1 + FillColor=FillColor or Color + FillColor[4]=FillAlpha or 0.15 + trigger.action.circleToAll(Coalition, MarkID, vec3, Radius, Color, FillColor, LineType, ReadOnly, Text or "") + return MarkID + end + + end -- Markings + + --- Rectangle to all. Creates a rectangle on the map from the COORDINATE in one corner to the end COORDINATE in the opposite corner. + -- Creates a line on the F10 map from one point to another. + -- @param #COORDINATE self + -- @param #COORDINATE Endpoint COORDIANTE in the opposite corner. + -- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All. + -- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red (default). + -- @param #number Alpha Transparency [0,1]. Default 1. + -- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value. + -- @param #number FillAlpha Transparency [0,1]. Default 0.15. + -- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. + -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. + -- @param #string Text (Optional) Text displayed when mark is added. Default none. + -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again. + function COORDINATE:RectToAll(Endpoint, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly, Text) + local MarkID = UTILS.GetMarkID() + if ReadOnly==nil then + ReadOnly=false + end + local vec3=Endpoint:GetVec3() + Coalition=Coalition or -1 + Color=Color or {1,0,0} + Color[4]=Alpha or 1.0 + LineType=LineType or 1 + FillColor=FillColor or Color + FillColor[4]=FillAlpha or 0.15 + trigger.action.rectToAll(Coalition, MarkID, self:GetVec3(), vec3, Color, FillColor, LineType, ReadOnly, Text or "") + return MarkID + end + + --- Creates a shape defined by 4 points on the F10 map. The first point is the current COORDINATE. The remaining three points need to be specified. + -- @param #COORDINATE self + -- @param #COORDINATE Coord2 Second COORDIANTE of the quad shape. + -- @param #COORDINATE Coord3 Third COORDIANTE of the quad shape. + -- @param #COORDINATE Coord4 Fourth COORDIANTE of the quad shape. + -- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All. + -- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red (default). + -- @param #number Alpha Transparency [0,1]. Default 1. + -- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value. + -- @param #number FillAlpha Transparency [0,1]. Default 0.15. + -- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. + -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. + -- @param #string Text (Optional) Text displayed when mark is added. Default none. + -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again. + function COORDINATE:QuadToAll(Coord2, Coord3, Coord4, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly, Text) + local MarkID = UTILS.GetMarkID() + if ReadOnly==nil then + ReadOnly=false + end + local point1=self:GetVec3() + local point2=Coord2:GetVec3() + local point3=Coord3:GetVec3() + local point4=Coord4:GetVec3() + Coalition=Coalition or -1 + Color=Color or {1,0,0} + Color[4]=Alpha or 1.0 + LineType=LineType or 1 + FillColor=FillColor or Color + FillColor[4]=FillAlpha or 0.15 + trigger.action.quadToAll(Coalition, MarkID, self:GetVec3(), point2, point3, point4, Color, FillColor, LineType, ReadOnly, Text or "") + return MarkID + end + + --- Creates a free form shape on the F10 map. The first point is the current COORDINATE. The remaining points need to be specified. + -- **NOTE**: A free form polygon must have **at least three points** in total and currently only **up to 10 points** in total are supported. + -- @param #COORDINATE self + -- @param #table Coordinates Table of coordinates of the remaining points of the shape. + -- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All. + -- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red (default). + -- @param #number Alpha Transparency [0,1]. Default 1. + -- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value. + -- @param #number FillAlpha Transparency [0,1]. Default 0.15. + -- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. + -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. + -- @param #string Text (Optional) Text displayed when mark is added. Default none. + -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again. + function COORDINATE:MarkupToAllFreeForm(Coordinates, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly, Text) + + local MarkID = UTILS.GetMarkID() + if ReadOnly==nil then + ReadOnly=false + end + + Coalition=Coalition or -1 + + Color=Color or {1,0,0} + Color[4]=Alpha or 1.0 + + LineType=LineType or 1 + + FillColor=FillColor or UTILS.DeepCopy(Color) + FillColor[4]=FillAlpha or 0.15 + + local vecs={} + vecs[1]=self:GetVec3() + for i,coord in ipairs(Coordinates) do + vecs[i+1]=coord:GetVec3() + end + + if #vecs<3 then + self:E("ERROR: A free form polygon needs at least three points!") + elseif #vecs==3 then + trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], Color, FillColor, LineType, ReadOnly, Text or "") + elseif #vecs==4 then + trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], Color, FillColor, LineType, ReadOnly, Text or "") + elseif #vecs==5 then + trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], vecs[5], Color, FillColor, LineType, ReadOnly, Text or "") + elseif #vecs==6 then + trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], vecs[5], vecs[6], Color, FillColor, LineType, ReadOnly, Text or "") + elseif #vecs==7 then + trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], vecs[5], vecs[6], vecs[7], Color, FillColor, LineType, ReadOnly, Text or "") + elseif #vecs==8 then + trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], vecs[5], vecs[6], vecs[7], vecs[8], Color, FillColor, LineType, ReadOnly, Text or "") + elseif #vecs==9 then + trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], vecs[5], vecs[6], vecs[7], vecs[8], vecs[9], Color, FillColor, LineType, ReadOnly, Text or "") + elseif #vecs==10 then + trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], vecs[5], vecs[6], vecs[7], vecs[8], vecs[9], vecs[10], Color, FillColor, LineType, ReadOnly, Text or "") + else + self:E("ERROR: Currently a free form polygon can only have 10 points in total!") + -- Unfortunately, unpack(vecs) does not work! So no idea how to generalize this :( + trigger.action.markupToAll(7, Coalition, MarkID, unpack(vecs), Color, FillColor, LineType, ReadOnly, Text or "") + end + + return MarkID + end + + --- Text to all. Creates a text imposed on the map at the COORDINATE. Text scales with the map. + -- @param #COORDINATE self + -- @param #string Text Text displayed on the F10 map. + -- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All. + -- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red (default). + -- @param #number Alpha Transparency [0,1]. Default 1. + -- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value. + -- @param #number FillAlpha Transparency [0,1]. Default 0.3. + -- @param #number FontSize Font size. Default 14. + -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. + -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again. + function COORDINATE:TextToAll(Text, Coalition, Color, Alpha, FillColor, FillAlpha, FontSize, ReadOnly) + local MarkID = UTILS.GetMarkID() + if ReadOnly==nil then + ReadOnly=false + end + Coalition=Coalition or -1 + Color=Color or {1,0,0} + Color[4]=Alpha or 1.0 + FillColor=FillColor or Color + FillColor[4]=FillAlpha or 0.3 + FontSize=FontSize or 14 + trigger.action.textToAll(Coalition, MarkID, self:GetVec3(), Color, FillColor, FontSize, ReadOnly, Text or "Hello World") + return MarkID + end + + --- Arrow to all. Creates an arrow from the COORDINATE to the endpoint COORDINATE on the F10 map. There is no control over other dimensions of the arrow. + -- @param #COORDINATE self + -- @param #COORDINATE Endpoint COORDINATE where the tip of the arrow is pointing at. + -- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All. + -- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red (default). + -- @param #number Alpha Transparency [0,1]. Default 1. + -- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value. + -- @param #number FillAlpha Transparency [0,1]. Default 0.15. + -- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. + -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. + -- @param #string Text (Optional) Text displayed when mark is added. Default none. + -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again. + function COORDINATE:ArrowToAll(Endpoint, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly, Text) + local MarkID = UTILS.GetMarkID() + if ReadOnly==nil then + ReadOnly=false + end + local vec3=Endpoint:GetVec3() + Coalition=Coalition or -1 + Color=Color or {1,0,0} + Color[4]=Alpha or 1.0 + LineType=LineType or 1 + FillColor=FillColor or Color + FillColor[4]=FillAlpha or 0.15 + --trigger.action.textToAll(Coalition, MarkID, self:GetVec3(), Color, FillColor, FontSize, ReadOnly, Text or "Hello World") + trigger.action.arrowToAll(Coalition, MarkID, vec3, self:GetVec3(), Color, FillColor, LineType, ReadOnly, Text or "") + return MarkID + end + + --- Returns if a Coordinate has Line of Sight (LOS) with the ToCoordinate. + -- @param #COORDINATE self + -- @param #COORDINATE ToCoordinate + -- @param #number Offset Height offset in meters. Default 2 m. + -- @return #boolean true If the ToCoordinate has LOS with the Coordinate, otherwise false. + function COORDINATE:IsLOS( ToCoordinate, Offset ) + + Offset=Offset or 2 + + -- Measurement of visibility should not be from the ground, so Adding a hypotethical 2 meters to each Coordinate. + local FromVec3 = self:GetVec3() + FromVec3.y = FromVec3.y + Offset + + local ToVec3 = ToCoordinate:GetVec3() + ToVec3.y = ToVec3.y + Offset + + local IsLOS = land.isVisible( FromVec3, ToVec3 ) + + return IsLOS + end + + + --- Returns if a Coordinate is in a certain Radius of this Coordinate in 2D plane using the X and Z axis. + -- @param #COORDINATE self + -- @param #COORDINATE Coordinate The coordinate that will be tested if it is in the radius of this coordinate. + -- @param #number Radius The radius of the circle on the 2D plane around this coordinate. + -- @return #boolean true if in the Radius. + function COORDINATE:IsInRadius( Coordinate, Radius ) + + local InVec2 = self:GetVec2() + local Vec2 = Coordinate:GetVec2() + + local InRadius = UTILS.IsInRadius( InVec2, Vec2, Radius) + + return InRadius + end + + + --- Returns if a Coordinate is in a certain radius of this Coordinate in 3D space using the X, Y and Z axis. + -- So Radius defines the radius of the a Sphere in 3D space around this coordinate. + -- @param #COORDINATE self + -- @param #COORDINATE ToCoordinate The coordinate that will be tested if it is in the radius of this coordinate. + -- @param #number Radius The radius of the sphere in the 3D space around this coordinate. + -- @return #boolean true if in the Sphere. + function COORDINATE:IsInSphere( Coordinate, Radius ) + + local InVec3 = self:GetVec3() + local Vec3 = Coordinate:GetVec3() + + local InSphere = UTILS.IsInSphere( InVec3, Vec3, Radius) + + return InSphere + end + + --- Get sun rise time for a specific date at the coordinate. + -- @param #COORDINATE self + -- @param #number Day The day. + -- @param #number Month The month. + -- @param #number Year The year. + -- @param #boolean InSeconds If true, return the sun rise time in seconds. + -- @return #string Sunrise time, e.g. "05:41". + function COORDINATE:GetSunriseAtDate(Day, Month, Year, InSeconds) + + -- Day of the year. + local DayOfYear=UTILS.GetDayOfYear(Year, Month, Day) + + local Latitude, Longitude=self:GetLLDDM() + + local Tdiff=UTILS.GMTToLocalTimeDifference() + + local sunrise=UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, true, Tdiff) + + if InSeconds then + return sunrise + else + return UTILS.SecondsToClock(sunrise, true) + end + + end + + --- Get sun rise time for a specific day of the year at the coordinate. + -- @param #COORDINATE self + -- @param #number DayOfYear The day of the year. + -- @param #boolean InSeconds If true, return the sun rise time in seconds. + -- @return #string Sunrise time, e.g. "05:41". + function COORDINATE:GetSunriseAtDayOfYear(DayOfYear, InSeconds) + + local Latitude, Longitude=self:GetLLDDM() + + local Tdiff=UTILS.GMTToLocalTimeDifference() + + local sunrise=UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, true, Tdiff) + + if InSeconds then + return sunrise + else + return UTILS.SecondsToClock(sunrise, true) + end + + end + + --- Get todays sun rise time. + -- @param #COORDINATE self + -- @param #boolean InSeconds If true, return the sun rise time in seconds. + -- @return #string Sunrise time, e.g. "05:41". + function COORDINATE:GetSunrise(InSeconds) + + -- Get current day of the year. + local DayOfYear=UTILS.GetMissionDayOfYear() + + -- Lat and long at this point. + local Latitude, Longitude=self:GetLLDDM() + + -- GMT time diff. + local Tdiff=UTILS.GMTToLocalTimeDifference() + + -- Sunrise in seconds of the day. + local sunrise=UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, true, Tdiff) + + local date=UTILS.GetDCSMissionDate() + + -- Debug output. + --self:I(string.format("Sun rise at lat=%.3f long=%.3f on %s (DayOfYear=%d): %s (%d sec of the day) (GMT %d)", Latitude, Longitude, date, DayOfYear, tostring(UTILS.SecondsToClock(sunrise)), sunrise, Tdiff)) + + if InSeconds then + return sunrise + else + return UTILS.SecondsToClock(sunrise, true) + end + + end + + --- Get minutes until the next sun rise at this coordinate. + -- @param #COORDINATE self + -- @param OnlyToday If true, only calculate the sun rise of today. If sun has already risen, the time in negative minutes since sunrise is reported. + -- @return #number Minutes to the next sunrise. + function COORDINATE:GetMinutesToSunrise(OnlyToday) + + -- Seconds of today + local time=UTILS.SecondsOfToday() + + -- Next Sunrise in seconds. + local sunrise=nil + + -- Time to sunrise. + local delta=nil + + if OnlyToday then + + --- + -- Sunrise of today + --- + + sunrise=self:GetSunrise(true) + + delta=sunrise-time + + else + + --- + -- Sunrise of tomorrow + --- + + -- Tomorrows day of the year. + local DayOfYear=UTILS.GetMissionDayOfYear()+1 + + local Latitude, Longitude=self:GetLLDDM() + + local Tdiff=UTILS.GMTToLocalTimeDifference() + + sunrise=UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, true, Tdiff) + + delta=sunrise+UTILS.SecondsToMidnight() + + end + + return delta/60 + end + + --- Check if it is day, i.e. if the sun has risen about the horizon at this coordinate. + -- @param #COORDINATE self + -- @param #string Clock (Optional) Time in format "HH:MM:SS+D", e.g. "05:40:00+3" to check if is day at 5:40 at third day after mission start. Default is to check right now. + -- @return #boolean If true, it is day. If false, it is night time. + function COORDINATE:IsDay(Clock) + + if Clock then + + local Time=UTILS.ClockToSeconds(Clock) + + local clock=UTILS.Split(Clock, "+")[1] + + -- Tomorrows day of the year. + local DayOfYear=UTILS.GetMissionDayOfYear(Time) + + local Latitude, Longitude=self:GetLLDDM() + + local Tdiff=UTILS.GMTToLocalTimeDifference() + + local sunrise=UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, true, Tdiff) + local sunset=UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, false, Tdiff) + + local time=UTILS.ClockToSeconds(clock) + + -- Check if time is between sunrise and sunset. + if time>sunrise and time<=sunset then + return true + else + return false + end + + else + + -- Todays sun rise in sec. + local sunrise=self:GetSunrise(true) + + -- Todays sun set in sec. + local sunset=self:GetSunset(true) + + -- Seconds passed since midnight. + local time=UTILS.SecondsOfToday() + + -- Check if time is between sunrise and sunset. + if time>sunrise and time<=sunset then + return true + else + return false + end + + end + + end + + --- Check if it is night, i.e. if the sun has set below the horizon at this coordinate. + -- @param #COORDINATE self + -- @param #string Clock (Optional) Time in format "HH:MM:SS+D", e.g. "05:40:00+3" to check if is night at 5:40 at third day after mission start. Default is to check right now. + -- @return #boolean If true, it is night. If false, it is day time. + function COORDINATE:IsNight(Clock) + return not self:IsDay(Clock) + end + + --- Get sun set time for a specific date at the coordinate. + -- @param #COORDINATE self + -- @param #number Day The day. + -- @param #number Month The month. + -- @param #number Year The year. + -- @param #boolean InSeconds If true, return the sun rise time in seconds. + -- @return #string Sunset time, e.g. "20:41". + function COORDINATE:GetSunsetAtDate(Day, Month, Year, InSeconds) + + -- Day of the year. + local DayOfYear=UTILS.GetDayOfYear(Year, Month, Day) + + local Latitude, Longitude=self:GetLLDDM() + + local Tdiff=UTILS.GMTToLocalTimeDifference() + + local sunset=UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, false, Tdiff) + + if InSeconds then + return sunset + else + return UTILS.SecondsToClock(sunset, true) + end + + end + + --- Get todays sun set time. + -- @param #COORDINATE self + -- @param #boolean InSeconds If true, return the sun set time in seconds. + -- @return #string Sunrise time, e.g. "20:41". + function COORDINATE:GetSunset(InSeconds) + + -- Get current day of the year. + local DayOfYear=UTILS.GetMissionDayOfYear() + + -- Lat and long at this point. + local Latitude, Longitude=self:GetLLDDM() + + -- GMT time diff. + local Tdiff=UTILS.GMTToLocalTimeDifference() + + -- Sunrise in seconds of the day. + local sunrise=UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, false, Tdiff) + + local date=UTILS.GetDCSMissionDate() + + -- Debug output. + --self:I(string.format("Sun set at lat=%.3f long=%.3f on %s (DayOfYear=%d): %s (%d sec of the day) (GMT %d)", Latitude, Longitude, date, DayOfYear, tostring(UTILS.SecondsToClock(sunrise)), sunrise, Tdiff)) + + if InSeconds then + return sunrise + else + return UTILS.SecondsToClock(sunrise, true) + end + + end + + --- Get minutes until the next sun set at this coordinate. + -- @param #COORDINATE self + -- @param OnlyToday If true, only calculate the sun set of today. If sun has already set, the time in negative minutes since sunset is reported. + -- @return #number Minutes to the next sunrise. + function COORDINATE:GetMinutesToSunset(OnlyToday) + + -- Seconds of today + local time=UTILS.SecondsOfToday() + + -- Next Sunset in seconds. + local sunset=nil + + -- Time to sunrise. + local delta=nil + + if OnlyToday then + + --- + -- Sunset of today + --- + + sunset=self:GetSunset(true) + + delta=sunset-time + + else + + --- + -- Sunset of tomorrow + --- + + -- Tomorrows day of the year. + local DayOfYear=UTILS.GetMissionDayOfYear()+1 + + local Latitude, Longitude=self:GetLLDDM() + + local Tdiff=UTILS.GMTToLocalTimeDifference() + + sunset=UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, false, Tdiff) + + delta=sunset+UTILS.SecondsToMidnight() + + end + + return delta/60 + end + + + --- Return a BR string from a COORDINATE to the COORDINATE. + -- @param #COORDINATE self + -- @param #COORDINATE FromCoordinate The coordinate to measure the distance and the bearing from. + -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object. + -- @return #string The BR text. + function COORDINATE:ToStringBR( FromCoordinate, Settings ) + local DirectionVec3 = FromCoordinate:GetDirectionVec3( self ) + local AngleRadians = self:GetAngleRadians( DirectionVec3 ) + local Distance = self:Get2DDistance( FromCoordinate ) + return "BR, " .. self:GetBRText( AngleRadians, Distance, Settings ) + end + + --- Return a BRAA string from a COORDINATE to the COORDINATE. + -- @param #COORDINATE self + -- @param #COORDINATE FromCoordinate The coordinate to measure the distance and the bearing from. + -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object. + -- @return #string The BR text. + function COORDINATE:ToStringBRA( FromCoordinate, Settings, Language ) + local DirectionVec3 = FromCoordinate:GetDirectionVec3( self ) + local AngleRadians = self:GetAngleRadians( DirectionVec3 ) + local Distance = FromCoordinate:Get2DDistance( self ) + local Altitude = self:GetAltitudeText() + return "BRA, " .. self:GetBRAText( AngleRadians, Distance, Settings, Language ) + end + + --- Return a BULLS string out of the BULLS of the coalition to the COORDINATE. + -- @param #COORDINATE self + -- @param DCS#coalition.side Coalition The coalition. + -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object. + -- @return #string The BR text. + function COORDINATE:ToStringBULLS( Coalition, Settings ) + local BullsCoordinate = COORDINATE:NewFromVec3( coalition.getMainRefPoint( Coalition ) ) + local DirectionVec3 = BullsCoordinate:GetDirectionVec3( self ) + local AngleRadians = self:GetAngleRadians( DirectionVec3 ) + local Distance = self:Get2DDistance( BullsCoordinate ) + local Altitude = self:GetAltitudeText() + return "BULLS, " .. self:GetBRText( AngleRadians, Distance, Settings ) + end + + --- Return an aspect string from a COORDINATE to the Angle of the object. + -- @param #COORDINATE self + -- @param #COORDINATE TargetCoordinate The target COORDINATE. + -- @return #string The Aspect string, which is Hot, Cold or Flanking. + function COORDINATE:ToStringAspect( TargetCoordinate ) + local Heading = self.Heading + local DirectionVec3 = self:GetDirectionVec3( TargetCoordinate ) + local Angle = self:GetAngleDegrees( DirectionVec3 ) + + if Heading then + local Aspect = Angle - Heading + if Aspect > -135 and Aspect <= -45 then + return "Flanking" + end + if Aspect > -45 and Aspect <= 45 then + return "Hot" + end + if Aspect > 45 and Aspect <= 135 then + return "Flanking" + end + if Aspect > 135 or Aspect <= -135 then + return "Cold" + end + end + return "" + end + + --- Get Latitude and Longitude in Degrees Decimal Minutes (DDM). + -- @param #COORDINATE self + -- @return #number Latitude in DDM. + -- @return #number Lontitude in DDM. + function COORDINATE:GetLLDDM() + return coord.LOtoLL( self:GetVec3() ) + end + + --- Provides a Lat Lon string in Degree Minute Second format. + -- @param #COORDINATE self + -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object. + -- @return #string The LL DMS Text + function COORDINATE:ToStringLLDMS( Settings ) + + local LL_Accuracy = Settings and Settings.LL_Accuracy or _SETTINGS.LL_Accuracy + local lat, lon = coord.LOtoLL( self:GetVec3() ) + return "LL DMS " .. UTILS.tostringLL( lat, lon, LL_Accuracy, true ) + end + + --- Provides a Lat Lon string in Degree Decimal Minute format. + -- @param #COORDINATE self + -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object. + -- @return #string The LL DDM Text + function COORDINATE:ToStringLLDDM( Settings ) + + local LL_Accuracy = Settings and Settings.LL_Accuracy or _SETTINGS.LL_Accuracy + local lat, lon = coord.LOtoLL( self:GetVec3() ) + return "LL DDM " .. UTILS.tostringLL( lat, lon, LL_Accuracy, false ) + end + + --- Provides a MGRS string + -- @param #COORDINATE self + -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object. + -- @return #string The MGRS Text + function COORDINATE:ToStringMGRS( Settings ) --R2.1 Fixes issue #424. + + local MGRS_Accuracy = Settings and Settings.MGRS_Accuracy or _SETTINGS.MGRS_Accuracy + local lat, lon = coord.LOtoLL( self:GetVec3() ) + local MGRS = coord.LLtoMGRS( lat, lon ) + return "MGRS " .. UTILS.tostringMGRS( MGRS, MGRS_Accuracy ) + end + + --- Provides a coordinate string of the point, based on a coordinate format system: + -- * Uses default settings in COORDINATE. + -- * Can be overridden if for a GROUP containing x clients, a menu was selected to override the default. + -- @param #COORDINATE self + -- @param #COORDINATE ReferenceCoord The refrence coordinate. + -- @param #string ReferenceName The refrence name. + -- @param Wrapper.Controllable#CONTROLLABLE Controllable + -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object. + -- @return #string The coordinate Text in the configured coordinate system. + function COORDINATE:ToStringFromRP( ReferenceCoord, ReferenceName, Controllable, Settings ) + + self:F2( { ReferenceCoord = ReferenceCoord, ReferenceName = ReferenceName } ) + + local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS + + local IsAir = Controllable and Controllable:IsAirPlane() or false + + if IsAir then + local DirectionVec3 = ReferenceCoord:GetDirectionVec3( self ) + local AngleRadians = self:GetAngleRadians( DirectionVec3 ) + local Distance = self:Get2DDistance( ReferenceCoord ) + return "Targets are the last seen " .. self:GetBRText( AngleRadians, Distance, Settings ) .. " from " .. ReferenceName + else + local DirectionVec3 = ReferenceCoord:GetDirectionVec3( self ) + local AngleRadians = self:GetAngleRadians( DirectionVec3 ) + local Distance = self:Get2DDistance( ReferenceCoord ) + return "Target are located " .. self:GetBRText( AngleRadians, Distance, Settings ) .. " from " .. ReferenceName + end + + return nil + + end + + --- Provides a coordinate string of the point, based on the A2G coordinate format system. + -- @param #COORDINATE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable + -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object. + -- @return #string The coordinate Text in the configured coordinate system. + function COORDINATE:ToStringA2G( Controllable, Settings ) + + self:F2( { Controllable = Controllable and Controllable:GetName() } ) + + local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS + + if Settings:IsA2G_BR() then + -- If no Controllable is given to calculate the BR from, then MGRS will be used!!! + if Controllable then + local Coordinate = Controllable:GetCoordinate() + return Controllable and self:ToStringBR( Coordinate, Settings ) or self:ToStringMGRS( Settings ) + else + return self:ToStringMGRS( Settings ) + end + end + if Settings:IsA2G_LL_DMS() then + return self:ToStringLLDMS( Settings ) + end + if Settings:IsA2G_LL_DDM() then + return self:ToStringLLDDM( Settings ) + end + if Settings:IsA2G_MGRS() then + return self:ToStringMGRS( Settings ) + end + + return nil + + end + + + --- Provides a coordinate string of the point, based on the A2A coordinate format system. + -- @param #COORDINATE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable + -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object. + -- @return #string The coordinate Text in the configured coordinate system. + function COORDINATE:ToStringA2A( Controllable, Settings, Language ) -- R2.2 + + self:F2( { Controllable = Controllable and Controllable:GetName() } ) + + local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS + + if Settings:IsA2A_BRAA() then + if Controllable then + local Coordinate = Controllable:GetCoordinate() + return self:ToStringBRA( Coordinate, Settings, Language ) + else + return self:ToStringMGRS( Settings, Language ) + end + end + if Settings:IsA2A_BULLS() then + local Coalition = Controllable:GetCoalition() + return self:ToStringBULLS( Coalition, Settings, Language ) + end + if Settings:IsA2A_LL_DMS() then + return self:ToStringLLDMS( Settings, Language ) + end + if Settings:IsA2A_LL_DDM() then + return self:ToStringLLDDM( Settings, Language ) + end + if Settings:IsA2A_MGRS() then + return self:ToStringMGRS( Settings, Language ) + end + + return nil + + end + + --- Provides a coordinate string of the point, based on a coordinate format system: + -- * Uses default settings in COORDINATE. + -- * Can be overridden if for a GROUP containing x clients, a menu was selected to override the default. + -- @param #COORDINATE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The controllable to retrieve the settings from, otherwise the default settings will be chosen. + -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object. + -- @param Tasking.Task#TASK Task The task for which coordinates need to be calculated. + -- @return #string The coordinate Text in the configured coordinate system. + function COORDINATE:ToString( Controllable, Settings, Task ) + +-- self:E( { Controllable = Controllable and Controllable:GetName() } ) + + local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS + + local ModeA2A = nil + + if Task then + if Task:IsInstanceOf( TASK_A2A ) then + ModeA2A = true + else + if Task:IsInstanceOf( TASK_A2G ) then + ModeA2A = false + else + if Task:IsInstanceOf( TASK_CARGO ) then + ModeA2A = false + end + if Task:IsInstanceOf( TASK_CAPTURE_ZONE ) then + ModeA2A = false + end + end + end + end + + + if ModeA2A == nil then + local IsAir = Controllable and ( Controllable:IsAirPlane() or Controllable:IsHelicopter() ) or false + if IsAir then + ModeA2A = true + else + ModeA2A = false + end + end + + + if ModeA2A == true then + return self:ToStringA2A( Controllable, Settings ) + else + return self:ToStringA2G( Controllable, Settings ) + end + + return nil + + end + + --- Provides a pressure string of the point, based on a measurement system: + -- * Uses default settings in COORDINATE. + -- * Can be overridden if for a GROUP containing x clients, a menu was selected to override the default. + -- @param #COORDINATE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable + -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object. + -- @return #string The pressure text in the configured measurement system. + function COORDINATE:ToStringPressure( Controllable, Settings ) -- R2.3 + + self:F2( { Controllable = Controllable and Controllable:GetName() } ) + + local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS + + return self:GetPressureText( nil, Settings ) + end + + --- Provides a wind string of the point, based on a measurement system: + -- * Uses default settings in COORDINATE. + -- * Can be overridden if for a GROUP containing x clients, a menu was selected to override the default. + -- @param #COORDINATE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable + -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object. + -- @return #string The wind text in the configured measurement system. + function COORDINATE:ToStringWind( Controllable, Settings ) + + self:F2( { Controllable = Controllable and Controllable:GetName() } ) + + local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS + + return self:GetWindText( nil, Settings ) + end + + --- Provides a temperature string of the point, based on a measurement system: + -- * Uses default settings in COORDINATE. + -- * Can be overridden if for a GROUP containing x clients, a menu was selected to override the default. + -- @param #COORDINATE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable + -- @param Core.Settings#SETTINGS + -- @return #string The temperature text in the configured measurement system. + function COORDINATE:ToStringTemperature( Controllable, Settings ) + + self:F2( { Controllable = Controllable and Controllable:GetName() } ) + + local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS + + return self:GetTemperatureText( nil, Settings ) + end + +end + +do -- POINT_VEC3 + + --- The POINT_VEC3 class + -- @type POINT_VEC3 + -- @field #number x The x coordinate in 3D space. + -- @field #number y The y coordinate in 3D space. + -- @field #number z The z coordiante in 3D space. + -- @field Utilities.Utils#SMOKECOLOR SmokeColor + -- @field Utilities.Utils#FLARECOLOR FlareColor + -- @field #POINT_VEC3.RoutePointAltType RoutePointAltType + -- @field #POINT_VEC3.RoutePointType RoutePointType + -- @field #POINT_VEC3.RoutePointAction RoutePointAction + -- @extends #COORDINATE + + + --- Defines a 3D point in the simulator and with its methods, you can use or manipulate the point in 3D space. + -- + -- **Important Note:** Most of the functions in this section were taken from MIST, and reworked to OO concepts. + -- In order to keep the credibility of the the author, + -- I want to emphasize that the formulas embedded in the MIST framework were created by Grimes or previous authors, + -- who you can find on the Eagle Dynamics Forums. + -- + -- + -- ## POINT_VEC3 constructor + -- + -- A new POINT_VEC3 object can be created with: + -- + -- * @{#POINT_VEC3.New}(): a 3D point. + -- * @{#POINT_VEC3.NewFromVec3}(): a 3D point created from a @{DCS#Vec3}. + -- + -- + -- ## Manupulate the X, Y, Z coordinates of the POINT_VEC3 + -- + -- A POINT_VEC3 class works in 3D space. It contains internally an X, Y, Z coordinate. + -- Methods exist to manupulate these coordinates. + -- + -- The current X, Y, Z axis can be retrieved with the methods @{#POINT_VEC3.GetX}(), @{#POINT_VEC3.GetY}(), @{#POINT_VEC3.GetZ}() respectively. + -- The methods @{#POINT_VEC3.SetX}(), @{#POINT_VEC3.SetY}(), @{#POINT_VEC3.SetZ}() change the respective axis with a new value. + -- The current axis values can be changed by using the methods @{#POINT_VEC3.AddX}(), @{#POINT_VEC3.AddY}(), @{#POINT_VEC3.AddZ}() + -- to add or substract a value from the current respective axis value. + -- Note that the Set and Add methods return the current POINT_VEC3 object, so these manipulation methods can be chained... For example: + -- + -- local Vec3 = PointVec3:AddX( 100 ):AddZ( 150 ):GetVec3() + -- + -- + -- ## 3D calculation methods + -- + -- Various calculation methods exist to use or manipulate 3D space. Find below a short description of each method: + -- + -- + -- ## Point Randomization + -- + -- Various methods exist to calculate random locations around a given 3D point. + -- + -- * @{#POINT_VEC3.GetRandomPointVec3InRadius}(): Provides a random 3D point around the current 3D point, in the given inner to outer band. + -- + -- + -- @field #POINT_VEC3 + POINT_VEC3 = { + ClassName = "POINT_VEC3", + Metric = true, + RoutePointAltType = { + BARO = "BARO", + }, + RoutePointType = { + TakeOffParking = "TakeOffParking", + TurningPoint = "Turning Point", + }, + RoutePointAction = { + FromParkingArea = "From Parking Area", + TurningPoint = "Turning Point", + }, + } + + --- RoutePoint AltTypes + -- @type POINT_VEC3.RoutePointAltType + -- @field BARO "BARO" + + --- RoutePoint Types + -- @type POINT_VEC3.RoutePointType + -- @field TakeOffParking "TakeOffParking" + -- @field TurningPoint "Turning Point" + + --- RoutePoint Actions + -- @type POINT_VEC3.RoutePointAction + -- @field FromParkingArea "From Parking Area" + -- @field TurningPoint "Turning Point" + + -- Constructor. + + --- Create a new POINT_VEC3 object. + -- @param #POINT_VEC3 self + -- @param DCS#Distance x The x coordinate of the Vec3 point, pointing to the North. + -- @param DCS#Distance y The y coordinate of the Vec3 point, pointing Upwards. + -- @param DCS#Distance z The z coordinate of the Vec3 point, pointing to the Right. + -- @return Core.Point#POINT_VEC3 + function POINT_VEC3:New( x, y, z ) + + local self = BASE:Inherit( self, COORDINATE:New( x, y, z ) ) -- Core.Point#POINT_VEC3 + self:F2( self ) + + return self + end + + --- Create a new POINT_VEC3 object from Vec2 coordinates. + -- @param #POINT_VEC3 self + -- @param DCS#Vec2 Vec2 The Vec2 point. + -- @param DCS#Distance LandHeightAdd (optional) Add a landheight. + -- @return Core.Point#POINT_VEC3 self + function POINT_VEC3:NewFromVec2( Vec2, LandHeightAdd ) + + local self = BASE:Inherit( self, COORDINATE:NewFromVec2( Vec2, LandHeightAdd ) ) -- Core.Point#POINT_VEC3 + self:F2( self ) + + return self + end + + + --- Create a new POINT_VEC3 object from Vec3 coordinates. + -- @param #POINT_VEC3 self + -- @param DCS#Vec3 Vec3 The Vec3 point. + -- @return Core.Point#POINT_VEC3 self + function POINT_VEC3:NewFromVec3( Vec3 ) + + local self = BASE:Inherit( self, COORDINATE:NewFromVec3( Vec3 ) ) -- Core.Point#POINT_VEC3 + self:F2( self ) + + return self + end + + + + --- Return the x coordinate of the POINT_VEC3. + -- @param #POINT_VEC3 self + -- @return #number The x coodinate. + function POINT_VEC3:GetX() + return self.x + end + + --- Return the y coordinate of the POINT_VEC3. + -- @param #POINT_VEC3 self + -- @return #number The y coodinate. + function POINT_VEC3:GetY() + return self.y + end + + --- Return the z coordinate of the POINT_VEC3. + -- @param #POINT_VEC3 self + -- @return #number The z coodinate. + function POINT_VEC3:GetZ() + return self.z + end + + --- Set the x coordinate of the POINT_VEC3. + -- @param #POINT_VEC3 self + -- @param #number x The x coordinate. + -- @return #POINT_VEC3 + function POINT_VEC3:SetX( x ) + self.x = x + return self + end + + --- Set the y coordinate of the POINT_VEC3. + -- @param #POINT_VEC3 self + -- @param #number y The y coordinate. + -- @return #POINT_VEC3 + function POINT_VEC3:SetY( y ) + self.y = y + return self + end + + --- Set the z coordinate of the POINT_VEC3. + -- @param #POINT_VEC3 self + -- @param #number z The z coordinate. + -- @return #POINT_VEC3 + function POINT_VEC3:SetZ( z ) + self.z = z + return self + end + + --- Add to the x coordinate of the POINT_VEC3. + -- @param #POINT_VEC3 self + -- @param #number x The x coordinate value to add to the current x coodinate. + -- @return #POINT_VEC3 + function POINT_VEC3:AddX( x ) + self.x = self.x + x + return self + end + + --- Add to the y coordinate of the POINT_VEC3. + -- @param #POINT_VEC3 self + -- @param #number y The y coordinate value to add to the current y coodinate. + -- @return #POINT_VEC3 + function POINT_VEC3:AddY( y ) + self.y = self.y + y + return self + end + + --- Add to the z coordinate of the POINT_VEC3. + -- @param #POINT_VEC3 self + -- @param #number z The z coordinate value to add to the current z coodinate. + -- @return #POINT_VEC3 + function POINT_VEC3:AddZ( z ) + self.z = self.z +z + return self + end + + --- Return a random POINT_VEC3 within an Outer Radius and optionally NOT within an Inner Radius of the POINT_VEC3. + -- @param #POINT_VEC3 self + -- @param DCS#Distance OuterRadius + -- @param DCS#Distance InnerRadius + -- @return #POINT_VEC3 + function POINT_VEC3:GetRandomPointVec3InRadius( OuterRadius, InnerRadius ) + + return POINT_VEC3:NewFromVec3( self:GetRandomVec3InRadius( OuterRadius, InnerRadius ) ) + end + +end + +do -- POINT_VEC2 + + --- @type POINT_VEC2 + -- @field DCS#Distance x The x coordinate in meters. + -- @field DCS#Distance y the y coordinate in meters. + -- @extends Core.Point#COORDINATE + + --- Defines a 2D point in the simulator. The height coordinate (if needed) will be the land height + an optional added height specified. + -- + -- ## POINT_VEC2 constructor + -- + -- A new POINT_VEC2 instance can be created with: + -- + -- * @{Core.Point#POINT_VEC2.New}(): a 2D point, taking an additional height parameter. + -- * @{Core.Point#POINT_VEC2.NewFromVec2}(): a 2D point created from a @{DCS#Vec2}. + -- + -- ## Manupulate the X, Altitude, Y coordinates of the 2D point + -- + -- A POINT_VEC2 class works in 2D space, with an altitude setting. It contains internally an X, Altitude, Y coordinate. + -- Methods exist to manupulate these coordinates. + -- + -- The current X, Altitude, Y axis can be retrieved with the methods @{#POINT_VEC2.GetX}(), @{#POINT_VEC2.GetAlt}(), @{#POINT_VEC2.GetY}() respectively. + -- The methods @{#POINT_VEC2.SetX}(), @{#POINT_VEC2.SetAlt}(), @{#POINT_VEC2.SetY}() change the respective axis with a new value. + -- The current Lat(itude), Alt(itude), Lon(gitude) values can also be retrieved with the methods @{#POINT_VEC2.GetLat}(), @{#POINT_VEC2.GetAlt}(), @{#POINT_VEC2.GetLon}() respectively. + -- The current axis values can be changed by using the methods @{#POINT_VEC2.AddX}(), @{#POINT_VEC2.AddAlt}(), @{#POINT_VEC2.AddY}() + -- to add or substract a value from the current respective axis value. + -- Note that the Set and Add methods return the current POINT_VEC2 object, so these manipulation methods can be chained... For example: + -- + -- local Vec2 = PointVec2:AddX( 100 ):AddY( 2000 ):GetVec2() + -- + -- @field #POINT_VEC2 + POINT_VEC2 = { + ClassName = "POINT_VEC2", + } + + + + --- POINT_VEC2 constructor. + -- @param #POINT_VEC2 self + -- @param DCS#Distance x The x coordinate of the Vec3 point, pointing to the North. + -- @param DCS#Distance y The y coordinate of the Vec3 point, pointing to the Right. + -- @param DCS#Distance LandHeightAdd (optional) The default height if required to be evaluated will be the land height of the x, y coordinate. You can specify an extra height to be added to the land height. + -- @return Core.Point#POINT_VEC2 + function POINT_VEC2:New( x, y, LandHeightAdd ) + + local LandHeight = land.getHeight( { ["x"] = x, ["y"] = y } ) + + LandHeightAdd = LandHeightAdd or 0 + LandHeight = LandHeight + LandHeightAdd + + local self = BASE:Inherit( self, COORDINATE:New( x, LandHeight, y ) ) -- Core.Point#POINT_VEC2 + self:F2( self ) + + return self + end + + --- Create a new POINT_VEC2 object from Vec2 coordinates. + -- @param #POINT_VEC2 self + -- @param DCS#Vec2 Vec2 The Vec2 point. + -- @return Core.Point#POINT_VEC2 self + function POINT_VEC2:NewFromVec2( Vec2, LandHeightAdd ) + + local LandHeight = land.getHeight( Vec2 ) + + LandHeightAdd = LandHeightAdd or 0 + LandHeight = LandHeight + LandHeightAdd + + local self = BASE:Inherit( self, COORDINATE:NewFromVec2( Vec2, LandHeightAdd ) ) -- #POINT_VEC2 + self:F2( self ) + + return self + end + + --- Create a new POINT_VEC2 object from Vec3 coordinates. + -- @param #POINT_VEC2 self + -- @param DCS#Vec3 Vec3 The Vec3 point. + -- @return Core.Point#POINT_VEC2 self + function POINT_VEC2:NewFromVec3( Vec3 ) + + local self = BASE:Inherit( self, COORDINATE:NewFromVec3( Vec3 ) ) -- #POINT_VEC2 + self:F2( self ) + + return self + end + + --- Return the x coordinate of the POINT_VEC2. + -- @param #POINT_VEC2 self + -- @return #number The x coodinate. + function POINT_VEC2:GetX() + return self.x + end + + --- Return the y coordinate of the POINT_VEC2. + -- @param #POINT_VEC2 self + -- @return #number The y coodinate. + function POINT_VEC2:GetY() + return self.z + end + + --- Set the x coordinate of the POINT_VEC2. + -- @param #POINT_VEC2 self + -- @param #number x The x coordinate. + -- @return #POINT_VEC2 + function POINT_VEC2:SetX( x ) + self.x = x + return self + end + + --- Set the y coordinate of the POINT_VEC2. + -- @param #POINT_VEC2 self + -- @param #number y The y coordinate. + -- @return #POINT_VEC2 + function POINT_VEC2:SetY( y ) + self.z = y + return self + end + + --- Return Return the Lat(itude) coordinate of the POINT_VEC2 (ie: (parent)POINT_VEC3.x). + -- @param #POINT_VEC2 self + -- @return #number The x coodinate. + function POINT_VEC2:GetLat() + return self.x + end + + --- Set the Lat(itude) coordinate of the POINT_VEC2 (ie: POINT_VEC3.x). + -- @param #POINT_VEC2 self + -- @param #number x The x coordinate. + -- @return #POINT_VEC2 + function POINT_VEC2:SetLat( x ) + self.x = x + return self + end + + --- Return the Lon(gitude) coordinate of the POINT_VEC2 (ie: (parent)POINT_VEC3.z). + -- @param #POINT_VEC2 self + -- @return #number The y coodinate. + function POINT_VEC2:GetLon() + return self.z + end + + --- Set the Lon(gitude) coordinate of the POINT_VEC2 (ie: POINT_VEC3.z). + -- @param #POINT_VEC2 self + -- @param #number y The y coordinate. + -- @return #POINT_VEC2 + function POINT_VEC2:SetLon( z ) + self.z = z + return self + end + + --- Return the altitude (height) of the land at the POINT_VEC2. + -- @param #POINT_VEC2 self + -- @return #number The land altitude. + function POINT_VEC2:GetAlt() + return self.y ~= 0 or land.getHeight( { x = self.x, y = self.z } ) + end + + --- Set the altitude of the POINT_VEC2. + -- @param #POINT_VEC2 self + -- @param #number Altitude The land altitude. If nothing (nil) is given, then the current land altitude is set. + -- @return #POINT_VEC2 + function POINT_VEC2:SetAlt( Altitude ) + self.y = Altitude or land.getHeight( { x = self.x, y = self.z } ) + return self + end + + --- Add to the x coordinate of the POINT_VEC2. + -- @param #POINT_VEC2 self + -- @param #number x The x coordinate. + -- @return #POINT_VEC2 + function POINT_VEC2:AddX( x ) + self.x = self.x + x + return self + end + + --- Add to the y coordinate of the POINT_VEC2. + -- @param #POINT_VEC2 self + -- @param #number y The y coordinate. + -- @return #POINT_VEC2 + function POINT_VEC2:AddY( y ) + self.z = self.z + y + return self + end + + --- Add to the current land height an altitude. + -- @param #POINT_VEC2 self + -- @param #number Altitude The Altitude to add. If nothing (nil) is given, then the current land altitude is set. + -- @return #POINT_VEC2 + function POINT_VEC2:AddAlt( Altitude ) + self.y = land.getHeight( { x = self.x, y = self.z } ) + Altitude or 0 + return self + end + + + --- Return a random POINT_VEC2 within an Outer Radius and optionally NOT within an Inner Radius of the POINT_VEC2. + -- @param #POINT_VEC2 self + -- @param DCS#Distance OuterRadius + -- @param DCS#Distance InnerRadius + -- @return #POINT_VEC2 + function POINT_VEC2:GetRandomPointVec2InRadius( OuterRadius, InnerRadius ) + self:F2( { OuterRadius, InnerRadius } ) + + return POINT_VEC2:NewFromVec2( self:GetRandomVec2InRadius( OuterRadius, InnerRadius ) ) + end + + -- TODO: Check this to replace + --- Calculate the distance from a reference @{#POINT_VEC2}. + -- @param #POINT_VEC2 self + -- @param #POINT_VEC2 PointVec2Reference The reference @{#POINT_VEC2}. + -- @return DCS#Distance The distance from the reference @{#POINT_VEC2} in meters. + function POINT_VEC2:DistanceFromPointVec2( PointVec2Reference ) + self:F2( PointVec2Reference ) + + local Distance = ( ( PointVec2Reference.x - self.x ) ^ 2 + ( PointVec2Reference.z - self.z ) ^2 ) ^ 0.5 + + self:T2( Distance ) + return Distance + end + +end +--- **Core** - Models a velocity or speed, which can be expressed in various formats according the settings. +-- +-- === +-- +-- ## Features: +-- +-- * Convert velocity in various metric systems. +-- * Set the velocity. +-- * Create a text in a specific format of a velocity. +-- +-- === +-- +-- ### Author: **FlightControl** +-- ### Contributions: +-- +-- === +-- +-- @module Core.Velocity +-- @image MOOSE.JPG + +do -- Velocity + + --- @type VELOCITY + -- @extends Core.Base#BASE + + + --- VELOCITY models a speed, which can be expressed in various formats according the Settings. + -- + -- ## VELOCITY constructor + -- + -- * @{#VELOCITY.New}(): Creates a new VELOCITY object. + -- + -- @field #VELOCITY + VELOCITY = { + ClassName = "VELOCITY", + } + + --- VELOCITY Constructor. + -- @param #VELOCITY self + -- @param #number VelocityMps The velocity in meters per second. + -- @return #VELOCITY + function VELOCITY:New( VelocityMps ) + local self = BASE:Inherit( self, BASE:New() ) -- #VELOCITY + self:F( {} ) + self.Velocity = VelocityMps + return self + end + + --- Set the velocity in Mps (meters per second). + -- @param #VELOCITY self + -- @param #number VelocityMps The velocity in meters per second. + -- @return #VELOCITY + function VELOCITY:Set( VelocityMps ) + self.Velocity = VelocityMps + return self + end + + --- Get the velocity in Mps (meters per second). + -- @param #VELOCITY self + -- @return #number The velocity in meters per second. + function VELOCITY:Get() + return self.Velocity + end + + --- Set the velocity in Kmph (kilometers per hour). + -- @param #VELOCITY self + -- @param #number VelocityKmph The velocity in kilometers per hour. + -- @return #VELOCITY + function VELOCITY:SetKmph( VelocityKmph ) + self.Velocity = UTILS.KmphToMps( VelocityKmph ) + return self + end + + --- Get the velocity in Kmph (kilometers per hour). + -- @param #VELOCITY self + -- @return #number The velocity in kilometers per hour. + function VELOCITY:GetKmph() + + return UTILS.MpsToKmph( self.Velocity ) + end + + --- Set the velocity in Miph (miles per hour). + -- @param #VELOCITY self + -- @param #number VelocityMiph The velocity in miles per hour. + -- @return #VELOCITY + function VELOCITY:SetMiph( VelocityMiph ) + self.Velocity = UTILS.MiphToMps( VelocityMiph ) + return self + end + + --- Get the velocity in Miph (miles per hour). + -- @param #VELOCITY self + -- @return #number The velocity in miles per hour. + function VELOCITY:GetMiph() + return UTILS.MpsToMiph( self.Velocity ) + end + + + --- Get the velocity in text, according the player @{Settings}. + -- @param #VELOCITY self + -- @param Core.Settings#SETTINGS Settings + -- @return #string The velocity in text. + function VELOCITY:GetText( Settings ) + local Settings = Settings or _SETTINGS + if self.Velocity ~= 0 then + if Settings:IsMetric() then + return string.format( "%d km/h", UTILS.MpsToKmph( self.Velocity ) ) + else + return string.format( "%d mi/h", UTILS.MpsToMiph( self.Velocity ) ) + end + else + return "stationary" + end + end + + --- Get the velocity in text, according the player or default @{Settings}. + -- @param #VELOCITY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable + -- @param Core.Settings#SETTINGS Settings + -- @return #string The velocity in text according the player or default @{Settings} + function VELOCITY:ToString( VelocityGroup, Settings ) -- R2.3 + self:F( { Group = VelocityGroup and VelocityGroup:GetName() } ) + local Settings = Settings or ( VelocityGroup and _DATABASE:GetPlayerSettings( VelocityGroup:GetPlayerName() ) ) or _SETTINGS + return self:GetText( Settings ) + end + +end + +do -- VELOCITY_POSITIONABLE + + --- @type VELOCITY_POSITIONABLE + -- @extends Core.Base#BASE + + + --- # VELOCITY_POSITIONABLE class, extends @{Core.Base#BASE} + -- + -- VELOCITY_POSITIONABLE monitors the speed of an @{Positionable} in the simulation, which can be expressed in various formats according the Settings. + -- + -- ## 1. VELOCITY_POSITIONABLE constructor + -- + -- * @{#VELOCITY_POSITIONABLE.New}(): Creates a new VELOCITY_POSITIONABLE object. + -- + -- @field #VELOCITY_POSITIONABLE + VELOCITY_POSITIONABLE = { + ClassName = "VELOCITY_POSITIONABLE", + } + + --- VELOCITY_POSITIONABLE Constructor. + -- @param #VELOCITY_POSITIONABLE self + -- @param Wrapper.Positionable#POSITIONABLE Positionable The Positionable to monitor the speed. + -- @return #VELOCITY_POSITIONABLE + function VELOCITY_POSITIONABLE:New( Positionable ) + local self = BASE:Inherit( self, VELOCITY:New() ) -- #VELOCITY_POSITIONABLE + self:F( {} ) + self.Positionable = Positionable + return self + end + + --- Get the velocity in Mps (meters per second). + -- @param #VELOCITY_POSITIONABLE self + -- @return #number The velocity in meters per second. + function VELOCITY_POSITIONABLE:Get() + return self.Positionable:GetVelocityMPS() or 0 + end + + --- Get the velocity in Kmph (kilometers per hour). + -- @param #VELOCITY_POSITIONABLE self + -- @return #number The velocity in kilometers per hour. + function VELOCITY_POSITIONABLE:GetKmph() + + return UTILS.MpsToKmph( self.Positionable:GetVelocityMPS() or 0) + end + + --- Get the velocity in Miph (miles per hour). + -- @param #VELOCITY_POSITIONABLE self + -- @return #number The velocity in miles per hour. + function VELOCITY_POSITIONABLE:GetMiph() + return UTILS.MpsToMiph( self.Positionable:GetVelocityMPS() or 0 ) + end + + --- Get the velocity in text, according the player or default @{Settings}. + -- @param #VELOCITY_POSITIONABLE self + -- @return #string The velocity in text according the player or default @{Settings} + function VELOCITY_POSITIONABLE:ToString() -- R2.3 + self:F( { Group = self.Positionable and self.Positionable:GetName() } ) + local Settings = Settings or ( self.Positionable and _DATABASE:GetPlayerSettings( self.Positionable:GetPlayerName() ) ) or _SETTINGS + self.Velocity = self.Positionable:GetVelocityMPS() + return self:GetText( Settings ) + end + +end--- **Core** - Informs the players using messages during a simulation. +-- +-- === +-- +-- ## Features: +-- +-- * A more advanced messaging system using the DCS message system. +-- * Time messages. +-- * Send messages based on a message type, which has a pre-defined duration that can be tweaked in SETTINGS. +-- * Send message to all players. +-- * Send messages to a coalition. +-- * Send messages to a specific group. +-- +-- === +-- +-- @module Core.Message +-- @image Core_Message.JPG + +--- The MESSAGE class +-- @type MESSAGE +-- @extends Core.Base#BASE + +--- Message System to display Messages to Clients, Coalitions or All. +-- Messages are shown on the display panel for an amount of seconds, and will then disappear. +-- Messages can contain a category which is indicating the category of the message. +-- +-- ## MESSAGE construction +-- +-- Messages are created with @{#MESSAGE.New}. Note that when the MESSAGE object is created, no message is sent yet. +-- To send messages, you need to use the To functions. +-- +-- ## Send messages to an audience +-- +-- Messages are sent: +-- +-- * To a @{Client} using @{#MESSAGE.ToClient}(). +-- * To a @{Wrapper.Group} using @{#MESSAGE.ToGroup}() +-- * To a coalition using @{#MESSAGE.ToCoalition}(). +-- * To the red coalition using @{#MESSAGE.ToRed}(). +-- * To the blue coalition using @{#MESSAGE.ToBlue}(). +-- * To all Players using @{#MESSAGE.ToAll}(). +-- +-- ## Send conditionally to an audience +-- +-- Messages can be sent conditionally to an audience (when a condition is true): +-- +-- * To all players using @{#MESSAGE.ToAllIf}(). +-- * To a coalition using @{#MESSAGE.ToCoalitionIf}(). +-- +-- === +-- +-- ### Author: **FlightControl** +-- ### Contributions: +-- +-- === +-- +-- @field #MESSAGE +MESSAGE = { + ClassName = "MESSAGE", + MessageCategory = 0, + MessageID = 0, +} + +--- Message Types +-- @type MESSAGE.Type +MESSAGE.Type = { + Update = "Update", + Information = "Information", + Briefing = "Briefing Report", + Overview = "Overview Report", + Detailed = "Detailed Report" +} + + +--- Creates a new MESSAGE object. Note that these MESSAGE objects are not yet displayed on the display panel. You must use the functions @{ToClient} or @{ToCoalition} or @{ToAll} to send these Messages to the respective recipients. +-- @param self +-- @param #string MessageText is the text of the Message. +-- @param #number MessageDuration is a number in seconds of how long the MESSAGE should be shown on the display panel. +-- @param #string MessageCategory (optional) is a string expressing the "category" of the Message. The category will be shown as the first text in the message followed by a ": ". +-- @param #boolean ClearScreen (optional) Clear all previous messages if true. +-- @return #MESSAGE +-- @usage +-- -- Create a series of new Messages. +-- -- MessageAll is meant to be sent to all players, for 25 seconds, and is classified as "Score". +-- -- MessageRED is meant to be sent to the RED players only, for 10 seconds, and is classified as "End of Mission", with ID "Win". +-- -- MessageClient1 is meant to be sent to a Client, for 25 seconds, and is classified as "Score", with ID "Score". +-- -- MessageClient1 is meant to be sent to a Client, for 25 seconds, and is classified as "Score", with ID "Score". +-- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", 25, "End of Mission" ) +-- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", 25, "Penalty" ) +-- MessageClient1 = MESSAGE:New( "Congratulations, you've just hit a target", 25, "Score" ) +-- MessageClient2 = MESSAGE:New( "Congratulations, you've just killed a target", 25, "Score") +function MESSAGE:New( MessageText, MessageDuration, MessageCategory, ClearScreen ) + local self = BASE:Inherit( self, BASE:New() ) + self:F( { MessageText, MessageDuration, MessageCategory } ) + + + self.MessageType = nil + + -- When no MessageCategory is given, we don't show it as a title... + if MessageCategory and MessageCategory ~= "" then + if MessageCategory:sub(-1) ~= "\n" then + self.MessageCategory = MessageCategory .. ": " + else + self.MessageCategory = MessageCategory:sub( 1, -2 ) .. ":\n" + end + else + self.MessageCategory = "" + end + + self.ClearScreen=false + if ClearScreen~=nil then + self.ClearScreen=ClearScreen + end + + self.MessageDuration = MessageDuration or 5 + self.MessageTime = timer.getTime() + self.MessageText = MessageText:gsub("^\n","",1):gsub("\n$","",1) + + self.MessageSent = false + self.MessageGroup = false + self.MessageCoalition = false + + return self +end + + +--- Creates a new MESSAGE object of a certain type. +-- Note that these MESSAGE objects are not yet displayed on the display panel. +-- You must use the functions @{ToClient} or @{ToCoalition} or @{ToAll} to send these Messages to the respective recipients. +-- The message display times are automatically defined based on the timing settings in the @{Settings} menu. +-- @param self +-- @param #string MessageText is the text of the Message. +-- @param #MESSAGE.Type MessageType The type of the message. +-- @param #boolean ClearScreen (optional) Clear all previous messages. +-- @return #MESSAGE +-- @usage +-- MessageAll = MESSAGE:NewType( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", MESSAGE.Type.Information ) +-- MessageRED = MESSAGE:NewType( "To the RED Players: You receive a penalty because you've killed one of your own units", MESSAGE.Type.Information ) +-- MessageClient1 = MESSAGE:NewType( "Congratulations, you've just hit a target", MESSAGE.Type.Update ) +-- MessageClient2 = MESSAGE:NewType( "Congratulations, you've just killed a target", MESSAGE.Type.Update ) +function MESSAGE:NewType( MessageText, MessageType, ClearScreen ) + + local self = BASE:Inherit( self, BASE:New() ) + self:F( { MessageText } ) + + self.MessageType = MessageType + + self.ClearScreen=false + if ClearScreen~=nil then + self.ClearScreen=ClearScreen + end + + self.MessageTime = timer.getTime() + self.MessageText = MessageText:gsub("^\n","",1):gsub("\n$","",1) + + return self +end + + + +--- Clears all previous messages from the screen before the new message is displayed. Not that this must come before all functions starting with ToX(), e.g. ToAll(), ToGroup() etc. +-- @param #MESSAGE self +-- @return #MESSAGE +function MESSAGE:Clear() + self:F() + self.ClearScreen=true + return self +end + + + +--- Sends a MESSAGE to a Client Group. Note that the Group needs to be defined within the ME with the skillset "Client" or "Player". +-- @param #MESSAGE self +-- @param Wrapper.Client#CLIENT Client is the Group of the Client. +-- @param Core.Settings#SETTINGS Settings Settings used to display the message. +-- @return #MESSAGE +-- @usage +-- -- Send the 2 messages created with the @{New} method to the Client Group. +-- -- Note that the Message of MessageClient2 is overwriting the Message of MessageClient1. +-- ClientGroup = Group.getByName( "ClientGroup" ) +-- +-- MessageClient1 = MESSAGE:New( "Congratulations, you've just hit a target", "Score", 25, "Score" ):ToClient( ClientGroup ) +-- MessageClient2 = MESSAGE:New( "Congratulations, you've just killed a target", "Score", 25, "Score" ):ToClient( ClientGroup ) +-- or +-- MESSAGE:New( "Congratulations, you've just hit a target", "Score", 25, "Score" ):ToClient( ClientGroup ) +-- MESSAGE:New( "Congratulations, you've just killed a target", "Score", 25, "Score" ):ToClient( ClientGroup ) +-- or +-- MessageClient1 = MESSAGE:New( "Congratulations, you've just hit a target", "Score", 25, "Score" ) +-- MessageClient2 = MESSAGE:New( "Congratulations, you've just killed a target", "Score", 25, "Score" ) +-- MessageClient1:ToClient( ClientGroup ) +-- MessageClient2:ToClient( ClientGroup ) +function MESSAGE:ToClient( Client, Settings ) + self:F( Client ) + + if Client and Client:GetClientGroupID() then + + if self.MessageType then + local Settings = Settings or ( Client and _DATABASE:GetPlayerSettings( Client:GetPlayerName() ) ) or _SETTINGS -- Core.Settings#SETTINGS + self.MessageDuration = Settings:GetMessageTime( self.MessageType ) + self.MessageCategory = "" -- self.MessageType .. ": " + end + + if self.MessageDuration ~= 0 then + local ClientGroupID = Client:GetClientGroupID() + self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration ) + trigger.action.outTextForGroup( ClientGroupID, self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration , self.ClearScreen) + end + end + + return self +end + +--- Sends a MESSAGE to a Group. +-- @param #MESSAGE self +-- @param Wrapper.Group#GROUP Group to which the message is displayed. +-- @return #MESSAGE Message object. +function MESSAGE:ToGroup( Group, Settings ) + self:F( Group.GroupName ) + + if Group then + + if self.MessageType then + local Settings = Settings or ( Group and _DATABASE:GetPlayerSettings( Group:GetPlayerName() ) ) or _SETTINGS -- Core.Settings#SETTINGS + self.MessageDuration = Settings:GetMessageTime( self.MessageType ) + self.MessageCategory = "" -- self.MessageType .. ": " + end + + if self.MessageDuration ~= 0 then + self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration ) + trigger.action.outTextForGroup( Group:GetID(), self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration, self.ClearScreen ) + end + end + + return self +end +--- Sends a MESSAGE to the Blue coalition. +-- @param #MESSAGE self +-- @return #MESSAGE +-- @usage +-- -- Send a message created with the @{New} method to the BLUE coalition. +-- MessageBLUE = MESSAGE:New( "To the BLUE Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToBlue() +-- or +-- MESSAGE:New( "To the BLUE Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToBlue() +-- or +-- MessageBLUE = MESSAGE:New( "To the BLUE Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ) +-- MessageBLUE:ToBlue() +function MESSAGE:ToBlue() + self:F() + + self:ToCoalition( coalition.side.BLUE ) + + return self +end + +--- Sends a MESSAGE to the Red Coalition. +-- @param #MESSAGE self +-- @return #MESSAGE +-- @usage +-- -- Send a message created with the @{New} method to the RED coalition. +-- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToRed() +-- or +-- MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToRed() +-- or +-- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ) +-- MessageRED:ToRed() +function MESSAGE:ToRed( ) + self:F() + + self:ToCoalition( coalition.side.RED ) + + return self +end + +--- Sends a MESSAGE to a Coalition. +-- @param #MESSAGE self +-- @param #DCS.coalition.side CoalitionSide @{#DCS.coalition.side} to which the message is displayed. +-- @param Core.Settings#SETTINGS Settings (Optional) Settings for message display. +-- @return #MESSAGE Message object. +-- @usage +-- -- Send a message created with the @{New} method to the RED coalition. +-- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToCoalition( coalition.side.RED ) +-- or +-- MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToCoalition( coalition.side.RED ) +-- or +-- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ) +-- MessageRED:ToCoalition( coalition.side.RED ) +function MESSAGE:ToCoalition( CoalitionSide, Settings ) + self:F( CoalitionSide ) + + if self.MessageType then + local Settings = Settings or _SETTINGS -- Core.Settings#SETTINGS + self.MessageDuration = Settings:GetMessageTime( self.MessageType ) + self.MessageCategory = "" -- self.MessageType .. ": " + end + + if CoalitionSide then + if self.MessageDuration ~= 0 then + self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration ) + trigger.action.outTextForCoalition( CoalitionSide, self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration, self.ClearScreen ) + end + end + + return self +end + +--- Sends a MESSAGE to a Coalition if the given Condition is true. +-- @param #MESSAGE self +-- @param CoalitionSide needs to be filled out by the defined structure of the standard scripting engine @{coalition.side}. +-- @param #boolean Condition Sends the message only if the condition is true. +-- @return #MESSAGE self +function MESSAGE:ToCoalitionIf( CoalitionSide, Condition ) + self:F( CoalitionSide ) + + if Condition and Condition == true then + self:ToCoalition( CoalitionSide ) + end + + return self +end + +--- Sends a MESSAGE to all players. +-- @param #MESSAGE self +-- @param Core.Settings#Settings Settings (Optional) Settings for message display. +-- @return #MESSAGE +-- @usage +-- -- Send a message created to all players. +-- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ):ToAll() +-- or +-- MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ):ToAll() +-- or +-- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ) +-- MessageAll:ToAll() +function MESSAGE:ToAll(Settings) + self:F() + + if self.MessageType then + local Settings = Settings or _SETTINGS -- Core.Settings#SETTINGS + self.MessageDuration = Settings:GetMessageTime( self.MessageType ) + self.MessageCategory = "" -- self.MessageType .. ": " + end + + if self.MessageDuration ~= 0 then + self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration ) + trigger.action.outText( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration, self.ClearScreen ) + end + + return self +end + + +--- Sends a MESSAGE to all players if the given Condition is true. +-- @param #MESSAGE self +-- @return #MESSAGE +function MESSAGE:ToAllIf( Condition ) + + if Condition and Condition == true then + self:ToAll() + end + + return self +end +--- **Core** - FSM (Finite State Machine) are objects that model and control long lasting business processes and workflow. +-- +-- === +-- +-- ## Features: +-- +-- * Provide a base class to model your own state machines. +-- * Trigger events synchronously. +-- * Trigger events asynchronously. +-- * Handle events before or after the event was triggered. +-- * Handle state transitions as a result of event before and after the state change. +-- * For internal moose purposes, further state machines have been designed: +-- - to handle controllables (groups and units). +-- - to handle tasks. +-- - to handle processes. +-- +-- === +-- +-- A Finite State Machine (FSM) models a process flow that transitions between various **States** through triggered **Events**. +-- +-- A FSM can only be in one of a finite number of states. +-- The machine is in only one state at a time; the state it is in at any given time is called the **current state**. +-- It can change from one state to another when initiated by an **__internal__ or __external__ triggering event**, which is called a **transition**. +-- An **FSM implementation** is defined by **a list of its states**, **its initial state**, and **the triggering events** for **each possible transition**. +-- An FSM implementation is composed out of **two parts**, a set of **state transition rules**, and an implementation set of **state transition handlers**, implementing those transitions. +-- +-- The FSM class supports a **hierarchical implementation of a Finite State Machine**, +-- that is, it allows to **embed existing FSM implementations in a master FSM**. +-- FSM hierarchies allow for efficient FSM re-use, **not having to re-invent the wheel every time again** when designing complex processes. +-- +-- ![Workflow Example](..\Presentations\FSM\Dia2.JPG) +-- +-- The above diagram shows a graphical representation of a FSM implementation for a **Task**, which guides a Human towards a Zone, +-- orders him to destroy x targets and account the results. +-- Other examples of ready made FSM could be: +-- +-- * route a plane to a zone flown by a human +-- * detect targets by an AI and report to humans +-- * account for destroyed targets by human players +-- * handle AI infantry to deploy from or embark to a helicopter or airplane or vehicle +-- * let an AI patrol a zone +-- +-- The **MOOSE framework** uses extensively the FSM class and derived FSM\_ classes, +-- because **the goal of MOOSE is to simplify mission design complexity for mission building**. +-- By efficiently utilizing the FSM class and derived classes, MOOSE allows mission designers to quickly build processes. +-- **Ready made FSM-based implementations classes** exist within the MOOSE framework that **can easily be re-used, +-- and tailored** by mission designers through **the implementation of Transition Handlers**. +-- Each of these FSM implementation classes start either with: +-- +-- * an acronym **AI\_**, which indicates an FSM implementation directing **AI controlled** @{GROUP} and/or @{UNIT}. These AI\_ classes derive the @{#FSM_CONTROLLABLE} class. +-- * an acronym **TASK\_**, which indicates an FSM implementation executing a @{TASK} executed by Groups of players. These TASK\_ classes derive the @{#FSM_TASK} class. +-- * an acronym **ACT\_**, which indicates an Sub-FSM implementation, directing **Humans actions** that need to be done in a @{TASK}, seated in a @{CLIENT} (slot) or a @{UNIT} (CA join). These ACT\_ classes derive the @{#FSM_PROCESS} class. +-- +-- Detailed explanations and API specifics are further below clarified and FSM derived class specifics are described in those class documentation sections. +-- +-- ##__Dislaimer:__ +-- The FSM class development is based on a finite state machine implementation made by Conroy Kyle. +-- The state machine can be found on [github](https://github.com/kyleconroy/lua-state-machine) +-- I've reworked this development (taken the concept), and created a **hierarchical state machine** out of it, embedded within the DCS simulator. +-- Additionally, I've added extendability and created an API that allows seamless FSM implementation. +-- +-- The following derived classes are available in the MOOSE framework, that implement a specialised form of a FSM: +-- +-- * @{#FSM_TASK}: Models Finite State Machines for @{Task}s. +-- * @{#FSM_PROCESS}: Models Finite State Machines for @{Task} actions, which control @{Client}s. +-- * @{#FSM_CONTROLLABLE}: Models Finite State Machines for @{Wrapper.Controllable}s, which are @{Wrapper.Group}s, @{Wrapper.Unit}s, @{Client}s. +-- * @{#FSM_SET}: Models Finite State Machines for @{Set}s. Note that these FSMs control multiple objects!!! So State concerns here +-- for multiple objects or the position of the state machine in the process. +-- +-- === +-- +-- +-- ### Author: **FlightControl** +-- ### Contributions: **funkyfranky** +-- +-- === +-- +-- @module Core.Fsm +-- @image Core_Finite_State_Machine.JPG + +do -- FSM + + --- @type FSM + -- @field #string ClassName Name of the class. + -- @field Core.Scheduler#SCHEDULER CallScheduler Call scheduler. + -- @field #table options Options. + -- @field #table subs Subs. + -- @field #table Scores Scores. + -- @field #string current Current state name. + -- @extends Core.Base#BASE + + + --- A Finite State Machine (FSM) models a process flow that transitions between various **States** through triggered **Events**. + -- + -- A FSM can only be in one of a finite number of states. + -- The machine is in only one state at a time; the state it is in at any given time is called the **current state**. + -- It can change from one state to another when initiated by an **__internal__ or __external__ triggering event**, which is called a **transition**. + -- An **FSM implementation** is defined by **a list of its states**, **its initial state**, and **the triggering events** for **each possible transition**. + -- An FSM implementation is composed out of **two parts**, a set of **state transition rules**, and an implementation set of **state transition handlers**, implementing those transitions. + -- + -- The FSM class supports a **hierarchical implementation of a Finite State Machine**, + -- that is, it allows to **embed existing FSM implementations in a master FSM**. + -- FSM hierarchies allow for efficient FSM re-use, **not having to re-invent the wheel every time again** when designing complex processes. + -- + -- ![Workflow Example](..\Presentations\FSM\Dia2.JPG) + -- + -- The above diagram shows a graphical representation of a FSM implementation for a **Task**, which guides a Human towards a Zone, + -- orders him to destroy x targets and account the results. + -- Other examples of ready made FSM could be: + -- + -- * route a plane to a zone flown by a human + -- * detect targets by an AI and report to humans + -- * account for destroyed targets by human players + -- * handle AI infantry to deploy from or embark to a helicopter or airplane or vehicle + -- * let an AI patrol a zone + -- + -- The **MOOSE framework** uses extensively the FSM class and derived FSM\_ classes, + -- because **the goal of MOOSE is to simplify mission design complexity for mission building**. + -- By efficiently utilizing the FSM class and derived classes, MOOSE allows mission designers to quickly build processes. + -- **Ready made FSM-based implementations classes** exist within the MOOSE framework that **can easily be re-used, + -- and tailored** by mission designers through **the implementation of Transition Handlers**. + -- Each of these FSM implementation classes start either with: + -- + -- * an acronym **AI\_**, which indicates an FSM implementation directing **AI controlled** @{GROUP} and/or @{UNIT}. These AI\_ classes derive the @{#FSM_CONTROLLABLE} class. + -- * an acronym **TASK\_**, which indicates an FSM implementation executing a @{TASK} executed by Groups of players. These TASK\_ classes derive the @{#FSM_TASK} class. + -- * an acronym **ACT\_**, which indicates an Sub-FSM implementation, directing **Humans actions** that need to be done in a @{TASK}, seated in a @{CLIENT} (slot) or a @{UNIT} (CA join). These ACT\_ classes derive the @{#FSM_PROCESS} class. + -- + -- ![Transition Rules and Transition Handlers and Event Triggers](..\Presentations\FSM\Dia3.JPG) + -- + -- The FSM class is the base class of all FSM\_ derived classes. It implements the main functionality to define and execute Finite State Machines. + -- The derived FSM\_ classes extend the Finite State Machine functionality to run a workflow process for a specific purpose or component. + -- + -- Finite State Machines have **Transition Rules**, **Transition Handlers** and **Event Triggers**. + -- + -- The **Transition Rules** define the "Process Flow Boundaries", that is, + -- the path that can be followed hopping from state to state upon triggered events. + -- If an event is triggered, and there is no valid path found for that event, + -- an error will be raised and the FSM will stop functioning. + -- + -- The **Transition Handlers** are special methods that can be defined by the mission designer, following a defined syntax. + -- If the FSM object finds a method of such a handler, then the method will be called by the FSM, passing specific parameters. + -- The method can then define its own custom logic to implement the FSM workflow, and to conduct other actions. + -- + -- The **Event Triggers** are methods that are defined by the FSM, which the mission designer can use to implement the workflow. + -- Most of the time, these Event Triggers are used within the Transition Handler methods, so that a workflow is created running through the state machine. + -- + -- As explained above, a FSM supports **Linear State Transitions** and **Hierarchical State Transitions**, and both can be mixed to make a comprehensive FSM implementation. + -- The below documentation has a seperate chapter explaining both transition modes, taking into account the **Transition Rules**, **Transition Handlers** and **Event Triggers**. + -- + -- ## FSM Linear Transitions + -- + -- Linear Transitions are Transition Rules allowing an FSM to transition from one or multiple possible **From** state(s) towards a **To** state upon a Triggered **Event**. + -- The Lineair transition rule evaluation will always be done from the **current state** of the FSM. + -- If no valid Transition Rule can be found in the FSM, the FSM will log an error and stop. + -- + -- ### FSM Transition Rules + -- + -- The FSM has transition rules that it follows and validates, as it walks the process. + -- These rules define when an FSM can transition from a specific state towards an other specific state upon a triggered event. + -- + -- The method @{#FSM.AddTransition}() specifies a new possible Transition Rule for the FSM. + -- + -- The initial state can be defined using the method @{#FSM.SetStartState}(). The default start state of an FSM is "None". + -- + -- Find below an example of a Linear Transition Rule definition for an FSM. + -- + -- local Fsm3Switch = FSM:New() -- #FsmDemo + -- FsmSwitch:SetStartState( "Off" ) + -- FsmSwitch:AddTransition( "Off", "SwitchOn", "On" ) + -- FsmSwitch:AddTransition( "Off", "SwitchMiddle", "Middle" ) + -- FsmSwitch:AddTransition( "On", "SwitchOff", "Off" ) + -- FsmSwitch:AddTransition( "Middle", "SwitchOff", "Off" ) + -- + -- The above code snippet models a 3-way switch Linear Transition: + -- + -- * It can be switched **On** by triggering event **SwitchOn**. + -- * It can be switched to the **Middle** position, by triggering event **SwitchMiddle**. + -- * It can be switched **Off** by triggering event **SwitchOff**. + -- * Note that once the Switch is **On** or **Middle**, it can only be switched **Off**. + -- + -- #### Some additional comments: + -- + -- Note that Linear Transition Rules **can be declared in a few variations**: + -- + -- * The From states can be **a table of strings**, indicating that the transition rule will be valid **if the current state** of the FSM will be **one of the given From states**. + -- * The From state can be a **"*"**, indicating that **the transition rule will always be valid**, regardless of the current state of the FSM. + -- + -- The below code snippet shows how the two last lines can be rewritten and consensed. + -- + -- FsmSwitch:AddTransition( { "On", "Middle" }, "SwitchOff", "Off" ) + -- + -- ### Transition Handling + -- + -- ![Transition Handlers](..\Presentations\FSM\Dia4.JPG) + -- + -- An FSM transitions in **4 moments** when an Event is being triggered and processed. + -- The mission designer can define for each moment specific logic within methods implementations following a defined API syntax. + -- These methods define the flow of the FSM process; because in those methods the FSM Internal Events will be triggered. + -- + -- * To handle **State** transition moments, create methods starting with OnLeave or OnEnter concatenated with the State name. + -- * To handle **Event** transition moments, create methods starting with OnBefore or OnAfter concatenated with the Event name. + -- + -- **The OnLeave and OnBefore transition methods may return false, which will cancel the transition!** + -- + -- Transition Handler methods need to follow the above specified naming convention, but are also passed parameters from the FSM. + -- These parameters are on the correct order: From, Event, To: + -- + -- * From = A string containing the From state. + -- * Event = A string containing the Event name that was triggered. + -- * To = A string containing the To state. + -- + -- On top, each of these methods can have a variable amount of parameters passed. See the example in section [1.1.3](#1.1.3\)-event-triggers). + -- + -- ### Event Triggers + -- + -- ![Event Triggers](..\Presentations\FSM\Dia5.JPG) + -- + -- The FSM creates for each Event two **Event Trigger methods**. + -- There are two modes how Events can be triggered, which is **synchronous** and **asynchronous**: + -- + -- * The method **FSM:Event()** triggers an Event that will be processed **synchronously** or **immediately**. + -- * The method **FSM:__Event( __seconds__ )** triggers an Event that will be processed **asynchronously** over time, waiting __x seconds__. + -- + -- The destinction between these 2 Event Trigger methods are important to understand. An asynchronous call will "log" the Event Trigger to be executed at a later time. + -- Processing will just continue. Synchronous Event Trigger methods are useful to change states of the FSM immediately, but may have a larger processing impact. + -- + -- The following example provides a little demonstration on the difference between synchronous and asynchronous Event Triggering. + -- + -- function FSM:OnAfterEvent( From, Event, To, Amount ) + -- self:T( { Amount = Amount } ) + -- end + -- + -- local Amount = 1 + -- FSM:__Event( 5, Amount ) + -- + -- Amount = Amount + 1 + -- FSM:Event( Text, Amount ) + -- + -- In this example, the **:OnAfterEvent**() Transition Handler implementation will get called when **Event** is being triggered. + -- Before we go into more detail, let's look at the last 4 lines of the example. + -- The last line triggers synchronously the **Event**, and passes Amount as a parameter. + -- The 3rd last line of the example triggers asynchronously **Event**. + -- Event will be processed after 5 seconds, and Amount is given as a parameter. + -- + -- The output of this little code fragment will be: + -- + -- * Amount = 2 + -- * Amount = 2 + -- + -- Because ... When Event was asynchronously processed after 5 seconds, Amount was set to 2. So be careful when processing and passing values and objects in asynchronous processing! + -- + -- ### Linear Transition Example + -- + -- This example is fully implemented in the MOOSE test mission on GITHUB: [FSM-100 - Transition Explanation](https://github.com/FlightControl-Master/MOOSE/blob/master/Moose%20Test%20Missions/FSM%20-%20Finite%20State%20Machine/FSM-100%20-%20Transition%20Explanation/FSM-100%20-%20Transition%20Explanation.lua) + -- + -- It models a unit standing still near Batumi, and flaring every 5 seconds while switching between a Green flare and a Red flare. + -- The purpose of this example is not to show how exciting flaring is, but it demonstrates how a Linear Transition FSM can be build. + -- Have a look at the source code. The source code is also further explained below in this section. + -- + -- The example creates a new FsmDemo object from class FSM. + -- It will set the start state of FsmDemo to state **Green**. + -- Two Linear Transition Rules are created, where upon the event **Switch**, + -- the FsmDemo will transition from state **Green** to **Red** and from **Red** back to **Green**. + -- + -- ![Transition Example](..\Presentations\FSM\Dia6.JPG) + -- + -- local FsmDemo = FSM:New() -- #FsmDemo + -- FsmDemo:SetStartState( "Green" ) + -- FsmDemo:AddTransition( "Green", "Switch", "Red" ) + -- FsmDemo:AddTransition( "Red", "Switch", "Green" ) + -- + -- In the above example, the FsmDemo could flare every 5 seconds a Green or a Red flare into the air. + -- The next code implements this through the event handling method **OnAfterSwitch**. + -- + -- ![Transition Flow](..\Presentations\FSM\Dia7.JPG) + -- + -- function FsmDemo:OnAfterSwitch( From, Event, To, FsmUnit ) + -- self:T( { From, Event, To, FsmUnit } ) + -- + -- if From == "Green" then + -- FsmUnit:Flare(FLARECOLOR.Green) + -- else + -- if From == "Red" then + -- FsmUnit:Flare(FLARECOLOR.Red) + -- end + -- end + -- self:__Switch( 5, FsmUnit ) -- Trigger the next Switch event to happen in 5 seconds. + -- end + -- + -- FsmDemo:__Switch( 5, FsmUnit ) -- Trigger the first Switch event to happen in 5 seconds. + -- + -- The OnAfterSwitch implements a loop. The last line of the code fragment triggers the Switch Event within 5 seconds. + -- Upon the event execution (after 5 seconds), the OnAfterSwitch method is called of FsmDemo (cfr. the double point notation!!! ":"). + -- The OnAfterSwitch method receives from the FSM the 3 transition parameter details ( From, Event, To ), + -- and one additional parameter that was given when the event was triggered, which is in this case the Unit that is used within OnSwitchAfter. + -- + -- function FsmDemo:OnAfterSwitch( From, Event, To, FsmUnit ) + -- + -- For debugging reasons the received parameters are traced within the DCS.log. + -- + -- self:T( { From, Event, To, FsmUnit } ) + -- + -- The method will check if the From state received is either "Green" or "Red" and will flare the respective color from the FsmUnit. + -- + -- if From == "Green" then + -- FsmUnit:Flare(FLARECOLOR.Green) + -- else + -- if From == "Red" then + -- FsmUnit:Flare(FLARECOLOR.Red) + -- end + -- end + -- + -- It is important that the Switch event is again triggered, otherwise, the FsmDemo would stop working after having the first Event being handled. + -- + -- FsmDemo:__Switch( 5, FsmUnit ) -- Trigger the next Switch event to happen in 5 seconds. + -- + -- The below code fragment extends the FsmDemo, demonstrating multiple **From states declared as a table**, adding a **Linear Transition Rule**. + -- The new event **Stop** will cancel the Switching process. + -- The transition for event Stop can be executed if the current state of the FSM is either "Red" or "Green". + -- + -- local FsmDemo = FSM:New() -- #FsmDemo + -- FsmDemo:SetStartState( "Green" ) + -- FsmDemo:AddTransition( "Green", "Switch", "Red" ) + -- FsmDemo:AddTransition( "Red", "Switch", "Green" ) + -- FsmDemo:AddTransition( { "Red", "Green" }, "Stop", "Stopped" ) + -- + -- The transition for event Stop can also be simplified, as any current state of the FSM is valid. + -- + -- FsmDemo:AddTransition( "*", "Stop", "Stopped" ) + -- + -- So... When FsmDemo:Stop() is being triggered, the state of FsmDemo will transition from Red or Green to Stopped. + -- And there is no transition handling method defined for that transition, thus, no new event is being triggered causing the FsmDemo process flow to halt. + -- + -- ## FSM Hierarchical Transitions + -- + -- Hierarchical Transitions allow to re-use readily available and implemented FSMs. + -- This becomes in very useful for mission building, where mission designers build complex processes and workflows, + -- combining smaller FSMs to one single FSM. + -- + -- The FSM can embed **Sub-FSMs** that will execute and return **multiple possible Return (End) States**. + -- Depending upon **which state is returned**, the main FSM can continue the flow **triggering specific events**. + -- + -- The method @{#FSM.AddProcess}() adds a new Sub-FSM to the FSM. + -- + -- === + -- + -- @field #FSM + -- + FSM = { + ClassName = "FSM", + } + + --- Creates a new FSM object. + -- @param #FSM self + -- @return #FSM + function FSM:New() + + -- Inherits from BASE + self = BASE:Inherit( self, BASE:New() ) + + self.options = options or {} + self.options.subs = self.options.subs or {} + self.current = self.options.initial or 'none' + self.Events = {} + self.subs = {} + self.endstates = {} + + self.Scores = {} + + self._StartState = "none" + self._Transitions = {} + self._Processes = {} + self._EndStates = {} + self._Scores = {} + self._EventSchedules = {} + + self.CallScheduler = SCHEDULER:New( self ) + + return self + end + + + --- Sets the start state of the FSM. + -- @param #FSM self + -- @param #string State A string defining the start state. + function FSM:SetStartState( State ) + self._StartState = State + self.current = State + end + + + --- Returns the start state of the FSM. + -- @param #FSM self + -- @return #string A string containing the start state. + function FSM:GetStartState() + return self._StartState or {} + end + + --- Add a new transition rule to the FSM. + -- A transition rule defines when and if the FSM can transition from a state towards another state upon a triggered event. + -- @param #FSM self + -- @param #table From Can contain a string indicating the From state or a table of strings containing multiple From states. + -- @param #string Event The Event name. + -- @param #string To The To state. + function FSM:AddTransition( From, Event, To ) + + local Transition = {} + Transition.From = From + Transition.Event = Event + Transition.To = To + + -- Debug message. + self:T2( Transition ) + + self._Transitions[Transition] = Transition + self:_eventmap( self.Events, Transition ) + end + + + --- Returns a table of the transition rules defined within the FSM. + -- @param #FSM self + -- @return #table Transitions. + function FSM:GetTransitions() + return self._Transitions or {} + end + + --- Set the default @{Process} template with key ProcessName providing the ProcessClass and the process object when it is assigned to a @{Wrapper.Controllable} by the task. + -- @param #FSM self + -- @param #table From Can contain a string indicating the From state or a table of strings containing multiple From states. + -- @param #string Event The Event name. + -- @param Core.Fsm#FSM_PROCESS Process An sub-process FSM. + -- @param #table ReturnEvents A table indicating for which returned events of the SubFSM which Event must be triggered in the FSM. + -- @return Core.Fsm#FSM_PROCESS The SubFSM. + function FSM:AddProcess( From, Event, Process, ReturnEvents ) + self:T( { From, Event } ) + + local Sub = {} + Sub.From = From + Sub.Event = Event + Sub.fsm = Process + Sub.StartEvent = "Start" + Sub.ReturnEvents = ReturnEvents + + self._Processes[Sub] = Sub + + self:_submap( self.subs, Sub, nil ) + + self:AddTransition( From, Event, From ) + + return Process + end + + + --- Returns a table of the SubFSM rules defined within the FSM. + -- @param #FSM self + -- @return #table Sub processes. + function FSM:GetProcesses() + + self:F( { Processes = self._Processes } ) + + return self._Processes or {} + end + + function FSM:GetProcess( From, Event ) + + for ProcessID, Process in pairs( self:GetProcesses() ) do + if Process.From == From and Process.Event == Event then + return Process.fsm + end + end + + error( "Sub-Process from state " .. From .. " with event " .. Event .. " not found!" ) + end + + function FSM:SetProcess( From, Event, Fsm ) + + for ProcessID, Process in pairs( self:GetProcesses() ) do + if Process.From == From and Process.Event == Event then + Process.fsm = Fsm + return true + end + end + + error( "Sub-Process from state " .. From .. " with event " .. Event .. " not found!" ) + end + + --- Adds an End state. + -- @param #FSM self + -- @param #string State The FSM state. + function FSM:AddEndState( State ) + self._EndStates[State] = State + self.endstates[State] = State + end + + --- Returns the End states. + -- @param #FSM self + -- @return #table End states. + function FSM:GetEndStates() + return self._EndStates or {} + end + + + --- Adds a score for the FSM to be achieved. + -- @param #FSM self + -- @param #string State is the state of the process when the score needs to be given. (See the relevant state descriptions of the process). + -- @param #string ScoreText is a text describing the score that is given according the status. + -- @param #number Score is a number providing the score of the status. + -- @return #FSM self + function FSM:AddScore( State, ScoreText, Score ) + self:F( { State, ScoreText, Score } ) + + self._Scores[State] = self._Scores[State] or {} + self._Scores[State].ScoreText = ScoreText + self._Scores[State].Score = Score + + return self + end + + --- Adds a score for the FSM_PROCESS to be achieved. + -- @param #FSM self + -- @param #string From is the From State of the main process. + -- @param #string Event is the Event of the main process. + -- @param #string State is the state of the process when the score needs to be given. (See the relevant state descriptions of the process). + -- @param #string ScoreText is a text describing the score that is given according the status. + -- @param #number Score is a number providing the score of the status. + -- @return #FSM self + function FSM:AddScoreProcess( From, Event, State, ScoreText, Score ) + self:F( { From, Event, State, ScoreText, Score } ) + + local Process = self:GetProcess( From, Event ) + + Process._Scores[State] = Process._Scores[State] or {} + Process._Scores[State].ScoreText = ScoreText + Process._Scores[State].Score = Score + + self:T( Process._Scores ) + + return Process + end + + --- Returns a table with the scores defined. + -- @param #FSM self + -- @return #table Scores. + function FSM:GetScores() + return self._Scores or {} + end + + --- Returns a table with the Subs defined. + -- @param #FSM self + -- @return #table Sub processes. + function FSM:GetSubs() + return self.options.subs + end + + --- Load call backs. + -- @param #FSM self + -- @param #table CallBackTable Table of call backs. + function FSM:LoadCallBacks( CallBackTable ) + + for name, callback in pairs( CallBackTable or {} ) do + self[name] = callback + end + + end + + --- Event map. + -- @param #FSM self + -- @param #table Events Events. + -- @param #table EventStructure Event structure. + function FSM:_eventmap( Events, EventStructure ) + + local Event = EventStructure.Event + local __Event = "__" .. EventStructure.Event + + self[Event] = self[Event] or self:_create_transition(Event) + self[__Event] = self[__Event] or self:_delayed_transition(Event) + + -- Debug message. + self:T2( "Added methods: " .. Event .. ", " .. __Event ) + + Events[Event] = self.Events[Event] or { map = {} } + self:_add_to_map( Events[Event].map, EventStructure ) + + end + + --- Sub maps. + -- @param #FSM self + -- @param #table subs Subs. + -- @param #table sub Sub. + -- @param #string name Name. + function FSM:_submap( subs, sub, name ) + + subs[sub.From] = subs[sub.From] or {} + subs[sub.From][sub.Event] = subs[sub.From][sub.Event] or {} + + -- Make the reference table weak. + -- setmetatable( subs[sub.From][sub.Event], { __mode = "k" } ) + + subs[sub.From][sub.Event][sub] = {} + subs[sub.From][sub.Event][sub].fsm = sub.fsm + subs[sub.From][sub.Event][sub].StartEvent = sub.StartEvent + subs[sub.From][sub.Event][sub].ReturnEvents = sub.ReturnEvents or {} -- these events need to be given to find the correct continue event ... if none given, the processing will stop. + subs[sub.From][sub.Event][sub].name = name + subs[sub.From][sub.Event][sub].fsmparent = self + + end + + --- Call handler. + -- @param #FSM self + -- @param #string step Step "onafter", "onbefore", "onenter", "onleave". + -- @param #string trigger Trigger. + -- @param #table params Parameters. + -- @param #string EventName Event name. + -- @return Value. + function FSM:_call_handler( step, trigger, params, EventName ) + --env.info(string.format("FF T=%.3f _call_handler step=%s, trigger=%s, event=%s", timer.getTime(), step, trigger, EventName)) + + local handler = step .. trigger + + if self[handler] then + + --[[ + if step == "onafter" or step == "OnAfter" then + self:T( ":::>" .. step .. params[2] .. " : " .. params[1] .. " >> " .. params[2] .. ">" .. step .. params[2] .. "()" .. " >> " .. params[3] ) + elseif step == "onbefore" or step == "OnBefore" then + self:T( ":::>" .. step .. params[2] .. " : " .. params[1] .. " >> " .. step .. params[2] .. "()" .. ">" .. params[2] .. " >> " .. params[3] ) + elseif step == "onenter" or step == "OnEnter" then + self:T( ":::>" .. step .. params[3] .. " : " .. params[1] .. " >> " .. params[2] .. " >> " .. step .. params[3] .. "()" .. ">" .. params[3] ) + elseif step == "onleave" or step == "OnLeave" then + self:T( ":::>" .. step .. params[1] .. " : " .. params[1] .. ">" .. step .. params[1] .. "()" .. " >> " .. params[2] .. " >> " .. params[3] ) + else + self:T( ":::>" .. step .. " : " .. params[1] .. " >> " .. params[2] .. " >> " .. params[3] ) + end + ]] + + self._EventSchedules[EventName] = nil + + -- Error handler. + local ErrorHandler = function( errmsg ) + env.info( "Error in SCHEDULER function:" .. errmsg ) + if BASE.Debug ~= nil then + env.info( BASE.Debug.traceback() ) + end + return errmsg + end + + --return self[handler](self, unpack( params )) + + -- Protected call. + local Result, Value = xpcall( function() return self[handler]( self, unpack( params ) ) end, ErrorHandler ) + return Value + end + + end + + --- Handler. + -- @param #FSM self + -- @param #string EventName Event name. + -- @param ... Arguments. + function FSM._handler( self, EventName, ... ) + + local Can, To = self:can( EventName ) + + if To == "*" then + To = self.current + end + + if Can then + + -- From state. + local From = self.current + + -- Parameters. + local Params = { From, EventName, To, ... } + + + if self["onleave".. From] or + self["OnLeave".. From] or + self["onbefore".. EventName] or + self["OnBefore".. EventName] or + self["onafter".. EventName] or + self["OnAfter".. EventName] or + self["onenter".. To] or + self["OnEnter".. To] then + + if self:_call_handler( "onbefore", EventName, Params, EventName ) == false then + self:T( "*** FSM *** Cancel" .. " *** " .. self.current .. " --> " .. EventName .. " --> " .. To .. " *** onbefore" .. EventName ) + return false + else + if self:_call_handler( "OnBefore", EventName, Params, EventName ) == false then + self:T( "*** FSM *** Cancel" .. " *** " .. self.current .. " --> " .. EventName .. " --> " .. To .. " *** OnBefore" .. EventName ) + return false + else + if self:_call_handler( "onleave", From, Params, EventName ) == false then + self:T( "*** FSM *** Cancel" .. " *** " .. self.current .. " --> " .. EventName .. " --> " .. To .. " *** onleave" .. From ) + return false + else + if self:_call_handler( "OnLeave", From, Params, EventName ) == false then + self:T( "*** FSM *** Cancel" .. " *** " .. self.current .. " --> " .. EventName .. " --> " .. To .. " *** OnLeave" .. From ) + return false + end + end + end + end + + else + + local ClassName = self:GetClassName() + + if ClassName == "FSM" then + self:T( "*** FSM *** Transit *** " .. self.current .. " --> " .. EventName .. " --> " .. To ) + end + + if ClassName == "FSM_TASK" then + self:T( "*** FSM *** Transit *** " .. self.current .. " --> " .. EventName .. " --> " .. To .. " *** Task: " .. self.TaskName ) + end + + if ClassName == "FSM_CONTROLLABLE" then + self:T( "*** FSM *** Transit *** " .. self.current .. " --> " .. EventName .. " --> " .. To .. " *** TaskUnit: " .. self.Controllable.ControllableName .. " *** " ) + end + + if ClassName == "FSM_PROCESS" then + self:T( "*** FSM *** Transit *** " .. self.current .. " --> " .. EventName .. " --> " .. To .. " *** Task: " .. self.Task:GetName() .. ", TaskUnit: " .. self.Controllable.ControllableName .. " *** " ) + end + end + + -- New current state. + self.current = To + + local execute = true + + local subtable = self:_gosub( From, EventName ) + + for _, sub in pairs( subtable ) do + + --if sub.nextevent then + -- self:F2( "nextevent = " .. sub.nextevent ) + -- self[sub.nextevent]( self ) + --end + + self:T( "*** FSM *** Sub *** " .. sub.StartEvent ) + + sub.fsm.fsmparent = self + sub.fsm.ReturnEvents = sub.ReturnEvents + sub.fsm[sub.StartEvent]( sub.fsm ) + + execute = false + end + + local fsmparent, Event = self:_isendstate( To ) + + if fsmparent and Event then + + self:T( "*** FSM *** End *** " .. Event ) + + self:_call_handler("onenter", To, Params, EventName ) + self:_call_handler("OnEnter", To, Params, EventName ) + self:_call_handler("onafter", EventName, Params, EventName ) + self:_call_handler("OnAfter", EventName, Params, EventName ) + self:_call_handler("onstate", "change", Params, EventName ) + + fsmparent[Event]( fsmparent ) + + execute = false + end + + if execute then + + self:_call_handler("onafter", EventName, Params, EventName ) + self:_call_handler("OnAfter", EventName, Params, EventName ) + + self:_call_handler("onenter", To, Params, EventName ) + self:_call_handler("OnEnter", To, Params, EventName ) + + self:_call_handler("onstate", "change", Params, EventName ) + + end + else + self:T( "*** FSM *** NO Transition *** " .. self.current .. " --> " .. EventName .. " --> ? " ) + end + + return nil + end + + --- Delayed transition. + -- @param #FSM self + -- @param #string EventName Event name. + -- @return #function Function. + function FSM:_delayed_transition( EventName ) + + return function( self, DelaySeconds, ... ) + + -- Debug. + self:T2( "Delayed Event: " .. EventName ) + + local CallID = 0 + if DelaySeconds ~= nil then + + if DelaySeconds < 0 then -- Only call the event ONCE! + + DelaySeconds = math.abs( DelaySeconds ) + + if not self._EventSchedules[EventName] then + + -- Call _handler. + CallID = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1, nil, nil, nil, 4, true ) + + -- Set call ID. + self._EventSchedules[EventName] = CallID + + -- Debug output. + self:T2(string.format("NEGATIVE Event %s delayed by %.1f sec SCHEDULED with CallID=%s", EventName, DelaySeconds, tostring(CallID))) + else + self:T2(string.format("NEGATIVE Event %s delayed by %.1f sec CANCELLED as we already have such an event in the queue.", EventName, DelaySeconds)) + -- reschedule + end + else + + CallID = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1, nil, nil, nil, 4, true ) + + self:T2(string.format("Event %s delayed by %.1f sec SCHEDULED with CallID=%s", EventName, DelaySeconds, tostring(CallID))) + end + else + error( "FSM: An asynchronous event trigger requires a DelaySeconds parameter!!! This can be positive or negative! Sorry, but will not process this." ) + end + + -- Debug. + self:T2( { CallID = CallID } ) + end + + end + + --- Create transition. + -- @param #FSM self + -- @param #string EventName Event name. + -- @return #function Function. + function FSM:_create_transition( EventName ) + return function( self, ... ) return self._handler( self, EventName , ... ) end + end + + --- Go sub. + -- @param #FSM self + -- @param #string ParentFrom Parent from state. + -- @param #string ParentEvent Parent event name. + -- @return #table Subs. + function FSM:_gosub( ParentFrom, ParentEvent ) + local fsmtable = {} + if self.subs[ParentFrom] and self.subs[ParentFrom][ParentEvent] then + self:T( { ParentFrom, ParentEvent, self.subs[ParentFrom], self.subs[ParentFrom][ParentEvent] } ) + return self.subs[ParentFrom][ParentEvent] + else + return {} + end + end + + --- Is end state. + -- @param #FSM self + -- @param #string Current Current state name. + -- @return #table FSM parent. + -- @return #string Event name. + function FSM:_isendstate( Current ) + local FSMParent = self.fsmparent + + if FSMParent and self.endstates[Current] then + --self:T( { state = Current, endstates = self.endstates, endstate = self.endstates[Current] } ) + FSMParent.current = Current + local ParentFrom = FSMParent.current + --self:T( { ParentFrom, self.ReturnEvents } ) + local Event = self.ReturnEvents[Current] + --self:T( { Event } ) + if Event then + return FSMParent, Event + else + --self:T( { "Could not find parent event name for state ", ParentFrom } ) + end + end + + return nil + end + + --- Add to map. + -- @param #FSM self + -- @param #table Map Map. + -- @param #table Event Event table. + function FSM:_add_to_map( Map, Event ) + self:F3( { Map, Event } ) + + if type(Event.From) == 'string' then + Map[Event.From] = Event.To + else + for _, From in ipairs(Event.From) do + Map[From] = Event.To + end + end + + self:T3( { Map, Event } ) + end + + --- Get current state. + -- @param #FSM self + -- @return #string Current FSM state. + function FSM:GetState() + return self.current + end + + --- Get current state. + -- @param #FSM self + -- @return #string Current FSM state. + function FSM:GetCurrentState() + return self.current + end + + --- Check if FSM is in state. + -- @param #FSM self + -- @param #string State State name. + -- @return #boolean If true, FSM is in this state. + function FSM:Is( State ) + return self.current == State + end + + --- Check if FSM is in state. + -- @param #FSM self + -- @param #string State State name. + -- @return #boolean If true, FSM is in this state. + function FSM:is(state) + return self.current == state + end + + --- Check if can do an event. + -- @param #FSM self + -- @param #string e Event name. + -- @return #boolean If true, FSM can do the event. + -- @return #string To state. + function FSM:can(e) + + local Event = self.Events[e] + + --self:F3( { self.current, Event } ) + + local To = Event and Event.map[self.current] or Event.map['*'] + + return To ~= nil, To + end + + --- Check if cannot do an event. + -- @param #FSM self + -- @param #string e Event name. + -- @return #boolean If true, FSM cannot do the event. + function FSM:cannot(e) + return not self:can(e) + end + +end + +do -- FSM_CONTROLLABLE + + --- @type FSM_CONTROLLABLE + -- @field Wrapper.Controllable#CONTROLLABLE Controllable + -- @extends Core.Fsm#FSM + + --- Models Finite State Machines for @{Wrapper.Controllable}s, which are @{Wrapper.Group}s, @{Wrapper.Unit}s, @{Client}s. + -- + -- === + -- + -- @field #FSM_CONTROLLABLE + FSM_CONTROLLABLE = { + ClassName = "FSM_CONTROLLABLE", + } + + --- Creates a new FSM_CONTROLLABLE object. + -- @param #FSM_CONTROLLABLE self + -- @param #table FSMT Finite State Machine Table + -- @param Wrapper.Controllable#CONTROLLABLE Controllable (optional) The CONTROLLABLE object that the FSM_CONTROLLABLE governs. + -- @return #FSM_CONTROLLABLE + function FSM_CONTROLLABLE:New( Controllable ) + + -- Inherits from BASE + local self = BASE:Inherit( self, FSM:New() ) -- Core.Fsm#FSM_CONTROLLABLE + + if Controllable then + self:SetControllable( Controllable ) + end + + self:AddTransition( "*", "Stop", "Stopped" ) + + --- OnBefore Transition Handler for Event Stop. + -- @function [parent=#FSM_CONTROLLABLE] OnBeforeStop + -- @param #FSM_CONTROLLABLE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Stop. + -- @function [parent=#FSM_CONTROLLABLE] OnAfterStop + -- @param #FSM_CONTROLLABLE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Stop. + -- @function [parent=#FSM_CONTROLLABLE] Stop + -- @param #FSM_CONTROLLABLE self + + --- Asynchronous Event Trigger for Event Stop. + -- @function [parent=#FSM_CONTROLLABLE] __Stop + -- @param #FSM_CONTROLLABLE self + -- @param #number Delay The delay in seconds. + + --- OnLeave Transition Handler for State Stopped. + -- @function [parent=#FSM_CONTROLLABLE] OnLeaveStopped + -- @param #FSM_CONTROLLABLE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnEnter Transition Handler for State Stopped. + -- @function [parent=#FSM_CONTROLLABLE] OnEnterStopped + -- @param #FSM_CONTROLLABLE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + return self + end + + --- OnAfter Transition Handler for Event Stop. + -- @function [parent=#FSM_CONTROLLABLE] OnAfterStop + -- @param #FSM_CONTROLLABLE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + function FSM_CONTROLLABLE:OnAfterStop(Controllable,From,Event,To) + + -- Clear all pending schedules + self.CallScheduler:Clear() + end + + --- Sets the CONTROLLABLE object that the FSM_CONTROLLABLE governs. + -- @param #FSM_CONTROLLABLE self + -- @param Wrapper.Controllable#CONTROLLABLE FSMControllable + -- @return #FSM_CONTROLLABLE + function FSM_CONTROLLABLE:SetControllable( FSMControllable ) + --self:F( FSMControllable:GetName() ) + self.Controllable = FSMControllable + end + + --- Gets the CONTROLLABLE object that the FSM_CONTROLLABLE governs. + -- @param #FSM_CONTROLLABLE self + -- @return Wrapper.Controllable#CONTROLLABLE + function FSM_CONTROLLABLE:GetControllable() + return self.Controllable + end + + function FSM_CONTROLLABLE:_call_handler( step, trigger, params, EventName ) + + local handler = step .. trigger + + local ErrorHandler = function( errmsg ) + + env.info( "Error in SCHEDULER function:" .. errmsg ) + if BASE.Debug ~= nil then + env.info( BASE.Debug.traceback() ) + end + + return errmsg + end + + if self[handler] then + self:T( "*** FSM *** " .. step .. " *** " .. params[1] .. " --> " .. params[2] .. " --> " .. params[3] .. " *** TaskUnit: " .. self.Controllable:GetName() ) + self._EventSchedules[EventName] = nil + local Result, Value = xpcall( function() return self[handler]( self, self.Controllable, unpack( params ) ) end, ErrorHandler ) + return Value + --return self[handler]( self, self.Controllable, unpack( params ) ) + end + end + +end + +do -- FSM_PROCESS + + --- @type FSM_PROCESS + -- @field Tasking.Task#TASK Task + -- @extends Core.Fsm#FSM_CONTROLLABLE + + + --- FSM_PROCESS class models Finite State Machines for @{Task} actions, which control @{Client}s. + -- + -- === + -- + -- @field #FSM_PROCESS FSM_PROCESS + -- + FSM_PROCESS = { + ClassName = "FSM_PROCESS", + } + + --- Creates a new FSM_PROCESS object. + -- @param #FSM_PROCESS self + -- @return #FSM_PROCESS + function FSM_PROCESS:New( Controllable, Task ) + + local self = BASE:Inherit( self, FSM_CONTROLLABLE:New() ) -- Core.Fsm#FSM_PROCESS + + --self:F( Controllable ) + + self:Assign( Controllable, Task ) + + return self + end + + function FSM_PROCESS:Init( FsmProcess ) + self:T( "No Initialisation" ) + end + + function FSM_PROCESS:_call_handler( step, trigger, params, EventName ) + + local handler = step .. trigger + + local ErrorHandler = function( errmsg ) + + env.info( "Error in FSM_PROCESS call handler:" .. errmsg ) + if BASE.Debug ~= nil then + env.info( BASE.Debug.traceback() ) + end + + return errmsg + end + + if self[handler] then + if handler ~= "onstatechange" then + self:T( "*** FSM *** " .. step .. " *** " .. params[1] .. " --> " .. params[2] .. " --> " .. params[3] .. " *** Task: " .. self.Task:GetName() .. ", TaskUnit: " .. self.Controllable:GetName() ) + end + self._EventSchedules[EventName] = nil + local Result, Value + if self.Controllable and self.Controllable:IsAlive() == true then + Result, Value = xpcall( function() return self[handler]( self, self.Controllable, self.Task, unpack( params ) ) end, ErrorHandler ) + end + return Value + --return self[handler]( self, self.Controllable, unpack( params ) ) + end + end + + --- Creates a new FSM_PROCESS object based on this FSM_PROCESS. + -- @param #FSM_PROCESS self + -- @return #FSM_PROCESS + function FSM_PROCESS:Copy( Controllable, Task ) + self:T( { self:GetClassNameAndID() } ) + + + local NewFsm = self:New( Controllable, Task ) -- Core.Fsm#FSM_PROCESS + + NewFsm:Assign( Controllable, Task ) + + -- Polymorphic call to initialize the new FSM_PROCESS based on self FSM_PROCESS + NewFsm:Init( self ) + + -- Set Start State + NewFsm:SetStartState( self:GetStartState() ) + + -- Copy Transitions + for TransitionID, Transition in pairs( self:GetTransitions() ) do + NewFsm:AddTransition( Transition.From, Transition.Event, Transition.To ) + end + + -- Copy Processes + for ProcessID, Process in pairs( self:GetProcesses() ) do + --self:E( { Process:GetName() } ) + local FsmProcess = NewFsm:AddProcess( Process.From, Process.Event, Process.fsm:Copy( Controllable, Task ), Process.ReturnEvents ) + end + + -- Copy End States + for EndStateID, EndState in pairs( self:GetEndStates() ) do + self:T( EndState ) + NewFsm:AddEndState( EndState ) + end + + -- Copy the score tables + for ScoreID, Score in pairs( self:GetScores() ) do + self:T( Score ) + NewFsm:AddScore( ScoreID, Score.ScoreText, Score.Score ) + end + + return NewFsm + end + + --- Removes an FSM_PROCESS object. + -- @param #FSM_PROCESS self + -- @return #FSM_PROCESS + function FSM_PROCESS:Remove() + self:F( { self:GetClassNameAndID() } ) + + self:F( "Clearing Schedules" ) + self.CallScheduler:Clear() + + -- Copy Processes + for ProcessID, Process in pairs( self:GetProcesses() ) do + if Process.fsm then + Process.fsm:Remove() + Process.fsm = nil + end + end + + return self + end + + --- Sets the task of the process. + -- @param #FSM_PROCESS self + -- @param Tasking.Task#TASK Task + -- @return #FSM_PROCESS + function FSM_PROCESS:SetTask( Task ) + + self.Task = Task + + return self + end + + --- Gets the task of the process. + -- @param #FSM_PROCESS self + -- @return Tasking.Task#TASK + function FSM_PROCESS:GetTask() + + return self.Task + end + + --- Gets the mission of the process. + -- @param #FSM_PROCESS self + -- @return Tasking.Mission#MISSION + function FSM_PROCESS:GetMission() + + return self.Task.Mission + end + + --- Gets the mission of the process. + -- @param #FSM_PROCESS self + -- @return Tasking.CommandCenter#COMMANDCENTER + function FSM_PROCESS:GetCommandCenter() + + return self:GetTask():GetMission():GetCommandCenter() + end + +-- TODO: Need to check and fix that an FSM_PROCESS is only for a UNIT. Not for a GROUP. + + --- Send a message of the @{Task} to the Group of the Unit. + -- @param #FSM_PROCESS self + function FSM_PROCESS:Message( Message ) + self:F( { Message = Message } ) + + local CC = self:GetCommandCenter() + local TaskGroup = self.Controllable:GetGroup() + + local PlayerName = self.Controllable:GetPlayerName() -- Only for a unit + PlayerName = PlayerName and " (" .. PlayerName .. ")" or "" -- If PlayerName is nil, then keep it nil, otherwise add brackets. + local Callsign = self.Controllable:GetCallsign() + local Prefix = Callsign and " @ " .. Callsign .. PlayerName or "" + + Message = Prefix .. ": " .. Message + CC:MessageToGroup( Message, TaskGroup ) + end + + + + + --- Assign the process to a @{Wrapper.Unit} and activate the process. + -- @param #FSM_PROCESS self + -- @param Task.Tasking#TASK Task + -- @param Wrapper.Unit#UNIT ProcessUnit + -- @return #FSM_PROCESS self + function FSM_PROCESS:Assign( ProcessUnit, Task ) + --self:T( { Task:GetName(), ProcessUnit:GetName() } ) + + self:SetControllable( ProcessUnit ) + self:SetTask( Task ) + + --self.ProcessGroup = ProcessUnit:GetGroup() + + return self + end + +-- function FSM_PROCESS:onenterAssigned( ProcessUnit, Task, From, Event, To ) +-- +-- if From( "Planned" ) then +-- self:T( "*** FSM *** Assign *** " .. Task:GetName() .. "/" .. ProcessUnit:GetName() .. " *** " .. From .. " --> " .. Event .. " --> " .. To ) +-- self.Task:Assign() +-- end +-- end + + function FSM_PROCESS:onenterFailed( ProcessUnit, Task, From, Event, To ) + self:T( "*** FSM *** Failed *** " .. Task:GetName() .. "/" .. ProcessUnit:GetName() .. " *** " .. From .. " --> " .. Event .. " --> " .. To ) + + self.Task:Fail() + end + + + --- StateMachine callback function for a FSM_PROCESS + -- @param #FSM_PROCESS self + -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function FSM_PROCESS:onstatechange( ProcessUnit, Task, From, Event, To ) + + if From ~= To then + self:T( "*** FSM *** Change *** " .. Task:GetName() .. "/" .. ProcessUnit:GetName() .. " *** " .. From .. " --> " .. Event .. " --> " .. To ) + end + +-- if self:IsTrace() then +-- MESSAGE:New( "@ Process " .. self:GetClassNameAndID() .. " : " .. Event .. " changed to state " .. To, 2 ):ToAll() +-- self:F2( { Scores = self._Scores, To = To } ) +-- end + + -- TODO: This needs to be reworked with a callback functions allocated within Task, and set within the mission script from the Task Objects... + if self._Scores[To] then + + local Task = self.Task + local Scoring = Task:GetScoring() + if Scoring then + Scoring:_AddMissionTaskScore( Task.Mission, ProcessUnit, self._Scores[To].ScoreText, self._Scores[To].Score ) + end + end + end + +end + +do -- FSM_TASK + + --- FSM_TASK class + -- @type FSM_TASK + -- @field Tasking.Task#TASK Task + -- @extends #FSM + + --- Models Finite State Machines for @{Tasking.Task}s. + -- + -- === + -- + -- @field #FSM_TASK FSM_TASK + -- + FSM_TASK = { + ClassName = "FSM_TASK", + } + + --- Creates a new FSM_TASK object. + -- @param #FSM_TASK self + -- @param #string TaskName The name of the task. + -- @return #FSM_TASK + function FSM_TASK:New( TaskName ) + + local self = BASE:Inherit( self, FSM_CONTROLLABLE:New() ) -- Core.Fsm#FSM_TASK + + self["onstatechange"] = self.OnStateChange + self.TaskName = TaskName + + return self + end + + function FSM_TASK:_call_handler( step, trigger, params, EventName ) + local handler = step .. trigger + + local ErrorHandler = function( errmsg ) + + env.info( "Error in SCHEDULER function:" .. errmsg ) + if BASE.Debug ~= nil then + env.info( BASE.Debug.traceback() ) + end + + return errmsg + end + + if self[handler] then + self:T( "*** FSM *** " .. step .. " *** " .. params[1] .. " --> " .. params[2] .. " --> " .. params[3] .. " *** Task: " .. self.TaskName ) + self._EventSchedules[EventName] = nil + --return self[handler]( self, unpack( params ) ) + local Result, Value = xpcall( function() return self[handler]( self, unpack( params ) ) end, ErrorHandler ) + return Value + end + end + +end -- FSM_TASK + +do -- FSM_SET + + --- FSM_SET class + -- @type FSM_SET + -- @field Core.Set#SET_BASE Set + -- @extends Core.Fsm#FSM + + + --- FSM_SET class models Finite State Machines for @{Set}s. Note that these FSMs control multiple objects!!! So State concerns here + -- for multiple objects or the position of the state machine in the process. + -- + -- === + -- + -- @field #FSM_SET FSM_SET + -- + FSM_SET = { + ClassName = "FSM_SET", + } + + --- Creates a new FSM_SET object. + -- @param #FSM_SET self + -- @param #table FSMT Finite State Machine Table + -- @param Set_SET_BASE FSMSet (optional) The Set object that the FSM_SET governs. + -- @return #FSM_SET + function FSM_SET:New( FSMSet ) + + -- Inherits from BASE + self = BASE:Inherit( self, FSM:New() ) -- Core.Fsm#FSM_SET + + if FSMSet then + self:Set( FSMSet ) + end + + return self + end + + --- Sets the SET_BASE object that the FSM_SET governs. + -- @param #FSM_SET self + -- @param Core.Set#SET_BASE FSMSet + -- @return #FSM_SET + function FSM_SET:Set( FSMSet ) + self:F( FSMSet ) + self.Set = FSMSet + end + + --- Gets the SET_BASE object that the FSM_SET governs. + -- @param #FSM_SET self + -- @return Core.Set#SET_BASE + function FSM_SET:Get() + return self.Controllable + end + + function FSM_SET:_call_handler( step, trigger, params, EventName ) + local handler = step .. trigger + if self[handler] then + self:T( "*** FSM *** " .. step .. " *** " .. params[1] .. " --> " .. params[2] .. " --> " .. params[3] ) + self._EventSchedules[EventName] = nil + return self[handler]( self, self.Set, unpack( params ) ) + end + end + +end -- FSM_SET + +--- **Core** - Spawn dynamically new groups of units in running missions. +-- +-- === +-- +-- ## Features: +-- +-- * Spawn new groups in running missions. +-- * Schedule spawning of new groups. +-- * Put limits on the amount of groups that can be spawned, and the amount of units that can be alive at the same time. +-- * Randomize the spawning location between different zones. +-- * Randomize the initial positions within the zones. +-- * Spawn in array formation. +-- * Spawn uncontrolled (for planes or helos only). +-- * Clean up inactive helicopters that "crashed". +-- * Place a hook to capture a spawn event, and tailor with customer code. +-- * Spawn late activated. +-- * Spawn with or without an initial delay. +-- * Respawn after landing, on the runway or at the ramp after engine shutdown. +-- * Spawn with custom heading, both for a group formation and for the units in the group. +-- * Spawn with different skills. +-- * Spawn with different liveries. +-- * Spawn with an inner and outer radius to set the initial position. +-- * Spawn with a randomize route. +-- * Spawn with a randomized template. +-- * Spawn with a randomized start points on a route. +-- * Spawn with an alternative name. +-- * Spawn and keep the unit names. +-- * Spawn with a different coalition and country. +-- * Enquiry methods to check on spawn status. +-- +-- === +-- +-- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master-release/SPA%20-%20Spawning) +-- +-- === +-- +-- ### [YouTube Playlist](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl1jirWIo4t4YxqN-HxjqRkL) +-- +-- === +-- +-- ### Author: **FlightControl** +-- ### Contributions: A lot of people within this community! +-- +-- === +-- +-- @module Core.Spawn +-- @image Core_Spawn.JPG + + +--- SPAWN Class +-- @type SPAWN +-- @field ClassName +-- @field #string SpawnTemplatePrefix +-- @field #string SpawnAliasPrefix +-- @field #number AliveUnits +-- @field #number MaxAliveUnits +-- @field #number SpawnIndex +-- @field #number MaxAliveGroups +-- @field #SPAWN.SpawnZoneTable SpawnZoneTable +-- @extends Core.Base#BASE + + +--- Allows to spawn dynamically new @{Core.Group}s. +-- +-- Each SPAWN object needs to be have related **template groups** setup in the Mission Editor (ME), +-- which is a normal group with the **Late Activation** flag set. +-- This template group will never be activated in your mission. +-- SPAWN uses that **template group** to reference to all the characteristics +-- (air, ground, livery, unit composition, formation, skill level etc) of each new group to be spawned. +-- +-- Therefore, when creating a SPAWN object, the @{#SPAWN.New} and @{#SPAWN.NewWithAlias} require +-- **the name of the template group** to be given as a string to those constructor methods. +-- +-- Initialization settings can be applied on the SPAWN object, +-- which modify the behaviour or the way groups are spawned. +-- These initialization methods have the prefix **Init**. +-- There are also spawn methods with the prefix **Spawn** and will spawn new groups in various ways. +-- +-- ### IMPORTANT! The methods with prefix **Init** must be used before any methods with prefix **Spawn** method are used, or unexpected results may appear!!! +-- +-- Because SPAWN can spawn multiple groups of a template group, +-- SPAWN has an **internal index** that keeps track +-- which was the latest group that was spawned. +-- +-- **Limits** can be set on how many groups can be spawn in each SPAWN object, +-- using the method @{#SPAWN.InitLimit}. SPAWN has 2 kind of limits: +-- +-- * The maximum amount of @{Wrapper.Unit}s that can be **alive** at the same time... +-- * The maximum amount of @{Wrapper.Group}s that can be **spawned**... This is more of a **resource**-type of limit. +-- +-- When new groups get spawned using the **Spawn** methods, +-- it will be evaluated whether any limits have been reached. +-- When no spawn limit is reached, a new group will be created by the spawning methods, +-- and the internal index will be increased with 1. +-- +-- These limits ensure that your mission does not accidentally get flooded with spawned groups. +-- Additionally, it also guarantees that independent of the group composition, +-- at any time, the most optimal amount of groups are alive in your mission. +-- For example, if your template group has a group composition of 10 units, and you specify a limit of 100 units alive at the same time, +-- with unlimited resources = :InitLimit( 100, 0 ) and 10 groups are alive, but two groups have only one unit alive in the group, +-- then a sequent Spawn(Scheduled) will allow a new group to be spawned!!! +-- +-- ### IMPORTANT!! If a limit has been reached, it is possible that a **Spawn** method returns **nil**, meaning, no @{Wrapper.Group} had been spawned!!! +-- +-- Spawned groups get **the same name** as the name of the template group. +-- Spawned units in those groups keep _by default_ **the same name** as the name of the template group. +-- However, because multiple groups and units are created from the template group, +-- a suffix is added to each spawned group and unit. +-- +-- Newly spawned groups will get the following naming structure at run-time: +-- +-- 1. Spawned groups will have the name _GroupName_#_nnn_, where _GroupName_ is the name of the **template group**, +-- and _nnn_ is a **counter from 0 to 999**. +-- 2. Spawned units will have the name _GroupName_#_nnn_-_uu_, +-- where _uu_ is a **counter from 0 to 99** for each new spawned unit belonging to the group. +-- +-- That being said, there is a way to keep the same unit names! +-- The method @{#SPAWN.InitKeepUnitNames}() will keep the same unit names as defined within the template group, thus: +-- +-- 3. Spawned units will have the name _UnitName_#_nnn_-_uu_, +-- where _UnitName_ is the **unit name as defined in the template group*, +-- and _uu_ is a **counter from 0 to 99** for each new spawned unit belonging to the group. +-- +-- Some **additional notes that need to be considered!!**: +-- +-- * templates are actually groups defined within the mission editor, with the flag "Late Activation" set. +-- As such, these groups are never used within the mission, but are used by the @{#SPAWN} module. +-- * It is important to defined BEFORE you spawn new groups, +-- a proper initialization of the SPAWN instance is done with the options you want to use. +-- * When designing a mission, NEVER name groups using a "#" within the name of the group Spawn template(s), +-- or the SPAWN module logic won't work anymore. +-- +-- ## SPAWN construction methods +-- +-- Create a new SPAWN object with the @{#SPAWN.New}() or the @{#SPAWN.NewWithAlias}() methods: +-- +-- * @{#SPAWN.New}(): Creates a new SPAWN object taking the name of the group that represents the GROUP template (definition). +-- * @{#SPAWN.NewWithAlias}(): Creates a new SPAWN object taking the name of the group that represents the GROUP template (definition), and gives each spawned @{Wrapper.Group} an different name. +-- +-- It is important to understand how the SPAWN class works internally. The SPAWN object created will contain internally a list of groups that will be spawned and that are already spawned. +-- The initialization methods will modify this list of groups so that when a group gets spawned, ALL information is already prepared when spawning. This is done for performance reasons. +-- So in principle, the group list will contain all parameters and configurations after initialization, and when groups get actually spawned, this spawning can be done quickly and efficient. +-- +-- ## SPAWN **Init**ialization methods +-- +-- A spawn object will behave differently based on the usage of **initialization** methods, which all start with the **Init** prefix: +-- +-- ### Unit Names +-- +-- * @{#SPAWN.InitKeepUnitNames}(): Keeps the unit names as defined within the mission editor, but note that anything after a # mark is ignored, and any spaces before and after the resulting name are removed. IMPORTANT! This method MUST be the first used after :New !!! +-- +-- ### Route randomization +-- +-- * @{#SPAWN.InitRandomizeRoute}(): Randomize the routes of spawned groups, and for air groups also optionally the height. +-- +-- ### Group composition randomization +-- +-- * @{#SPAWN.InitRandomizeTemplate}(): Randomize the group templates so that when a new group is spawned, a random group template is selected from one of the templates defined. +-- +-- ### Uncontrolled +-- +-- * @{#SPAWN.InitUnControlled}(): Spawn plane groups uncontrolled. +-- +-- ### Array formation +-- +-- * @{#SPAWN.InitArray}(): Make groups visible before they are actually activated, and order these groups like a batallion in an array. +-- +-- ### Position randomization +-- +-- * @{#SPAWN.InitRandomizePosition}(): Randomizes the position of @{Wrapper.Group}s that are spawned within a **radius band**, given an Outer and Inner radius, from the point that the spawn happens. +-- * @{#SPAWN.InitRandomizeUnits}(): Randomizes the @{Wrapper.Unit}s in the @{Wrapper.Group} that is spawned within a **radius band**, given an Outer and Inner radius. +-- * @{#SPAWN.InitRandomizeZones}(): Randomizes the spawning between a predefined list of @{Zone}s that are declared using this function. Each zone can be given a probability factor. +-- +-- ### Enable / Disable AI when spawning a new @{Wrapper.Group} +-- +-- * @{#SPAWN.InitAIOn}(): Turns the AI On when spawning the new @{Wrapper.Group} object. +-- * @{#SPAWN.InitAIOff}(): Turns the AI Off when spawning the new @{Wrapper.Group} object. +-- * @{#SPAWN.InitAIOnOff}(): Turns the AI On or Off when spawning the new @{Wrapper.Group} object. +-- +-- ### Limit scheduled spawning +-- +-- * @{#SPAWN.InitLimit}(): Limits the amount of groups that can be alive at the same time and that can be dynamically spawned. +-- +-- ### Delay initial scheduled spawn +-- +-- * @{#SPAWN.InitDelayOnOff}(): Turns the inital delay On/Off when scheduled spawning the first @{Wrapper.Group} object. +-- * @{#SPAWN.InitDelayOn}(): Turns the inital delay On when scheduled spawning the first @{Wrapper.Group} object. +-- * @{#SPAWN.InitDelayOff}(): Turns the inital delay Off when scheduled spawning the first @{Wrapper.Group} object. +-- +-- ### Repeat spawned @{Wrapper.Group}s upon landing +-- +-- * @{#SPAWN.InitRepeat}() or @{#SPAWN.InitRepeatOnLanding}(): This method is used to re-spawn automatically the same group after it has landed. +-- * @{#SPAWN.InitRepeatOnEngineShutDown}(): This method is used to re-spawn automatically the same group after it has landed and it shuts down the engines at the ramp. +-- +-- +-- ## SPAWN **Spawn** methods +-- +-- Groups can be spawned at different times and methods: +-- +-- ### **Single** spawning methods +-- +-- * @{#SPAWN.Spawn}(): Spawn one new group based on the last spawned index. +-- * @{#SPAWN.ReSpawn}(): Re-spawn a group based on a given index. +-- * @{#SPAWN.SpawnFromVec3}(): Spawn a new group from a Vec3 coordinate. (The group will can be spawned at a point in the air). +-- * @{#SPAWN.SpawnFromVec2}(): Spawn a new group from a Vec2 coordinate. (The group will be spawned at land height ). +-- * @{#SPAWN.SpawnFromStatic}(): Spawn a new group from a structure, taking the position of a @{Static}. +-- * @{#SPAWN.SpawnFromUnit}(): Spawn a new group taking the position of a @{Wrapper.Unit}. +-- * @{#SPAWN.SpawnInZone}(): Spawn a new group in a @{Zone}. +-- * @{#SPAWN.SpawnAtAirbase}(): Spawn a new group at an @{Wrapper.Airbase}, which can be an airdrome, ship or helipad. +-- +-- Note that @{#SPAWN.Spawn} and @{#SPAWN.ReSpawn} return a @{Wrapper.Group#GROUP.New} object, that contains a reference to the DCSGroup object. +-- You can use the @{GROUP} object to do further actions with the DCSGroup. +-- +-- ### **Scheduled** spawning methods +-- +-- * @{#SPAWN.SpawnScheduled}(): Spawn groups at scheduled but randomized intervals. +--- * @{#SPAWN.SpawnScheduleStart}(): Start or continue to spawn groups at scheduled time intervals. +-- * @{#SPAWN.SpawnScheduleStop}(): Stop the spawning of groups at scheduled time intervals. +-- +-- +-- ## Retrieve alive GROUPs spawned by the SPAWN object +-- +-- The SPAWN class administers which GROUPS it has reserved (in stock) or has created during mission execution. +-- Every time a SPAWN object spawns a new GROUP object, a reference to the GROUP object is added to an internal table of GROUPS. +-- SPAWN provides methods to iterate through that internal GROUP object reference table: +-- +-- * @{#SPAWN.GetFirstAliveGroup}(): Will find the first alive GROUP it has spawned, and return the alive GROUP object and the first Index where the first alive GROUP object has been found. +-- * @{#SPAWN.GetNextAliveGroup}(): Will find the next alive GROUP object from a given Index, and return a reference to the alive GROUP object and the next Index where the alive GROUP has been found. +-- * @{#SPAWN.GetLastAliveGroup}(): Will find the last alive GROUP object, and will return a reference to the last live GROUP object and the last Index where the last alive GROUP object has been found. +-- +-- You can use the methods @{#SPAWN.GetFirstAliveGroup}() and sequently @{#SPAWN.GetNextAliveGroup}() to iterate through the alive GROUPS within the SPAWN object, and to actions... See the respective methods for an example. +-- The method @{#SPAWN.GetGroupFromIndex}() will return the GROUP object reference from the given Index, dead or alive... +-- +-- ## Spawned cleaning of inactive groups +-- +-- Sometimes, it will occur during a mission run-time, that ground or especially air objects get damaged, and will while being damged stop their activities, while remaining alive. +-- In such cases, the SPAWN object will just sit there and wait until that group gets destroyed, but most of the time it won't, +-- and it may occur that no new groups are or can be spawned as limits are reached. +-- To prevent this, a @{#SPAWN.InitCleanUp}() initialization method has been defined that will silently monitor the status of each spawned group. +-- Once a group has a velocity = 0, and has been waiting for a defined interval, that group will be cleaned or removed from run-time. +-- There is a catch however :-) If a damaged group has returned to an airbase within the coalition, that group will not be considered as "lost"... +-- In such a case, when the inactive group is cleaned, a new group will Re-spawned automatically. +-- This models AI that has succesfully returned to their airbase, to restart their combat activities. +-- Check the @{#SPAWN.InitCleanUp}() for further info. +-- +-- ## Catch the @{Wrapper.Group} Spawn Event in a callback function! +-- +-- When using the @{#SPAWN.SpawnScheduled)() method, new @{Wrapper.Group}s are created following the spawn time interval parameters. +-- When a new @{Wrapper.Group} is spawned, you maybe want to execute actions with that group spawned at the spawn event. +-- The SPAWN class supports this functionality through the method @{#SPAWN.OnSpawnGroup}( **function( SpawnedGroup ) end ** ), +-- which takes a function as a parameter that you can define locally. +-- Whenever a new @{Wrapper.Group} is spawned, the given function is called, and the @{Wrapper.Group} that was just spawned, is given as a parameter. +-- As a result, your spawn event handling function requires one parameter to be declared, which will contain the spawned @{Wrapper.Group} object. +-- A coding example is provided at the description of the @{#SPAWN.OnSpawnGroup}( **function( SpawnedGroup ) end ** ) method. +-- +-- ## Delay the initial spawning +-- +-- When using the @{#SPAWN.SpawnScheduled)() method, the default behaviour of this method will be that it will spawn the initial (first) @{Wrapper.Group} +-- immediately when :SpawnScheduled() is initiated. The methods @{#SPAWN.InitDelayOnOff}() and @{#SPAWN.InitDelayOn}() can be used to +-- activate a delay before the first @{Wrapper.Group} is spawned. For completeness, a method @{#SPAWN.InitDelayOff}() is also available, that +-- can be used to switch off the initial delay. Because there is no delay by default, this method would only be used when a +-- @{#SPAWN.SpawnScheduleStop}() ; @{#SPAWN.SpawnScheduleStart}() sequence would have been used. +-- +-- +-- @field #SPAWN SPAWN +-- +SPAWN = { + ClassName = "SPAWN", + SpawnTemplatePrefix = nil, + SpawnAliasPrefix = nil, +} + + +--- Enumerator for spawns at airbases +-- @type SPAWN.Takeoff +-- @extends Wrapper.Group#GROUP.Takeoff + +--- @field #SPAWN.Takeoff Takeoff +SPAWN.Takeoff = { + Air = 1, + Runway = 2, + Hot = 3, + Cold = 4, +} + +--- @type SPAWN.SpawnZoneTable +-- @list SpawnZone + + +--- Creates the main object to spawn a @{Wrapper.Group} defined in the DCS ME. +-- @param #SPAWN self +-- @param #string SpawnTemplatePrefix is the name of the Group in the ME that defines the Template. Each new group will have the name starting with SpawnTemplatePrefix. +-- @return #SPAWN +-- @usage +-- -- NATO helicopters engaging in the battle field. +-- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ) +-- @usage local Plane = SPAWN:New( "Plane" ) -- Creates a new local variable that can initiate new planes with the name "Plane#ddd" using the template "Plane" as defined within the ME. +function SPAWN:New( SpawnTemplatePrefix ) + local self = BASE:Inherit( self, BASE:New() ) -- #SPAWN + self:F( { SpawnTemplatePrefix } ) + + local TemplateGroup = GROUP:FindByName( SpawnTemplatePrefix ) + if TemplateGroup then + self.SpawnTemplatePrefix = SpawnTemplatePrefix + self.SpawnIndex = 0 + self.SpawnCount = 0 -- The internal counter of the amount of spawning the has happened since SpawnStart. + self.AliveUnits = 0 -- Contains the counter how many units are currently alive + self.SpawnIsScheduled = false -- Reflects if the spawning for this SpawnTemplatePrefix is going to be scheduled or not. + self.SpawnTemplate = self._GetTemplate( self, SpawnTemplatePrefix ) -- Contains the template structure for a Group Spawn from the Mission Editor. Note that this group must have lateActivation always on!!! + self.Repeat = false -- Don't repeat the group from Take-Off till Landing and back Take-Off by ReSpawning. + self.UnControlled = false -- When working in UnControlled mode, all planes are Spawned in UnControlled mode before the scheduler starts. + self.SpawnInitLimit = false -- By default, no InitLimit + self.SpawnMaxUnitsAlive = 0 -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time. + self.SpawnMaxGroups = 0 -- The maximum amount of groups that can be spawned. + self.SpawnRandomize = false -- Sets the randomization flag of new Spawned units to false. + self.SpawnVisible = false -- Flag that indicates if all the Groups of the SpawnGroup need to be visible when Spawned. + self.AIOnOff = true -- The AI is on by default when spawning a group. + self.SpawnUnControlled = false + self.SpawnInitKeepUnitNames = false -- Overwrite unit names by default with group name. + self.DelayOnOff = false -- No intial delay when spawning the first group. + self.SpawnGrouping = nil -- No grouping. + self.SpawnInitLivery = nil -- No special livery. + self.SpawnInitSkill = nil -- No special skill. + self.SpawnInitFreq = nil -- No special frequency. + self.SpawnInitModu = nil -- No special modulation. + self.SpawnInitRadio = nil -- No radio comms setting. + self.SpawnInitModex = nil + self.SpawnInitAirbase = nil + self.TweakedTemplate = false -- Check if the user is using self made template. + + self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. + else + error( "SPAWN:New: There is no group declared in the mission editor with SpawnTemplatePrefix = '" .. SpawnTemplatePrefix .. "'" ) + end + + self:SetEventPriority( 5 ) + self.SpawnHookScheduler = SCHEDULER:New( nil ) + + return self +end + +--- Creates a new SPAWN instance to create new groups based on the defined template and using a new alias for each new group. +-- @param #SPAWN self +-- @param #string SpawnTemplatePrefix is the name of the Group in the ME that defines the Template. +-- @param #string SpawnAliasPrefix is the name that will be given to the Group at runtime. +-- @return #SPAWN +-- @usage +-- -- NATO helicopters engaging in the battle field. +-- Spawn_BE_KA50 = SPAWN:NewWithAlias( 'BE KA-50@RAMP-Ground Defense', 'Helicopter Attacking a City' ) +-- @usage local PlaneWithAlias = SPAWN:NewWithAlias( "Plane", "Bomber" ) -- Creates a new local variable that can instantiate new planes with the name "Bomber#ddd" using the template "Plane" as defined within the ME. +function SPAWN:NewWithAlias( SpawnTemplatePrefix, SpawnAliasPrefix ) + local self = BASE:Inherit( self, BASE:New() ) + self:F( { SpawnTemplatePrefix, SpawnAliasPrefix } ) + + local TemplateGroup = GROUP:FindByName( SpawnTemplatePrefix ) + if TemplateGroup then + self.SpawnTemplatePrefix = SpawnTemplatePrefix + self.SpawnAliasPrefix = SpawnAliasPrefix + self.SpawnIndex = 0 + self.SpawnCount = 0 -- The internal counter of the amount of spawning the has happened since SpawnStart. + self.AliveUnits = 0 -- Contains the counter how many units are currently alive + self.SpawnIsScheduled = false -- Reflects if the spawning for this SpawnTemplatePrefix is going to be scheduled or not. + self.SpawnTemplate = self._GetTemplate( self, SpawnTemplatePrefix ) -- Contains the template structure for a Group Spawn from the Mission Editor. Note that this group must have lateActivation always on!!! + self.Repeat = false -- Don't repeat the group from Take-Off till Landing and back Take-Off by ReSpawning. + self.UnControlled = false -- When working in UnControlled mode, all planes are Spawned in UnControlled mode before the scheduler starts. + self.SpawnInitLimit = false -- By default, no InitLimit + self.SpawnMaxUnitsAlive = 0 -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time. + self.SpawnMaxGroups = 0 -- The maximum amount of groups that can be spawned. + self.SpawnRandomize = false -- Sets the randomization flag of new Spawned units to false. + self.SpawnVisible = false -- Flag that indicates if all the Groups of the SpawnGroup need to be visible when Spawned. + self.AIOnOff = true -- The AI is on by default when spawning a group. + self.SpawnUnControlled = false + self.SpawnInitKeepUnitNames = false -- Overwrite unit names by default with group name. + self.DelayOnOff = false -- No intial delay when spawning the first group. + self.SpawnGrouping = nil -- No grouping. + self.SpawnInitLivery = nil -- No special livery. + self.SpawnInitSkill = nil -- No special skill. + self.SpawnInitFreq = nil -- No special frequency. + self.SpawnInitModu = nil -- No special modulation. + self.SpawnInitRadio = nil -- No radio comms setting. + self.SpawnInitModex = nil + self.SpawnInitAirbase = nil + self.TweakedTemplate = false -- Check if the user is using self made template. + + self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. + else + error( "SPAWN:New: There is no group declared in the mission editor with SpawnTemplatePrefix = '" .. SpawnTemplatePrefix .. "'" ) + end + + self:SetEventPriority( 5 ) + self.SpawnHookScheduler = SCHEDULER:New( nil ) + + return self +end + + +--- Creates a new SPAWN instance to create new groups based on the provided template. +-- @param #SPAWN self +-- @param #table SpawnTemplate is the Template of the Group. This must be a valid Group Template structure! +-- @param #string SpawnTemplatePrefix is the name of the Group that will be given at each spawn. +-- @param #string SpawnAliasPrefix (optional) is the name that will be given to the Group at runtime. +-- @return #SPAWN +-- @usage +-- -- Create a new SPAWN object based on a Group Template defined from scratch. +-- Spawn_BE_KA50 = SPAWN:NewWithAlias( 'BE KA-50@RAMP-Ground Defense', 'Helicopter Attacking a City' ) +-- @usage +-- -- Create a new CSAR_Spawn object based on a normal Group Template to spawn a soldier. +-- local CSAR_Spawn = SPAWN:NewWithFromTemplate( Template, "CSAR", "Pilot" ) +function SPAWN:NewFromTemplate( SpawnTemplate, SpawnTemplatePrefix, SpawnAliasPrefix ) + local self = BASE:Inherit( self, BASE:New() ) + self:F( { SpawnTemplate, SpawnTemplatePrefix, SpawnAliasPrefix } ) + if SpawnAliasPrefix == nil or SpawnAliasPrefix == "" then + BASE:I("ERROR: in function NewFromTemplate, required paramter SpawnAliasPrefix is not set") + return nil + end + + if SpawnTemplate then + self.SpawnTemplate = SpawnTemplate -- Contains the template structure for a Group Spawn from the Mission Editor. Note that this group must have lateActivation always on!!! + self.SpawnTemplatePrefix = SpawnTemplatePrefix + self.SpawnAliasPrefix = SpawnAliasPrefix + self.SpawnIndex = 0 + self.SpawnCount = 0 -- The internal counter of the amount of spawning the has happened since SpawnStart. + self.AliveUnits = 0 -- Contains the counter how many units are currently alive + self.SpawnIsScheduled = false -- Reflects if the spawning for this SpawnTemplatePrefix is going to be scheduled or not. + self.Repeat = false -- Don't repeat the group from Take-Off till Landing and back Take-Off by ReSpawning. + self.UnControlled = false -- When working in UnControlled mode, all planes are Spawned in UnControlled mode before the scheduler starts. + self.SpawnInitLimit = false -- By default, no InitLimit. + self.SpawnMaxUnitsAlive = 0 -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time. + self.SpawnMaxGroups = 0 -- The maximum amount of groups that can be spawned. + self.SpawnRandomize = false -- Sets the randomization flag of new Spawned units to false. + self.SpawnVisible = false -- Flag that indicates if all the Groups of the SpawnGroup need to be visible when Spawned. + self.AIOnOff = true -- The AI is on by default when spawning a group. + self.SpawnUnControlled = false + self.SpawnInitKeepUnitNames = false -- Overwrite unit names by default with group name. + self.DelayOnOff = false -- No intial delay when spawning the first group. + self.Grouping = nil -- No grouping. + self.SpawnInitLivery = nil -- No special livery. + self.SpawnInitSkill = nil -- No special skill. + self.SpawnInitFreq = nil -- No special frequency. + self.SpawnInitModu = nil -- No special modulation. + self.SpawnInitRadio = nil -- No radio comms setting. + self.SpawnInitModex = nil + self.SpawnInitAirbase = nil + self.TweakedTemplate = true -- Check if the user is using self made template. + + self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. + else + error( "There is no template provided for SpawnTemplatePrefix = '" .. SpawnTemplatePrefix .. "'" ) + end + + self:SetEventPriority( 5 ) + self.SpawnHookScheduler = SCHEDULER:New( nil ) + + return self +end + + +--- Stops any more repeat spawns from happening once the UNIT count of Alive units, spawned by the same SPAWN object, exceeds the first parameter. Also can stop spawns from happening once a total GROUP still alive is met. +-- Exceptionally powerful when combined with SpawnSchedule for Respawning. +-- Note that this method is exceptionally important to balance the performance of the mission. Depending on the machine etc, a mission can only process a maximum amount of units. +-- If the time interval must be short, but there should not be more Units or Groups alive than a maximum amount of units, then this method should be used... +-- When a @{#SPAWN.New} is executed and the limit of the amount of units alive is reached, then no new spawn will happen of the group, until some of these units of the spawn object will be destroyed. +-- @param #SPAWN self +-- @param #number SpawnMaxUnitsAlive The maximum amount of units that can be alive at runtime. +-- @param #number SpawnMaxGroups The maximum amount of groups that can be spawned. When the limit is reached, then no more actual spawns will happen of the group. +-- This parameter is useful to define a maximum amount of airplanes, ground troops, helicopters, ships etc within a supply area. +-- This parameter accepts the value 0, which defines that there are no maximum group limits, but there are limits on the maximum of units that can be alive at the same time. +-- @return #SPAWN self +-- @usage +-- -- NATO helicopters engaging in the battle field. +-- -- This helicopter group consists of one Unit. So, this group will SPAWN maximum 2 groups simultaneously within the DCSRTE. +-- -- There will be maximum 24 groups spawned during the whole mission lifetime. +-- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):InitLimit( 2, 24 ) +function SPAWN:InitLimit( SpawnMaxUnitsAlive, SpawnMaxGroups ) + self:F( { self.SpawnTemplatePrefix, SpawnMaxUnitsAlive, SpawnMaxGroups } ) + + self.SpawnInitLimit = true + self.SpawnMaxUnitsAlive = SpawnMaxUnitsAlive -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time. + self.SpawnMaxGroups = SpawnMaxGroups -- The maximum amount of groups that can be spawned. + + for SpawnGroupID = 1, self.SpawnMaxGroups do + self:_InitializeSpawnGroups( SpawnGroupID ) + end + + return self +end + +--- Keeps the unit names as defined within the mission editor, +-- but note that anything after a # mark is ignored, +-- and any spaces before and after the resulting name are removed. +-- IMPORTANT! This method MUST be the first used after :New !!! +-- @param #SPAWN self +-- @param #boolean KeepUnitNames (optional) If true, the unit names are kept, false or not provided to make new unit names. +-- @return #SPAWN self +function SPAWN:InitKeepUnitNames( KeepUnitNames ) + self:F( ) + + self.SpawnInitKeepUnitNames = KeepUnitNames or true + + return self +end + + +--- Flags that the spawned groups must be spawned late activated. +-- @param #SPAWN self +-- @param #boolean LateActivated (optional) If true, the spawned groups are late activated. +-- @return #SPAWN self +function SPAWN:InitLateActivated( LateActivated ) + self:F( ) + + self.LateActivated = LateActivated or true + + return self +end + +--- Set spawns to happen at a particular airbase. Only for aircraft, of course. +-- @param #SPAWN self +-- @param #string AirbaseName Name of the airbase. +-- @param #number Takeoff (Optional) Takeoff type. Can be SPAWN.Takeoff.Hot (default), SPAWN.Takeoff.Cold or SPAWN.Takeoff.Runway. +-- @param #number TerminalTyple (Optional) The terminal type. +-- @return #SPAWN self +function SPAWN:InitAirbase( AirbaseName, Takeoff, TerminalType ) + self:F( ) + + self.SpawnInitAirbase=AIRBASE:FindByName(AirbaseName) + + self.SpawnInitTakeoff=Takeoff or SPAWN.Takeoff.Hot + + self.SpawnInitTerminalType=TerminalType + + return self +end + + +--- Defines the Heading for the new spawned units. +-- The heading can be given as one fixed degree, or can be randomized between minimum and maximum degrees. +-- @param #SPAWN self +-- @param #number HeadingMin The minimum or fixed heading in degrees. +-- @param #number HeadingMax (optional) The maximum heading in degrees. This there is no maximum heading, then the heading will be fixed for all units using minimum heading. +-- @return #SPAWN self +-- @usage +-- +-- Spawn = SPAWN:New( ... ) +-- +-- -- Spawn the units pointing to 100 degrees. +-- Spawn:InitHeading( 100 ) +-- +-- -- Spawn the units pointing between 100 and 150 degrees. +-- Spawn:InitHeading( 100, 150 ) +-- +function SPAWN:InitHeading( HeadingMin, HeadingMax ) + self:F( ) + + self.SpawnInitHeadingMin = HeadingMin + self.SpawnInitHeadingMax = HeadingMax + + return self +end + + +--- Defines the heading of the overall formation of the new spawned group. +-- The heading can be given as one fixed degree, or can be randomized between minimum and maximum degrees. +-- The Group's formation as laid out in its template will be rotated around the first unit in the group +-- Group individual units facings will rotate to match. If InitHeading is also applied to this SPAWN then that will take precedence for individual unit facings. +-- Note that InitGroupHeading does *not* rotate the groups route; only its initial facing! +-- @param #SPAWN self +-- @param #number HeadingMin The minimum or fixed heading in degrees. +-- @param #number HeadingMax (optional) The maximum heading in degrees. This there is no maximum heading, then the heading for the group will be HeadingMin. +-- @param #number unitVar (optional) Individual units within the group will have their heading randomized by +/- unitVar degrees. Default is zero. +-- @return #SPAWN self +-- @usage +-- +-- mySpawner = SPAWN:New( ... ) +-- +-- -- Spawn the Group with the formation rotated +100 degrees around unit #1, compared to the mission template. +-- mySpawner:InitGroupHeading( 100 ) +-- +-- Spawn the Group with the formation rotated units between +100 and +150 degrees around unit #1, compared to the mission template, and with individual units varying by +/- 10 degrees from their templated facing. +-- mySpawner:InitGroupHeading( 100, 150, 10 ) +-- +-- Spawn the Group with the formation rotated -60 degrees around unit #1, compared to the mission template, but with all units facing due north regardless of how they were laid out in the template. +-- mySpawner:InitGroupHeading(-60):InitHeading(0) +-- or +-- mySpawner:InitHeading(0):InitGroupHeading(-60) +-- +function SPAWN:InitGroupHeading( HeadingMin, HeadingMax, unitVar ) + self:F({HeadingMin=HeadingMin, HeadingMax=HeadingMax, unitVar=unitVar}) + + self.SpawnInitGroupHeadingMin = HeadingMin + self.SpawnInitGroupHeadingMax = HeadingMax + self.SpawnInitGroupUnitVar = unitVar + return self +end + + +--- Sets the coalition of the spawned group. Note that it might be necessary to also set the country explicitly! +-- @param #SPAWN self +-- @param DCS#coalition.side Coalition Coalition of the group as number of enumerator: +-- +-- * @{DCS#coaliton.side.NEUTRAL} +-- * @{DCS#coaliton.side.RED} +-- * @{DCS#coalition.side.BLUE} +-- +-- @return #SPAWN self +function SPAWN:InitCoalition( Coalition ) + self:F({coalition=Coalition}) + + self.SpawnInitCoalition = Coalition + + return self +end + +--- Sets the country of the spawn group. Note that the country determins the coalition of the group depending on which country is defined to be on which side for each specific mission! +-- @param #SPAWN self +-- @param #number Country Country id as number or enumerator: +-- +-- * @{DCS#country.id.RUSSIA} +-- * @{DCS#county.id.USA} +-- +-- @return #SPAWN self +function SPAWN:InitCountry( Country ) + self:F( ) + + self.SpawnInitCountry = Country + + return self +end + + +--- Sets category ID of the group. +-- @param #SPAWN self +-- @param #number Category Category id. +-- @return #SPAWN self +function SPAWN:InitCategory( Category ) + self:F( ) + + self.SpawnInitCategory = Category + + return self +end + +--- Sets livery of the group. +-- @param #SPAWN self +-- @param #string Livery Livery name. Note that this is not necessarily the same name as displayed in the mission edior. +-- @return #SPAWN self +function SPAWN:InitLivery( Livery ) + self:F({livery=Livery} ) + + self.SpawnInitLivery = Livery + + return self +end + +--- Sets skill of the group. +-- @param #SPAWN self +-- @param #string Skill Skill, possible values "Average", "Good", "High", "Excellent" or "Random". +-- @return #SPAWN self +function SPAWN:InitSkill( Skill ) + self:F({skill=Skill}) + if Skill:lower()=="average" then + self.SpawnInitSkill="Average" + elseif Skill:lower()=="good" then + self.SpawnInitSkill="Good" + elseif Skill:lower()=="excellent" then + self.SpawnInitSkill="Excellent" + elseif Skill:lower()=="random" then + self.SpawnInitSkill="Random" + else + self.SpawnInitSkill="High" + end + + return self +end + +--- Sets the radio comms on or off. Same as checking/unchecking the COMM box in the mission editor. +-- @param #SPAWN self +-- @param #number switch If true (or nil), enables the radio comms. If false, disables the radio for the spawned group. +-- @return #SPAWN self +function SPAWN:InitRadioCommsOnOff(switch) + self:F({switch=switch} ) + self.SpawnInitRadio=switch or true + return self +end + +--- Sets the radio frequency of the group. +-- @param #SPAWN self +-- @param #number frequency The frequency in MHz. +-- @return #SPAWN self +function SPAWN:InitRadioFrequency(frequency) + self:F({frequency=frequency} ) + + self.SpawnInitFreq=frequency + + return self +end + +--- Set radio modulation. Default is AM. +-- @param #SPAWN self +-- @param #string modulation Either "FM" or "AM". If no value is given, modulation is set to AM. +-- @return #SPAWN self +function SPAWN:InitRadioModulation(modulation) + self:F({modulation=modulation}) + if modulation and modulation:lower()=="fm" then + self.SpawnInitModu=radio.modulation.FM + else + self.SpawnInitModu=radio.modulation.AM + end + return self +end + +--- Sets the modex of the first unit of the group. If more units are in the group, the number is increased by one with every unit. +-- @param #SPAWN self +-- @param #number modex Modex of the first unit. +-- @return #SPAWN self +function SPAWN:InitModex(modex) + + if modex then + self.SpawnInitModex=tonumber(modex) + end + + return self +end + + +--- Randomizes the defined route of the SpawnTemplatePrefix group in the ME. This is very useful to define extra variation of the behaviour of groups. +-- @param #SPAWN self +-- @param #number SpawnStartPoint is the waypoint where the randomization begins. +-- Note that the StartPoint = 0 equaling the point where the group is spawned. +-- @param #number SpawnEndPoint is the waypoint where the randomization ends counting backwards. +-- This parameter is useful to avoid randomization to end at a waypoint earlier than the last waypoint on the route. +-- @param #number SpawnRadius is the radius in meters in which the randomization of the new waypoints, with the original waypoint of the original template located in the middle ... +-- @param #number SpawnHeight (optional) Specifies the **additional** height in meters that can be added to the base height specified at each waypoint in the ME. +-- @return #SPAWN +-- @usage +-- -- NATO helicopters engaging in the battle field. +-- -- The KA-50 has waypoints Start point ( =0 or SP ), 1, 2, 3, 4, End point (= 5 or DP). +-- -- Waypoints 2 and 3 will only be randomized. The others will remain on their original position with each new spawn of the helicopter. +-- -- The randomization of waypoint 2 and 3 will take place within a radius of 2000 meters. +-- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):InitRandomizeRoute( 2, 2, 2000 ) +function SPAWN:InitRandomizeRoute( SpawnStartPoint, SpawnEndPoint, SpawnRadius, SpawnHeight ) + self:F( { self.SpawnTemplatePrefix, SpawnStartPoint, SpawnEndPoint, SpawnRadius, SpawnHeight } ) + + self.SpawnRandomizeRoute = true + self.SpawnRandomizeRouteStartPoint = SpawnStartPoint + self.SpawnRandomizeRouteEndPoint = SpawnEndPoint + self.SpawnRandomizeRouteRadius = SpawnRadius + self.SpawnRandomizeRouteHeight = SpawnHeight + + for GroupID = 1, self.SpawnMaxGroups do + self:_RandomizeRoute( GroupID ) + end + + return self +end + +--- Randomizes the position of @{Wrapper.Group}s that are spawned within a **radius band**, given an Outer and Inner radius, from the point that the spawn happens. +-- @param #SPAWN self +-- @param #boolean RandomizePosition If true, SPAWN will perform the randomization of the @{Wrapper.Group}s position between a given outer and inner radius. +-- @param DCS#Distance OuterRadius (optional) The outer radius in meters where the new group will be spawned. +-- @param DCS#Distance InnerRadius (optional) The inner radius in meters where the new group will NOT be spawned. +-- @return #SPAWN +function SPAWN:InitRandomizePosition( RandomizePosition, OuterRadius, InnerRadius ) + self:F( { self.SpawnTemplatePrefix, RandomizePosition, OuterRadius, InnerRadius } ) + + self.SpawnRandomizePosition = RandomizePosition or false + self.SpawnRandomizePositionOuterRadius = OuterRadius or 0 + self.SpawnRandomizePositionInnerRadius = InnerRadius or 0 + + for GroupID = 1, self.SpawnMaxGroups do + self:_RandomizeRoute( GroupID ) + end + + return self +end + + +--- Randomizes the UNITs that are spawned within a radius band given an Outer and Inner radius. +-- @param #SPAWN self +-- @param #boolean RandomizeUnits If true, SPAWN will perform the randomization of the @{UNIT}s position within the group between a given outer and inner radius. +-- @param DCS#Distance OuterRadius (optional) The outer radius in meters where the new group will be spawned. +-- @param DCS#Distance InnerRadius (optional) The inner radius in meters where the new group will NOT be spawned. +-- @return #SPAWN +-- @usage +-- -- NATO helicopters engaging in the battle field. +-- -- The KA-50 has waypoints Start point ( =0 or SP ), 1, 2, 3, 4, End point (= 5 or DP). +-- -- Waypoints 2 and 3 will only be randomized. The others will remain on their original position with each new spawn of the helicopter. +-- -- The randomization of waypoint 2 and 3 will take place within a radius of 2000 meters. +-- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):InitRandomizeRoute( 2, 2, 2000 ) +function SPAWN:InitRandomizeUnits( RandomizeUnits, OuterRadius, InnerRadius ) + self:F( { self.SpawnTemplatePrefix, RandomizeUnits, OuterRadius, InnerRadius } ) + + self.SpawnRandomizeUnits = RandomizeUnits or false + self.SpawnOuterRadius = OuterRadius or 0 + self.SpawnInnerRadius = InnerRadius or 0 + + for GroupID = 1, self.SpawnMaxGroups do + self:_RandomizeRoute( GroupID ) + end + + return self +end + +--- This method is rather complicated to understand. But I'll try to explain. +-- This method becomes useful when you need to spawn groups with random templates of groups defined within the mission editor, +-- but they will all follow the same Template route and have the same prefix name. +-- In other words, this method randomizes between a defined set of groups the template to be used for each new spawn of a group. +-- @param #SPAWN self +-- @param #string SpawnTemplatePrefixTable A table with the names of the groups defined within the mission editor, from which one will be choosen when a new group will be spawned. +-- @return #SPAWN +-- @usage +-- -- NATO Tank Platoons invading Gori. +-- -- Choose between 13 different 'US Tank Platoon' configurations for each new SPAWN the Group to be spawned for the +-- -- 'US Tank Platoon Left', 'US Tank Platoon Middle' and 'US Tank Platoon Right' SpawnTemplatePrefixes. +-- -- Each new SPAWN will randomize the route, with a defined time interval of 200 seconds with 40% time variation (randomization) and +-- -- with a limit set of maximum 12 Units alive simulteneously and 150 Groups to be spawned during the whole mission. +-- Spawn_US_Platoon = { 'US Tank Platoon 1', 'US Tank Platoon 2', 'US Tank Platoon 3', 'US Tank Platoon 4', 'US Tank Platoon 5', +-- 'US Tank Platoon 6', 'US Tank Platoon 7', 'US Tank Platoon 8', 'US Tank Platoon 9', 'US Tank Platoon 10', +-- 'US Tank Platoon 11', 'US Tank Platoon 12', 'US Tank Platoon 13' } +-- Spawn_US_Platoon_Left = SPAWN:New( 'US Tank Platoon Left' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 ) +-- Spawn_US_Platoon_Middle = SPAWN:New( 'US Tank Platoon Middle' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 ) +-- Spawn_US_Platoon_Right = SPAWN:New( 'US Tank Platoon Right' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 ) +function SPAWN:InitRandomizeTemplate( SpawnTemplatePrefixTable ) + self:F( { self.SpawnTemplatePrefix, SpawnTemplatePrefixTable } ) + + self.SpawnTemplatePrefixTable = SpawnTemplatePrefixTable + self.SpawnRandomizeTemplate = true + + for SpawnGroupID = 1, self.SpawnMaxGroups do + self:_RandomizeTemplate( SpawnGroupID ) + end + + return self +end + + +--- Randomize templates to be used as the unit representatives for the Spawned group, defined using a SET_GROUP object. +-- This method becomes useful when you need to spawn groups with random templates of groups defined within the mission editor, +-- but they will all follow the same Template route and have the same prefix name. +-- In other words, this method randomizes between a defined set of groups the template to be used for each new spawn of a group. +-- @param #SPAWN self +-- @param Core.Set#SET_GROUP SpawnTemplateSet A SET_GROUP object set, that contains the groups that are possible unit representatives of the group to be spawned. +-- @return #SPAWN +-- @usage +-- -- NATO Tank Platoons invading Gori. +-- +-- -- Choose between different 'US Tank Platoon Template' configurations to be spawned for the +-- -- 'US Tank Platoon Left', 'US Tank Platoon Middle' and 'US Tank Platoon Right' SPAWN objects. +-- +-- -- Each new SPAWN will randomize the route, with a defined time interval of 200 seconds with 40% time variation (randomization) and +-- -- with a limit set of maximum 12 Units alive simulteneously and 150 Groups to be spawned during the whole mission. +-- +-- Spawn_US_PlatoonSet = SET_GROUP:New():FilterPrefixes( "US Tank Platoon Templates" ):FilterOnce() +-- +-- --- Now use the Spawn_US_PlatoonSet to define the templates using InitRandomizeTemplateSet. +-- Spawn_US_Platoon_Left = SPAWN:New( 'US Tank Platoon Left' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplateSet( Spawn_US_PlatoonSet ):InitRandomizeRoute( 3, 3, 2000 ) +-- Spawn_US_Platoon_Middle = SPAWN:New( 'US Tank Platoon Middle' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplateSet( Spawn_US_PlatoonSet ):InitRandomizeRoute( 3, 3, 2000 ) +-- Spawn_US_Platoon_Right = SPAWN:New( 'US Tank Platoon Right' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplateSet( Spawn_US_PlatoonSet ):InitRandomizeRoute( 3, 3, 2000 ) +function SPAWN:InitRandomizeTemplateSet( SpawnTemplateSet ) -- R2.3 + self:F( { self.SpawnTemplatePrefix } ) + + self.SpawnTemplatePrefixTable = SpawnTemplateSet:GetSetNames() + self.SpawnRandomizeTemplate = true + + for SpawnGroupID = 1, self.SpawnMaxGroups do + self:_RandomizeTemplate( SpawnGroupID ) + end + + return self +end + + +--- Randomize templates to be used as the unit representatives for the Spawned group, defined by specifying the prefix names. +-- This method becomes useful when you need to spawn groups with random templates of groups defined within the mission editor, +-- but they will all follow the same Template route and have the same prefix name. +-- In other words, this method randomizes between a defined set of groups the template to be used for each new spawn of a group. +-- @param #SPAWN self +-- @param #string SpawnTemplatePrefixes A string or a list of string that contains the prefixes of the groups that are possible unit representatives of the group to be spawned. +-- @return #SPAWN +-- @usage +-- -- NATO Tank Platoons invading Gori. +-- +-- -- Choose between different 'US Tank Platoon Templates' configurations to be spawned for the +-- -- 'US Tank Platoon Left', 'US Tank Platoon Middle' and 'US Tank Platoon Right' SPAWN objects. +-- +-- -- Each new SPAWN will randomize the route, with a defined time interval of 200 seconds with 40% time variation (randomization) and +-- -- with a limit set of maximum 12 Units alive simulteneously and 150 Groups to be spawned during the whole mission. +-- +-- Spawn_US_Platoon_Left = SPAWN:New( 'US Tank Platoon Left' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplatePrefixes( "US Tank Platoon Templates" ):InitRandomizeRoute( 3, 3, 2000 ) +-- Spawn_US_Platoon_Middle = SPAWN:New( 'US Tank Platoon Middle' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplatePrefixes( "US Tank Platoon Templates" ):InitRandomizeRoute( 3, 3, 2000 ) +-- Spawn_US_Platoon_Right = SPAWN:New( 'US Tank Platoon Right' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplatePrefixes( "US Tank Platoon Templates" ):InitRandomizeRoute( 3, 3, 2000 ) +function SPAWN:InitRandomizeTemplatePrefixes( SpawnTemplatePrefixes ) --R2.3 + self:F( { self.SpawnTemplatePrefix } ) + + local SpawnTemplateSet = SET_GROUP:New():FilterPrefixes( SpawnTemplatePrefixes ):FilterOnce() + + self:InitRandomizeTemplateSet( SpawnTemplateSet ) + + return self +end + + +--- When spawning a new group, make the grouping of the units according the InitGrouping setting. +-- @param #SPAWN self +-- @param #number Grouping Indicates the maximum amount of units in the group. +-- @return #SPAWN +function SPAWN:InitGrouping( Grouping ) -- R2.2 + self:F( { self.SpawnTemplatePrefix, Grouping } ) + + self.SpawnGrouping = Grouping + + return self +end + + + +--- This method provides the functionality to randomize the spawning of the Groups at a given list of zones of different types. +-- @param #SPAWN self +-- @param #table SpawnZoneTable A table with @{Zone} objects. If this table is given, then each spawn will be executed within the given list of @{Zone}s objects. +-- @return #SPAWN +-- @usage +-- -- Create a zone table of the 2 zones. +-- ZoneTable = { ZONE:New( "Zone1" ), ZONE:New( "Zone2" ) } +-- +-- Spawn_Vehicle_1 = SPAWN:New( "Spawn Vehicle 1" ) +-- :InitLimit( 10, 10 ) +-- :InitRandomizeRoute( 1, 1, 200 ) +-- :InitRandomizeZones( ZoneTable ) +-- :SpawnScheduled( 5, .5 ) +-- +function SPAWN:InitRandomizeZones( SpawnZoneTable ) + self:F( { self.SpawnTemplatePrefix, SpawnZoneTable } ) + + self.SpawnZoneTable = SpawnZoneTable + self.SpawnRandomizeZones = true + + for SpawnGroupID = 1, self.SpawnMaxGroups do + self:_RandomizeZones( SpawnGroupID ) + end + + return self +end + + + + + +--- For planes and helicopters, when these groups go home and land on their home airbases and farps, they normally would taxi to the parking spot, shut-down their engines and wait forever until the Group is removed by the runtime environment. +-- This method is used to re-spawn automatically (so no extra call is needed anymore) the same group after it has landed. +-- This will enable a spawned group to be re-spawned after it lands, until it is destroyed... +-- Note: When the group is respawned, it will re-spawn from the original airbase where it took off. +-- So ensure that the routes for groups that respawn, always return to the original airbase, or players may get confused ... +-- @param #SPAWN self +-- @return #SPAWN self +-- @usage +-- -- RU Su-34 - AI Ship Attack +-- -- Re-SPAWN the Group(s) after each landing and Engine Shut-Down automatically. +-- SpawnRU_SU34 = SPAWN +-- :New( 'Su-34' ) +-- :Schedule( 2, 3, 1800, 0.4 ) +-- :SpawnUncontrolled() +-- :InitRandomizeRoute( 1, 1, 3000 ) +-- :InitRepeatOnEngineShutDown() +-- +function SPAWN:InitRepeat() + self:F( { self.SpawnTemplatePrefix, self.SpawnIndex } ) + + self.Repeat = true + self.RepeatOnEngineShutDown = false + self.RepeatOnLanding = true + + return self +end + +--- Respawn group after landing. +-- @param #SPAWN self +-- @return #SPAWN self +-- @usage +-- -- RU Su-34 - AI Ship Attack +-- -- Re-SPAWN the Group(s) after each landing and Engine Shut-Down automatically. +-- SpawnRU_SU34 = SPAWN +-- :New( 'Su-34' ) +-- :InitRandomizeRoute( 1, 1, 3000 ) +-- :InitRepeatOnLanding() +-- :Spawn() +function SPAWN:InitRepeatOnLanding() + self:F( { self.SpawnTemplatePrefix } ) + + self:InitRepeat() + self.RepeatOnEngineShutDown = false + self.RepeatOnLanding = true + + return self +end + + +--- Respawn after landing when its engines have shut down. +-- @param #SPAWN self +-- @return #SPAWN self +-- @usage +-- -- RU Su-34 - AI Ship Attack +-- -- Re-SPAWN the Group(s) after each landing and Engine Shut-Down automatically. +-- SpawnRU_SU34 = SPAWN +-- :New( 'Su-34' ) +-- :SpawnUncontrolled() +-- :InitRandomizeRoute( 1, 1, 3000 ) +-- :InitRepeatOnEngineShutDown() +-- :Spawn() +function SPAWN:InitRepeatOnEngineShutDown() + self:F( { self.SpawnTemplatePrefix } ) + + self:InitRepeat() + self.RepeatOnEngineShutDown = true + self.RepeatOnLanding = false + + return self +end + + +--- Delete groups that have not moved for X seconds - AIR ONLY!!! +-- DO NOT USE ON GROUPS THAT DO NOT MOVE OR YOUR SERVER WILL BURN IN HELL (Pikes - April 2020) +-- When groups are still alive and have become inactive due to damage and are unable to contribute anything, then this group will be removed at defined intervals in seconds. +-- @param #SPAWN self +-- @param #string SpawnCleanUpInterval The interval to check for inactive groups within seconds. +-- @return #SPAWN self +-- @usage +-- Spawn_Helicopter:InitCleanUp( 20 ) -- CleanUp the spawning of the helicopters every 20 seconds when they become inactive. +function SPAWN:InitCleanUp( SpawnCleanUpInterval ) + self:F( { self.SpawnTemplatePrefix, SpawnCleanUpInterval } ) + + self.SpawnCleanUpInterval = SpawnCleanUpInterval + self.SpawnCleanUpTimeStamps = {} + + local SpawnGroup, SpawnCursor = self:GetFirstAliveGroup() + self:T( { "CleanUp Scheduler:", SpawnGroup } ) + + --self.CleanUpFunction = routines.scheduleFunction( self._SpawnCleanUpScheduler, { self }, timer.getTime() + 1, SpawnCleanUpInterval ) + self.CleanUpScheduler = SCHEDULER:New( self, self._SpawnCleanUpScheduler, {}, 1, SpawnCleanUpInterval, 0.2 ) + return self +end + + + +--- Makes the groups visible before start (like a batallion). +-- The method will take the position of the group as the first position in the array. +-- CAUTION: this directive will NOT work with OnSpawnGroup function. +-- @param #SPAWN self +-- @param #number SpawnAngle The angle in degrees how the groups and each unit of the group will be positioned. +-- @param #number SpawnWidth The amount of Groups that will be positioned on the X axis. +-- @param #number SpawnDeltaX The space between each Group on the X-axis. +-- @param #number SpawnDeltaY The space between each Group on the Y-axis. +-- @return #SPAWN self +-- @usage +-- -- Define an array of Groups. +-- Spawn_BE_Ground = SPAWN +-- :New( 'BE Ground' ) +-- :InitLimit( 2, 24 ) +-- :InitArray( 90, 10, 100, 50 ) +-- +function SPAWN:InitArray( SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY ) + self:F( { self.SpawnTemplatePrefix, SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY } ) + + self.SpawnVisible = true -- When the first Spawn executes, all the Groups need to be made visible before start. + + local SpawnX = 0 + local SpawnY = 0 + local SpawnXIndex = 0 + local SpawnYIndex = 0 + + for SpawnGroupID = 1, self.SpawnMaxGroups do + self:T( { SpawnX, SpawnY, SpawnXIndex, SpawnYIndex } ) + + self.SpawnGroups[SpawnGroupID].Visible = true + self.SpawnGroups[SpawnGroupID].Spawned = false + + SpawnXIndex = SpawnXIndex + 1 + if SpawnWidth and SpawnWidth ~= 0 then + if SpawnXIndex >= SpawnWidth then + SpawnXIndex = 0 + SpawnYIndex = SpawnYIndex + 1 + end + end + + local SpawnRootX = self.SpawnGroups[SpawnGroupID].SpawnTemplate.x + local SpawnRootY = self.SpawnGroups[SpawnGroupID].SpawnTemplate.y + + self:_TranslateRotate( SpawnGroupID, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle ) + + self.SpawnGroups[SpawnGroupID].SpawnTemplate.lateActivation = true + self.SpawnGroups[SpawnGroupID].SpawnTemplate.visible = true + + self.SpawnGroups[SpawnGroupID].Visible = true + + self:HandleEvent( EVENTS.Birth, self._OnBirth ) + self:HandleEvent( EVENTS.Dead, self._OnDeadOrCrash ) + self:HandleEvent( EVENTS.Crash, self._OnDeadOrCrash ) + self:HandleEvent( EVENTS.RemoveUnit, self._OnDeadOrCrash ) + if self.Repeat then + self:HandleEvent( EVENTS.Takeoff, self._OnTakeOff ) + self:HandleEvent( EVENTS.Land, self._OnLand ) + end + if self.RepeatOnEngineShutDown then + self:HandleEvent( EVENTS.EngineShutdown, self._OnEngineShutDown ) + end + + self.SpawnGroups[SpawnGroupID].Group = _DATABASE:Spawn( self.SpawnGroups[SpawnGroupID].SpawnTemplate ) + + SpawnX = SpawnXIndex * SpawnDeltaX + SpawnY = SpawnYIndex * SpawnDeltaY + end + + return self +end + +do -- AI methods + --- Turns the AI On or Off for the @{Wrapper.Group} when spawning. + -- @param #SPAWN self + -- @param #boolean AIOnOff A value of true sets the AI On, a value of false sets the AI Off. + -- @return #SPAWN The SPAWN object + function SPAWN:InitAIOnOff( AIOnOff ) + + self.AIOnOff = AIOnOff + return self + end + + --- Turns the AI On for the @{Wrapper.Group} when spawning. + -- @param #SPAWN self + -- @return #SPAWN The SPAWN object + function SPAWN:InitAIOn() + + return self:InitAIOnOff( true ) + end + + --- Turns the AI Off for the @{Wrapper.Group} when spawning. + -- @param #SPAWN self + -- @return #SPAWN The SPAWN object + function SPAWN:InitAIOff() + + return self:InitAIOnOff( false ) + end + +end -- AI methods + +do -- Delay methods + --- Turns the Delay On or Off for the first @{Wrapper.Group} scheduled spawning. + -- The default value is that for scheduled spawning, there is an initial delay when spawning the first @{Wrapper.Group}. + -- @param #SPAWN self + -- @param #boolean DelayOnOff A value of true sets the Delay On, a value of false sets the Delay Off. + -- @return #SPAWN The SPAWN object + function SPAWN:InitDelayOnOff( DelayOnOff ) + + self.DelayOnOff = DelayOnOff + return self + end + + --- Turns the Delay On for the @{Wrapper.Group} when spawning. + -- @param #SPAWN self + -- @return #SPAWN The SPAWN object + function SPAWN:InitDelayOn() + + return self:InitDelayOnOff( true ) + end + + --- Turns the Delay Off for the @{Wrapper.Group} when spawning. + -- @param #SPAWN self + -- @return #SPAWN The SPAWN object + function SPAWN:InitDelayOff() + + return self:InitDelayOnOff( false ) + end + +end -- Delay methods + +--- Will spawn a group based on the internal index. +-- Note: Uses @{DATABASE} module defined in MOOSE. +-- @param #SPAWN self +-- @return Wrapper.Group#GROUP The group that was spawned. You can use this group for further actions. +function SPAWN:Spawn() + self:F( { self.SpawnTemplatePrefix, self.SpawnIndex, self.AliveUnits } ) + + if self.SpawnInitAirbase then + return self:SpawnAtAirbase(self.SpawnInitAirbase, self.SpawnInitTakeoff, nil, self.SpawnInitTerminalType) + else + return self:SpawnWithIndex( self.SpawnIndex + 1 ) + end + +end + +--- Will re-spawn a group based on a given index. +-- Note: Uses @{DATABASE} module defined in MOOSE. +-- @param #SPAWN self +-- @param #string SpawnIndex The index of the group to be spawned. +-- @return Wrapper.Group#GROUP The group that was spawned. You can use this group for further actions. +function SPAWN:ReSpawn( SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, SpawnIndex } ) + + if not SpawnIndex then + SpawnIndex = 1 + end + +-- TODO: This logic makes DCS crash and i don't know why (yet). -- ED (Pikes -- not in the least bit scary to see this, right?) + local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) + local WayPoints = SpawnGroup and SpawnGroup.WayPoints or nil + if SpawnGroup then + local SpawnDCSGroup = SpawnGroup:GetDCSObject() + if SpawnDCSGroup then + SpawnGroup:Destroy() + end + end + + local SpawnGroup = self:SpawnWithIndex( SpawnIndex ) + if SpawnGroup and WayPoints then + -- If there were WayPoints set, then Re-Execute those WayPoints! + SpawnGroup:WayPointInitialize( WayPoints ) + SpawnGroup:WayPointExecute( 1, 5 ) + end + + if SpawnGroup.ReSpawnFunction then + SpawnGroup:ReSpawnFunction() + end + + SpawnGroup:ResetEvents() + + return SpawnGroup +end + + +--- Set the spawn index to a specified index number. +-- This method can be used to "reset" the spawn counter to a specific index number. +-- This will actually enable a respawn of groups from the specific index. +-- @param #SPAWN self +-- @param #string SpawnIndex The index of the group from where the spawning will start again. The default value would be 0, which means a complete reset of the spawnindex. +-- @return #SPAWN self +function SPAWN:SetSpawnIndex( SpawnIndex ) + self.SpawnIndex = SpawnIndex or 0 +end + + +--- Will spawn a group with a specified index number. +-- Uses @{DATABASE} global object defined in MOOSE. +-- @param #SPAWN self +-- @param #string SpawnIndex The index of the group to be spawned. +-- @return Wrapper.Group#GROUP The group that was spawned. You can use this group for further actions. +function SPAWN:SpawnWithIndex( SpawnIndex, NoBirth ) + self:F2( { SpawnTemplatePrefix = self.SpawnTemplatePrefix, SpawnIndex = SpawnIndex, AliveUnits = self.AliveUnits, SpawnMaxGroups = self.SpawnMaxGroups } ) + + if self:_GetSpawnIndex( SpawnIndex ) then + + if self.SpawnGroups[self.SpawnIndex].Visible then + self.SpawnGroups[self.SpawnIndex].Group:Activate() + else + + local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate + self:T( SpawnTemplate.name ) + + if SpawnTemplate then + + local PointVec3 = POINT_VEC3:New( SpawnTemplate.route.points[1].x, SpawnTemplate.route.points[1].alt, SpawnTemplate.route.points[1].y ) + self:T( { "Current point of ", self.SpawnTemplatePrefix, PointVec3 } ) + + -- If RandomizePosition, then Randomize the formation in the zone band, keeping the template. + if self.SpawnRandomizePosition then + local RandomVec2 = PointVec3:GetRandomVec2InRadius( self.SpawnRandomizePositionOuterRadius, self.SpawnRandomizePositionInnerRadius ) + local CurrentX = SpawnTemplate.units[1].x + local CurrentY = SpawnTemplate.units[1].y + SpawnTemplate.x = RandomVec2.x + SpawnTemplate.y = RandomVec2.y + for UnitID = 1, #SpawnTemplate.units do + SpawnTemplate.units[UnitID].x = SpawnTemplate.units[UnitID].x + ( RandomVec2.x - CurrentX ) + SpawnTemplate.units[UnitID].y = SpawnTemplate.units[UnitID].y + ( RandomVec2.y - CurrentY ) + self:T( 'SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) + end + end + + -- If RandomizeUnits, then Randomize the formation at the start point. + if self.SpawnRandomizeUnits then + for UnitID = 1, #SpawnTemplate.units do + local RandomVec2 = PointVec3:GetRandomVec2InRadius( self.SpawnOuterRadius, self.SpawnInnerRadius ) + SpawnTemplate.units[UnitID].x = RandomVec2.x + SpawnTemplate.units[UnitID].y = RandomVec2.y + self:T( 'SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) + end + end + + -- Get correct heading in Radians. + local function _Heading(courseDeg) + local h + if courseDeg<=180 then + h=math.rad(courseDeg) + else + h=-math.rad(360-courseDeg) + end + return h + end + + local Rad180 = math.rad(180) + local function _HeadingRad(courseRad) + if courseRad<=Rad180 then + return courseRad + else + return -((2*Rad180)-courseRad) + end + end + + -- Generate a random value somewhere between two floating point values. + local function _RandomInRange ( min, max ) + if min and max then + return min + ( math.random()*(max-min) ) + else + return min + end + end + + -- Apply InitGroupHeading rotation if requested. + -- We do this before InitHeading unit rotation so that can take precedence + -- NOTE: Does *not* rotate the groups route; only its initial facing. + if self.SpawnInitGroupHeadingMin and #SpawnTemplate.units > 0 then + + local pivotX = SpawnTemplate.units[1].x -- unit #1 is the pivot point + local pivotY = SpawnTemplate.units[1].y + + local headingRad = math.rad(_RandomInRange(self.SpawnInitGroupHeadingMin or 0,self.SpawnInitGroupHeadingMax)) + local cosHeading = math.cos(headingRad) + local sinHeading = math.sin(headingRad) + + local unitVarRad = math.rad(self.SpawnInitGroupUnitVar or 0) + + for UnitID = 1, #SpawnTemplate.units do + + if UnitID > 1 then -- don't rotate position of unit #1 + local unitXOff = SpawnTemplate.units[UnitID].x - pivotX -- rotate position offset around unit #1 + local unitYOff = SpawnTemplate.units[UnitID].y - pivotY + + SpawnTemplate.units[UnitID].x = pivotX + (unitXOff*cosHeading) - (unitYOff*sinHeading) + SpawnTemplate.units[UnitID].y = pivotY + (unitYOff*cosHeading) + (unitXOff*sinHeading) + end + + -- adjust heading of all units, including unit #1 + local unitHeading = SpawnTemplate.units[UnitID].heading + headingRad -- add group rotation to units default rotation + SpawnTemplate.units[UnitID].heading = _HeadingRad(_RandomInRange(unitHeading-unitVarRad, unitHeading+unitVarRad)) + SpawnTemplate.units[UnitID].psi = -SpawnTemplate.units[UnitID].heading + + end + + end + + -- If Heading is given, point all the units towards the given Heading. Overrides any heading set in InitGroupHeading above. + if self.SpawnInitHeadingMin then + for UnitID = 1, #SpawnTemplate.units do + SpawnTemplate.units[UnitID].heading = _Heading(_RandomInRange(self.SpawnInitHeadingMin, self.SpawnInitHeadingMax)) + SpawnTemplate.units[UnitID].psi = -SpawnTemplate.units[UnitID].heading + end + end + + -- Set livery. + if self.SpawnInitLivery then + for UnitID = 1, #SpawnTemplate.units do + SpawnTemplate.units[UnitID].livery_id = self.SpawnInitLivery + end + end + + -- Set skill. + if self.SpawnInitSkill then + for UnitID = 1, #SpawnTemplate.units do + SpawnTemplate.units[UnitID].skill = self.SpawnInitSkill + end + end + + -- Set tail number. + if self.SpawnInitModex then + for UnitID = 1, #SpawnTemplate.units do + SpawnTemplate.units[UnitID].onboard_num = string.format("%03d", self.SpawnInitModex+(UnitID-1)) + end + end + + -- Set radio comms on/off. + if self.SpawnInitRadio then + SpawnTemplate.communication=self.SpawnInitRadio + end + + -- Set radio frequency. + if self.SpawnInitFreq then + SpawnTemplate.frequency=self.SpawnInitFreq + end + + -- Set radio modulation. + if self.SpawnInitModu then + SpawnTemplate.modulation=self.SpawnInitModu + end + + -- Set country, coaliton and categroy. + SpawnTemplate.CategoryID = self.SpawnInitCategory or SpawnTemplate.CategoryID + SpawnTemplate.CountryID = self.SpawnInitCountry or SpawnTemplate.CountryID + SpawnTemplate.CoalitionID = self.SpawnInitCoalition or SpawnTemplate.CoalitionID + + +-- if SpawnTemplate.CategoryID == Group.Category.HELICOPTER or SpawnTemplate.CategoryID == Group.Category.AIRPLANE then +-- if SpawnTemplate.route.points[1].type == "TakeOffParking" then +-- SpawnTemplate.uncontrolled = self.SpawnUnControlled +-- end +-- end + end + + if not NoBirth then + self:HandleEvent( EVENTS.Birth, self._OnBirth ) + end + self:HandleEvent( EVENTS.Dead, self._OnDeadOrCrash ) + self:HandleEvent( EVENTS.Crash, self._OnDeadOrCrash ) + self:HandleEvent( EVENTS.RemoveUnit, self._OnDeadOrCrash ) + if self.Repeat then + self:HandleEvent( EVENTS.Takeoff, self._OnTakeOff ) + self:HandleEvent( EVENTS.Land, self._OnLand ) + end + if self.RepeatOnEngineShutDown then + self:HandleEvent( EVENTS.EngineShutdown, self._OnEngineShutDown ) + end + + self.SpawnGroups[self.SpawnIndex].Group = _DATABASE:Spawn( SpawnTemplate ) + + local SpawnGroup = self.SpawnGroups[self.SpawnIndex].Group -- Wrapper.Group#GROUP + + --TODO: Need to check if this function doesn't need to be scheduled, as the group may not be immediately there! + if SpawnGroup then + + SpawnGroup:SetAIOnOff( self.AIOnOff ) + end + + self:T3( SpawnTemplate.name ) + + -- If there is a SpawnFunction hook defined, call it. + if self.SpawnFunctionHook then + -- delay calling this for .1 seconds so that it hopefully comes after the BIRTH event of the group. + self.SpawnHookScheduler:Schedule( nil, self.SpawnFunctionHook, { self.SpawnGroups[self.SpawnIndex].Group, unpack( self.SpawnFunctionArguments)}, 0.1 ) + end + -- TODO: Need to fix this by putting an "R" in the name of the group when the group repeats. + --if self.Repeat then + -- _DATABASE:SetStatusGroup( SpawnTemplate.name, "ReSpawn" ) + --end + end + + + self.SpawnGroups[self.SpawnIndex].Spawned = true + return self.SpawnGroups[self.SpawnIndex].Group + else + --self:E( { self.SpawnTemplatePrefix, "No more Groups to Spawn:", SpawnIndex, self.SpawnMaxGroups } ) + end + + return nil +end + +--- Spawns new groups at varying time intervals. +-- This is useful if you want to have continuity within your missions of certain (AI) groups to be present (alive) within your missions. +-- @param #SPAWN self +-- @param #number SpawnTime The time interval defined in seconds between each new spawn of new groups. +-- @param #number SpawnTimeVariation The variation to be applied on the defined time interval between each new spawn. +-- The variation is a number between 0 and 1, representing the %-tage of variation to be applied on the time interval. +-- @return #SPAWN self +-- @usage +-- -- NATO helicopters engaging in the battle field. +-- -- The time interval is set to SPAWN new helicopters between each 600 seconds, with a time variation of 50%. +-- -- The time variation in this case will be between 450 seconds and 750 seconds. +-- -- This is calculated as follows: +-- -- Low limit: 600 * ( 1 - 0.5 / 2 ) = 450 +-- -- High limit: 600 * ( 1 + 0.5 / 2 ) = 750 +-- -- Between these two values, a random amount of seconds will be choosen for each new spawn of the helicopters. +-- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):SpawnScheduled( 600, 0.5 ) +function SPAWN:SpawnScheduled( SpawnTime, SpawnTimeVariation ) + self:F( { SpawnTime, SpawnTimeVariation } ) + + if SpawnTime ~= nil and SpawnTimeVariation ~= nil then + local InitialDelay = 0 + if self.DelayOnOff == true then + InitialDelay = math.random( SpawnTime - SpawnTime * SpawnTimeVariation, SpawnTime + SpawnTime * SpawnTimeVariation ) + end + self.SpawnScheduler = SCHEDULER:New( self, self._Scheduler, {}, InitialDelay, SpawnTime, SpawnTimeVariation ) + end + + return self +end + +--- Will re-start the spawning scheduler. +-- Note: This method is only required to be called when the schedule was stopped. +-- @param #SPAWN self +-- @return #SPAWN +function SPAWN:SpawnScheduleStart() + self:F( { self.SpawnTemplatePrefix } ) + + self.SpawnScheduler:Start() + return self +end + +--- Will stop the scheduled spawning scheduler. +-- @param #SPAWN self +-- @return #SPAWN +function SPAWN:SpawnScheduleStop() + self:F( { self.SpawnTemplatePrefix } ) + + self.SpawnScheduler:Stop() + return self +end + + +--- Allows to place a CallFunction hook when a new group spawns. +-- The provided method will be called when a new group is spawned, including its given parameters. +-- The first parameter of the SpawnFunction is the @{Wrapper.Group#GROUP} that was spawned. +-- @param #SPAWN self +-- @param #function SpawnCallBackFunction The function to be called when a group spawns. +-- @param SpawnFunctionArguments A random amount of arguments to be provided to the function when the group spawns. +-- @return #SPAWN +-- @usage +-- -- Declare SpawnObject and call a function when a new Group is spawned. +-- local SpawnObject = SPAWN +-- :New( "SpawnObject" ) +-- :InitLimit( 2, 10 ) +-- :OnSpawnGroup( +-- function( SpawnGroup ) +-- SpawnGroup:E( "I am spawned" ) +-- end +-- ) +-- :SpawnScheduled( 300, 0.3 ) +-- +function SPAWN:OnSpawnGroup( SpawnCallBackFunction, ... ) + self:F( "OnSpawnGroup" ) + + self.SpawnFunctionHook = SpawnCallBackFunction + self.SpawnFunctionArguments = {} + if arg then + self.SpawnFunctionArguments = arg + end + + return self +end + +--- Will spawn a group at an @{Wrapper.Airbase}. +-- This method is mostly advisable to be used if you want to simulate spawning units at an airbase. +-- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. +-- You can use the returned group to further define the route to be followed. +-- +-- The @{Wrapper.Airbase#AIRBASE} object must refer to a valid airbase known in the sim. +-- You can use the following enumerations to search for the pre-defined airbases on the current known maps of DCS: +-- +-- * @{Wrapper.Airbase#AIRBASE.Caucasus}: The airbases on the Caucasus map. +-- * @{Wrapper.Airbase#AIRBASE.Nevada}: The airbases on the Nevada (NTTR) map. +-- * @{Wrapper.Airbase#AIRBASE.Normandy}: The airbases on the Normandy map. +-- +-- Use the method @{Wrapper.Airbase#AIRBASE.FindByName}() to retrieve the airbase object. +-- The known AIRBASE objects are automatically imported at mission start by MOOSE. +-- Therefore, there isn't any New() constructor defined for AIRBASE objects. +-- +-- Ships and Farps are added within the mission, and are therefore not known. +-- For these AIRBASE objects, there isn't an @{Wrapper.Airbase#AIRBASE} enumeration defined. +-- You need to provide the **exact name** of the airbase as the parameter to the @{Wrapper.Airbase#AIRBASE.FindByName}() method! +-- +-- @param #SPAWN self +-- @param Wrapper.Airbase#AIRBASE SpawnAirbase The @{Wrapper.Airbase} where to spawn the group. +-- @param #SPAWN.Takeoff Takeoff (optional) The location and takeoff method. Default is Hot. +-- @param #number TakeoffAltitude (optional) The altitude above the ground. +-- @param Wrapper.Airbase#AIRBASE.TerminalType TerminalType (optional) The terminal type the aircraft should be spawned at. See @{Wrapper.Airbase#AIRBASE.TerminalType}. +-- @param #boolean EmergencyAirSpawn (optional) If true (default), groups are spawned in air if there is no parking spot at the airbase. If false, nothing is spawned if no parking spot is available. +-- @param #table Parkingdata (optional) Table holding the coordinates and terminal ids for all units of the group. Spawning will be forced to happen at exactily these spots! +-- @return Wrapper.Group#GROUP The group that was spawned or nil when nothing was spawned. +-- @usage +-- Spawn_Plane = SPAWN:New( "Plane" ) +-- Spawn_Plane:SpawnAtAirbase( AIRBASE:FindByName( AIRBASE.Caucasus.Krymsk ), SPAWN.Takeoff.Cold ) +-- Spawn_Plane:SpawnAtAirbase( AIRBASE:FindByName( AIRBASE.Caucasus.Krymsk ), SPAWN.Takeoff.Hot ) +-- Spawn_Plane:SpawnAtAirbase( AIRBASE:FindByName( AIRBASE.Caucasus.Krymsk ), SPAWN.Takeoff.Runway ) +-- +-- Spawn_Plane:SpawnAtAirbase( AIRBASE:FindByName( "Carrier" ), SPAWN.Takeoff.Cold ) +-- +-- Spawn_Heli = SPAWN:New( "Heli") +-- +-- Spawn_Heli:SpawnAtAirbase( AIRBASE:FindByName( "FARP Cold" ), SPAWN.Takeoff.Cold ) +-- Spawn_Heli:SpawnAtAirbase( AIRBASE:FindByName( "FARP Hot" ), SPAWN.Takeoff.Hot ) +-- Spawn_Heli:SpawnAtAirbase( AIRBASE:FindByName( "FARP Runway" ), SPAWN.Takeoff.Runway ) +-- Spawn_Heli:SpawnAtAirbase( AIRBASE:FindByName( "FARP Air" ), SPAWN.Takeoff.Air ) +-- +-- Spawn_Heli:SpawnAtAirbase( AIRBASE:FindByName( "Carrier" ), SPAWN.Takeoff.Cold ) +-- +-- Spawn_Plane:SpawnAtAirbase( AIRBASE:FindByName( AIRBASE.Caucasus.Krymsk ), SPAWN.Takeoff.Cold, nil, AIRBASE.TerminalType.OpenBig ) +-- +function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalType, EmergencyAirSpawn, Parkingdata ) -- R2.2, R2.4 + self:F( { self.SpawnTemplatePrefix, SpawnAirbase, Takeoff, TakeoffAltitude, TerminalType } ) + + -- Get position of airbase. + local PointVec3 = SpawnAirbase:GetCoordinate() + self:T2(PointVec3) + + -- Set take off type. Default is hot. + Takeoff = Takeoff or SPAWN.Takeoff.Hot + + -- By default, groups are spawned in air if no parking spot is available. + if EmergencyAirSpawn==nil then + EmergencyAirSpawn=true + end + + self:F( { SpawnIndex = self.SpawnIndex } ) + + if self:_GetSpawnIndex( self.SpawnIndex + 1 ) then + + -- Get group template. + local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate + + self:F( { SpawnTemplate = SpawnTemplate } ) + + if SpawnTemplate then + + -- Check if the aircraft with the specified SpawnIndex is already spawned. + -- If yes, ensure that the aircraft is spawned at the same aircraft spot. + + local GroupAlive = self:GetGroupFromIndex( self.SpawnIndex ) + + self:F( { GroupAlive = GroupAlive } ) + + -- Debug output + self:T( { "Current point of ", self.SpawnTemplatePrefix, SpawnAirbase } ) + + -- Template group, unit and its attributes. + local TemplateGroup = GROUP:FindByName(self.SpawnTemplatePrefix) + local TemplateUnit=TemplateGroup:GetUnit(1) + + -- General category of spawned group. + local group=TemplateGroup + local istransport=group:HasAttribute("Transports") and group:HasAttribute("Planes") + local isawacs=group:HasAttribute("AWACS") + local isfighter=group:HasAttribute("Fighters") or group:HasAttribute("Interceptors") or group:HasAttribute("Multirole fighters") or (group:HasAttribute("Bombers") and not group:HasAttribute("Strategic bombers")) + local isbomber=group:HasAttribute("Strategic bombers") + local istanker=group:HasAttribute("Tankers") + local ishelo=TemplateUnit:HasAttribute("Helicopters") + + -- Number of units in the group. With grouping this can actually differ from the template group size! + local nunits=#SpawnTemplate.units + + -- First waypoint of the group. + local SpawnPoint = SpawnTemplate.route.points[1] + + -- These are only for ships and FARPS. + SpawnPoint.linkUnit = nil + SpawnPoint.helipadId = nil + SpawnPoint.airdromeId = nil + + -- Get airbase ID and category. + local AirbaseID = SpawnAirbase:GetID() + local AirbaseCategory = SpawnAirbase:GetAirbaseCategory() + self:F( { AirbaseCategory = AirbaseCategory } ) + + -- Set airdromeId. + if AirbaseCategory == Airbase.Category.SHIP then + SpawnPoint.linkUnit = AirbaseID + SpawnPoint.helipadId = AirbaseID + elseif AirbaseCategory == Airbase.Category.HELIPAD then + SpawnPoint.linkUnit = AirbaseID + SpawnPoint.helipadId = AirbaseID + elseif AirbaseCategory == Airbase.Category.AIRDROME then + SpawnPoint.airdromeId = AirbaseID + end + + -- Set waypoint type/action. + SpawnPoint.alt = 0 + SpawnPoint.type = GROUPTEMPLATE.Takeoff[Takeoff][1] -- type + SpawnPoint.action = GROUPTEMPLATE.Takeoff[Takeoff][2] -- action + + -- Check if we spawn on ground. + local spawnonground=not (Takeoff==SPAWN.Takeoff.Air) + self:T({spawnonground=spawnonground, TOtype=Takeoff, TOair=Takeoff==SPAWN.Takeoff.Air}) + + -- Check where we actually spawn if we spawn on ground. + local spawnonship=false + local spawnonfarp=false + local spawnonrunway=false + local spawnonairport=false + if spawnonground then + if AirbaseCategory == Airbase.Category.SHIP then + spawnonship=true + elseif AirbaseCategory == Airbase.Category.HELIPAD then + spawnonfarp=true + elseif AirbaseCategory == Airbase.Category.AIRDROME then + spawnonairport=true + end + spawnonrunway=Takeoff==SPAWN.Takeoff.Runway + end + + -- Array with parking spots coordinates. + local parkingspots={} + local parkingindex={} + local spots + + -- Spawn happens on ground, i.e. at an airbase, a FARP or a ship. + if spawnonground and not SpawnTemplate.parked then + + + -- Number of free parking spots. + local nfree=0 + + -- Set terminal type. + local termtype=TerminalType + if spawnonrunway then + if spawnonship then + -- Looks like there are no runway spawn spots on the stennis! + if ishelo then + termtype=AIRBASE.TerminalType.HelicopterUsable + else + termtype=AIRBASE.TerminalType.OpenMedOrBig + end + else + termtype=AIRBASE.TerminalType.Runway + end + end + + -- Scan options. Might make that input somehow. + local scanradius=50 + local scanunits=true + local scanstatics=true + local scanscenery=false + local verysafe=false + + -- Number of free parking spots at the airbase. + if spawnonship or spawnonfarp or spawnonrunway then + -- These places work procedural and have some kind of build in queue ==> Less effort. + self:T(string.format("Group %s is spawned on farp/ship/runway %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) + nfree=SpawnAirbase:GetFreeParkingSpotsNumber(termtype, true) + spots=SpawnAirbase:GetFreeParkingSpotsTable(termtype, true) + --[[ + elseif Parkingdata~=nil then + -- Parking data explicitly set by user as input parameter. + nfree=#Parkingdata + spots=Parkingdata + ]] + else + if ishelo then + if termtype==nil then + -- Helo is spawned. Try exclusive helo spots first. + self:T(string.format("Helo group %s is at %s using terminal type %d.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), AIRBASE.TerminalType.HelicopterOnly)) + spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup, AIRBASE.TerminalType.HelicopterOnly, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits, Parkingdata) + nfree=#spots + if nfree=1 then + + -- All units get the same spot. DCS takes care of the rest. + for i=1,nunits do + table.insert(parkingspots, spots[1].Coordinate) + table.insert(parkingindex, spots[1].TerminalID) + end + -- This is actually used... + PointVec3=spots[1].Coordinate + + else + -- If there is absolutely no spot ==> air start! + _notenough=true + end + + elseif spawnonairport then + + if nfree>=nunits then + + for i=1,nunits do + table.insert(parkingspots, spots[i].Coordinate) + table.insert(parkingindex, spots[i].TerminalID) + end + + else + -- Not enough spots for the whole group ==> air start! + _notenough=true + end + end + + -- Not enough spots ==> Prepare airstart. + if _notenough then + + if EmergencyAirSpawn and not self.SpawnUnControlled then + self:E(string.format("WARNING: Group %s has no parking spots at %s ==> air start!", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) + + -- Not enough parking spots at the airport ==> Spawn in air. + spawnonground=false + spawnonship=false + spawnonfarp=false + spawnonrunway=false + + -- Set waypoint type/action to turning point. + SpawnPoint.type = GROUPTEMPLATE.Takeoff[GROUP.Takeoff.Air][1] -- type = Turning Point + SpawnPoint.action = GROUPTEMPLATE.Takeoff[GROUP.Takeoff.Air][2] -- action = Turning Point + + -- Adjust altitude to be 500-1000 m above the airbase. + PointVec3.x=PointVec3.x+math.random(-500,500) + PointVec3.z=PointVec3.z+math.random(-500,500) + if ishelo then + PointVec3.y=PointVec3:GetLandHeight()+math.random(100,1000) + else + -- Randomize position so that multiple AC wont be spawned on top even in air. + PointVec3.y=PointVec3:GetLandHeight()+math.random(500,2500) + end + + Takeoff=GROUP.Takeoff.Air + else + self:E(string.format("WARNING: Group %s has no parking spots at %s ==> No emergency air start or uncontrolled spawning ==> No spawn!", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) + return nil + end + end + + else + + -- Air start requested initially ==> Set altitude. + if TakeoffAltitude then + PointVec3.y=TakeoffAltitude + else + if ishelo then + PointVec3.y=PointVec3:GetLandHeight()+math.random(100,1000) + else + -- Randomize position so that multiple AC wont be spawned on top even in air. + PointVec3.y=PointVec3:GetLandHeight()+math.random(500,2500) + end + end + + end + + if not SpawnTemplate.parked then + -- Translate the position of the Group Template to the Vec3. + + SpawnTemplate.parked = true + + for UnitID = 1, nunits do + self:T2('Before Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) + + -- Template of the current unit. + local UnitTemplate = SpawnTemplate.units[UnitID] + + -- Tranlate position and preserve the relative position/formation of all aircraft. + local SX = UnitTemplate.x + local SY = UnitTemplate.y + local BX = SpawnTemplate.route.points[1].x + local BY = SpawnTemplate.route.points[1].y + local TX = PointVec3.x + (SX-BX) + local TY = PointVec3.z + (SY-BY) + + if spawnonground then + + -- Ships and FARPS seem to have a build in queue. + if spawnonship or spawnonfarp or spawnonrunway then + + self:T(string.format("Group %s spawning at farp, ship or runway %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) + + -- Spawn on ship. We take only the position of the ship. + SpawnTemplate.units[UnitID].x = PointVec3.x --TX + SpawnTemplate.units[UnitID].y = PointVec3.z --TY + SpawnTemplate.units[UnitID].alt = PointVec3.y + + else + + self:T(string.format("Group %s spawning at airbase %s on parking spot id %d", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), parkingindex[UnitID])) + + -- Get coordinates of parking spot. + SpawnTemplate.units[UnitID].x = parkingspots[UnitID].x + SpawnTemplate.units[UnitID].y = parkingspots[UnitID].z + SpawnTemplate.units[UnitID].alt = parkingspots[UnitID].y + + --parkingspots[UnitID]:MarkToAll(string.format("Group %s spawning at airbase %s on parking spot id %d", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), parkingindex[UnitID])) + end + + else + + self:T(string.format("Group %s spawning in air at %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) + + -- Spawn in air as requested initially. Original template orientation is perserved, altitude is already correctly set. + SpawnTemplate.units[UnitID].x = TX + SpawnTemplate.units[UnitID].y = TY + SpawnTemplate.units[UnitID].alt = PointVec3.y + + end + + -- Parking spot id. + UnitTemplate.parking = nil + UnitTemplate.parking_id = nil + if parkingindex[UnitID] then + UnitTemplate.parking = parkingindex[UnitID] + end + + -- Debug output. + self:T(string.format("Group %s unit number %d: Parking = %s",self.SpawnTemplatePrefix, UnitID, tostring(UnitTemplate.parking))) + self:T(string.format("Group %s unit number %d: Parking ID = %s",self.SpawnTemplatePrefix, UnitID, tostring(UnitTemplate.parking_id))) + self:T2('After Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) + end + end + + -- Set gereral spawnpoint position. + SpawnPoint.x = PointVec3.x + SpawnPoint.y = PointVec3.z + SpawnPoint.alt = PointVec3.y + + SpawnTemplate.x = PointVec3.x + SpawnTemplate.y = PointVec3.z + + SpawnTemplate.uncontrolled = self.SpawnUnControlled + + -- Spawn group. + local GroupSpawned = self:SpawnWithIndex( self.SpawnIndex ) + + -- When spawned in the air, we need to generate a Takeoff Event. + if Takeoff == GROUP.Takeoff.Air then + for UnitID, UnitSpawned in pairs( GroupSpawned:GetUnits() ) do + SCHEDULER:New( nil, BASE.CreateEventTakeoff, { GroupSpawned, timer.getTime(), UnitSpawned:GetDCSObject() } , 5 ) + end + end + + -- Check if we accidentally spawned on the runway. Needs to be schedules, because group is not immidiately alive. + if Takeoff~=SPAWN.Takeoff.Runway and Takeoff~=SPAWN.Takeoff.Air and spawnonairport then + SCHEDULER:New(nil, AIRBASE.CheckOnRunWay, {SpawnAirbase, GroupSpawned, 75, true} , 1.0) + end + + return GroupSpawned + end + end + + return nil +end + +--- Spawn a group on an @{Wrapper.Airbase} at a specific parking spot. +-- @param #SPAWN self +-- @param Wrapper.Airbase#AIRBASE Airbase The @{Wrapper.Airbase} where to spawn the group. +-- @param #table Spots Table of parking spot IDs. Note that these in general are different from the numbering in the mission editor! +-- @param #SPAWN.Takeoff Takeoff (Optional) Takeoff type, i.e. either SPAWN.Takeoff.Cold or SPAWN.Takeoff.Hot. Default is Hot. +-- @return Wrapper.Group#GROUP The group that was spawned or nil when nothing was spawned. +function SPAWN:SpawnAtParkingSpot(Airbase, Spots, Takeoff) -- R2.5 + self:F({Airbase=Airbase, Spots=Spots, Takeoff=Takeoff}) + + -- Ensure that Spots parameter is a table. + if type(Spots)~="table" then + Spots={Spots} + end + + -- Get template group. + local group=GROUP:FindByName(self.SpawnTemplatePrefix) + + -- Get number of units in group. + local nunits=self.SpawnGrouping or #group:GetUnits() + + -- Quick check. + if nunits then + + -- Check that number of provided parking spots is large enough. + if #Spots=nunits then + return self:SpawnAtAirbase(Airbase, Takeoff, nil, nil, nil, Parkingdata) + else + self:E("ERROR: Could not find enough free parking spots!") + end + + + else + self:E("ERROR: Could not get number of units in group!") + end + + return nil +end + +--- Will park a group at an @{Wrapper.Airbase}. +-- +-- @param #SPAWN self +-- @param Wrapper.Airbase#AIRBASE SpawnAirbase The @{Wrapper.Airbase} where to spawn the group. +-- @param Wrapper.Airbase#AIRBASE.TerminalType TerminalType (optional) The terminal type the aircraft should be spawned at. See @{Wrapper.Airbase#AIRBASE.TerminalType}. +-- @param #table Parkingdata (optional) Table holding the coordinates and terminal ids for all units of the group. Spawning will be forced to happen at exactily these spots! +-- @return #nil Nothing is returned! +function SPAWN:ParkAircraft( SpawnAirbase, TerminalType, Parkingdata, SpawnIndex ) + + self:F( { SpawnIndex = SpawnIndex, SpawnMaxGroups = self.SpawnMaxGroups } ) + + -- Get position of airbase. + local PointVec3 = SpawnAirbase:GetCoordinate() + self:T2(PointVec3) + + -- Set take off type. Default is hot. + local Takeoff = SPAWN.Takeoff.Cold + + -- Get group template. + local SpawnTemplate = self.SpawnGroups[SpawnIndex].SpawnTemplate + + if SpawnTemplate then + + -- Check if the aircraft with the specified SpawnIndex is already spawned. + -- If yes, ensure that the aircraft is spawned at the same aircraft spot. + + local GroupAlive = self:GetGroupFromIndex( SpawnIndex ) + + -- Debug output + self:T( { "Current point of ", self.SpawnTemplatePrefix, SpawnAirbase } ) + + -- Template group, unit and its attributes. + local TemplateGroup = GROUP:FindByName(self.SpawnTemplatePrefix) + local TemplateUnit=TemplateGroup:GetUnit(1) + local ishelo=TemplateUnit:HasAttribute("Helicopters") + local isbomber=TemplateUnit:HasAttribute("Bombers") + local istransport=TemplateUnit:HasAttribute("Transports") + local isfighter=TemplateUnit:HasAttribute("Battleplanes") + + -- Number of units in the group. With grouping this can actually differ from the template group size! + local nunits=#SpawnTemplate.units + + -- First waypoint of the group. + local SpawnPoint = SpawnTemplate.route.points[1] + + -- These are only for ships and FARPS. + SpawnPoint.linkUnit = nil + SpawnPoint.helipadId = nil + SpawnPoint.airdromeId = nil + + -- Get airbase ID and category. + local AirbaseID = SpawnAirbase:GetID() + local AirbaseCategory = SpawnAirbase:GetAirbaseCategory() + self:F( { AirbaseCategory = AirbaseCategory } ) + + -- Set airdromeId. + if AirbaseCategory == Airbase.Category.SHIP then + SpawnPoint.linkUnit = AirbaseID + SpawnPoint.helipadId = AirbaseID + elseif AirbaseCategory == Airbase.Category.HELIPAD then + SpawnPoint.linkUnit = AirbaseID + SpawnPoint.helipadId = AirbaseID + elseif AirbaseCategory == Airbase.Category.AIRDROME then + SpawnPoint.airdromeId = AirbaseID + end + + -- Set waypoint type/action. + SpawnPoint.alt = 0 + SpawnPoint.type = GROUPTEMPLATE.Takeoff[Takeoff][1] -- type + SpawnPoint.action = GROUPTEMPLATE.Takeoff[Takeoff][2] -- action + + -- Check if we spawn on ground. + local spawnonground=not (Takeoff==SPAWN.Takeoff.Air) + self:T({spawnonground=spawnonground, TOtype=Takeoff, TOair=Takeoff==SPAWN.Takeoff.Air}) + + -- Check where we actually spawn if we spawn on ground. + local spawnonship=false + local spawnonfarp=false + local spawnonrunway=false + local spawnonairport=false + if spawnonground then + if AirbaseCategory == Airbase.Category.SHIP then + spawnonship=true + elseif AirbaseCategory == Airbase.Category.HELIPAD then + spawnonfarp=true + elseif AirbaseCategory == Airbase.Category.AIRDROME then + spawnonairport=true + end + spawnonrunway=Takeoff==SPAWN.Takeoff.Runway + end + + -- Array with parking spots coordinates. + local parkingspots={} + local parkingindex={} + local spots + + -- Spawn happens on ground, i.e. at an airbase, a FARP or a ship. + if spawnonground and not SpawnTemplate.parked then + + + -- Number of free parking spots. + local nfree=0 + + -- Set terminal type. + local termtype=TerminalType + + -- Scan options. Might make that input somehow. + local scanradius=50 + local scanunits=true + local scanstatics=true + local scanscenery=false + local verysafe=false + + -- Number of free parking spots at the airbase. + if spawnonship or spawnonfarp or spawnonrunway then + -- These places work procedural and have some kind of build in queue ==> Less effort. + self:T(string.format("Group %s is spawned on farp/ship/runway %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) + nfree=SpawnAirbase:GetFreeParkingSpotsNumber(termtype, true) + spots=SpawnAirbase:GetFreeParkingSpotsTable(termtype, true) + --[[ + elseif Parkingdata~=nil then + -- Parking data explicitly set by user as input parameter. + nfree=#Parkingdata + spots=Parkingdata + ]] + else + if ishelo then + if termtype==nil then + -- Helo is spawned. Try exclusive helo spots first. + self:T(string.format("Helo group %s is at %s using terminal type %d.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), AIRBASE.TerminalType.HelicopterOnly)) + spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup, AIRBASE.TerminalType.HelicopterOnly, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits, Parkingdata) + nfree=#spots + if nfree=1 then + + -- All units get the same spot. DCS takes care of the rest. + for i=1,nunits do + table.insert(parkingspots, spots[1].Coordinate) + table.insert(parkingindex, spots[1].TerminalID) + end + -- This is actually used... + PointVec3=spots[1].Coordinate + + else + -- If there is absolutely no spot ==> air start! + _notenough=true + end + + elseif spawnonairport then + + if nfree>=nunits then + + for i=1,nunits do + table.insert(parkingspots, spots[i].Coordinate) + table.insert(parkingindex, spots[i].TerminalID) + end + + else + -- Not enough spots for the whole group ==> air start! + _notenough=true + end + end + + -- Not enough spots ==> Prepare airstart. + if _notenough then + + if not self.SpawnUnControlled then + else + self:E(string.format("WARNING: Group %s has no parking spots at %s ==> No emergency air start or uncontrolled spawning ==> No spawn!", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) + return nil + end + end + + else + + end + + if not SpawnTemplate.parked then + -- Translate the position of the Group Template to the Vec3. + + SpawnTemplate.parked = true + + for UnitID = 1, nunits do + self:F('Before Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) + + -- Template of the current unit. + local UnitTemplate = SpawnTemplate.units[UnitID] + + -- Tranlate position and preserve the relative position/formation of all aircraft. + local SX = UnitTemplate.x + local SY = UnitTemplate.y + local BX = SpawnTemplate.route.points[1].x + local BY = SpawnTemplate.route.points[1].y + local TX = PointVec3.x + (SX-BX) + local TY = PointVec3.z + (SY-BY) + + if spawnonground then + + -- Ships and FARPS seem to have a build in queue. + if spawnonship or spawnonfarp or spawnonrunway then + + self:T(string.format("Group %s spawning at farp, ship or runway %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) + + -- Spawn on ship. We take only the position of the ship. + SpawnTemplate.units[UnitID].x = PointVec3.x --TX + SpawnTemplate.units[UnitID].y = PointVec3.z --TY + SpawnTemplate.units[UnitID].alt = PointVec3.y + + else + + self:T(string.format("Group %s spawning at airbase %s on parking spot id %d", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), parkingindex[UnitID])) + + -- Get coordinates of parking spot. + SpawnTemplate.units[UnitID].x = parkingspots[UnitID].x + SpawnTemplate.units[UnitID].y = parkingspots[UnitID].z + SpawnTemplate.units[UnitID].alt = parkingspots[UnitID].y + + --parkingspots[UnitID]:MarkToAll(string.format("Group %s spawning at airbase %s on parking spot id %d", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), parkingindex[UnitID])) + end + + else + + self:T(string.format("Group %s spawning in air at %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) + + -- Spawn in air as requested initially. Original template orientation is perserved, altitude is already correctly set. + SpawnTemplate.units[UnitID].x = TX + SpawnTemplate.units[UnitID].y = TY + SpawnTemplate.units[UnitID].alt = PointVec3.y + + end + + -- Parking spot id. + UnitTemplate.parking = nil + UnitTemplate.parking_id = nil + if parkingindex[UnitID] then + UnitTemplate.parking = parkingindex[UnitID] + end + + -- Debug output. + self:T2(string.format("Group %s unit number %d: Parking = %s",self.SpawnTemplatePrefix, UnitID, tostring(UnitTemplate.parking))) + self:T2(string.format("Group %s unit number %d: Parking ID = %s",self.SpawnTemplatePrefix, UnitID, tostring(UnitTemplate.parking_id))) + self:T2('After Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) + end + end + + -- Set general spawnpoint position. + SpawnPoint.x = PointVec3.x + SpawnPoint.y = PointVec3.z + SpawnPoint.alt = PointVec3.y + + SpawnTemplate.x = PointVec3.x + SpawnTemplate.y = PointVec3.z + + SpawnTemplate.uncontrolled = true + + -- Spawn group. + local GroupSpawned = self:SpawnWithIndex( SpawnIndex, true ) + + -- When spawned in the air, we need to generate a Takeoff Event. + if Takeoff == GROUP.Takeoff.Air then + for UnitID, UnitSpawned in pairs( GroupSpawned:GetUnits() ) do + SCHEDULER:New( nil, BASE.CreateEventTakeoff, { GroupSpawned, timer.getTime(), UnitSpawned:GetDCSObject() } , 5 ) + end + end + + -- Check if we accidentally spawned on the runway. Needs to be schedules, because group is not immidiately alive. + if Takeoff~=SPAWN.Takeoff.Runway and Takeoff~=SPAWN.Takeoff.Air and spawnonairport then + SCHEDULER:New(nil, AIRBASE.CheckOnRunWay, {SpawnAirbase, GroupSpawned, 75, true} , 1.0) + end + + end + +end + +--- Will park a group at an @{Wrapper.Airbase}. +-- This method is mostly advisable to be used if you want to simulate parking units at an airbase and be visible. +-- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. +-- +-- All groups that are in the spawn collection and that are alive, and not in the air, are parked. +-- +-- The @{Wrapper.Airbase#AIRBASE} object must refer to a valid airbase known in the sim. +-- You can use the following enumerations to search for the pre-defined airbases on the current known maps of DCS: +-- +-- * @{Wrapper.Airbase#AIRBASE.Caucasus}: The airbases on the Caucasus map. +-- * @{Wrapper.Airbase#AIRBASE.Nevada}: The airbases on the Nevada (NTTR) map. +-- * @{Wrapper.Airbase#AIRBASE.Normandy}: The airbases on the Normandy map. +-- +-- Use the method @{Wrapper.Airbase#AIRBASE.FindByName}() to retrieve the airbase object. +-- The known AIRBASE objects are automatically imported at mission start by MOOSE. +-- Therefore, there isn't any New() constructor defined for AIRBASE objects. +-- +-- Ships and Farps are added within the mission, and are therefore not known. +-- For these AIRBASE objects, there isn't an @{Wrapper.Airbase#AIRBASE} enumeration defined. +-- You need to provide the **exact name** of the airbase as the parameter to the @{Wrapper.Airbase#AIRBASE.FindByName}() method! +-- +-- @param #SPAWN self +-- @param Wrapper.Airbase#AIRBASE SpawnAirbase The @{Wrapper.Airbase} where to spawn the group. +-- @param Wrapper.Airbase#AIRBASE.TerminalType TerminalType (optional) The terminal type the aircraft should be spawned at. See @{Wrapper.Airbase#AIRBASE.TerminalType}. +-- @param #table Parkingdata (optional) Table holding the coordinates and terminal ids for all units of the group. Spawning will be forced to happen at exactily these spots! +-- @return #nil Nothing is returned! +-- @usage +-- Spawn_Plane = SPAWN:New( "Plane" ) +-- Spawn_Plane:ParkAtAirbase( AIRBASE:FindByName( AIRBASE.Caucasus.Krymsk ) ) +-- +-- Spawn_Heli = SPAWN:New( "Heli") +-- +-- Spawn_Heli:ParkAtAirbase( AIRBASE:FindByName( "FARP Cold" ) ) +-- +-- Spawn_Heli:ParkAtAirbase( AIRBASE:FindByName( "Carrier" ) ) +-- +-- Spawn_Plane:ParkAtAirbase( AIRBASE:FindByName( AIRBASE.Caucasus.Krymsk ), AIRBASE.TerminalType.OpenBig ) +-- +function SPAWN:ParkAtAirbase( SpawnAirbase, TerminalType, Parkingdata ) -- R2.2, R2.4, R2.5 + self:F( { self.SpawnTemplatePrefix, SpawnAirbase, TerminalType } ) + + self:ParkAircraft( SpawnAirbase, TerminalType, Parkingdata, 1 ) + + for SpawnIndex = 2, self.SpawnMaxGroups do + self:ParkAircraft( SpawnAirbase, TerminalType, Parkingdata, SpawnIndex ) + --self:ScheduleOnce( SpawnIndex * 0.1, SPAWN.ParkAircraft, self, SpawnAirbase, TerminalType, Parkingdata, SpawnIndex ) + end + + self:SetSpawnIndex( 0 ) + + return nil +end + +--- Will spawn a group from a Vec3 in 3D space. +-- This method is mostly advisable to be used if you want to simulate spawning units in the air, like helicopters or airplanes. +-- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. +-- You can use the returned group to further define the route to be followed. +-- @param #SPAWN self +-- @param DCS#Vec3 Vec3 The Vec3 coordinates where to spawn the group. +-- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. +-- @return Wrapper.Group#GROUP that was spawned. +-- @return #nil Nothing was spawned. +function SPAWN:SpawnFromVec3( Vec3, SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, Vec3, SpawnIndex } ) + + local PointVec3 = POINT_VEC3:NewFromVec3( Vec3 ) + self:T2(PointVec3) + + if SpawnIndex then + else + SpawnIndex = self.SpawnIndex + 1 + end + + if self:_GetSpawnIndex( SpawnIndex ) then + + local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate + + if SpawnTemplate then + + self:T( { "Current point of ", self.SpawnTemplatePrefix, Vec3 } ) + + local TemplateHeight = SpawnTemplate.route and SpawnTemplate.route.points[1].alt or nil + + SpawnTemplate.route = SpawnTemplate.route or {} + SpawnTemplate.route.points = SpawnTemplate.route.points or {} + SpawnTemplate.route.points[1] = SpawnTemplate.route.points[1] or {} + SpawnTemplate.route.points[1].x = SpawnTemplate.route.points[1].x or 0 + SpawnTemplate.route.points[1].y = SpawnTemplate.route.points[1].y or 0 + + -- Translate the position of the Group Template to the Vec3. + for UnitID = 1, #SpawnTemplate.units do + --self:T( 'Before Translation SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) + local UnitTemplate = SpawnTemplate.units[UnitID] + local SX = UnitTemplate.x or 0 + local SY = UnitTemplate.y or 0 + local BX = SpawnTemplate.route.points[1].x + local BY = SpawnTemplate.route.points[1].y + local TX = Vec3.x + ( SX - BX ) + local TY = Vec3.z + ( SY - BY ) + SpawnTemplate.units[UnitID].x = TX + SpawnTemplate.units[UnitID].y = TY + if SpawnTemplate.CategoryID ~= Group.Category.SHIP then + SpawnTemplate.units[UnitID].alt = Vec3.y or TemplateHeight + end + self:T( 'After Translation SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) + end + SpawnTemplate.route.points[1].x = Vec3.x + SpawnTemplate.route.points[1].y = Vec3.z + if SpawnTemplate.CategoryID ~= Group.Category.SHIP then + SpawnTemplate.route.points[1].alt = Vec3.y or TemplateHeight + end + SpawnTemplate.x = Vec3.x + SpawnTemplate.y = Vec3.z + SpawnTemplate.alt = Vec3.y or TemplateHeight + + return self:SpawnWithIndex( self.SpawnIndex ) + end + end + + return nil +end + + +--- Will spawn a group from a Coordinate in 3D space. +-- This method is mostly advisable to be used if you want to simulate spawning units in the air, like helicopters or airplanes. +-- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. +-- You can use the returned group to further define the route to be followed. +-- @param #SPAWN self +-- @param Core.Point#Coordinate Coordinate The Coordinate coordinates where to spawn the group. +-- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. +-- @return Wrapper.Group#GROUP that was spawned. +-- @return #nil Nothing was spawned. +function SPAWN:SpawnFromCoordinate( Coordinate, SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, SpawnIndex } ) + + return self:SpawnFromVec3( Coordinate:GetVec3(), SpawnIndex ) +end + + + +--- Will spawn a group from a PointVec3 in 3D space. +-- This method is mostly advisable to be used if you want to simulate spawning units in the air, like helicopters or airplanes. +-- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. +-- You can use the returned group to further define the route to be followed. +-- @param #SPAWN self +-- @param Core.Point#POINT_VEC3 PointVec3 The PointVec3 coordinates where to spawn the group. +-- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. +-- @return Wrapper.Group#GROUP that was spawned. +-- @return #nil Nothing was spawned. +-- @usage +-- +-- local SpawnPointVec3 = ZONE:New( ZoneName ):GetPointVec3( 2000 ) -- Get the center of the ZONE object at 2000 meters from the ground. +-- +-- -- Spawn at the zone center position at 2000 meters from the ground! +-- SpawnAirplanes:SpawnFromPointVec3( SpawnPointVec3 ) +-- +function SPAWN:SpawnFromPointVec3( PointVec3, SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, SpawnIndex } ) + + return self:SpawnFromVec3( PointVec3:GetVec3(), SpawnIndex ) +end + + +--- Will spawn a group from a Vec2 in 3D space. +-- This method is mostly advisable to be used if you want to simulate spawning groups on the ground from air units, like vehicles. +-- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. +-- You can use the returned group to further define the route to be followed. +-- @param #SPAWN self +-- @param DCS#Vec2 Vec2 The Vec2 coordinates where to spawn the group. +-- @param #number MinHeight (optional) The minimum height to spawn an airborne group into the zone. +-- @param #number MaxHeight (optional) The maximum height to spawn an airborne group into the zone. +-- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. +-- @return Wrapper.Group#GROUP that was spawned. +-- @return #nil Nothing was spawned. +-- @usage +-- +-- local SpawnVec2 = ZONE:New( ZoneName ):GetVec2() +-- +-- -- Spawn at the zone center position at the height specified in the ME of the group template! +-- SpawnAirplanes:SpawnFromVec2( SpawnVec2 ) +-- +-- -- Spawn from the static position at the height randomized between 2000 and 4000 meters. +-- SpawnAirplanes:SpawnFromVec2( SpawnVec2, 2000, 4000 ) +-- +function SPAWN:SpawnFromVec2( Vec2, MinHeight, MaxHeight, SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, self.SpawnIndex, Vec2, MinHeight, MaxHeight, SpawnIndex } ) + + local Height = nil + + if MinHeight and MaxHeight then + Height = math.random( MinHeight, MaxHeight) + end + + return self:SpawnFromVec3( { x = Vec2.x, y = Height, z = Vec2.y }, SpawnIndex ) -- y can be nil. In this case, spawn on the ground for vehicles, and in the template altitude for air. +end + + +--- Will spawn a group from a POINT_VEC2 in 3D space. +-- This method is mostly advisable to be used if you want to simulate spawning groups on the ground from air units, like vehicles. +-- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. +-- You can use the returned group to further define the route to be followed. +-- @param #SPAWN self +-- @param Core.Point#POINT_VEC2 PointVec2 The PointVec2 coordinates where to spawn the group. +-- @param #number MinHeight (optional) The minimum height to spawn an airborne group into the zone. +-- @param #number MaxHeight (optional) The maximum height to spawn an airborne group into the zone. +-- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. +-- @return Wrapper.Group#GROUP that was spawned. +-- @return #nil Nothing was spawned. +-- @usage +-- +-- local SpawnPointVec2 = ZONE:New( ZoneName ):GetPointVec2() +-- +-- -- Spawn at the zone center position at the height specified in the ME of the group template! +-- SpawnAirplanes:SpawnFromPointVec2( SpawnPointVec2 ) +-- +-- -- Spawn from the static position at the height randomized between 2000 and 4000 meters. +-- SpawnAirplanes:SpawnFromPointVec2( SpawnPointVec2, 2000, 4000 ) +-- +function SPAWN:SpawnFromPointVec2( PointVec2, MinHeight, MaxHeight, SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, self.SpawnIndex } ) + + return self:SpawnFromVec2( PointVec2:GetVec2(), MinHeight, MaxHeight, SpawnIndex ) +end + + + +--- Will spawn a group from a hosting unit. This method is mostly advisable to be used if you want to simulate spawning from air units, like helicopters, which are dropping infantry into a defined Landing Zone. +-- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. +-- You can use the returned group to further define the route to be followed. +-- @param #SPAWN self +-- @param Wrapper.Unit#UNIT HostUnit The air or ground unit dropping or unloading the group. +-- @param #number MinHeight (optional) The minimum height to spawn an airborne group into the zone. +-- @param #number MaxHeight (optional) The maximum height to spawn an airborne group into the zone. +-- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. +-- @return Wrapper.Group#GROUP that was spawned. +-- @return #nil Nothing was spawned. +-- @usage +-- +-- local SpawnStatic = STATIC:FindByName( StaticName ) +-- +-- -- Spawn from the static position at the height specified in the ME of the group template! +-- SpawnAirplanes:SpawnFromUnit( SpawnStatic ) +-- +-- -- Spawn from the static position at the height randomized between 2000 and 4000 meters. +-- SpawnAirplanes:SpawnFromUnit( SpawnStatic, 2000, 4000 ) +-- +function SPAWN:SpawnFromUnit( HostUnit, MinHeight, MaxHeight, SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, HostUnit, MinHeight, MaxHeight, SpawnIndex } ) + + if HostUnit and HostUnit:IsAlive() ~= nil then -- and HostUnit:getUnit(1):inAir() == false then + return self:SpawnFromVec2( HostUnit:GetVec2(), MinHeight, MaxHeight, SpawnIndex ) + end + + return nil +end + +--- Will spawn a group from a hosting static. This method is mostly advisable to be used if you want to simulate spawning from buldings and structures (static buildings). +-- You can use the returned group to further define the route to be followed. +-- @param #SPAWN self +-- @param Wrapper.Static#STATIC HostStatic The static dropping or unloading the group. +-- @param #number MinHeight (optional) The minimum height to spawn an airborne group into the zone. +-- @param #number MaxHeight (optional) The maximum height to spawn an airborne group into the zone. +-- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. +-- @return Wrapper.Group#GROUP that was spawned. +-- @return #nil Nothing was spawned. +-- @usage +-- +-- local SpawnStatic = STATIC:FindByName( StaticName ) +-- +-- -- Spawn from the static position at the height specified in the ME of the group template! +-- SpawnAirplanes:SpawnFromStatic( SpawnStatic ) +-- +-- -- Spawn from the static position at the height randomized between 2000 and 4000 meters. +-- SpawnAirplanes:SpawnFromStatic( SpawnStatic, 2000, 4000 ) +-- +function SPAWN:SpawnFromStatic( HostStatic, MinHeight, MaxHeight, SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, HostStatic, MinHeight, MaxHeight, SpawnIndex } ) + + if HostStatic and HostStatic:IsAlive() then + return self:SpawnFromVec2( HostStatic:GetVec2(), MinHeight, MaxHeight, SpawnIndex ) + end + + return nil +end + +--- Will spawn a Group within a given @{Zone}. +-- The @{Zone} can be of any type derived from @{Core.Zone#ZONE_BASE}. +-- Once the @{Wrapper.Group} is spawned within the zone, the @{Wrapper.Group} will continue on its route. +-- The **first waypoint** (where the group is spawned) is replaced with the zone location coordinates. +-- @param #SPAWN self +-- @param Core.Zone#ZONE Zone The zone where the group is to be spawned. +-- @param #boolean RandomizeGroup (optional) Randomization of the @{Wrapper.Group} position in the zone. +-- @param #number MinHeight (optional) The minimum height to spawn an airborne group into the zone. +-- @param #number MaxHeight (optional) The maximum height to spawn an airborne group into the zone. +-- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. +-- @return Wrapper.Group#GROUP that was spawned. +-- @return #nil when nothing was spawned. +-- @usage +-- +-- local SpawnZone = ZONE:New( ZoneName ) +-- +-- -- Spawn at the zone center position at the height specified in the ME of the group template! +-- SpawnAirplanes:SpawnInZone( SpawnZone ) +-- +-- -- Spawn in the zone at a random position at the height specified in the Me of the group template. +-- SpawnAirplanes:SpawnInZone( SpawnZone, true ) +-- +-- -- Spawn in the zone at a random position at the height randomized between 2000 and 4000 meters. +-- SpawnAirplanes:SpawnInZone( SpawnZone, true, 2000, 4000 ) +-- +-- -- Spawn at the zone center position at the height randomized between 2000 and 4000 meters. +-- SpawnAirplanes:SpawnInZone( SpawnZone, false, 2000, 4000 ) +-- +-- -- Spawn at the zone center position at the height randomized between 2000 and 4000 meters. +-- SpawnAirplanes:SpawnInZone( SpawnZone, nil, 2000, 4000 ) +-- +function SPAWN:SpawnInZone( Zone, RandomizeGroup, MinHeight, MaxHeight, SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, Zone, RandomizeGroup, MinHeight, MaxHeight, SpawnIndex } ) + + if Zone then + if RandomizeGroup then + return self:SpawnFromVec2( Zone:GetRandomVec2(), MinHeight, MaxHeight, SpawnIndex ) + else + return self:SpawnFromVec2( Zone:GetVec2(), MinHeight, MaxHeight, SpawnIndex ) + end + end + + return nil +end + +--- (**AIR**) Will spawn a plane group in UnControlled or Controlled mode... +-- This will be similar to the uncontrolled flag setting in the ME. +-- You can use UnControlled mode to simulate planes startup and ready for take-off but aren't moving (yet). +-- ReSpawn the plane in Controlled mode, and the plane will move... +-- @param #SPAWN self +-- @param #boolean UnControlled true if UnControlled, false if Controlled. +-- @return #SPAWN self +function SPAWN:InitUnControlled( UnControlled ) + self:F2( { self.SpawnTemplatePrefix, UnControlled } ) + + self.SpawnUnControlled = ( UnControlled == true ) and true or nil + + for SpawnGroupID = 1, self.SpawnMaxGroups do + self.SpawnGroups[SpawnGroupID].UnControlled = self.SpawnUnControlled + end + + return self +end + + +--- Get the Coordinate of the Group that is Late Activated as the template for the SPAWN object. +-- @param #SPAWN self +-- @return Core.Point#COORDINATE The Coordinate +function SPAWN:GetCoordinate() + + local LateGroup = GROUP:FindByName( self.SpawnTemplatePrefix ) + if LateGroup then + return LateGroup:GetCoordinate() + end + + return nil +end + + +--- Will return the SpawnGroupName either with with a specific count number or without any count. +-- @param #SPAWN self +-- @param #number SpawnIndex Is the number of the Group that is to be spawned. +-- @return #string SpawnGroupName +function SPAWN:SpawnGroupName( SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, SpawnIndex } ) + + local SpawnPrefix = self.SpawnTemplatePrefix + if self.SpawnAliasPrefix then + SpawnPrefix = self.SpawnAliasPrefix + end + + if SpawnIndex then + local SpawnName = string.format( '%s#%03d', SpawnPrefix, SpawnIndex ) + self:T( SpawnName ) + return SpawnName + else + self:T( SpawnPrefix ) + return SpawnPrefix + end + +end + +--- Will find the first alive @{Wrapper.Group} it has spawned, and return the alive @{Wrapper.Group} object and the first Index where the first alive @{Wrapper.Group} object has been found. +-- @param #SPAWN self +-- @return Wrapper.Group#GROUP, #number The @{Wrapper.Group} object found, the new Index where the group was found. +-- @return #nil, #nil When no group is found, #nil is returned. +-- @usage +-- -- Find the first alive @{Wrapper.Group} object of the SpawnPlanes SPAWN object @{Wrapper.Group} collection that it has spawned during the mission. +-- local GroupPlane, Index = SpawnPlanes:GetFirstAliveGroup() +-- while GroupPlane ~= nil do +-- -- Do actions with the GroupPlane object. +-- GroupPlane, Index = SpawnPlanes:GetNextAliveGroup( Index ) +-- end +function SPAWN:GetFirstAliveGroup() + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } ) + + for SpawnIndex = 1, self.SpawnCount do + local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) + if SpawnGroup and SpawnGroup:IsAlive() then + return SpawnGroup, SpawnIndex + end + end + + return nil, nil +end + + +--- Will find the next alive @{Wrapper.Group} object from a given Index, and return a reference to the alive @{Wrapper.Group} object and the next Index where the alive @{Wrapper.Group} has been found. +-- @param #SPAWN self +-- @param #number SpawnIndexStart A Index holding the start position to search from. This method can also be used to find the first alive @{Wrapper.Group} object from the given Index. +-- @return Wrapper.Group#GROUP, #number The next alive @{Wrapper.Group} object found, the next Index where the next alive @{Wrapper.Group} object was found. +-- @return #nil, #nil When no alive @{Wrapper.Group} object is found from the start Index position, #nil is returned. +-- @usage +-- -- Find the first alive @{Wrapper.Group} object of the SpawnPlanes SPAWN object @{Wrapper.Group} collection that it has spawned during the mission. +-- local GroupPlane, Index = SpawnPlanes:GetFirstAliveGroup() +-- while GroupPlane ~= nil do +-- -- Do actions with the GroupPlane object. +-- GroupPlane, Index = SpawnPlanes:GetNextAliveGroup( Index ) +-- end +function SPAWN:GetNextAliveGroup( SpawnIndexStart ) + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnIndexStart } ) + + SpawnIndexStart = SpawnIndexStart + 1 + for SpawnIndex = SpawnIndexStart, self.SpawnCount do + local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) + if SpawnGroup and SpawnGroup:IsAlive() then + return SpawnGroup, SpawnIndex + end + end + + return nil, nil +end + +--- Will find the last alive @{Wrapper.Group} object, and will return a reference to the last live @{Wrapper.Group} object and the last Index where the last alive @{Wrapper.Group} object has been found. +-- @param #SPAWN self +-- @return Wrapper.Group#GROUP, #number The last alive @{Wrapper.Group} object found, the last Index where the last alive @{Wrapper.Group} object was found. +-- @return #nil, #nil When no alive @{Wrapper.Group} object is found, #nil is returned. +-- @usage +-- -- Find the last alive @{Wrapper.Group} object of the SpawnPlanes SPAWN object @{Wrapper.Group} collection that it has spawned during the mission. +-- local GroupPlane, Index = SpawnPlanes:GetLastAliveGroup() +-- if GroupPlane then -- GroupPlane can be nil!!! +-- -- Do actions with the GroupPlane object. +-- end +function SPAWN:GetLastAliveGroup() + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } ) + + for SpawnIndex = self.SpawnCount, 1, -1 do -- Added + local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) + if SpawnGroup and SpawnGroup:IsAlive() then + self.SpawnIndex = SpawnIndex + return SpawnGroup + end + end + + self.SpawnIndex = nil + return nil +end + + + +--- Get the group from an index. +-- Returns the group from the SpawnGroups list. +-- If no index is given, it will return the first group in the list. +-- @param #SPAWN self +-- @param #number SpawnIndex The index of the group to return. +-- @return Wrapper.Group#GROUP self +function SPAWN:GetGroupFromIndex( SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnIndex } ) + + if not SpawnIndex then + SpawnIndex = 1 + end + + if self.SpawnGroups and self.SpawnGroups[SpawnIndex] then + local SpawnGroup = self.SpawnGroups[SpawnIndex].Group + return SpawnGroup + else + return nil + end +end + + +--- Return the prefix of a SpawnUnit. +-- The method will search for a #-mark, and will return the text before the #-mark. +-- It will return nil of no prefix was found. +-- @param #SPAWN self +-- @param DCS#UNIT DCSUnit The @{DCSUnit} to be searched. +-- @return #string The prefix +-- @return #nil Nothing found +function SPAWN:_GetPrefixFromGroup( SpawnGroup ) + self:F3( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnGroup } ) + + local GroupName = SpawnGroup:GetName() + if GroupName then + local SpawnPrefix = string.match( GroupName, ".*#" ) + if SpawnPrefix then + SpawnPrefix = SpawnPrefix:sub( 1, -2 ) + end + return SpawnPrefix + end + + return nil +end + + +--- Get the index from a given group. +-- The function will search the name of the group for a #, and will return the number behind the #-mark. +function SPAWN:GetSpawnIndexFromGroup( SpawnGroup ) + self:F2( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnGroup } ) + + local IndexString = string.match( SpawnGroup:GetName(), "#(%d*)$" ):sub( 2 ) + local Index = tonumber( IndexString ) + + self:T3( IndexString, Index ) + return Index + +end + +--- Return the last maximum index that can be used. +function SPAWN:_GetLastIndex() + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } ) + + return self.SpawnMaxGroups +end + +--- Initalize the SpawnGroups collection. +-- @param #SPAWN self +function SPAWN:_InitializeSpawnGroups( SpawnIndex ) + self:F3( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnIndex } ) + + if not self.SpawnGroups[SpawnIndex] then + self.SpawnGroups[SpawnIndex] = {} + self.SpawnGroups[SpawnIndex].Visible = false + self.SpawnGroups[SpawnIndex].Spawned = false + self.SpawnGroups[SpawnIndex].UnControlled = false + self.SpawnGroups[SpawnIndex].SpawnTime = 0 + + self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix = self.SpawnTemplatePrefix + self.SpawnGroups[SpawnIndex].SpawnTemplate = self:_Prepare( self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix, SpawnIndex ) + end + + self:_RandomizeTemplate( SpawnIndex ) + self:_RandomizeRoute( SpawnIndex ) + --self:_TranslateRotate( SpawnIndex ) + + return self.SpawnGroups[SpawnIndex] +end + + + +--- Gets the CategoryID of the Group with the given SpawnPrefix +function SPAWN:_GetGroupCategoryID( SpawnPrefix ) + local TemplateGroup = Group.getByName( SpawnPrefix ) + + if TemplateGroup then + return TemplateGroup:getCategory() + else + return nil + end +end + +--- Gets the CoalitionID of the Group with the given SpawnPrefix +function SPAWN:_GetGroupCoalitionID( SpawnPrefix ) + local TemplateGroup = Group.getByName( SpawnPrefix ) + + if TemplateGroup then + return TemplateGroup:getCoalition() + else + return nil + end +end + +--- Gets the CountryID of the Group with the given SpawnPrefix +function SPAWN:_GetGroupCountryID( SpawnPrefix ) + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnPrefix } ) + + local TemplateGroup = Group.getByName( SpawnPrefix ) + + if TemplateGroup then + local TemplateUnits = TemplateGroup:getUnits() + return TemplateUnits[1]:getCountry() + else + return nil + end +end + +--- Gets the Group Template from the ME environment definition. +-- This method used the @{DATABASE} object, which contains ALL initial and new spawned object in MOOSE. +-- @param #SPAWN self +-- @param #string SpawnTemplatePrefix +-- @return @SPAWN self +function SPAWN:_GetTemplate( SpawnTemplatePrefix ) + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnTemplatePrefix } ) + + local SpawnTemplate = nil + + local Template = _DATABASE.Templates.Groups[SpawnTemplatePrefix].Template + self:F( { Template = Template } ) + + SpawnTemplate = UTILS.DeepCopy( _DATABASE.Templates.Groups[SpawnTemplatePrefix].Template ) + + if SpawnTemplate == nil then + error( 'No Template returned for SpawnTemplatePrefix = ' .. SpawnTemplatePrefix ) + end + + --SpawnTemplate.SpawnCoalitionID = self:_GetGroupCoalitionID( SpawnTemplatePrefix ) + --SpawnTemplate.SpawnCategoryID = self:_GetGroupCategoryID( SpawnTemplatePrefix ) + --SpawnTemplate.SpawnCountryID = self:_GetGroupCountryID( SpawnTemplatePrefix ) + + self:T3( { SpawnTemplate } ) + return SpawnTemplate +end + +--- Prepares the new Group Template. +-- @param #SPAWN self +-- @param #string SpawnTemplatePrefix +-- @param #number SpawnIndex +-- @return #SPAWN self +function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) --R2.2 + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } ) + +-- if not self.SpawnTemplate then +-- self.SpawnTemplate = self:_GetTemplate( SpawnTemplatePrefix ) +-- end + + local SpawnTemplate + if self.TweakedTemplate ~= nil and self.TweakedTemplate == true then + BASE:I("WARNING: You are using a tweaked template.") + SpawnTemplate = self.SpawnTemplate + else + SpawnTemplate = self:_GetTemplate( SpawnTemplatePrefix ) + SpawnTemplate.name = self:SpawnGroupName( SpawnIndex ) + end + + + SpawnTemplate.groupId = nil + --SpawnTemplate.lateActivation = false + SpawnTemplate.lateActivation = self.LateActivated or false + + if SpawnTemplate.CategoryID == Group.Category.GROUND then + self:T3( "For ground units, visible needs to be false..." ) + SpawnTemplate.visible = false + end + + if self.SpawnGrouping then + local UnitAmount = #SpawnTemplate.units + self:F( { UnitAmount = UnitAmount, SpawnGrouping = self.SpawnGrouping } ) + if UnitAmount > self.SpawnGrouping then + for UnitID = self.SpawnGrouping + 1, UnitAmount do + SpawnTemplate.units[UnitID] = nil + end + else + if UnitAmount < self.SpawnGrouping then + for UnitID = UnitAmount + 1, self.SpawnGrouping do + SpawnTemplate.units[UnitID] = UTILS.DeepCopy( SpawnTemplate.units[1] ) + SpawnTemplate.units[UnitID].unitId = nil + end + end + end + end + + if self.SpawnInitKeepUnitNames == false then + for UnitID = 1, #SpawnTemplate.units do + SpawnTemplate.units[UnitID].name = string.format( SpawnTemplate.name .. '-%02d', UnitID ) + SpawnTemplate.units[UnitID].unitId = nil + end + else + for UnitID = 1, #SpawnTemplate.units do + local UnitPrefix, Rest = string.match( SpawnTemplate.units[UnitID].name, "^([^#]+)#?" ):gsub( "^%s*(.-)%s*$", "%1" ) + self:T( { UnitPrefix, Rest } ) + + SpawnTemplate.units[UnitID].name = string.format( '%s#%03d-%02d', UnitPrefix, SpawnIndex, UnitID ) + SpawnTemplate.units[UnitID].unitId = nil + end + end + + -- Callsign + for UnitID = 1, #SpawnTemplate.units do + local Callsign = SpawnTemplate.units[UnitID].callsign + if Callsign then + if type(Callsign) ~= "number" then -- blue callsign + Callsign[2] = ( ( SpawnIndex - 1 ) % 10 ) + 1 + local CallsignName = SpawnTemplate.units[UnitID].callsign["name"] -- #string + local CallsignLen = CallsignName:len() + SpawnTemplate.units[UnitID].callsign["name"] = CallsignName:sub(1,CallsignLen) .. SpawnTemplate.units[UnitID].callsign[2] .. SpawnTemplate.units[UnitID].callsign[3] + else + SpawnTemplate.units[UnitID].callsign = Callsign + SpawnIndex + end + end + end + + self:T3( { "Template:", SpawnTemplate } ) + return SpawnTemplate + +end + +--- Private method randomizing the routes. +-- @param #SPAWN self +-- @param #number SpawnIndex The index of the group to be spawned. +-- @return #SPAWN +function SPAWN:_RandomizeRoute( SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeRoute, self.SpawnRandomizeRouteStartPoint, self.SpawnRandomizeRouteEndPoint, self.SpawnRandomizeRouteRadius } ) + + if self.SpawnRandomizeRoute then + local SpawnTemplate = self.SpawnGroups[SpawnIndex].SpawnTemplate + local RouteCount = #SpawnTemplate.route.points + + for t = self.SpawnRandomizeRouteStartPoint + 1, ( RouteCount - self.SpawnRandomizeRouteEndPoint ) do + + SpawnTemplate.route.points[t].x = SpawnTemplate.route.points[t].x + math.random( self.SpawnRandomizeRouteRadius * -1, self.SpawnRandomizeRouteRadius ) + SpawnTemplate.route.points[t].y = SpawnTemplate.route.points[t].y + math.random( self.SpawnRandomizeRouteRadius * -1, self.SpawnRandomizeRouteRadius ) + + -- Manage randomization of altitude for airborne units ... + if SpawnTemplate.CategoryID == Group.Category.AIRPLANE or SpawnTemplate.CategoryID == Group.Category.HELICOPTER then + if SpawnTemplate.route.points[t].alt and self.SpawnRandomizeRouteHeight then + SpawnTemplate.route.points[t].alt = SpawnTemplate.route.points[t].alt + math.random( 1, self.SpawnRandomizeRouteHeight ) + end + else + SpawnTemplate.route.points[t].alt = nil + end + + self:T( 'SpawnTemplate.route.points[' .. t .. '].x = ' .. SpawnTemplate.route.points[t].x .. ', SpawnTemplate.route.points[' .. t .. '].y = ' .. SpawnTemplate.route.points[t].y ) + end + end + + self:_RandomizeZones( SpawnIndex ) + + return self +end + +--- Private method that randomizes the template of the group. +-- @param #SPAWN self +-- @param #number SpawnIndex +-- @return #SPAWN self +function SPAWN:_RandomizeTemplate( SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeTemplate } ) + + if self.SpawnRandomizeTemplate then + self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix = self.SpawnTemplatePrefixTable[ math.random( 1, #self.SpawnTemplatePrefixTable ) ] + self.SpawnGroups[SpawnIndex].SpawnTemplate = self:_Prepare( self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix, SpawnIndex ) + self.SpawnGroups[SpawnIndex].SpawnTemplate.route = UTILS.DeepCopy( self.SpawnTemplate.route ) + self.SpawnGroups[SpawnIndex].SpawnTemplate.x = self.SpawnTemplate.x + self.SpawnGroups[SpawnIndex].SpawnTemplate.y = self.SpawnTemplate.y + self.SpawnGroups[SpawnIndex].SpawnTemplate.start_time = self.SpawnTemplate.start_time + local OldX = self.SpawnGroups[SpawnIndex].SpawnTemplate.units[1].x + local OldY = self.SpawnGroups[SpawnIndex].SpawnTemplate.units[1].y + for UnitID = 1, #self.SpawnGroups[SpawnIndex].SpawnTemplate.units do + self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].heading = self.SpawnTemplate.units[1].heading + self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].x = self.SpawnTemplate.units[1].x + ( self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].x - OldX ) + self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].y = self.SpawnTemplate.units[1].y + ( self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].y - OldY ) + self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].alt = self.SpawnTemplate.units[1].alt + end + end + + self:_RandomizeRoute( SpawnIndex ) + + return self +end + +--- Private method that randomizes the @{Zone}s where the Group will be spawned. +-- @param #SPAWN self +-- @param #number SpawnIndex +-- @return #SPAWN self +function SPAWN:_RandomizeZones( SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeZones } ) + + if self.SpawnRandomizeZones then + local SpawnZone = nil -- Core.Zone#ZONE_BASE + while not SpawnZone do + self:T( { SpawnZoneTableCount = #self.SpawnZoneTable, self.SpawnZoneTable } ) + local ZoneID = math.random( #self.SpawnZoneTable ) + self:T( ZoneID ) + SpawnZone = self.SpawnZoneTable[ ZoneID ]:GetZoneMaybe() + end + + self:T( "Preparing Spawn in Zone", SpawnZone:GetName() ) + + local SpawnVec2 = SpawnZone:GetRandomVec2() + + self:T( { SpawnVec2 = SpawnVec2 } ) + + local SpawnTemplate = self.SpawnGroups[SpawnIndex].SpawnTemplate + + self:T( { Route = SpawnTemplate.route } ) + + for UnitID = 1, #SpawnTemplate.units do + local UnitTemplate = SpawnTemplate.units[UnitID] + self:T( 'Before Translation SpawnTemplate.units['..UnitID..'].x = ' .. UnitTemplate.x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. UnitTemplate.y ) + local SX = UnitTemplate.x + local SY = UnitTemplate.y + local BX = SpawnTemplate.route.points[1].x + local BY = SpawnTemplate.route.points[1].y + local TX = SpawnVec2.x + ( SX - BX ) + local TY = SpawnVec2.y + ( SY - BY ) + UnitTemplate.x = TX + UnitTemplate.y = TY + -- TODO: Manage altitude based on landheight... + --SpawnTemplate.units[UnitID].alt = SpawnVec2: + self:T( 'After Translation SpawnTemplate.units['..UnitID..'].x = ' .. UnitTemplate.x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. UnitTemplate.y ) + end + SpawnTemplate.x = SpawnVec2.x + SpawnTemplate.y = SpawnVec2.y + SpawnTemplate.route.points[1].x = SpawnVec2.x + SpawnTemplate.route.points[1].y = SpawnVec2.y + end + + return self + +end + +function SPAWN:_TranslateRotate( SpawnIndex, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle ) + self:F( { self.SpawnTemplatePrefix, SpawnIndex, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle } ) + + -- Translate + local TranslatedX = SpawnX + local TranslatedY = SpawnY + + -- Rotate + -- From Wikipedia: https://en.wikipedia.org/wiki/Rotation_matrix#Common_rotations + -- x' = x \cos \theta - y \sin \theta\ + -- y' = x \sin \theta + y \cos \theta\ + local RotatedX = - TranslatedX * math.cos( math.rad( SpawnAngle ) ) + + TranslatedY * math.sin( math.rad( SpawnAngle ) ) + local RotatedY = TranslatedX * math.sin( math.rad( SpawnAngle ) ) + + TranslatedY * math.cos( math.rad( SpawnAngle ) ) + + -- Assign + self.SpawnGroups[SpawnIndex].SpawnTemplate.x = SpawnRootX - RotatedX + self.SpawnGroups[SpawnIndex].SpawnTemplate.y = SpawnRootY + RotatedY + + + local SpawnUnitCount = table.getn( self.SpawnGroups[SpawnIndex].SpawnTemplate.units ) + for u = 1, SpawnUnitCount do + + -- Translate + local TranslatedX = SpawnX + local TranslatedY = SpawnY - 10 * ( u - 1 ) + + -- Rotate + local RotatedX = - TranslatedX * math.cos( math.rad( SpawnAngle ) ) + + TranslatedY * math.sin( math.rad( SpawnAngle ) ) + local RotatedY = TranslatedX * math.sin( math.rad( SpawnAngle ) ) + + TranslatedY * math.cos( math.rad( SpawnAngle ) ) + + -- Assign + self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].x = SpawnRootX - RotatedX + self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].y = SpawnRootY + RotatedY + self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].heading = self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].heading + math.rad( SpawnAngle ) + end + + return self +end + +--- Get the next index of the groups to be spawned. This method is complicated, as it is used at several spaces. +-- @param #SPAWN self +-- @param #number SpawnIndex Spawn index. +-- @return #number self.SpawnIndex +function SPAWN:_GetSpawnIndex( SpawnIndex ) + self:F2( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnMaxGroups, self.SpawnMaxUnitsAlive, self.AliveUnits, #self.SpawnTemplate.units } ) + + if ( self.SpawnMaxGroups == 0 ) or ( SpawnIndex <= self.SpawnMaxGroups ) then + if ( self.SpawnMaxUnitsAlive == 0 ) or ( self.AliveUnits + #self.SpawnTemplate.units <= self.SpawnMaxUnitsAlive ) or self.UnControlled == true then + self:F( { SpawnCount = self.SpawnCount, SpawnIndex = SpawnIndex } ) + if SpawnIndex and SpawnIndex >= self.SpawnCount + 1 then + self.SpawnCount = self.SpawnCount + 1 + SpawnIndex = self.SpawnCount + end + self.SpawnIndex = SpawnIndex + if not self.SpawnGroups[self.SpawnIndex] then + self:_InitializeSpawnGroups( self.SpawnIndex ) + end + else + return nil + end + else + return nil + end + + return self.SpawnIndex +end + + +-- TODO Need to delete this... _DATABASE does this now ... + +--- @param #SPAWN self +-- @param Core.Event#EVENTDATA EventData +function SPAWN:_OnBirth( EventData ) + self:F( self.SpawnTemplatePrefix ) + + local SpawnGroup = EventData.IniGroup + + if SpawnGroup then + local EventPrefix = self:_GetPrefixFromGroup( SpawnGroup ) + if EventPrefix then -- EventPrefix can be nil if no # is found, which means, no spawnable group! + self:T( { "Birth Event:", EventPrefix, self.SpawnTemplatePrefix } ) + if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then + self.AliveUnits = self.AliveUnits + 1 + self:T( "Alive Units: " .. self.AliveUnits ) + end + end + end + +end + +--- Obscolete +-- @todo Need to delete this... _DATABASE does this now ... + +--- @param #SPAWN self +-- @param Core.Event#EVENTDATA EventData +function SPAWN:_OnDeadOrCrash( EventData ) + self:F( self.SpawnTemplatePrefix ) + + local SpawnGroup = EventData.IniGroup + + if SpawnGroup then + local EventPrefix = self:_GetPrefixFromGroup( SpawnGroup ) + if EventPrefix then -- EventPrefix can be nil if no # is found, which means, no spawnable group! + self:T( { "Dead event: " .. EventPrefix } ) + if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then + self.AliveUnits = self.AliveUnits - 1 + self:T( "Alive Units: " .. self.AliveUnits ) + end + end + end +end + +--- Will detect AIR Units taking off... When the event takes place, the spawned Group is registered as airborne... +-- This is needed to ensure that Re-SPAWNing only is done for landed AIR Groups. +-- @param #SPAWN self +-- @param Core.Event#EVENTDATA EventData +function SPAWN:_OnTakeOff( EventData ) + self:F( self.SpawnTemplatePrefix ) + + local SpawnGroup = EventData.IniGroup + if SpawnGroup then + local EventPrefix = self:_GetPrefixFromGroup( SpawnGroup ) + if EventPrefix then -- EventPrefix can be nil if no # is found, which means, no spawnable group! + self:T( { "TakeOff event: " .. EventPrefix } ) + if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then + self:T( "self.Landed = false" ) + SpawnGroup:SetState( SpawnGroup, "Spawn_Landed", false ) + end + end + end +end + +--- Will detect AIR Units landing... When the event takes place, the spawned Group is registered as landed. +-- This is needed to ensure that Re-SPAWNing is only done for landed AIR Groups. +-- @param #SPAWN self +-- @param Core.Event#EVENTDATA EventData +function SPAWN:_OnLand( EventData ) + self:F( self.SpawnTemplatePrefix ) + + local SpawnGroup = EventData.IniGroup + if SpawnGroup then + local EventPrefix = self:_GetPrefixFromGroup( SpawnGroup ) + if EventPrefix then -- EventPrefix can be nil if no # is found, which means, no spawnable group! + self:T( { "Land event: " .. EventPrefix } ) + if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then + -- TODO: Check if this is the last unit of the group that lands. + SpawnGroup:SetState( SpawnGroup, "Spawn_Landed", true ) + if self.RepeatOnLanding then + local SpawnGroupIndex = self:GetSpawnIndexFromGroup( SpawnGroup ) + self:T( { "Landed:", "ReSpawn:", SpawnGroup:GetName(), SpawnGroupIndex } ) + --self:ReSpawn( SpawnGroupIndex ) + -- Delay respawn by three seconds due to DCS 2.5.4.26368 OB bug https://github.com/FlightControl-Master/MOOSE/issues/1076 + -- Bug was initially only for engine shutdown event but after ED "fixed" it, it now happens on landing events. + SCHEDULER:New(nil, self.ReSpawn, {self, SpawnGroupIndex}, 3) + end + end + end + end +end + +--- Will detect AIR Units shutting down their engines ... +-- When the event takes place, and the method @{RepeatOnEngineShutDown} was called, the spawned Group will Re-SPAWN. +-- But only when the Unit was registered to have landed. +-- @param #SPAWN self +-- @param Core.Event#EVENTDATA EventData +function SPAWN:_OnEngineShutDown( EventData ) + self:F( self.SpawnTemplatePrefix ) + + local SpawnGroup = EventData.IniGroup + if SpawnGroup then + local EventPrefix = self:_GetPrefixFromGroup( SpawnGroup ) + if EventPrefix then -- EventPrefix can be nil if no # is found, which means, no spawnable group! + self:T( { "EngineShutdown event: " .. EventPrefix } ) + if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then + -- todo: test if on the runway + local Landed = SpawnGroup:GetState( SpawnGroup, "Spawn_Landed" ) + if Landed and self.RepeatOnEngineShutDown then + local SpawnGroupIndex = self:GetSpawnIndexFromGroup( SpawnGroup ) + self:T( { "EngineShutDown: ", "ReSpawn:", SpawnGroup:GetName(), SpawnGroupIndex } ) + --self:ReSpawn( SpawnGroupIndex ) + -- Delay respawn by three seconds due to DCS 2.5.4 OB bug https://github.com/FlightControl-Master/MOOSE/issues/1076 + SCHEDULER:New(nil, self.ReSpawn, {self, SpawnGroupIndex}, 3) + end + end + end + end +end + +--- This function is called automatically by the Spawning scheduler. +-- It is the internal worker method SPAWNing new Groups on the defined time intervals. +-- @param #SPAWN self +function SPAWN:_Scheduler() + self:F2( { "_Scheduler", self.SpawnTemplatePrefix, self.SpawnAliasPrefix, self.SpawnIndex, self.SpawnMaxGroups, self.SpawnMaxUnitsAlive } ) + + -- Validate if there are still groups left in the batch... + self:Spawn() + + return true +end + +--- Schedules the CleanUp of Groups +-- @param #SPAWN self +-- @return #boolean True = Continue Scheduler +function SPAWN:_SpawnCleanUpScheduler() + self:F( { "CleanUp Scheduler:", self.SpawnTemplatePrefix } ) + + local SpawnGroup, SpawnCursor = self:GetFirstAliveGroup() + self:T( { "CleanUp Scheduler:", SpawnGroup, SpawnCursor } ) + + while SpawnGroup do + + local SpawnUnits = SpawnGroup:GetUnits() + + for UnitID, UnitData in pairs( SpawnUnits ) do + + local SpawnUnit = UnitData -- Wrapper.Unit#UNIT + local SpawnUnitName = SpawnUnit:GetName() + + + self.SpawnCleanUpTimeStamps[SpawnUnitName] = self.SpawnCleanUpTimeStamps[SpawnUnitName] or {} + local Stamp = self.SpawnCleanUpTimeStamps[SpawnUnitName] + self:T( { SpawnUnitName, Stamp } ) + + if Stamp.Vec2 then + if SpawnUnit:InAir() == false and SpawnUnit:GetVelocityKMH() < 1 then + local NewVec2 = SpawnUnit:GetVec2() + if (Stamp.Vec2.x == NewVec2.x and Stamp.Vec2.y == NewVec2.y) or (SpawnUnit:GetLife() <= 1) then + -- If the plane is not moving or dead , and is on the ground, assign it with a timestamp... + if Stamp.Time + self.SpawnCleanUpInterval < timer.getTime() then + self:T( { "CleanUp Scheduler:", "ReSpawning:", SpawnGroup:GetName() } ) + self:ReSpawn( SpawnCursor ) + Stamp.Vec2 = nil + Stamp.Time = nil + end + else + Stamp.Time = timer.getTime() + Stamp.Vec2 = SpawnUnit:GetVec2() + end + else + Stamp.Vec2 = nil + Stamp.Time = nil + end + else + if SpawnUnit:InAir() == false then + Stamp.Vec2 = SpawnUnit:GetVec2() + if (SpawnUnit:GetVelocityKMH() < 1) then + Stamp.Time = timer.getTime() + end + else + Stamp.Time = nil + Stamp.Vec2 = nil + end + end + end + + SpawnGroup, SpawnCursor = self:GetNextAliveGroup( SpawnCursor ) + + self:T( { "CleanUp Scheduler:", SpawnGroup, SpawnCursor } ) + + end + + return true -- Repeat + +end +--- **Core** - Spawn statics. +-- +-- === +-- +-- ## Features: +-- +-- * Spawn new statics from a static already defined in the mission editor. +-- * Spawn new statics from a given template. +-- * Spawn new statics from a given type. +-- * Spawn with a custom heading and location. +-- * Spawn within a zone. +-- * Spawn statics linked to units, .e.g on aircraft carriers. +-- +-- === +-- +-- # Demo Missions +-- +-- ## [SPAWNSTATIC Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/SPS%20-%20Spawning%20Statics) +-- +-- +-- === +-- +-- # YouTube Channel +-- +-- ## [SPAWNSTATIC YouTube Channel]() [No videos yet!] +-- +-- === +-- +-- ### Author: **FlightControl** +-- ### Contributions: **funkyfranky** +-- +-- === +-- +-- @module Core.SpawnStatic +-- @image Core_Spawnstatic.JPG + +--- @type SPAWNSTATIC +-- @field #string SpawnTemplatePrefix Name of the template group. +-- @field #number CountryID Country ID. +-- @field #number CoalitionID Coalition ID. +-- @field #number CategoryID Category ID. +-- @field #number SpawnIndex Running number increased with each new Spawn. +-- @field Wrapper.Unit#UNIT InitLinkUnit The unit the static is linked to. +-- @field #number InitOffsetX Link offset X coordinate. +-- @field #number InitOffsetY Link offset Y coordinate. +-- @field #number InitOffsetAngle Link offset angle in degrees. +-- @field #number InitStaticHeading Heading of the static. +-- @field #string InitStaticLivery Livery for aircraft. +-- @field #string InitStaticShape Shape of teh static. +-- @field #string InitStaticType Type of the static. +-- @field #string InitStaticCategory Categrory of the static. +-- @field #string InitStaticName Name of the static. +-- @field Core.Point#COORDINATE InitStaticCoordinate Coordinate where to spawn the static. +-- @field #boolean InitDead Set static to be dead if true. +-- @field #boolean InitCargo If true, static can act as cargo. +-- @field #number InitCargoMass Mass of cargo in kg. +-- @extends Core.Base#BASE + + +--- Allows to spawn dynamically new @{Static}s into your mission. +-- +-- Through creating a copy of an existing static object template as defined in the Mission Editor (ME), SPAWNSTATIC can retireve the properties of the defined static object template (like type, category etc), +-- and "copy" these properties to create a new static object and place it at the desired coordinate. +-- +-- New spawned @{Static}s get **the same name** as the name of the template Static, or gets the given name when a new name is provided at the Spawn method. +-- By default, spawned @{Static}s will follow a naming convention at run-time: +-- +-- * Spawned @{Static}s will have the name _StaticName_#_nnn_, where _StaticName_ is the name of the **Template Static**, and _nnn_ is a **counter from 0 to 99999**. +-- +-- # SPAWNSTATIC Constructors +-- +-- Firstly, we need to create a SPAWNSTATIC object that will be used to spawn new statics into the mission. There are three ways to do this. +-- +-- ## Use another Static +-- +-- A new SPAWNSTATIC object can be created using another static by the @{#SPAWNSTATIC.NewFromStatic}() function. All parameters such as position, heading, country will be initialized +-- from the static. +-- +-- ## From a Template +-- +-- A SPAWNSTATIC object can also be created from a template table using the @{#SPAWNSTATIC.NewFromTemplate}(SpawnTemplate, CountryID) function. All parameters are taken from the template. +-- +-- ## From a Type +-- +-- A very basic method is to create a SPAWNSTATIC object by just giving the type of the static. All parameters must be initialized from the InitXYZ functions described below. Otherwise default values +-- are used. For example, if no spawn coordinate is given, the static will be created at the origin of the map. +-- +-- # Setting Parameters +-- +-- Parameters such as the spawn position, heading, country etc. can be set via :Init*XYZ* functions. Note that these functions must be given before the actual spawn command! +-- +-- * @{#SPAWNSTATIC.InitCoordinate}(Coordinate) Sets the coordinate where the static is spawned. Statics are always spawnd on the ground. +-- * @{#SPAWNSTATIC.InitHeading}(Heading) sets the orientation of the static. +-- * @{#SPAWNSTATIC.InitLivery}(LiveryName) sets the livery of the static. Not all statics support this. +-- * @{#SPAWNSTATIC.InitType}(StaticType) sets the type of the static. +-- * @{#SPAWNSTATIC.InitShape}(StaticType) sets the shape of the static. Not all statics have this parameter. +-- * @{#SPAWNSTATIC.InitNamePrefix}(NamePrefix) sets the name prefix of the spawned statics. +-- * @{#SPAWNSTATIC.InitCountry}(CountryID) sets the country and therefore the coalition of the spawned statics. +-- * @{#SPAWNSTATIC.InitLinkToUnit}(Unit, OffsetX, OffsetY, OffsetAngle) links the static to a unit, e.g. to an aircraft carrier. +-- +-- # Spawning the Statics +-- +-- Once the SPAWNSTATIC object is created and parameters are initialized, the spawn command can be given. There are different methods where some can be used to directly set parameters +-- such as position and heading. +-- +-- * @{#SPAWNSTATIC.Spawn}(Heading, NewName) spawns the static with the set parameters. Optionally, heading and name can be given. The name **must be unique**! +-- * @{#SPAWNSTATIC.SpawnFromCoordinate}(Coordinate, Heading, NewName) spawn the static at the given coordinate. Optionally, heading and name can be given. The name **must be unique**! +-- * @{#SPAWNSTATIC.SpawnFromPointVec2}(PointVec2, Heading, NewName) spawns the static at a POINT_VEC2 coordinate. Optionally, heading and name can be given. The name **must be unique**! +-- * @{#SPAWNSTATIC.SpawnFromZone}(Zone, Heading, NewName) spawns the static at the center of a @{Zone}. Optionally, heading and name can be given. The name **must be unique**! +-- +-- @field #SPAWNSTATIC SPAWNSTATIC +-- +SPAWNSTATIC = { + ClassName = "SPAWNSTATIC", + SpawnIndex = 0, +} + +--- Static template table data. +-- @type SPAWNSTATIC.TemplateData +-- @field #string name Name of the static. +-- @field #string type Type of the static. +-- @field #string category Category of the static. +-- @field #number x X-coordinate of the static. +-- @field #number y Y-coordinate of teh static. +-- @field #number heading Heading in rad. +-- @field #boolean dead Static is dead if true. +-- @field #string livery_id Livery name. +-- @field #number unitId Unit ID. +-- @field #number groupId Group ID. +-- @field #table offsets Offset parameters when linked to a unit. +-- @field #number mass Cargo mass in kg. +-- @field #boolean canCargo Static can be a cargo. + +--- Creates the main object to spawn a @{Static} defined in the mission editor (ME). +-- @param #SPAWNSTATIC self +-- @param #string SpawnTemplateName Name of the static object in the ME. Each new static will have the name starting with this prefix. +-- @param DCS#country.id SpawnCountryID (Optional) The ID of the country. +-- @return #SPAWNSTATIC self +function SPAWNSTATIC:NewFromStatic(SpawnTemplateName, SpawnCountryID) + + local self = BASE:Inherit( self, BASE:New() ) -- #SPAWNSTATIC + + local TemplateStatic, CoalitionID, CategoryID, CountryID = _DATABASE:GetStaticGroupTemplate(SpawnTemplateName) + + if TemplateStatic then + self.SpawnTemplatePrefix = SpawnTemplateName + self.TemplateStaticUnit = UTILS.DeepCopy(TemplateStatic.units[1]) + self.CountryID = SpawnCountryID or CountryID + self.CategoryID = CategoryID + self.CoalitionID = CoalitionID + self.SpawnIndex = 0 + else + error( "SPAWNSTATIC:New: There is no static declared in the mission editor with SpawnTemplatePrefix = '" .. tostring(SpawnTemplateName) .. "'" ) + end + + self:SetEventPriority( 5 ) + + return self +end + +--- Creates the main object to spawn a @{Static} given a template table. +-- @param #SPAWNSTATIC self +-- @param #table SpawnTemplate Template used for spawning. +-- @param DCS#country.id CountryID The ID of the country. Default `country.id.USA`. +-- @return #SPAWNSTATIC self +function SPAWNSTATIC:NewFromTemplate(SpawnTemplate, CountryID) + + local self = BASE:Inherit( self, BASE:New() ) -- #SPAWNSTATIC + + self.TemplateStaticUnit = UTILS.DeepCopy(SpawnTemplate) + self.SpawnTemplatePrefix = SpawnTemplate.name + self.CountryID = CountryID or country.id.USA + + return self +end + +--- Creates the main object to spawn a @{Static} from a given type. +-- NOTE that you have to init many other parameters as spawn coordinate etc. +-- @param #SPAWNSTATIC self +-- @param #string StaticType Type of the static. +-- @param #string StaticCategory Category of the static, e.g. "Planes". +-- @param DCS#country.id CountryID The ID of the country. Default `country.id.USA`. +-- @return #SPAWNSTATIC self +function SPAWNSTATIC:NewFromType(StaticType, StaticCategory, CountryID) + + local self = BASE:Inherit( self, BASE:New() ) -- #SPAWNSTATIC + + self.InitStaticType=StaticType + self.InitStaticCategory=StaticCategory + self.CountryID=CountryID or country.id.USA + self.SpawnTemplatePrefix=self.InitStaticType + + self.InitStaticCoordinate=COORDINATE:New(0, 0, 0) + self.InitStaticHeading=0 + + return self +end + +--- Initialize heading of the spawned static. +-- @param #SPAWNSTATIC self +-- @param Core.Point#COORDINATE Coordinate Position where the static is spawned. +-- @return #SPAWNSTATIC self +function SPAWNSTATIC:InitCoordinate(Coordinate) + self.InitStaticCoordinate=Coordinate + return self +end + +--- Initialize heading of the spawned static. +-- @param #SPAWNSTATIC self +-- @param #number Heading The heading in degrees. +-- @return #SPAWNSTATIC self +function SPAWNSTATIC:InitHeading(Heading) + self.InitStaticHeading=Heading + return self +end + +--- Initialize livery of the spawned static. +-- @param #SPAWNSTATIC self +-- @param #string LiveryName Name of the livery to use. +-- @return #SPAWNSTATIC self +function SPAWNSTATIC:InitLivery(LiveryName) + self.InitStaticLivery=LiveryName + return self +end + +--- Initialize type of the spawned static. +-- @param #SPAWNSTATIC self +-- @param #string StaticType Type of the static, e.g. "FA-18C_hornet". +-- @return #SPAWNSTATIC self +function SPAWNSTATIC:InitType(StaticType) + self.InitStaticType=StaticType + return self +end + +--- Initialize shape of the spawned static. Required by some but not all statics. +-- @param #SPAWNSTATIC self +-- @param #string StaticShape Shape of the static, e.g. "carrier_tech_USA". +-- @return #SPAWNSTATIC self +function SPAWNSTATIC:InitShape(StaticShape) + self.InitStaticShape=StaticShape + return self +end + +--- Initialize parameters for spawning FARPs. +-- @param #SPAWNSTATIC self +-- @param #number CallsignID Callsign ID. Default 1 (="London"). +-- @param #number Frequency Frequency in MHz. Default 127.5 MHz. +-- @param #number Modulation Modulation 0=AM, 1=FM. +-- @return #SPAWNSTATIC self +function SPAWNSTATIC:InitFARP(CallsignID, Frequency, Modulation) + self.InitFarp=true + self.InitFarpCallsignID=CallsignID or 1 + self.InitFarpFreq=Frequency or 127.5 + self.InitFarpModu=Modulation or 0 + return self +end + +--- Initialize cargo mass. +-- @param #SPAWNSTATIC self +-- @param #number Mass Mass of the cargo in kg. +-- @return #SPAWNSTATIC self +function SPAWNSTATIC:InitCargoMass(Mass) + self.InitCargoMass=Mass + return self +end + +--- Initialize as cargo. +-- @param #SPAWNSTATIC self +-- @param #boolean IsCargo If true, this static can act as cargo. +-- @return #SPAWNSTATIC self +function SPAWNSTATIC:InitCargo(IsCargo) + self.InitCargo=IsCargo + return self +end + +--- Initialize country of the spawned static. This determines the category. +-- @param #SPAWNSTATIC self +-- @param #string CountryID The country ID, e.g. country.id.USA. +-- @return #SPAWNSTATIC self +function SPAWNSTATIC:InitCountry(CountryID) + self.CountryID=CountryID + return self +end + +--- Initialize name prefix statics get. This will be appended by "#0001", "#0002" etc. +-- @param #SPAWNSTATIC self +-- @param #string NamePrefix Name prefix of statics spawned. Will append #0001, etc to the name. +-- @return #SPAWNSTATIC self +function SPAWNSTATIC:InitNamePrefix(NamePrefix) + self.SpawnTemplatePrefix=NamePrefix + return self +end + +--- Init link to a unit. +-- @param #SPAWNSTATIC self +-- @param Wrapper.Unit#UNIT Unit The unit to which the static is linked. +-- @param #number OffsetX Offset in X. +-- @param #number OffsetY Offset in Y. +-- @param #number OffsetAngle Offset angle in degrees. +-- @return #SPAWNSTATIC self +function SPAWNSTATIC:InitLinkToUnit(Unit, OffsetX, OffsetY, OffsetAngle) + + self.InitLinkUnit=Unit + self.InitOffsetX=OffsetX or 0 + self.InitOffsetY=OffsetY or 0 + self.InitOffsetAngle=OffsetAngle or 0 + + return self +end + +--- Spawn a new STATIC object. +-- @param #SPAWNSTATIC self +-- @param #number Heading (Optional) The heading of the static, which is a number in degrees from 0 to 360. Default is the heading of the template. +-- @param #string NewName (Optional) The name of the new static. +-- @return Wrapper.Static#STATIC The static spawned. +function SPAWNSTATIC:Spawn(Heading, NewName) + + if Heading then + self.InitStaticHeading=Heading + end + + if NewName then + self.InitStaticName=NewName + end + + return self:_SpawnStatic(self.TemplateStaticUnit, self.CountryID) + +end + +--- Creates a new @{Static} from a POINT_VEC2. +-- @param #SPAWNSTATIC self +-- @param Core.Point#POINT_VEC2 PointVec2 The 2D coordinate where to spawn the static. +-- @param #number Heading The heading of the static, which is a number in degrees from 0 to 360. +-- @param #string NewName (Optional) The name of the new static. +-- @return Wrapper.Static#STATIC The static spawned. +function SPAWNSTATIC:SpawnFromPointVec2(PointVec2, Heading, NewName) + + local vec2={x=PointVec2:GetX(), y=PointVec2:GetY()} + + local Coordinate=COORDINATE:NewFromVec2(vec2) + + return self:SpawnFromCoordinate(Coordinate, Heading, NewName) +end + + +--- Creates a new @{Static} from a COORDINATE. +-- @param #SPAWNSTATIC self +-- @param Core.Point#COORDINATE Coordinate The 3D coordinate where to spawn the static. +-- @param #number Heading (Optional) Heading The heading of the static in degrees. Default is 0 degrees. +-- @param #string NewName (Optional) The name of the new static. +-- @return Wrapper.Static#STATIC The spawned STATIC object. +function SPAWNSTATIC:SpawnFromCoordinate(Coordinate, Heading, NewName) + + -- Set up coordinate. + self.InitStaticCoordinate=Coordinate + + if Heading then + self.InitStaticHeading=Heading + end + + if NewName then + self.InitStaticName=NewName + end + + return self:_SpawnStatic(self.TemplateStaticUnit, self.CountryID) +end + + +--- Creates a new @{Static} from a @{Zone}. +-- @param #SPAWNSTATIC self +-- @param Core.Zone#ZONE_BASE Zone The Zone where to spawn the static. +-- @param #number Heading (Optional)The heading of the static in degrees. Default is the heading of the template. +-- @param #string NewName (Optional) The name of the new static. +-- @return Wrapper.Static#STATIC The static spawned. +function SPAWNSTATIC:SpawnFromZone(Zone, Heading, NewName) + + -- Spawn the new static at the center of the zone. + local Static = self:SpawnFromPointVec2( Zone:GetPointVec2(), Heading, NewName ) + + return Static +end + +--- Spawns a new static using a given template. Additionally, the country ID needs to be specified, which also determines the coalition of the spawned static. +-- @param #SPAWNSTATIC self +-- @param #SPAWNSTATIC.TemplateData Template Spawn unit template. +-- @param #number CountryID The country ID. +-- @return Wrapper.Static#STATIC The static spawned. +function SPAWNSTATIC:_SpawnStatic(Template, CountryID) + + Template=Template or {} + + local CountryID=CountryID or self.CountryID + + if self.InitStaticType then + Template.type=self.InitStaticType + end + + if self.InitStaticCategory then + Template.category=self.InitStaticCategory + end + + if self.InitStaticCoordinate then + Template.x = self.InitStaticCoordinate.x + Template.y = self.InitStaticCoordinate.z + Template.alt = self.InitStaticCoordinate.y + end + + if self.InitStaticHeading then + Template.heading = math.rad(self.InitStaticHeading) + end + + if self.InitStaticShape then + Template.shape_name=self.InitStaticShape + end + + if self.InitStaticLivery then + Template.livery_id=self.InitStaticLivery + end + + if self.InitDead~=nil then + Template.dead=self.InitDead + end + + if self.InitCargo~=nil then + Template.canCargo=self.InitCargo + end + + if self.InitCargoMass~=nil then + Template.mass=self.InitCargoMass + end + + if self.InitLinkUnit then + Template.linkUnit=self.InitLinkUnit:GetID() + Template.linkOffset=true + Template.offsets={} + Template.offsets.y=self.InitOffsetY + Template.offsets.x=self.InitOffsetX + Template.offsets.angle=self.InitOffsetAngle and math.rad(self.InitOffsetAngle) or 0 + end + + if self.InitFarp then + Template.heliport_callsign_id = self.InitFarpCallsignID + Template.heliport_frequency = self.InitFarpFreq + Template.heliport_modulation = self.InitFarpModu + Template.unitId=nil + end + + -- Increase spawn index counter. + self.SpawnIndex = self.SpawnIndex + 1 + + -- Name of the spawned static. + Template.name = self.InitStaticName or string.format("%s#%05d", self.SpawnTemplatePrefix, self.SpawnIndex) + + -- Add and register the new static. + local mystatic=_DATABASE:AddStatic(Template.name) + + -- Debug output. + self:T(Template) + + -- Add static to the game. + local Static=nil + + if self.InitFarp then + + local TemplateGroup={} + TemplateGroup.units={} + TemplateGroup.units[1]=Template + + TemplateGroup.visible=true + TemplateGroup.hidden=false + TemplateGroup.x=Template.x + TemplateGroup.y=Template.y + TemplateGroup.name=Template.name + + self:T("Spawning FARP") + self:T({Template=Template}) + self:T({TemplateGroup=TemplateGroup}) + + -- ED's dirty way to spawn FARPS. + Static=coalition.addGroup(CountryID, -1, TemplateGroup) + else + Static=coalition.addStaticObject(CountryID, Template) + end + + return mystatic +end +--- **Core** - Timer scheduler. +-- +-- **Main Features:** +-- +-- * Delay function calls +-- * Easy set up and little overhead +-- * Set start, stop and time interval +-- * Define max function calls +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Core.Timer +-- @image Core_Scheduler.JPG + + +--- TIMER class. +-- @type TIMER +-- @field #string ClassName Name of the class. +-- @field #string lid Class id string for output to DCS log file. +-- @field #number tid Timer ID returned by the DCS API function. +-- @field #number uid Unique ID of the timer. +-- @field #function func Timer function. +-- @field #table para Parameters passed to the timer function. +-- @field #number Tstart Relative start time in seconds. +-- @field #number Tstop Relative stop time in seconds. +-- @field #number dT Time interval between function calls in seconds. +-- @field #number ncalls Counter of function calls. +-- @field #number ncallsMax Max number of function calls. If reached, timer is stopped. +-- @field #boolean isrunning If `true`, timer is running. Else it was not started yet or was stopped. +-- @extends Core.Base#BASE + +--- *Better three hours too soon than a minute too late.* - William Shakespeare +-- +-- === +-- +-- ![Banner Image](..\Presentations\Timer\TIMER_Main.jpg) +-- +-- # The TIMER Concept +-- +-- The TIMER class is the little sister of the @{Core.Scheduler#SCHEDULER} class. It does the same thing but is a bit easier to use and has less overhead. It should be sufficient in many cases. +-- +-- It provides an easy interface to the DCS [timer.scheduleFunction](https://wiki.hoggitworld.com/view/DCS_func_scheduleFunction). +-- +-- # Construction +-- +-- A new TIMER is created by the @{#TIMER.New}(*func*, *...*) function +-- +-- local mytimer=TIMER:New(myfunction, a, b) +-- +-- The first parameter *func* is the function that is called followed by the necessary comma separeted parameters that are passed to that function. +-- +-- ## Starting the Timer +-- +-- The timer is started by the @{#TIMER.Start}(*Tstart*, *dT*, *Duration*) function +-- +-- mytimer:Start(5, 1, 20) +-- +-- where +-- +-- * *Tstart* is the relative start time in seconds. In the example, the first function call happens after 5 sec. +-- * *dT* is the time interval between function calls in seconds. Above, the function is called once per second. +-- * *Duration* is the duration in seconds after which the timer is stopped. This is relative to the start time. Here, the timer will run for 20 seconds. +-- +-- Note that +-- +-- * if *Tstart* is not specified (*nil*), the first function call happens immediately, i.e. after one millisecond. +-- * if *dT* is not specified (*nil*), the function is called only once. +-- * if *Duration* is not specified (*nil*), the timer runs forever or until stopped manually or until the max function calls are reached (see below). +-- +-- For example, +-- +-- mytimer:Start(3) -- Will call the function once after 3 seconds. +-- mytimer:Start(nil, 0.5) -- Will call right now and then every 0.5 sec until all eternity. +-- mytimer:Start(nil, 2.0, 20) -- Will call right now and then every 2.0 sec for 20 sec. +-- mytimer:Start(1.0, nil, 10) -- Does not make sense as the function is only called once anyway. +-- +-- ## Stopping the Timer +-- +-- The timer can be stopped manually by the @{#TIMER.Stop}(*Delay*) function +-- +-- mytimer:Stop() +-- +-- If the optional paramter *Delay* is specified, the timer is stopped after *delay* seconds. +-- +-- ## Limit Function Calls +-- +-- The timer can be stopped after a certain amount of function calles with the @{#TIMER.SetMaxFunctionCalls}(*Nmax*) function +-- +-- mytimer:SetMaxFunctionCalls(20) +-- +-- where *Nmax* is the number of calls after which the timer is stopped, here 20. +-- +-- For example, +-- +-- mytimer:SetMaxFunctionCalls(66):Start(1.0, 0.1) +-- +-- will start the timer after one second and call the function every 0.1 seconds. Once the function has been called 66 times, the timer is stopped. +-- +-- +-- @field #TIMER +TIMER = { + ClassName = "TIMER", + lid = nil, +} + +--- Timer ID. +_TIMERID=0 + +--- Timer data base. +--_TIMERDB={} + +--- TIMER class version. +-- @field #string version +TIMER.version="0.1.1" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: A lot. +-- TODO: Write docs. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new TIMER object. +-- @param #TIMER self +-- @param #function Function The function to call. +-- @param ... Parameters passed to the function if any. +-- @return #TIMER self +function TIMER:New(Function, ...) + + -- Inherit BASE. + local self=BASE:Inherit(self, BASE:New()) --#TIMER + + -- Function to call. + self.func=Function + + -- Function arguments. + self.para=arg or {} + + -- Number of function calls. + self.ncalls=0 + + -- Not running yet. + self.isrunning=false + + -- Increase counter + _TIMERID=_TIMERID+1 + + -- Set UID. + self.uid=_TIMERID + + -- Log id. + self.lid=string.format("TIMER UID=%d | ", self.uid) + + -- Add to DB. + --_TIMERDB[self.uid]=self + + return self +end + +--- Create a new TIMER object. +-- @param #TIMER self +-- @param #number Tstart Relative start time in seconds. +-- @param #number dT Interval between function calls in seconds. If not specified `nil`, the function is called only once. +-- @param #number Duration Time in seconds for how long the timer is running. If not specified `nil`, the timer runs forever or until stopped manually by the `TIMER:Stop()` function. +-- @return #TIMER self +function TIMER:Start(Tstart, dT, Duration) + + -- Current time. + local Tnow=timer.getTime() + + -- Start time in sec. + self.Tstart=Tstart and Tnow+Tstart or Tnow+0.001 -- one millisecond delay if Tstart=nil + + -- Set time interval. + self.dT=dT + + -- Stop time. + if Duration then + self.Tstop=self.Tstart+Duration + end + + -- Call DCS timer function. + self.tid=timer.scheduleFunction(self._Function, self, self.Tstart) + + -- Set log id. + self.lid=string.format("TIMER UID=%d/%d | ", self.uid, self.tid) + + -- Is now running. + self.isrunning=true + + -- Debug info. + self:T(self.lid..string.format("Starting Timer in %.3f sec, dT=%s, Tstop=%s", self.Tstart-Tnow, tostring(self.dT), tostring(self.Tstop))) + + return self +end + +--- Stop the timer by removing the timer function. +-- @param #TIMER self +-- @param #number Delay (Optional) Delay in seconds, before the timer is stopped. +-- @return #TIMER self +function TIMER:Stop(Delay) + + if Delay and Delay>0 then + + self.Tstop=timer.getTime()+Delay + + else + + if self.tid then + + -- Remove timer function. + self:T(self.lid..string.format("Stopping timer by removing timer function after %d calls!", self.ncalls)) + timer.removeFunction(self.tid) + + -- Not running any more. + self.isrunning=false + + -- Remove DB entry. + --_TIMERDB[self.uid]=nil + + end + + end + + return self +end + +--- Set max number of function calls. When the function has been called this many times, the TIMER is stopped. +-- @param #TIMER self +-- @param #number Nmax Set number of max function calls. +-- @return #TIMER self +function TIMER:SetMaxFunctionCalls(Nmax) + self.ncallsMax=Nmax + return self +end + +--- Check if the timer has been started and was not stopped. +-- @param #TIMER self +-- @return #boolean If `true`, the timer is running. +function TIMER:IsRunning() + return self.isrunning +end + +--- Call timer function. +-- @param #TIMER self +-- @param #number time DCS model time in seconds. +-- @return #number Time when the function is called again or `nil` if the timer is stopped. +function TIMER:_Function(time) + + -- Call function. + self.func(unpack(self.para)) + + -- Increase number of calls. + self.ncalls=self.ncalls+1 + + -- Next time. + local Tnext=self.dT and time+self.dT or nil + + -- Check if we stop the timer. + local stopme=false + if Tnext==nil then + -- No next time. + self:T(self.lid..string.format("No next time as dT=nil ==> Stopping timer after %d function calls", self.ncalls)) + stopme=true + elseif self.Tstop and Tnext>self.Tstop then + -- Stop time passed. + self:T(self.lid..string.format("Stop time passed %.3f > %.3f ==> Stopping timer after %d function calls", Tnext, self.Tstop, self.ncalls)) + stopme=true + elseif self.ncallsMax and self.ncalls>=self.ncallsMax then + -- Number of max function calls reached. + self:T(self.lid..string.format("Max function calls Nmax=%d reached ==> Stopping timer after %d function calls", self.ncallsMax, self.ncalls)) + stopme=true + end + + if stopme then + -- Remove timer function. + self:Stop() + return nil + else + -- Call again in Tnext seconds. + return Tnext + end +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- **Core** - Models the process to achieve goal(s). +-- +-- === +-- +-- ## Features: +-- +-- * Define the goal. +-- * Monitor the goal achievement. +-- * Manage goal contribution by players. +-- +-- === +-- +-- Classes that implement a goal achievement, will derive from GOAL to implement the ways how the achievements can be realized. +-- +-- === +-- +-- ### Author: **FlightControl** +-- ### Contributions: **funkyfranky** +-- +-- === +-- +-- @module Core.Goal +-- @image Core_Goal.JPG + + +do -- Goal + + --- @type GOAL + -- @extends Core.Fsm#FSM + + + --- Models processes that have an objective with a defined achievement. Derived classes implement the ways how the achievements can be realized. + -- + -- # 1. GOAL constructor + -- + -- * @{#GOAL.New}(): Creates a new GOAL object. + -- + -- # 2. GOAL is a finite state machine (FSM). + -- + -- ## 2.1. GOAL States + -- + -- * **Pending**: The goal object is in progress. + -- * **Achieved**: The goal objective is Achieved. + -- + -- ## 2.2. GOAL Events + -- + -- * **Achieved**: Set the goal objective to Achieved. + -- + -- # 3. Player contributions. + -- + -- Goals are most of the time achieved by players. These player achievements can be registered as part of the goal achievement. + -- Use @{#GOAL.AddPlayerContribution}() to add a player contribution to the goal. + -- The player contributions are based on a points system, an internal counter per player. + -- So once the goal has been achieved, the player contributions can be queried using @{#GOAL.GetPlayerContributions}(), + -- that retrieves all contributions done by the players. For one player, the contribution can be queried using @{#GOAL.GetPlayerContribution}(). + -- The total amount of player contributions can be queried using @{#GOAL.GetTotalContributions}(). + -- + -- # 4. Goal achievement. + -- + -- Once the goal is achieved, the mission designer will need to trigger the goal achievement using the **Achieved** event. + -- The underlying 2 examples will achieve the goals for the `Goal` object: + -- + -- Goal:Achieved() -- Achieve the goal immediately. + -- Goal:__Achieved( 30 ) -- Achieve the goal within 30 seconds. + -- + -- # 5. Check goal achievement. + -- + -- The method @{#GOAL.IsAchieved}() will return true if the goal is achieved (the trigger **Achieved** was executed). + -- You can use this method to check asynchronously if a goal has been achieved, for example using a scheduler. + -- + -- @field #GOAL + GOAL = { + ClassName = "GOAL", + } + + --- @field #table GOAL.Players + GOAL.Players = {} + + --- @field #number GOAL.TotalContributions + GOAL.TotalContributions = 0 + + --- GOAL Constructor. + -- @param #GOAL self + -- @return #GOAL + function GOAL:New() + + local self = BASE:Inherit( self, FSM:New() ) -- #GOAL + self:F( {} ) + + --- Achieved State for GOAL + -- @field GOAL.Achieved + + --- Achieved State Handler OnLeave for GOAL + -- @function [parent=#GOAL] OnLeaveAchieved + -- @param #GOAL self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Achieved State Handler OnEnter for GOAL + -- @function [parent=#GOAL] OnEnterAchieved + -- @param #GOAL self + -- @param #string From + -- @param #string Event + -- @param #string To + + + self:SetStartState( "Pending" ) + self:AddTransition( "*", "Achieved", "Achieved" ) + + --- Achieved Handler OnBefore for GOAL + -- @function [parent=#GOAL] OnBeforeAchieved + -- @param #GOAL self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Achieved Handler OnAfter for GOAL + -- @function [parent=#GOAL] OnAfterAchieved + -- @param #GOAL self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Achieved Trigger for GOAL + -- @function [parent=#GOAL] Achieved + -- @param #GOAL self + + --- Achieved Asynchronous Trigger for GOAL + -- @function [parent=#GOAL] __Achieved + -- @param #GOAL self + -- @param #number Delay + + self:SetEventPriority( 5 ) + + return self + end + + + --- Add a new contribution by a player. + -- @param #GOAL self + -- @param #string PlayerName The name of the player. + function GOAL:AddPlayerContribution( PlayerName ) + self:F({PlayerName}) + self.Players[PlayerName] = self.Players[PlayerName] or 0 + self.Players[PlayerName] = self.Players[PlayerName] + 1 + self.TotalContributions = self.TotalContributions + 1 + end + + + --- @param #GOAL self + -- @param #number Player contribution. + function GOAL:GetPlayerContribution( PlayerName ) + return self.Players[PlayerName] or 0 + end + + + --- Get the players who contributed to achieve the goal. + -- The result is a list of players, sorted by the name of the players. + -- @param #GOAL self + -- @return #list The list of players, indexed by the player name. + function GOAL:GetPlayerContributions() + return self.Players or {} + end + + + --- Gets the total contributions that happened to achieve the goal. + -- The result is a number. + -- @param #GOAL self + -- @return #number The total number of contributions. 0 is returned if there were no contributions (yet). + function GOAL:GetTotalContributions() + return self.TotalContributions or 0 + end + + + + --- Validates if the goal is achieved. + -- @param #GOAL self + -- @return #boolean true if the goal is achieved. + function GOAL:IsAchieved() + return self:Is( "Achieved" ) + end + +end--- **Core** - Management of spotting logistics, that can be activated and deactivated upon command. +-- +-- === +-- +-- SPOT implements the DCS Spot class functionality, but adds additional luxury to be able to: +-- +-- * Spot for a defined duration. +-- * Updates of laer spot position every 0.2 seconds for moving targets. +-- * Wiggle the spot at the target. +-- * Provide a @{Wrapper.Unit} as a target, instead of a point. +-- * Implement a status machine, LaseOn, LaseOff. +-- +-- === +-- +-- # Demo Missions +-- +-- ### [SPOT Demo Missions source code]() +-- +-- ### [SPOT Demo Missions, only for beta testers]() +-- +-- ### [ALL Demo Missions pack of the last release](https://github.com/FlightControl-Master/MOOSE_MISSIONS/releases) +-- +-- === +-- +-- # YouTube Channel +-- +-- ### [SPOT YouTube Channel]() +-- +-- === +-- +-- ### Author: **FlightControl** +-- ### Contributions: +-- +-- * [**Ciribob**](https://forums.eagle.ru/member.php?u=112175): Showing the way how to lase targets + how laser codes work!!! Explained the autolase script. +-- * [**EasyEB**](https://forums.eagle.ru/member.php?u=112055): Ideas and Beta Testing +-- * [**Wingthor**](https://forums.eagle.ru/member.php?u=123698): Beta Testing +-- +-- === +-- +-- @module Core.Spot +-- @image Core_Spot.JPG + + +do + + --- @type SPOT + -- @extends Core.Fsm#FSM + + + --- Implements the target spotting or marking functionality, but adds additional luxury to be able to: + -- + -- * Mark targets for a defined duration. + -- * Updates of laer spot position every 0.2 seconds for moving targets. + -- * Wiggle the spot at the target. + -- * Provide a @{Wrapper.Unit} as a target, instead of a point. + -- * Implement a status machine, LaseOn, LaseOff. + -- + -- ## 1. SPOT constructor + -- + -- * @{#SPOT.New}(..\Presentations\SPOT\Dia2.JPG): Creates a new SPOT object. + -- + -- ## 2. SPOT is a FSM + -- + -- ![Process]() + -- + -- ### 2.1 SPOT States + -- + -- * **Off**: Lasing is switched off. + -- * **On**: Lasing is switched on. + -- * **Destroyed**: Target is destroyed. + -- + -- ### 2.2 SPOT Events + -- + -- * **@{#SPOT.LaseOn}(Target, LaserCode, Duration)**: Lase to a target. + -- * **@{#SPOT.LaseOff}()**: Stop lasing the target. + -- * **@{#SPOT.Lasing}()**: Target is being lased. + -- * **@{#SPOT.Destroyed}()**: Triggered when target is destroyed. + -- + -- ## 3. Check if a Target is being lased + -- + -- The method @{#SPOT.IsLasing}() indicates whether lasing is on or off. + -- + -- @field #SPOT + SPOT = { + ClassName = "SPOT", + } + + --- SPOT Constructor. + -- @param #SPOT self + -- @param Wrapper.Unit#UNIT Recce Unit that is lasing + -- @return #SPOT + function SPOT:New( Recce ) + + local self = BASE:Inherit( self, FSM:New() ) -- #SPOT + self:F( {} ) + + self:SetStartState( "Off" ) + self:AddTransition( "Off", "LaseOn", "On" ) + + --- LaseOn Handler OnBefore for SPOT + -- @function [parent=#SPOT] OnBeforeLaseOn + -- @param #SPOT self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- LaseOn Handler OnAfter for SPOT + -- @function [parent=#SPOT] OnAfterLaseOn + -- @param #SPOT self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- LaseOn Trigger for SPOT + -- @function [parent=#SPOT] LaseOn + -- @param #SPOT self + -- @param Wrapper.Positionable#POSITIONABLE Target + -- @param #number LaserCode Laser code. + -- @param #number Duration Duration of lasing in seconds. + + --- LaseOn Asynchronous Trigger for SPOT + -- @function [parent=#SPOT] __LaseOn + -- @param #SPOT self + -- @param #number Delay + -- @param Wrapper.Positionable#POSITIONABLE Target + -- @param #number LaserCode Laser code. + -- @param #number Duration Duration of lasing in seconds. + + self:AddTransition( "Off", "LaseOnCoordinate", "On" ) + + --- LaseOnCoordinate Handler OnBefore for SPOT. + -- @function [parent=#SPOT] OnBeforeLaseOnCoordinate + -- @param #SPOT self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- LaseOnCoordinate Handler OnAfter for SPOT. + -- @function [parent=#SPOT] OnAfterLaseOnCoordinate + -- @param #SPOT self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- LaseOnCoordinate Trigger for SPOT. + -- @function [parent=#SPOT] LaseOnCoordinate + -- @param #SPOT self + -- @param Core.Point#COORDINATE Coordinate The coordinate to lase. + -- @param #number LaserCode Laser code. + -- @param #number Duration Duration of lasing in seconds. + + --- LaseOn Asynchronous Trigger for SPOT + -- @function [parent=#SPOT] __LaseOn + -- @param #SPOT self + -- @param #number Delay + -- @param Wrapper.Positionable#POSITIONABLE Target + -- @param #number LaserCode Laser code. + -- @param #number Duration Duration of lasing in seconds. + + + + self:AddTransition( "On", "Lasing", "On" ) + self:AddTransition( { "On", "Destroyed" } , "LaseOff", "Off" ) + + --- LaseOff Handler OnBefore for SPOT + -- @function [parent=#SPOT] OnBeforeLaseOff + -- @param #SPOT self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- LaseOff Handler OnAfter for SPOT + -- @function [parent=#SPOT] OnAfterLaseOff + -- @param #SPOT self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- LaseOff Trigger for SPOT + -- @function [parent=#SPOT] LaseOff + -- @param #SPOT self + + --- LaseOff Asynchronous Trigger for SPOT + -- @function [parent=#SPOT] __LaseOff + -- @param #SPOT self + -- @param #number Delay + + self:AddTransition( "*" , "Destroyed", "Destroyed" ) + + --- Destroyed Handler OnBefore for SPOT + -- @function [parent=#SPOT] OnBeforeDestroyed + -- @param #SPOT self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Destroyed Handler OnAfter for SPOT + -- @function [parent=#SPOT] OnAfterDestroyed + -- @param #SPOT self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Destroyed Trigger for SPOT + -- @function [parent=#SPOT] Destroyed + -- @param #SPOT self + + --- Destroyed Asynchronous Trigger for SPOT + -- @function [parent=#SPOT] __Destroyed + -- @param #SPOT self + -- @param #number Delay + + + + self.Recce = Recce + + self.LaseScheduler = SCHEDULER:New( self ) + + self:SetEventPriority( 5 ) + + self.Lasing = false + + return self + end + + --- On after LaseOn event. Activates the laser spot. + -- @param #SPOT self + -- @param From + -- @param Event + -- @param To + -- @param Wrapper.Positionable#POSITIONABLE Target Unit that is being lased. + -- @param #number LaserCode Laser code. + -- @param #number Duration Duration of lasing in seconds. + function SPOT:onafterLaseOn( From, Event, To, Target, LaserCode, Duration ) + self:F( { "LaseOn", Target, LaserCode, Duration } ) + + local function StopLase( self ) + self:LaseOff() + end + + self.Target = Target + self.LaserCode = LaserCode + + self.Lasing = true + + local RecceDcsUnit = self.Recce:GetDCSObject() + + self.SpotIR = Spot.createInfraRed( RecceDcsUnit, { x = 0, y = 2, z = 0 }, Target:GetPointVec3():AddY(1):GetVec3() ) + self.SpotLaser = Spot.createLaser( RecceDcsUnit, { x = 0, y = 2, z = 0 }, Target:GetPointVec3():AddY(1):GetVec3(), LaserCode ) + + if Duration then + self.ScheduleID = self.LaseScheduler:Schedule( self, StopLase, {self}, Duration ) + end + + self:HandleEvent( EVENTS.Dead ) + + self:__Lasing( -1 ) + end + + + --- On after LaseOnCoordinate event. Activates the laser spot. + -- @param #SPOT self + -- @param From + -- @param Event + -- @param To + -- @param Core.Point#COORDINATE Coordinate The coordinate at which the laser is pointing. + -- @param #number LaserCode Laser code. + -- @param #number Duration Duration of lasing in seconds. + function SPOT:onafterLaseOnCoordinate(From, Event, To, Coordinate, LaserCode, Duration) + self:F( { "LaseOnCoordinate", Coordinate, LaserCode, Duration } ) + + local function StopLase( self ) + self:LaseOff() + end + + self.Target = nil + self.TargetCoord=Coordinate + self.LaserCode = LaserCode + + self.Lasing = true + + local RecceDcsUnit = self.Recce:GetDCSObject() + + self.SpotIR = Spot.createInfraRed( RecceDcsUnit, { x = 0, y = 1, z = 0 }, Coordinate:GetVec3() ) + self.SpotLaser = Spot.createLaser( RecceDcsUnit, { x = 0, y = 1, z = 0 }, Coordinate:GetVec3(), LaserCode ) + + if Duration then + self.ScheduleID = self.LaseScheduler:Schedule( self, StopLase, {self}, Duration ) + end + + self:__Lasing(-1) + end + + --- @param #SPOT self + -- @param Core.Event#EVENTDATA EventData + function SPOT:OnEventDead(EventData) + self:F( { Dead = EventData.IniDCSUnitName, Target = self.Target } ) + if self.Target then + if EventData.IniDCSUnitName == self.Target:GetName() then + self:F( {"Target dead ", self.Target:GetName() } ) + self:Destroyed() + self:LaseOff() + end + end + end + + --- @param #SPOT self + -- @param From + -- @param Event + -- @param To + function SPOT:onafterLasing( From, Event, To ) + + if self.Target and self.Target:IsAlive() then + self.SpotIR:setPoint( self.Target:GetPointVec3():AddY(1):AddY(math.random(-100,100)/100):AddX(math.random(-100,100)/100):GetVec3() ) + self.SpotLaser:setPoint( self.Target:GetPointVec3():AddY(1):GetVec3() ) + self:__Lasing( -0.2 ) + elseif self.TargetCoord then + + -- Wiggle the IR spot a bit. + local irvec3={x=self.TargetCoord.x+math.random(-100,100)/100, y=self.TargetCoord.y+math.random(-100,100)/100, z=self.TargetCoord.z} --#DCS.Vec3 + local lsvec3={x=self.TargetCoord.x, y=self.TargetCoord.y, z=self.TargetCoord.z} --#DCS.Vec3 + + self.SpotIR:setPoint(irvec3) + self.SpotLaser:setPoint(lsvec3) + + self:__Lasing(-0.25) + else + self:F( { "Target is not alive", self.Target:IsAlive() } ) + end + + end + + --- @param #SPOT self + -- @param From + -- @param Event + -- @param To + -- @return #SPOT + function SPOT:onafterLaseOff( From, Event, To ) + + self:F( {"Stopped lasing for ", self.Target and self.Target:GetName() or "coord", SpotIR = self.SportIR, SpotLaser = self.SpotLaser } ) + + self.Lasing = false + + self.SpotIR:destroy() + self.SpotLaser:destroy() + + self.SpotIR = nil + self.SpotLaser = nil + + if self.ScheduleID then + self.LaseScheduler:Stop(self.ScheduleID) + end + self.ScheduleID = nil + + self.Target = nil + + return self + end + + --- Check if the SPOT is lasing + -- @param #SPOT self + -- @return #boolean true if it is lasing + function SPOT:IsLasing() + return self.Lasing + end + +end--- **Core** - A* Pathfinding. +-- +-- **Main Features:** +-- +-- * Find path from A to B. +-- * Pre-defined as well as custom valid neighbour functions. +-- * Pre-defined as well as custom cost functions. +-- * Easy rectangular grid setup. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Core.Astar +-- @image CORE_Astar.png + + +--- ASTAR class. +-- @type ASTAR +-- @field #string ClassName Name of the class. +-- @field #boolean Debug Debug mode. Messages to all about status. +-- @field #string lid Class id string for output to DCS log file. +-- @field #table nodes Table of nodes. +-- @field #number counter Node counter. +-- @field #number Nnodes Number of nodes. +-- @field #number nvalid Number of nvalid calls. +-- @field #number nvalidcache Number of cached valid evals. +-- @field #number ncost Number of cost evaluations. +-- @field #number ncostcache Number of cached cost evals. +-- @field #ASTAR.Node startNode Start node. +-- @field #ASTAR.Node endNode End node. +-- @field Core.Point#COORDINATE startCoord Start coordinate. +-- @field Core.Point#COORDINATE endCoord End coordinate. +-- @field #function ValidNeighbourFunc Function to check if a node is valid. +-- @field #table ValidNeighbourArg Optional arguments passed to the valid neighbour function. +-- @field #function CostFunc Function to calculate the heuristic "cost" to go from one node to another. +-- @field #table CostArg Optional arguments passed to the cost function. +-- @extends Core.Base#BASE + +--- **When nothing goes right... Go left!** +-- +-- === +-- +-- ![Banner Image](..\Presentations\Astar\ASTAR_Main.jpg) +-- +-- # The ASTAR Concept +-- +-- Pathfinding algorithm. +-- +-- +-- # Start and Goal +-- +-- The first thing we need to define is obviously the place where we want to start and where we want to go eventually. +-- +-- ## Start +-- +-- The start +-- +-- ## Goal +-- +-- +-- # Nodes +-- +-- ## Rectangular Grid +-- +-- A rectangular grid can be created using the @{#ASTAR.CreateGrid}(*ValidSurfaceTypes, BoxHY, SpaceX, deltaX, deltaY, MarkGrid*), where +-- +-- * *ValidSurfaceTypes* is a table of valid surface types. By default all surface types are valid. +-- * *BoxXY* is the width of the grid perpendicular the the line between start and end node. Default is 40,000 meters (40 km). +-- * *SpaceX* is the additional space behind the start and end nodes. Default is 20,000 meters (20 km). +-- * *deltaX* is the grid spacing between nodes in the direction of start and end node. Default is 2,000 meters (2 km). +-- * *deltaY* is the grid spacing perpendicular to the direction of start and end node. Default is the same as *deltaX*. +-- * *MarkGrid* If set to *true*, this places marker on the F10 map on each grid node. Note that this can stall DCS if too many nodes are created. +-- +-- ## Valid Surfaces +-- +-- Certain unit types can only travel on certain surfaces types, for example +-- +-- * Naval units can only travel on water (that also excludes shallow water in DCS currently), +-- * Ground units can only traval on land. +-- +-- By restricting the surface type in the grid construction, we also reduce the number of nodes, which makes the algorithm more efficient. +-- +-- ## Box Width (BoxHY) +-- +-- The box width needs to be large enough to capture all paths you want to consider. +-- +-- ## Space in X +-- +-- The space in X value is important if the algorithm needs to to backwards from the start node or needs to extend even further than the end node. +-- +-- ## Grid Spacing +-- +-- The grid spacing is an important factor as it determines the number of nodes and hence the performance of the algorithm. It should be as large as possible. +-- However, if the value is too large, the algorithm might fail to get a valid path. +-- +-- A good estimate of the grid spacing is to set it to be smaller (~ half the size) of the smallest gap you need to path. +-- +-- # Valid Neighbours +-- +-- The A* algorithm needs to know if a transition from one node to another is allowed or not. By default, hopping from one node to another is always possible. +-- +-- ## Line of Sight +-- +-- For naval +-- +-- +-- # Heuristic Cost +-- +-- In order to determine the optimal path, the pathfinding algorithm needs to know, how costly it is to go from one node to another. +-- Often, this can simply be determined by the distance between two nodes. Therefore, the default cost function is set to be the 2D distance between two nodes. +-- +-- +-- # Calculate the Path +-- +-- Finally, we have to calculate the path. This is done by the @{ASTAR.GetPath}(*ExcludeStart, ExcludeEnd*) function. This function returns a table of nodes, which +-- describe the optimal path from the start node to the end node. +-- +-- By default, the start and end node are include in the table that is returned. +-- +-- Note that a valid path must not always exist. So you should check if the function returns *nil*. +-- +-- Common reasons that a path cannot be found are: +-- +-- * The grid is too small ==> increase grid size, e.g. *BoxHY* and/or *SpaceX* if you use a rectangular grid. +-- * The grid spacing is too large ==> decrease *deltaX* and/or *deltaY* +-- * There simply is no valid path ==> you are screwed :( +-- +-- +-- # Examples +-- +-- ## Strait of Hormuz +-- +-- Carrier Group finds its way through the Stait of Hormuz. +-- +-- ## +-- +-- +-- +-- @field #ASTAR +ASTAR = { + ClassName = "ASTAR", + Debug = nil, + lid = nil, + nodes = {}, + counter = 1, + Nnodes = 0, + ncost = 0, + ncostcache = 0, + nvalid = 0, + nvalidcache = 0, +} + +--- Node data. +-- @type ASTAR.Node +-- @field #number id Node id. +-- @field Core.Point#COORDINATE coordinate Coordinate of the node. +-- @field #number surfacetype Surface type. +-- @field #table valid Cached valid/invalid nodes. +-- @field #table cost Cached cost. + +--- ASTAR infinity. +-- @field #number INF +ASTAR.INF=1/0 + +--- ASTAR class version. +-- @field #string version +ASTAR.version="0.4.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Add more valid neighbour functions. +-- TODO: Write docs. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new ASTAR object. +-- @param #ASTAR self +-- @return #ASTAR self +function ASTAR:New() + + -- Inherit everything from INTEL class. + local self=BASE:Inherit(self, BASE:New()) --#ASTAR + + self.lid="ASTAR | " + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set coordinate from where to start. +-- @param #ASTAR self +-- @param Core.Point#COORDINATE Coordinate Start coordinate. +-- @return #ASTAR self +function ASTAR:SetStartCoordinate(Coordinate) + + self.startCoord=Coordinate + + return self +end + +--- Set coordinate where you want to go. +-- @param #ASTAR self +-- @param Core.Point#COORDINATE Coordinate end coordinate. +-- @return #ASTAR self +function ASTAR:SetEndCoordinate(Coordinate) + + self.endCoord=Coordinate + + return self +end + +--- Create a node from a given coordinate. +-- @param #ASTAR self +-- @param Core.Point#COORDINATE Coordinate The coordinate where to create the node. +-- @return #ASTAR.Node The node. +function ASTAR:GetNodeFromCoordinate(Coordinate) + + local node={} --#ASTAR.Node + + node.coordinate=Coordinate + node.surfacetype=Coordinate:GetSurfaceType() + node.id=self.counter + + node.valid={} + node.cost={} + + self.counter=self.counter+1 + + return node +end + + +--- Add a node to the table of grid nodes. +-- @param #ASTAR self +-- @param #ASTAR.Node Node The node to be added. +-- @return #ASTAR self +function ASTAR:AddNode(Node) + + self.nodes[Node.id]=Node + self.Nnodes=self.Nnodes+1 + + return self +end + +--- Add a node to the table of grid nodes specifying its coordinate. +-- @param #ASTAR self +-- @param Core.Point#COORDINATE Coordinate The coordinate where the node is created. +-- @return #ASTAR.Node The node. +function ASTAR:AddNodeFromCoordinate(Coordinate) + + local node=self:GetNodeFromCoordinate(Coordinate) + + self:AddNode(node) + + return node +end + +--- Check if the coordinate of a node has is at a valid surface type. +-- @param #ASTAR self +-- @param #ASTAR.Node Node The node to be added. +-- @param #table SurfaceTypes Surface types, for example `{land.SurfaceType.WATER}`. By default all surface types are valid. +-- @return #boolean If true, surface type of node is valid. +function ASTAR:CheckValidSurfaceType(Node, SurfaceTypes) + + if SurfaceTypes then + + if type(SurfaceTypes)~="table" then + SurfaceTypes={SurfaceTypes} + end + + for _,surface in pairs(SurfaceTypes) do + if surface==Node.surfacetype then + return true + end + end + + return false + + else + return true + end + +end + +--- Add a function to determine if a neighbour of a node is valid. +-- @param #ASTAR self +-- @param #function NeighbourFunction Function that needs to return *true* for a neighbour to be valid. +-- @param ... Condition function arguments if any. +-- @return #ASTAR self +function ASTAR:SetValidNeighbourFunction(NeighbourFunction, ...) + + self.ValidNeighbourFunc=NeighbourFunction + + self.ValidNeighbourArg={} + if arg then + self.ValidNeighbourArg=arg + end + + return self +end + + +--- Set valid neighbours to require line of sight between two nodes. +-- @param #ASTAR self +-- @param #number CorridorWidth Width of LoS corridor in meters. +-- @return #ASTAR self +function ASTAR:SetValidNeighbourLoS(CorridorWidth) + + self:SetValidNeighbourFunction(ASTAR.LoS, CorridorWidth) + + return self +end + +--- Set valid neighbours to be in a certain distance. +-- @param #ASTAR self +-- @param #number MaxDistance Max distance between nodes in meters. Default is 2000 m. +-- @return #ASTAR self +function ASTAR:SetValidNeighbourDistance(MaxDistance) + + self:SetValidNeighbourFunction(ASTAR.DistMax, MaxDistance) + + return self +end + +--- Set valid neighbours to be in a certain distance. +-- @param #ASTAR self +-- @param #number MaxDistance Max distance between nodes in meters. Default is 2000 m. +-- @return #ASTAR self +function ASTAR:SetValidNeighbourRoad(MaxDistance) + + self:SetValidNeighbourFunction(ASTAR.Road, MaxDistance) + + return self +end + +--- Set the function which calculates the "cost" to go from one to another node. +-- The first to arguments of this function are always the two nodes under consideration. But you can add optional arguments. +-- Very often the distance between nodes is a good measure for the cost. +-- @param #ASTAR self +-- @param #function CostFunction Function that returns the "cost". +-- @param ... Condition function arguments if any. +-- @return #ASTAR self +function ASTAR:SetCostFunction(CostFunction, ...) + + self.CostFunc=CostFunction + + self.CostArg={} + if arg then + self.CostArg=arg + end + + return self +end + +--- Set heuristic cost to go from one node to another to be their 2D distance. +-- @param #ASTAR self +-- @return #ASTAR self +function ASTAR:SetCostDist2D() + + self:SetCostFunction(ASTAR.Dist2D) + + return self +end + +--- Set heuristic cost to go from one node to another to be their 3D distance. +-- @param #ASTAR self +-- @return #ASTAR self +function ASTAR:SetCostDist3D() + + self:SetCostFunction(ASTAR.Dist3D) + + return self +end + +--- Set heuristic cost to go from one node to another to be their 3D distance. +-- @param #ASTAR self +-- @return #ASTAR self +function ASTAR:SetCostRoad() + + self:SetCostFunction(ASTAR) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Grid functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a rectangular grid of nodes between star and end coordinate. +-- The coordinate system is oriented along the line between start and end point. +-- @param #ASTAR self +-- @param #table ValidSurfaceTypes Valid surface types. By default is all surfaces are allowed. +-- @param #number BoxHY Box "height" in meters along the y-coordinate. Default 40000 meters (40 km). +-- @param #number SpaceX Additional space in meters before start and after end coordinate. Default 10000 meters (10 km). +-- @param #number deltaX Increment in the direction of start to end coordinate in meters. Default 2000 meters. +-- @param #number deltaY Increment perpendicular to the direction of start to end coordinate in meters. Default is same as deltaX. +-- @param #boolean MarkGrid If true, create F10 map markers at grid nodes. +-- @return #ASTAR self +function ASTAR:CreateGrid(ValidSurfaceTypes, BoxHY, SpaceX, deltaX, deltaY, MarkGrid) + + -- Note that internally + -- x coordinate is z: x-->z Line from start to end + -- y coordinate is x: y-->x Perpendicular + + -- Grid length and width. + local Dz=SpaceX or 10000 + local Dx=BoxHY and BoxHY/2 or 20000 + + -- Increments. + local dz=deltaX or 2000 + local dx=deltaY or dz + + -- Heading from start to end coordinate. + local angle=self.startCoord:HeadingTo(self.endCoord) + + --Distance between start and end. + local dist=self.startCoord:Get2DDistance(self.endCoord)+2*Dz + + -- Origin of map. Needed to translate back to wanted position. + local co=COORDINATE:New(0, 0, 0) + local do1=co:Get2DDistance(self.startCoord) + local ho1=co:HeadingTo(self.startCoord) + + -- Start of grid. + local xmin=-Dx + local zmin=-Dz + + -- Number of grid points. + local nz=dist/dz+1 + local nx=2*Dx/dx+1 + + -- Debug info. + local text=string.format("Building grid with nx=%d ny=%d => total=%d nodes", nx, nz, nx*nz) + self:T(self.lid..text) + + -- Loop over x and z coordinate to create a 2D grid. + for i=1,nx do + + -- x coordinate perpendicular to z. + local x=xmin+dx*(i-1) + + for j=1,nz do + + -- z coordinate connecting start and end. + local z=zmin+dz*(j-1) + + -- Rotate 2D. + local vec3=UTILS.Rotate2D({x=x, y=0, z=z}, angle) + + -- Coordinate of the node. + local c=COORDINATE:New(vec3.z, vec3.y, vec3.x):Translate(do1, ho1, true) + + -- Create a node at this coordinate. + local node=self:GetNodeFromCoordinate(c) + + -- Check if node has valid surface type. + if self:CheckValidSurfaceType(node, ValidSurfaceTypes) then + + if MarkGrid then + c:MarkToAll(string.format("i=%d, j=%d surface=%d", i, j, node.surfacetype)) + end + + -- Add node to grid. + self:AddNode(node) + + end + + end + end + + -- Debug info. + local text=string.format("Done building grid!") + self:T2(self.lid..text) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Valid neighbour functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Function to check if two nodes have line of sight (LoS). +-- @param #ASTAR.Node nodeA First node. +-- @param #ASTAR.Node nodeB Other node. +-- @param #number corridor (Optional) Width of corridor in meters. +-- @return #boolean If true, two nodes have LoS. +function ASTAR.LoS(nodeA, nodeB, corridor) + + local offset=1 + + local dx=corridor and corridor/2 or nil + local dy=dx + + local cA=nodeA.coordinate:GetVec3() + local cB=nodeB.coordinate:GetVec3() + cA.y=offset + cB.y=offset + + local los=land.isVisible(cA, cB) + + if los and corridor then + + -- Heading from A to B. + local heading=nodeA.coordinate:HeadingTo(nodeB.coordinate) + + local Ap=UTILS.VecTranslate(cA, dx, heading+90) + local Bp=UTILS.VecTranslate(cB, dx, heading+90) + + los=land.isVisible(Ap, Bp) + + if los then + + local Am=UTILS.VecTranslate(cA, dx, heading-90) + local Bm=UTILS.VecTranslate(cB, dx, heading-90) + + los=land.isVisible(Am, Bm) + end + + end + + return los +end + +--- Function to check if two nodes have a road connection. +-- @param #ASTAR.Node nodeA First node. +-- @param #ASTAR.Node nodeB Other node. +-- @return #boolean If true, two nodes are connected via a road. +function ASTAR.Road(nodeA, nodeB) + + local path=land.findPathOnRoads("roads", nodeA.coordinate.x, nodeA.coordinate.z, nodeB.coordinate.x, nodeB.coordinate.z) + + if path then + return true + else + return false + end + +end + +--- Function to check if distance between two nodes is less than a threshold distance. +-- @param #ASTAR.Node nodeA First node. +-- @param #ASTAR.Node nodeB Other node. +-- @param #number distmax Max distance in meters. Default is 2000 m. +-- @return #boolean If true, distance between the two nodes is below threshold. +function ASTAR.DistMax(nodeA, nodeB, distmax) + + distmax=distmax or 2000 + + local dist=nodeA.coordinate:Get2DDistance(nodeB.coordinate) + + return dist<=distmax +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Heuristic cost functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Heuristic cost is given by the 2D distance between the nodes. +-- @param #ASTAR.Node nodeA First node. +-- @param #ASTAR.Node nodeB Other node. +-- @return #number Distance between the two nodes. +function ASTAR.Dist2D(nodeA, nodeB) + local dist=nodeA.coordinate:Get2DDistance(nodeB) + return dist +end + +--- Heuristic cost is given by the 3D distance between the nodes. +-- @param #ASTAR.Node nodeA First node. +-- @param #ASTAR.Node nodeB Other node. +-- @return #number Distance between the two nodes. +function ASTAR.Dist3D(nodeA, nodeB) + local dist=nodeA.coordinate:Get3DDistance(nodeB.coordinate) + return dist +end + +--- Heuristic cost is given by the distance between the nodes on road. +-- @param #ASTAR.Node nodeA First node. +-- @param #ASTAR.Node nodeB Other node. +-- @return #number Distance between the two nodes. +function ASTAR.DistRoad(nodeA, nodeB) + + -- Get the path. + local path=land.findPathOnRoads("roads", nodeA.coordinate.x, nodeA.coordinate.z, nodeB.coordinate.x, nodeB.coordinate.z) + + if path then + + local dist=0 + + for i=2,#path do + local b=path[i] --DCS#Vec2 + local a=path[i-1] --DCS#Vec2 + + dist=dist+UTILS.VecDist2D(a,b) + + end + + return dist + end + + + return math.huge +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Find the closest node from a given coordinate. +-- @param #ASTAR self +-- @param Core.Point#COORDINATE Coordinate. +-- @return #ASTAR.Node Cloest node to the coordinate. +-- @return #number Distance to closest node in meters. +function ASTAR:FindClosestNode(Coordinate) + + local distMin=math.huge + local closeNode=nil + + for _,_node in pairs(self.nodes) do + local node=_node --#ASTAR.Node + + local dist=node.coordinate:Get2DDistance(Coordinate) + + if dist1000 then + self:T(self.lid.."Adding start node to node grid!") + self:AddNode(node) + end + + return self +end + +--- Add a node. +-- @param #ASTAR self +-- @param #ASTAR.Node Node The node to be added to the nodes table. +-- @return #ASTAR self +function ASTAR:FindEndNode() + + local node, dist=self:FindClosestNode(self.endCoord) + + self.endNode=node + + if dist>1000 then + self:T(self.lid.."Adding end node to node grid!") + self:AddNode(node) + end + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Main A* pathfinding function +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- A* pathfinding function. This seaches the path along nodes between start and end nodes/coordinates. +-- @param #ASTAR self +-- @param #boolean ExcludeStartNode If *true*, do not include start node in found path. Default is to include it. +-- @param #boolean ExcludeEndNode If *true*, do not include end node in found path. Default is to include it. +-- @return #table Table of nodes from start to finish. +function ASTAR:GetPath(ExcludeStartNode, ExcludeEndNode) + + self:FindStartNode() + self:FindEndNode() + + local nodes=self.nodes + local start=self.startNode + local goal=self.endNode + + -- Sets. + local openset = {} + local closedset = {} + local came_from = {} + local g_score = {} + local f_score = {} + + openset[start.id]=true + local Nopen=1 + + -- Initial scores. + g_score[start.id]=0 + f_score[start.id]=g_score[start.id]+self:_HeuristicCost(start, goal) + + -- Set start time. + local T0=timer.getAbsTime() + + -- Debug message. + local text=string.format("Starting A* pathfinding with %d Nodes", self.Nnodes) + self:T(self.lid..text) + + local Tstart=UTILS.GetOSTime() + + -- Loop while we still have an open set. + while Nopen > 0 do + + -- Get current node. + local current=self:_LowestFscore(openset, f_score) + + -- Check if we are at the end node. + if current.id==goal.id then + + local path=self:_UnwindPath({}, came_from, goal) + + if not ExcludeEndNode then + table.insert(path, goal) + end + + if ExcludeStartNode then + table.remove(path, 1) + end + + local Tstop=UTILS.GetOSTime() + + local dT=nil + if Tstart and Tstop then + dT=Tstop-Tstart + end + + -- Debug message. + local text=string.format("Found path with %d nodes (%d total)", #path, self.Nnodes) + if dT then + text=text..string.format(", OS Time %.6f sec", dT) + end + text=text..string.format(", Nvalid=%d [%d cached]", self.nvalid, self.nvalidcache) + text=text..string.format(", Ncost=%d [%d cached]", self.ncost, self.ncostcache) + self:T(self.lid..text) + + return path + end + + -- Move Node from open to closed set. + openset[current.id]=nil + Nopen=Nopen-1 + closedset[current.id]=true + + -- Get neighbour nodes. + local neighbors=self:_NeighbourNodes(current, nodes) + + -- Loop over neighbours. + for _,neighbor in pairs(neighbors) do + + if self:_NotIn(closedset, neighbor.id) then + + local tentative_g_score=g_score[current.id]+self:_DistNodes(current, neighbor) + + if self:_NotIn(openset, neighbor.id) or tentative_g_score < g_score[neighbor.id] then + + came_from[neighbor]=current + + g_score[neighbor.id]=tentative_g_score + f_score[neighbor.id]=g_score[neighbor.id]+self:_HeuristicCost(neighbor, goal) + + if self:_NotIn(openset, neighbor.id) then + -- Add to open set. + openset[neighbor.id]=true + Nopen=Nopen+1 + end + + end + end + end + end + + -- Debug message. + local text=string.format("WARNING: Could NOT find valid path!") + self:E(self.lid..text) + MESSAGE:New(text, 60, "ASTAR"):ToAllIf(self.Debug) + + return nil -- no valid path +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- A* pathfinding helper functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Heuristic "cost" function to go from node A to node B. Default is the distance between the nodes. +-- @param #ASTAR self +-- @param #ASTAR.Node nodeA Node A. +-- @param #ASTAR.Node nodeB Node B. +-- @return #number "Cost" to go from node A to node B. +function ASTAR:_HeuristicCost(nodeA, nodeB) + + -- Counter. + self.ncost=self.ncost+1 + + -- Get chached cost if available. + local cost=nodeA.cost[nodeB.id] + if cost~=nil then + self.ncostcache=self.ncostcache+1 + return cost + end + + local cost=nil + if self.CostFunc then + cost=self.CostFunc(nodeA, nodeB, unpack(self.CostArg)) + else + cost=self:_DistNodes(nodeA, nodeB) + end + + nodeA.cost[nodeB.id]=cost + nodeB.cost[nodeA.id]=cost -- Symmetric problem. + + return cost +end + +--- Check if going from a node to a neighbour is possible. +-- @param #ASTAR self +-- @param #ASTAR.Node node A node. +-- @param #ASTAR.Node neighbor Neighbour node. +-- @return #boolean If true, transition between nodes is possible. +function ASTAR:_IsValidNeighbour(node, neighbor) + + -- Counter. + self.nvalid=self.nvalid+1 + + local valid=node.valid[neighbor.id] + if valid~=nil then + --env.info(string.format("Node %d has valid=%s neighbour %d", node.id, tostring(valid), neighbor.id)) + self.nvalidcache=self.nvalidcache+1 + return valid + end + + local valid=nil + if self.ValidNeighbourFunc then + valid=self.ValidNeighbourFunc(node, neighbor, unpack(self.ValidNeighbourArg)) + else + valid=true + end + + node.valid[neighbor.id]=valid + neighbor.valid[node.id]=valid -- Symmetric problem. + + return valid +end + +--- Calculate 2D distance between two nodes. +-- @param #ASTAR self +-- @param #ASTAR.Node nodeA Node A. +-- @param #ASTAR.Node nodeB Node B. +-- @return #number Distance between nodes in meters. +function ASTAR:_DistNodes(nodeA, nodeB) + return nodeA.coordinate:Get2DDistance(nodeB.coordinate) +end + +--- Function that calculates the lowest F score. +-- @param #ASTAR self +-- @param #table set The set of nodes IDs. +-- @param #number f_score F score. +-- @return #ASTAR.Node Best node. +function ASTAR:_LowestFscore(set, f_score) + + local lowest, bestNode = ASTAR.INF, nil + + for nid,node in pairs(set) do + + local score=f_score[nid] + + if score Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start the FSM. + self:AddTransition("*", "MarkAdded", "*") -- Start the FSM. + self:AddTransition("*", "MarkChanged", "*") -- Start the FSM. + self:AddTransition("*", "MarkDeleted", "*") -- Start the FSM. + self:AddTransition("Running", "Stop", "Stopped") -- Stop the FSM. + + self:HandleEvent(EVENTS.MarkAdded, self.OnEventMark) + self:HandleEvent(EVENTS.MarkChange, self.OnEventMark) + self:HandleEvent(EVENTS.MarkRemoved, self.OnEventMark) + + -- start + self:I(self.lid..string.format("started for %s",self.Tag)) + self:__Start(1) + return self + + ------------------- + -- PSEUDO Functions + ------------------- + + --- On after "MarkAdded" event. Triggered when a Marker is added to the F10 map. + -- @function [parent=#MARKEROPS_BASE] OnAfterMarkAdded + -- @param #MARKEROPS_BASE self + -- @param #string From The From state + -- @param #string Event The Event called + -- @param #string To The To state + -- @param #string Text The text on the marker + -- @param #table Keywords Table of matching keywords found in the Event text + -- @param Core.Point#COORDINATE Coord Coordinate of the marker. + + --- On after "MarkChanged" event. Triggered when a Marker is changed on the F10 map. + -- @function [parent=#MARKEROPS_BASE] OnAfterMarkChanged + -- @param #MARKEROPS_BASE self + -- @param #string From The From state + -- @param #string Event The Event called + -- @param #string To The To state + -- @param #string Text The text on the marker + -- @param #table Keywords Table of matching keywords found in the Event text + -- @param Core.Point#COORDINATE Coord Coordinate of the marker. + + --- On after "MarkDeleted" event. Triggered when a Marker is deleted from the F10 map. + -- @function [parent=#MARKEROPS_BASE] OnAfterMarkDeleted + -- @param #MARKEROPS_BASE self + -- @param #string From The From state + -- @param #string Event The Event called + -- @param #string To The To state + + --- "Stop" trigger. Used to stop the function an unhandle events + -- @function [parent=#MARKEROPS_BASE] Stop + +end + +--- (internal) Handle events. +-- @param #MARKEROPS self +-- @param Core.Event#EVENTDATA Event +function MARKEROPS_BASE:OnEventMark(Event) + self:T({Event}) + if Event == nil or Event.idx == nil then + self:E("Skipping onEvent. Event or Event.idx unknown.") + return true + end + --position + local vec3={y=Event.pos.y, x=Event.pos.x, z=Event.pos.z} + local coord=COORDINATE:NewFromVec3(vec3) + if self.debug then + local coordtext = coord:ToStringLLDDM() + local text = tostring(Event.text) + local m = MESSAGE:New(string.format("Mark added at %s with text: %s",coordtext,text),10,"Info",false):ToAll() + end + -- decision + if Event.id==world.event.S_EVENT_MARK_ADDED then + self:T({event="S_EVENT_MARK_ADDED", carrier=self.groupname, vec3=Event.pos}) + -- Handle event + local Eventtext = tostring(Event.text) + if Eventtext~=nil then + if self:_MatchTag(Eventtext) then + local matchtable = self:_MatchKeywords(Eventtext) + self:MarkAdded(Eventtext,matchtable,coord) + end + end + elseif Event.id==world.event.S_EVENT_MARK_CHANGE then + self:T({event="S_EVENT_MARK_CHANGE", carrier=self.groupname, vec3=Event.pos}) + -- Handle event. + local Eventtext = tostring(Event.text) + if Eventtext~=nil then + if self:_MatchTag(Eventtext) then + local matchtable = self:_MatchKeywords(Eventtext) + self:MarkChanged(Eventtext,matchtable,coord) + end + end + elseif Event.id==world.event.S_EVENT_MARK_REMOVED then + self:T({event="S_EVENT_MARK_REMOVED", carrier=self.groupname, vec3=Event.pos}) + -- Hande event. + local Eventtext = tostring(Event.text) + if Eventtext~=nil then + if self:_MatchTag(Eventtext) then + self:MarkDeleted() + end + end + end +end + +--- (internal) Match tag. +-- @param #MARKEROPS self +-- @param #string Eventtext Text added to the marker. +-- @return #boolean +function MARKEROPS_BASE:_MatchTag(Eventtext) + local matches = false + local type = string.lower(self.Tag) -- #string + if string.find(string.lower(Eventtext),type) then + matches = true --event text contains tag + end + return matches +end + +--- (internal) Match keywords table. +-- @param #MARKEROPS self +-- @param #string Eventtext Text added to the marker. +-- @return #table +function MARKEROPS_BASE:_MatchKeywords(Eventtext) + local matchtable = {} + local keytable = self.Keywords + for _index,_word in pairs (keytable) do + if string.find(string.lower(Eventtext),string.lower(_word))then + table.insert(matchtable,_word) + end + end + return matchtable +end + +--- On before "MarkAdded" event. Triggered when a Marker is added to the F10 map. + -- @param #MARKEROPS_BASE self + -- @param #string From The From state + -- @param #string Event The Event called + -- @param #string To The To state + -- @param #string Text The text on the marker + -- @param #table Keywords Table of matching keywords found in the Event text + -- @param Core.Point#COORDINATE Coord Coordinate of the marker. +function MARKEROPS_BASE:onbeforeMarkAdded(From,Event,To,Text,Keywords,Coord) + self:T({self.lid,From,Event,To,Text,Keywords,Coord:ToStringLLDDM()}) +end + +--- On before "MarkChanged" event. Triggered when a Marker is changed on the F10 map. + -- @param #MARKEROPS_BASE self + -- @param #string From The From state + -- @param #string Event The Event called + -- @param #string To The To state + -- @param #string Text The text on the marker + -- @param #table Keywords Table of matching keywords found in the Event text + -- @param Core.Point#COORDINATE Coord Coordinate of the marker. +function MARKEROPS_BASE:onbeforeMarkChanged(From,Event,To,Text,Keywords,Coord) + self:T({self.lid,From,Event,To,Text,Keywords,Coord:ToStringLLDDM()}) +end + +--- On before "MarkDeleted" event. Triggered when a Marker is removed from the F10 map. + -- @param #MARKEROPS_BASE self + -- @param #string From The From state + -- @param #string Event The Event called + -- @param #string To The To state +function MARKEROPS_BASE:onbeforeMarkDeleted(From,Event,To) + self:T({self.lid,From,Event,To}) +end + +--- On enter "Stopped" event. Unsubscribe events. + -- @param #MARKEROPS_BASE self + -- @param #string From The From state + -- @param #string Event The Event called + -- @param #string To The To state +function MARKEROPS_BASE:onenterStopped(From,Event,To) + self:T({self.lid,From,Event,To}) + -- unsubscribe from events + self:UnHandleEvent(EVENTS.MarkAdded) + self:UnHandleEvent(EVENTS.MarkChange) + self:UnHandleEvent(EVENTS.MarkRemoved) +end + +-------------------------------------------------------------------------- +-- MARKEROPS_BASE Class Definition End. +-------------------------------------------------------------------------- +--- **Wrapper** -- OBJECT wraps the DCS Object derived objects. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- ### Contributions: +-- +-- === +-- +-- @module Wrapper.Object +-- @image MOOSE.JPG + + +--- @type OBJECT +-- @extends Core.Base#BASE +-- @field #string ObjectName The name of the Object. + + +--- Wrapper class to hendle the DCS Object objects. +-- +-- * Support all DCS Object APIs. +-- * Enhance with Object specific APIs not in the DCS Object API set. +-- * Manage the "state" of the DCS Object. +-- +-- ## OBJECT constructor: +-- +-- The OBJECT class provides the following functions to construct a OBJECT instance: +-- +-- * @{Wrapper.Object#OBJECT.New}(): Create a OBJECT instance. +-- +-- @field #OBJECT +OBJECT = { + ClassName = "OBJECT", + ObjectName = "", +} + +--- A DCSObject +-- @type DCSObject +-- @field id_ The ID of the controllable in DCS + +--- Create a new OBJECT from a DCSObject +-- @param #OBJECT self +-- @param DCS#Object ObjectName The Object name +-- @return #OBJECT self +function OBJECT:New( ObjectName, Test ) + local self = BASE:Inherit( self, BASE:New() ) + self:F2( ObjectName ) + self.ObjectName = ObjectName + + return self +end + + +--- Returns the unit's unique identifier. +-- @param Wrapper.Object#OBJECT self +-- @return DCS#Object.ID ObjectID or #nil if the DCS Object is not existing or alive. Note that the ID is passed as a string and not a number. +function OBJECT:GetID() + + local DCSObject = self:GetDCSObject() + + if DCSObject then + local ObjectID = DCSObject:getID() + return ObjectID + end + + BASE:E( { "Cannot GetID", Name = self.ObjectName, Class = self:GetClassName() } ) + + return nil +end + +--- Destroys the OBJECT. +-- @param #OBJECT self +-- @return #boolean true if the object is destroyed. +-- @return #nil The DCS Unit is not existing or alive. +function OBJECT:Destroy() + + local DCSObject = self:GetDCSObject() + + if DCSObject then + --BASE:CreateEventCrash( timer.getTime(), DCSObject ) + DCSObject:destroy( false ) + return true + end + + BASE:E( { "Cannot Destroy", Name = self.ObjectName, Class = self:GetClassName() } ) + + return nil +end + + + + + + +--- **Wrapper** -- IDENTIFIABLE is an intermediate class wrapping DCS Object class derived Objects. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- ### Contributions: +-- +-- === +-- +-- @module Wrapper.Identifiable +-- @image MOOSE.JPG + +--- @type IDENTIFIABLE +-- @extends Wrapper.Object#OBJECT +-- @field #string IdentifiableName The name of the identifiable. + +--- Wrapper class to handle the DCS Identifiable objects. +-- +-- * Support all DCS Identifiable APIs. +-- * Enhance with Identifiable specific APIs not in the DCS Identifiable API set. +-- * Manage the "state" of the DCS Identifiable. +-- +-- ## IDENTIFIABLE constructor +-- +-- The IDENTIFIABLE class provides the following functions to construct a IDENTIFIABLE instance: +-- +-- * @{#IDENTIFIABLE.New}(): Create a IDENTIFIABLE instance. +-- +-- @field #IDENTIFIABLE +IDENTIFIABLE = { + ClassName = "IDENTIFIABLE", + IdentifiableName = "", +} + +local _CategoryName = { + [Unit.Category.AIRPLANE] = "Airplane", + [Unit.Category.HELICOPTER] = "Helicoper", + [Unit.Category.GROUND_UNIT] = "Ground Identifiable", + [Unit.Category.SHIP] = "Ship", + [Unit.Category.STRUCTURE] = "Structure", + } + +--- Create a new IDENTIFIABLE from a DCSIdentifiable +-- @param #IDENTIFIABLE self +-- @param #string IdentifiableName The DCS Identifiable name +-- @return #IDENTIFIABLE self +function IDENTIFIABLE:New( IdentifiableName ) + local self = BASE:Inherit( self, OBJECT:New( IdentifiableName ) ) + self:F2( IdentifiableName ) + self.IdentifiableName = IdentifiableName + return self +end + +--- Returns if the Identifiable is alive. +-- If the Identifiable is not alive, nil is returned. +-- If the Identifiable is alive, true is returned. +-- @param #IDENTIFIABLE self +-- @return #boolean true if Identifiable is alive. +-- @return #nil if the Identifiable is not existing or is not alive. +function IDENTIFIABLE:IsAlive() + self:F3( self.IdentifiableName ) + + local DCSIdentifiable = self:GetDCSObject() -- DCS#Object + + if DCSIdentifiable then + local IdentifiableIsAlive = DCSIdentifiable:isExist() + return IdentifiableIsAlive + end + + return false +end + + + + +--- Returns DCS Identifiable object name. +-- The function provides access to non-activated objects too. +-- @param #IDENTIFIABLE self +-- @return #string The name of the DCS Identifiable. +-- @return #nil The DCS Identifiable is not existing or alive. +function IDENTIFIABLE:GetName() + self:F2( self.IdentifiableName ) + + local IdentifiableName = self.IdentifiableName + return IdentifiableName +end + + +--- Returns the type name of the DCS Identifiable. +-- @param #IDENTIFIABLE self +-- @return #string The type name of the DCS Identifiable. +function IDENTIFIABLE:GetTypeName() + self:F2( self.IdentifiableName ) + + local DCSIdentifiable = self:GetDCSObject() + + if DCSIdentifiable then + local IdentifiableTypeName = DCSIdentifiable:getTypeName() + self:T3( IdentifiableTypeName ) + return IdentifiableTypeName + end + + self:F( self.ClassName .. " " .. self.IdentifiableName .. " not found!" ) + return nil +end + + +--- Returns object category of the DCS Identifiable. One of +-- +-- * Object.Category.UNIT = 1 +-- * Object.Category.WEAPON = 2 +-- * Object.Category.STATIC = 3 +-- * Object.Category.BASE = 4 +-- * Object.Category.SCENERY = 5 +-- * Object.Category.Cargo = 6 +-- +-- @param #IDENTIFIABLE self +-- @return DCS#Object.Category The category ID, i.e. a number. +function IDENTIFIABLE:GetCategory() + self:F2( self.ObjectName ) + + local DCSObject = self:GetDCSObject() + if DCSObject then + local ObjectCategory = DCSObject:getCategory() + self:T3( ObjectCategory ) + return ObjectCategory + end + + return nil +end + + +--- Returns the DCS Identifiable category name as defined within the DCS Identifiable Descriptor. +-- @param #IDENTIFIABLE self +-- @return #string The DCS Identifiable Category Name +function IDENTIFIABLE:GetCategoryName() + local DCSIdentifiable = self:GetDCSObject() + + if DCSIdentifiable then + local IdentifiableCategoryName = _CategoryName[ self:GetDesc().category ] + return IdentifiableCategoryName + end + + self:F( self.ClassName .. " " .. self.IdentifiableName .. " not found!" ) + return nil +end + +--- Returns coalition of the Identifiable. +-- @param #IDENTIFIABLE self +-- @return DCS#coalition.side The side of the coalition. +-- @return #nil The DCS Identifiable is not existing or alive. +function IDENTIFIABLE:GetCoalition() + self:F2( self.IdentifiableName ) + + local DCSIdentifiable = self:GetDCSObject() + + if DCSIdentifiable then + local IdentifiableCoalition = DCSIdentifiable:getCoalition() + self:T3( IdentifiableCoalition ) + return IdentifiableCoalition + end + + self:F( self.ClassName .. " " .. self.IdentifiableName .. " not found!" ) + return nil +end + +--- Returns the name of the coalition of the Identifiable. +-- @param #IDENTIFIABLE self +-- @return #string The name of the coalition. +-- @return #nil The DCS Identifiable is not existing or alive. +function IDENTIFIABLE:GetCoalitionName() + self:F2( self.IdentifiableName ) + + local DCSIdentifiable = self:GetDCSObject() + + if DCSIdentifiable then + + -- Get coaliton ID. + local IdentifiableCoalition = DCSIdentifiable:getCoalition() + self:T3( IdentifiableCoalition ) + + return UTILS.GetCoalitionName(IdentifiableCoalition) + + end + + self:F( self.ClassName .. " " .. self.IdentifiableName .. " not found!" ) + return nil +end + +--- Returns country of the Identifiable. +-- @param #IDENTIFIABLE self +-- @return DCS#country.id The country identifier. +-- @return #nil The DCS Identifiable is not existing or alive. +function IDENTIFIABLE:GetCountry() + self:F2( self.IdentifiableName ) + + local DCSIdentifiable = self:GetDCSObject() + + if DCSIdentifiable then + local IdentifiableCountry = DCSIdentifiable:getCountry() + self:T3( IdentifiableCountry ) + return IdentifiableCountry + end + + self:F( self.ClassName .. " " .. self.IdentifiableName .. " not found!" ) + return nil +end + +--- Returns country name of the Identifiable. +-- @param #IDENTIFIABLE self +-- @return #string Name of the country. +function IDENTIFIABLE:GetCountryName() + self:F2( self.IdentifiableName ) + local countryid=self:GetCountry() + for name,id in pairs(country.id) do + if countryid==id then + return name + end + end +end + +--- Returns Identifiable descriptor. Descriptor type depends on Identifiable category. +-- @param #IDENTIFIABLE self +-- @return DCS#Object.Desc The Identifiable descriptor. +-- @return #nil The DCS Identifiable is not existing or alive. +function IDENTIFIABLE:GetDesc() + self:F2( self.IdentifiableName ) + + local DCSIdentifiable = self:GetDCSObject() -- DCS#Object + + if DCSIdentifiable then + local IdentifiableDesc = DCSIdentifiable:getDesc() + self:T2( IdentifiableDesc ) + return IdentifiableDesc + end + + self:F( self.ClassName .. " " .. self.IdentifiableName .. " not found!" ) + return nil +end + +--- Check if the Object has the attribute. +-- @param #IDENTIFIABLE self +-- @param #string AttributeName The attribute name. +-- @return #boolean true if the attribute exists. +-- @return #nil The DCS Identifiable is not existing or alive. +function IDENTIFIABLE:HasAttribute( AttributeName ) + self:F2( self.IdentifiableName ) + + local DCSIdentifiable = self:GetDCSObject() + + if DCSIdentifiable then + local IdentifiableHasAttribute = DCSIdentifiable:hasAttribute( AttributeName ) + self:T2( IdentifiableHasAttribute ) + return IdentifiableHasAttribute + end + + self:F( self.ClassName .. " " .. self.IdentifiableName .. " not found!" ) + return nil +end + +--- Gets the CallSign of the IDENTIFIABLE, which is a blank by default. +-- @param #IDENTIFIABLE self +-- @return #string The CallSign of the IDENTIFIABLE. +function IDENTIFIABLE:GetCallsign() + return '' +end + + +function IDENTIFIABLE:GetThreatLevel() + + return 0, "Scenery" +end +--- **Wrapper** -- POSITIONABLE wraps DCS classes that are "positionable". +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- ### Contributions: **Hardcard**, **funkyfranky** +-- +-- === +-- +-- @module Wrapper.Positionable +-- @image Wrapper_Positionable.JPG + +--- @type POSITIONABLE.__ Methods which are not intended for mission designers, but which are used interally by the moose designer :-) +-- @extends Wrapper.Identifiable#IDENTIFIABLE + +--- @type POSITIONABLE +-- @field Core.Point#COORDINATE coordinate Coordinate object. +-- @field Core.Point#POINT_VEC3 pointvec3 Point Vec3 object. +-- @extends Wrapper.Identifiable#IDENTIFIABLE + + +--- Wrapper class to handle the POSITIONABLE objects. +-- +-- * Support all DCS APIs. +-- * Enhance with POSITIONABLE specific APIs not in the DCS API set. +-- * Manage the "state" of the POSITIONABLE. +-- +-- ## POSITIONABLE constructor +-- +-- The POSITIONABLE class provides the following functions to construct a POSITIONABLE instance: +-- +-- * @{#POSITIONABLE.New}(): Create a POSITIONABLE instance. +-- +-- ## Get the current speed +-- +-- There are 3 methods that can be used to determine the speed. +-- Use @{#POSITIONABLE.GetVelocityKMH}() to retrieve the current speed in km/h. Use @{#POSITIONABLE.GetVelocityMPS}() to retrieve the speed in meters per second. +-- The method @{#POSITIONABLE.GetVelocity}() returns the speed vector (a Vec3). +-- +-- ## Get the current altitude +-- +-- Altitude can be retrieved using the method @{#POSITIONABLE.GetHeight}() and returns the current altitude in meters from the orthonormal plane. +-- +-- +-- @field #POSITIONABLE +POSITIONABLE = { + ClassName = "POSITIONABLE", + PositionableName = "", + coordinate = nil, + pointvec3 = nil, +} + +--- @field #POSITIONABLE.__ +POSITIONABLE.__ = {} + +--- @field #POSITIONABLE.__.Cargo +POSITIONABLE.__.Cargo = {} + + +--- A DCSPositionable +-- @type DCSPositionable +-- @field id_ The ID of the controllable in DCS + +--- Create a new POSITIONABLE from a DCSPositionable +-- @param #POSITIONABLE self +-- @param #string PositionableName The POSITIONABLE name +-- @return #POSITIONABLE self +function POSITIONABLE:New( PositionableName ) + local self = BASE:Inherit( self, IDENTIFIABLE:New( PositionableName ) ) -- #POSITIONABLE + + self.PositionableName = PositionableName + return self +end + +--- Destroys the POSITIONABLE. +-- @param #POSITIONABLE self +-- @param #boolean GenerateEvent (Optional) true if you want to generate a crash or dead event for the unit. +-- @return #nil The DCS Unit is not existing or alive. +-- @usage +-- -- Air unit example: destroy the Helicopter and generate a S_EVENT_CRASH for each unit in the Helicopter group. +-- Helicopter = UNIT:FindByName( "Helicopter" ) +-- Helicopter:Destroy( true ) +-- @usage +-- -- Ground unit example: destroy the Tanks and generate a S_EVENT_DEAD for each unit in the Tanks group. +-- Tanks = UNIT:FindByName( "Tanks" ) +-- Tanks:Destroy( true ) +-- @usage +-- -- Ship unit example: destroy the Ship silently. +-- Ship = STATIC:FindByName( "Ship" ) +-- Ship:Destroy() +-- +-- @usage +-- -- Destroy without event generation example. +-- Ship = STATIC:FindByName( "Boat" ) +-- Ship:Destroy( false ) -- Don't generate an event upon destruction. +-- +function POSITIONABLE:Destroy( GenerateEvent ) + self:F2( self.ObjectName ) + + local DCSObject = self:GetDCSObject() + + if DCSObject then + + local UnitGroup = self:GetGroup() + local UnitGroupName = UnitGroup:GetName() + self:F( { UnitGroupName = UnitGroupName } ) + + if GenerateEvent and GenerateEvent == true then + if self:IsAir() then + self:CreateEventCrash( timer.getTime(), DCSObject ) + else + self:CreateEventDead( timer.getTime(), DCSObject ) + end + elseif GenerateEvent == false then + -- Do nothing! + else + self:CreateEventRemoveUnit( timer.getTime(), DCSObject ) + end + + USERFLAG:New( UnitGroupName ):Set( 100 ) + DCSObject:destroy() + end + + return nil +end + +--- Returns the DCS object. Polymorphic for other classes like UNIT, STATIC, GROUP, AIRBASE. +-- @param #POSITIONABLE self +-- @return DCS#Object The DCS object. +function POSITIONABLE:GetDCSObject() + return nil +end + +--- Returns a pos3 table of the objects current position and orientation in 3D space. X, Y, Z values are unit vectors defining the objects orientation. +-- Coordinates are dependent on the position of the maps origin. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return DCS#Position3 Table consisting of the point and orientation tables. +function POSITIONABLE:GetPosition() + self:F2( self.PositionableName ) + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + local PositionablePosition = DCSPositionable:getPosition() + self:T3( PositionablePosition ) + return PositionablePosition + end + + BASE:E( { "Cannot GetPositionVec3", Positionable = self, Alive = self:IsAlive() } ) + return nil +end + +--- Returns a {@DCS#Vec3} table of the objects current orientation in 3D space. X, Y, Z values are unit vectors defining the objects orientation. +-- X is the orientation parallel to the movement of the object, Z perpendicular and Y vertical orientation. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return DCS#Vec3 X orientation, i.e. parallel to the direction of movement. +-- @return DCS#Vec3 Y orientation, i.e. vertical. +-- @return DCS#Vec3 Z orientation, i.e. perpendicular to the direction of movement. +function POSITIONABLE:GetOrientation() + local position=self:GetPosition() + if position then + return position.x, position.y, position.z + else + BASE:E( { "Cannot GetOrientation", Positionable = self, Alive = self:IsAlive() } ) + return nil, nil, nil + end +end + +--- Returns a {@DCS#Vec3} table of the objects current X orientation in 3D space, i.e. along the direction of movement. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return DCS#Vec3 X orientation, i.e. parallel to the direction of movement. +function POSITIONABLE:GetOrientationX() + local position=self:GetPosition() + if position then + return position.x + else + BASE:E( { "Cannot GetOrientationX", Positionable = self, Alive = self:IsAlive() } ) + return nil + end +end + +--- Returns a {@DCS#Vec3} table of the objects current Y orientation in 3D space, i.e. vertical orientation. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return DCS#Vec3 Y orientation, i.e. vertical. +function POSITIONABLE:GetOrientationY() + local position=self:GetPosition() + if position then + return position.y + else + BASE:E( { "Cannot GetOrientationY", Positionable = self, Alive = self:IsAlive() } ) + return nil + end +end + +--- Returns a {@DCS#Vec3} table of the objects current Z orientation in 3D space, i.e. perpendicular to direction of movement. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return DCS#Vec3 Z orientation, i.e. perpendicular to movement. +function POSITIONABLE:GetOrientationZ() + local position=self:GetPosition() + if position then + return position.z + else + BASE:E( { "Cannot GetOrientationZ", Positionable = self, Alive = self:IsAlive() } ) + return nil + end +end + +--- Returns the @{DCS#Position3} position vectors indicating the point and direction vectors in 3D of the POSITIONABLE within the mission. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return DCS#Position The 3D position vectors of the POSITIONABLE. +-- @return #nil The POSITIONABLE is not existing or alive. +function POSITIONABLE:GetPositionVec3() + self:F2( self.PositionableName ) + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + local PositionablePosition = DCSPositionable:getPosition().p + self:T3( PositionablePosition ) + return PositionablePosition + end + + BASE:E( { "Cannot GetPositionVec3", Positionable = self, Alive = self:IsAlive() } ) + + return nil +end + +--- Returns the @{DCS#Vec3} vector indicating the 3D vector of the POSITIONABLE within the mission. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return DCS#Vec3 The 3D point vector of the POSITIONABLE or `nil` if it is not existing or alive. +function POSITIONABLE:GetVec3() + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + + local vec3=DCSPositionable:getPoint() + + if vec3 then + return vec3 + else + self:E("ERROR: Cannot get vec3!") + end + end + + -- ERROR! + self:E( { "Cannot GetVec3", Positionable = self, Alive = self:IsAlive() } ) + return nil +end + +--- Returns the @{DCS#Vec2} vector indicating the point in 2D of the POSITIONABLE within the mission. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return DCS#Vec2 The 2D point vector of the POSITIONABLE or #nil if it is not existing or alive. +function POSITIONABLE:GetVec2() + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + + local Vec3=DCSPositionable:getPoint() --DCS#Vec3 + + return {x=Vec3.x, y=Vec3.z} + end + + self:E( { "Cannot GetVec2", Positionable = self, Alive = self:IsAlive() } ) + + return nil +end + +--- Returns a POINT_VEC2 object indicating the point in 2D of the POSITIONABLE within the mission. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return Core.Point#POINT_VEC2 The 2D point vector of the POSITIONABLE. +-- @return #nil The POSITIONABLE is not existing or alive. +function POSITIONABLE:GetPointVec2() + self:F2( self.PositionableName ) + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + local PositionableVec3 = DCSPositionable:getPosition().p + + local PositionablePointVec2 = POINT_VEC2:NewFromVec3( PositionableVec3 ) + + --self:F( PositionablePointVec2 ) + return PositionablePointVec2 + end + + self:E( { "Cannot GetPointVec2", Positionable = self, Alive = self:IsAlive() } ) + + return nil +end + +--- Returns a POINT_VEC3 object indicating the point in 3D of the POSITIONABLE within the mission. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return Core.Point#POINT_VEC3 The 3D point vector of the POSITIONABLE. +-- @return #nil The POSITIONABLE is not existing or alive. +function POSITIONABLE:GetPointVec3() + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + + -- Get 3D vector. + local PositionableVec3 = self:GetPositionVec3() + + if false and self.pointvec3 then + + -- Update vector. + self.pointvec3.x=PositionableVec3.x + self.pointvec3.y=PositionableVec3.y + self.pointvec3.z=PositionableVec3.z + + else + + -- Create a new POINT_VEC3 object. + self.pointvec3=POINT_VEC3:NewFromVec3(PositionableVec3) + + end + + return self.pointvec3 + end + + BASE:E( { "Cannot GetPointVec3", Positionable = self, Alive = self:IsAlive() } ) + + return nil +end + +--- Returns a COORDINATE object indicating the point in 3D of the POSITIONABLE within the mission. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return Core.Point#COORDINATE The COORDINATE of the POSITIONABLE. +function POSITIONABLE:GetCoord() + + -- Get DCS object. + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + + -- Get the current position. + local Vec3 = self:GetVec3() + + if self.coordinate then + + -- Update vector. + self.coordinate.x=Vec3.x + self.coordinate.y=Vec3.y + self.coordinate.z=Vec3.z + + else + + -- New COORDINATE. + self.coordinate=COORDINATE:NewFromVec3(Vec3) + + end + + return self.coordinate + end + + -- Error message. + BASE:E( { "Cannot GetCoordinate", Positionable = self, Alive = self:IsAlive() } ) + + return nil +end + +--- Returns a COORDINATE object indicating the point in 3D of the POSITIONABLE within the mission. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return Core.Point#COORDINATE The COORDINATE of the POSITIONABLE. +function POSITIONABLE:GetCoordinate() + + -- Get DCS object. + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + + -- Get the current position. + local PositionableVec3 = self:GetVec3() + + local coord=COORDINATE:NewFromVec3(PositionableVec3) + + -- Return a new coordiante object. + return coord + + end + + -- Error message. + self:E( { "Cannot GetCoordinate", Positionable = self, Alive = self:IsAlive() } ) + return nil +end + +--- Returns a COORDINATE object, which is offset with respect to the orientation of the POSITIONABLE. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @param #number x Offset in the direction "the nose" of the unit is pointing in meters. Default 0 m. +-- @param #number y Offset "above" the unit in meters. Default 0 m. +-- @param #number z Offset in the direction "the wing" of the unit is pointing in meters. z>0 starboard, z<0 port. Default 0 m. +-- @return Core.Point#COORDINATE The COORDINATE of the offset with respect to the orientation of the POSITIONABLE. +function POSITIONABLE:GetOffsetCoordinate(x,y,z) + + -- Default if nil. + x=x or 0 + y=y or 0 + z=z or 0 + + -- Vectors making up the coordinate system. + local X=self:GetOrientationX() + local Y=self:GetOrientationY() + local Z=self:GetOrientationZ() + + -- Offset vector: x meters ahead, z meters starboard, y meters above. + local A={x=x, y=y, z=z} + + -- Scale components of orthonormal coordinate vectors. + local x={x=X.x*A.x, y=X.y*A.x, z=X.z*A.x} + local y={x=Y.x*A.y, y=Y.y*A.y, z=Y.z*A.y} + local z={x=Z.x*A.z, y=Z.y*A.z, z=Z.z*A.z} + + -- Add up vectors in the unit coordinate system ==> this gives the offset vector relative the the origin of the map. + local a={x=x.x+y.x+z.x, y=x.y+y.y+z.y, z=x.z+y.z+z.z} + + -- Vector from the origin of the map to the unit. + local u=self:GetVec3() + + -- Translate offset vector from map origin to the unit: v=u+a. + local v={x=a.x+u.x, y=a.y+u.y, z=a.z+u.z} + + local coord=COORDINATE:NewFromVec3(v) + + -- Return the offset coordinate. + return coord +end + +--- Returns a random @{DCS#Vec3} vector within a range, indicating the point in 3D of the POSITIONABLE within the mission. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @param #number Radius +-- @return DCS#Vec3 The 3D point vector of the POSITIONABLE. +-- @return #nil The POSITIONABLE is not existing or alive. +-- @usage +-- -- If Radius is ignored, returns the DCS#Vec3 of first UNIT of the GROUP +function POSITIONABLE:GetRandomVec3( Radius ) + self:F2( self.PositionableName ) + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + local PositionablePointVec3 = DCSPositionable:getPosition().p + + if Radius then + local PositionableRandomVec3 = {} + local angle = math.random() * math.pi*2; + PositionableRandomVec3.x = PositionablePointVec3.x + math.cos( angle ) * math.random() * Radius; + PositionableRandomVec3.y = PositionablePointVec3.y + PositionableRandomVec3.z = PositionablePointVec3.z + math.sin( angle ) * math.random() * Radius; + + self:T3( PositionableRandomVec3 ) + return PositionableRandomVec3 + else + self:F("Radius is nil, returning the PointVec3 of the POSITIONABLE", PositionablePointVec3) + return PositionablePointVec3 + end + end + + BASE:E( { "Cannot GetRandomVec3", Positionable = self, Alive = self:IsAlive() } ) + + return nil +end + + +--- Get the bounding box of the underlying POSITIONABLE DCS Object. +-- @param #POSITIONABLE self +-- @return DCS#Box3 The bounding box of the POSITIONABLE. +-- @return #nil The POSITIONABLE is not existing or alive. +function POSITIONABLE:GetBoundingBox() --R2.1 + self:F2() + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + local PositionableDesc = DCSPositionable:getDesc() --DCS#Desc + if PositionableDesc then + local PositionableBox = PositionableDesc.box + return PositionableBox + end + end + + BASE:E( { "Cannot GetBoundingBox", Positionable = self, Alive = self:IsAlive() } ) + + return nil +end + + +--- Get the object size. +-- @param #POSITIONABLE self +-- @return DCS#Distance Max size of object in x, z or 0 if bounding box could not be obtained. +-- @return DCS#Distance Length x or 0 if bounding box could not be obtained. +-- @return DCS#Distance Height y or 0 if bounding box could not be obtained. +-- @return DCS#Distance Width z or 0 if bounding box could not be obtained. +function POSITIONABLE:GetObjectSize() + + -- Get bounding box. + local box=self:GetBoundingBox() + + if box then + local x=box.max.x+math.abs(box.min.x) --length + local y=box.max.y+math.abs(box.min.y) --height + local z=box.max.z+math.abs(box.min.z) --width + return math.max(x,z), x , y, z + end + + return 0,0,0,0 +end + +--- Get the bounding radius of the underlying POSITIONABLE DCS Object. +-- @param #POSITIONABLE self +-- @param #number mindist (Optional) If bounding box is smaller than this value, mindist is returned. +-- @return DCS#Distance The bounding radius of the POSITIONABLE or #nil if the POSITIONABLE is not existing or alive. +function POSITIONABLE:GetBoundingRadius(mindist) + self:F2() + + local Box = self:GetBoundingBox() + + local boxmin=mindist or 0 + if Box then + local X = Box.max.x - Box.min.x + local Z = Box.max.z - Box.min.z + local CX = X / 2 + local CZ = Z / 2 + return math.max( math.max( CX, CZ ), boxmin ) + end + + BASE:E( { "Cannot GetBoundingRadius", Positionable = self, Alive = self:IsAlive() } ) + + return nil +end + +--- Returns the altitude of the POSITIONABLE. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return DCS#Distance The altitude of the POSITIONABLE. +-- @return #nil The POSITIONABLE is not existing or alive. +function POSITIONABLE:GetAltitude() + self:F2() + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + local PositionablePointVec3 = DCSPositionable:getPoint() --DCS#Vec3 + return PositionablePointVec3.y + end + + BASE:E( { "Cannot GetAltitude", Positionable = self, Alive = self:IsAlive() } ) + + return nil +end + +--- Returns if the Positionable is located above a runway. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return #boolean true if Positionable is above a runway. +-- @return #nil The POSITIONABLE is not existing or alive. +function POSITIONABLE:IsAboveRunway() + self:F2( self.PositionableName ) + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + + local Vec2 = self:GetVec2() + local SurfaceType = land.getSurfaceType( Vec2 ) + local IsAboveRunway = SurfaceType == land.SurfaceType.RUNWAY + + self:T2( IsAboveRunway ) + return IsAboveRunway + end + + BASE:E( { "Cannot IsAboveRunway", Positionable = self, Alive = self:IsAlive() } ) + + return nil +end + + +function POSITIONABLE:GetSize() + + local DCSObject = self:GetDCSObject() + + if DCSObject then + return 1 + else + return 0 + end +end + + + +--- Returns the POSITIONABLE heading in degrees. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return #number The POSITIONABLE heading in degrees or `nil` if not existing or alive. +function POSITIONABLE:GetHeading() + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + + local PositionablePosition = DCSPositionable:getPosition() + + if PositionablePosition then + local PositionableHeading = math.atan2( PositionablePosition.x.z, PositionablePosition.x.x ) + + if PositionableHeading < 0 then + PositionableHeading = PositionableHeading + 2 * math.pi + end + + PositionableHeading = PositionableHeading * 180 / math.pi + + return PositionableHeading + end + end + + self:E({"Cannot GetHeading", Positionable = self, Alive = self:IsAlive()}) + + return nil +end + +-- Is Methods + +--- Returns if the unit is of an air category. +-- If the unit is a helicopter or a plane, then this method will return true, otherwise false. +-- @param #POSITIONABLE self +-- @return #boolean Air category evaluation result. +function POSITIONABLE:IsAir() + self:F2() + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitDescriptor = DCSUnit:getDesc() + self:T3( { UnitDescriptor.category, Unit.Category.AIRPLANE, Unit.Category.HELICOPTER } ) + + local IsAirResult = ( UnitDescriptor.category == Unit.Category.AIRPLANE ) or ( UnitDescriptor.category == Unit.Category.HELICOPTER ) + + self:T3( IsAirResult ) + return IsAirResult + end + + return nil +end + +--- Returns if the unit is of an ground category. +-- If the unit is a ground vehicle or infantry, this method will return true, otherwise false. +-- @param #POSITIONABLE self +-- @return #boolean Ground category evaluation result. +function POSITIONABLE:IsGround() + self:F2() + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitDescriptor = DCSUnit:getDesc() + self:T3( { UnitDescriptor.category, Unit.Category.GROUND_UNIT } ) + + local IsGroundResult = ( UnitDescriptor.category == Unit.Category.GROUND_UNIT ) + + self:T3( IsGroundResult ) + return IsGroundResult + end + + return nil +end + + +--- Returns if the unit is of ship category. +-- @param #POSITIONABLE self +-- @return #boolean Ship category evaluation result. +function POSITIONABLE:IsShip() + self:F2() + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitDescriptor = DCSUnit:getDesc() + + local IsShip = ( UnitDescriptor.category == Unit.Category.SHIP ) + + return IsShip + end + + return nil +end + + +--- Returns if the unit is a submarine. +-- @param #POSITIONABLE self +-- @return #boolean Submarines attributes result. +function POSITIONABLE:IsSubmarine() + self:F2() + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitDescriptor = DCSUnit:getDesc() + if UnitDescriptor.attributes["Submarines"] == true then + return true + else + return false + end + end + + return nil +end + + +--- Returns true if the POSITIONABLE is in the air. +-- Polymorphic, is overridden in GROUP and UNIT. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return #boolean true if in the air. +-- @return #nil The POSITIONABLE is not existing or alive. +function POSITIONABLE:InAir() + self:F2( self.PositionableName ) + + return nil +end + + +--- Returns the a @{Velocity} object from the positionable. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return Core.Velocity#VELOCITY Velocity The Velocity object. +-- @return #nil The POSITIONABLE is not existing or alive. +function POSITIONABLE:GetVelocity() + self:F2( self.PositionableName ) + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + local Velocity = VELOCITY:New( self ) + return Velocity + end + + BASE:E( { "Cannot GetVelocity", Positionable = self, Alive = self:IsAlive() } ) + + return nil +end + + + +--- Returns the POSITIONABLE velocity Vec3 vector. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return DCS#Vec3 The velocity Vec3 vector +-- @return #nil The POSITIONABLE is not existing or alive. +function POSITIONABLE:GetVelocityVec3() + self:F2( self.PositionableName ) + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable and DCSPositionable:isExist() then + local PositionableVelocityVec3 = DCSPositionable:getVelocity() + self:T3( PositionableVelocityVec3 ) + return PositionableVelocityVec3 + end + + BASE:E( { "Cannot GetVelocityVec3", Positionable = self, Alive = self:IsAlive() } ) + + return nil +end + +--- Get relative velocity with respect to another POSITIONABLE. +-- @param #POSITIONABLE self +-- @param #POSITIONABLE positionable Other positionable. +-- @return #number Relative velocity in m/s. +function POSITIONABLE:GetRelativeVelocity(positionable) + self:F2( self.PositionableName ) + + local v1=self:GetVelocityVec3() + local v2=positionable:GetVelocityVec3() + + local vtot=UTILS.VecAdd(v1,v2) + + return UTILS.VecNorm(vtot) +end + + +--- Returns the POSITIONABLE height in meters. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return DCS#Vec3 The height of the positionable. +-- @return #nil The POSITIONABLE is not existing or alive. +function POSITIONABLE:GetHeight() --R2.1 + self:F2( self.PositionableName ) + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + local PositionablePosition = DCSPositionable:getPosition() + if PositionablePosition then + local PositionableHeight = PositionablePosition.p.y + self:T2( PositionableHeight ) + return PositionableHeight + end + end + + return nil +end + + +--- Returns the POSITIONABLE velocity in km/h. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return #number The velocity in km/h +function POSITIONABLE:GetVelocityKMH() + self:F2( self.PositionableName ) + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable and DCSPositionable:isExist() then + local VelocityVec3 = self:GetVelocityVec3() + local Velocity = ( VelocityVec3.x ^ 2 + VelocityVec3.y ^ 2 + VelocityVec3.z ^ 2 ) ^ 0.5 -- in meters / sec + local Velocity = Velocity * 3.6 -- now it is in km/h. + self:T3( Velocity ) + return Velocity + end + + return 0 +end + +--- Returns the POSITIONABLE velocity in meters per second. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return #number The velocity in meters per second. +function POSITIONABLE:GetVelocityMPS() + self:F2( self.PositionableName ) + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable and DCSPositionable:isExist() then + local VelocityVec3 = self:GetVelocityVec3() + local Velocity = ( VelocityVec3.x ^ 2 + VelocityVec3.y ^ 2 + VelocityVec3.z ^ 2 ) ^ 0.5 -- in meters / sec + self:T3( Velocity ) + return Velocity + end + + return 0 +end + +--- Returns the POSITIONABLE velocity in knots. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return #number The velocity in knots. +function POSITIONABLE:GetVelocityKNOTS() + self:F2( self.PositionableName ) + return UTILS.MpsToKnots(self:GetVelocityMPS()) +end + +--- Returns the Angle of Attack of a positionable. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return #number Angle of attack in degrees. +function POSITIONABLE:GetAoA() + + -- Get position of the unit. + local unitpos = self:GetPosition() + + if unitpos then + + -- Get velocity vector of the unit. + local unitvel = self:GetVelocityVec3() + + if unitvel and UTILS.VecNorm(unitvel)~=0 then + + -- Get wind vector including turbulences. + local wind=self:GetCoordinate():GetWindWithTurbulenceVec3() + + -- Include wind vector. + unitvel.x=unitvel.x-wind.x + unitvel.y=unitvel.y-wind.y + unitvel.z=unitvel.z-wind.z + + -- Unit velocity transformed into aircraft axes directions. + local AxialVel = {} + + -- Transform velocity components in direction of aircraft axes. + AxialVel.x = UTILS.VecDot(unitpos.x, unitvel) + AxialVel.y = UTILS.VecDot(unitpos.y, unitvel) + AxialVel.z = UTILS.VecDot(unitpos.z, unitvel) + + -- AoA is angle between unitpos.x and the x and y velocities. + local AoA = math.acos(UTILS.VecDot({x = 1, y = 0, z = 0}, {x = AxialVel.x, y = AxialVel.y, z = 0})/UTILS.VecNorm({x = AxialVel.x, y = AxialVel.y, z = 0})) + + --Set correct direction: + if AxialVel.y > 0 then + AoA = -AoA + end + + -- Return AoA value in degrees. + return math.deg(AoA) + end + + end + + return nil +end + +--- Returns the unit's climb or descent angle. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return #number Climb or descent angle in degrees. Or 0 if velocity vector norm is zero (or nil). Or nil, if the position of the POSITIONABLE returns nil. +function POSITIONABLE:GetClimbAngle() + + -- Get position of the unit. + local unitpos = self:GetPosition() + + if unitpos then + + -- Get velocity vector of the unit. + local unitvel = self:GetVelocityVec3() + + if unitvel and UTILS.VecNorm(unitvel)~=0 then + + -- Calculate climb angle. + local angle=math.asin(unitvel.y/UTILS.VecNorm(unitvel)) + + -- Return angle in degrees. + return math.deg(angle) + else + return 0 + end + end + + return nil +end + +--- Returns the pitch angle of a unit. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return #number Pitch ange in degrees. +function POSITIONABLE:GetPitch() + + -- Get position of the unit. + local unitpos = self:GetPosition() + + if unitpos then + return math.deg(math.asin(unitpos.x.y)) + end + + return nil +end + +--- Returns the roll angle of a unit. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return #number Pitch ange in degrees. +function POSITIONABLE:GetRoll() + + -- Get position of the unit. + local unitpos = self:GetPosition() + + if unitpos then + + --first, make a vector that is perpendicular to y and unitpos.x with cross product + local cp = UTILS.VecCross(unitpos.x, {x = 0, y = 1, z = 0}) + + --now, get dot product of of this cross product with unitpos.z + local dp = UTILS.VecDot(cp, unitpos.z) + + --now get the magnitude of the roll (magnitude of the angle between two vectors is acos(vec1.vec2/|vec1||vec2|) + local Roll = math.acos(dp/(UTILS.VecNorm(cp)*UTILS.VecNorm(unitpos.z))) + + --now, have to get sign of roll. + -- by convention, making right roll positive + -- to get sign of roll, use the y component of unitpos.z. For right roll, y component is negative. + + if unitpos.z.y > 0 then -- left roll, flip the sign of the roll + Roll = -Roll + end + + return math.deg(Roll) + end +end + +--- Returns the yaw angle of a unit. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return #number Yaw ange in degrees. +function POSITIONABLE:GetYaw() + + local unitpos = self:GetPosition() + if unitpos then + -- get unit velocity + local unitvel = self:GetVelocityVec3() + + if unitvel and UTILS.VecNorm(unitvel) ~= 0 then --must have non-zero velocity! + local AxialVel = {} --unit velocity transformed into aircraft axes directions + + --transform velocity components in direction of aircraft axes. + AxialVel.x = UTILS.VecDot(unitpos.x, unitvel) + AxialVel.y = UTILS.VecDot(unitpos.y, unitvel) + AxialVel.z = UTILS.VecDot(unitpos.z, unitvel) + + --Yaw is the angle between unitpos.x and the x and z velocities + --define right yaw as positive + local Yaw = math.acos(UTILS.VecDot({x = 1, y = 0, z = 0}, {x = AxialVel.x, y = 0, z = AxialVel.z})/UTILS.VecNorm({x = AxialVel.x, y = 0, z = AxialVel.z})) + + --now set correct direction: + if AxialVel.z > 0 then + Yaw = -Yaw + end + return Yaw + end + end + +end + + +--- Returns the message text with the callsign embedded (if there is one). +-- @param #POSITIONABLE self +-- @param #string Message The message text +-- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +-- @return #string The message text +function POSITIONABLE:GetMessageText( Message, Name ) + + local DCSObject = self:GetDCSObject() + if DCSObject then + local Callsign = string.format( "%s", ( ( Name ~= "" and Name ) or self:GetCallsign() ~= "" and self:GetCallsign() ) or self:GetName() ) + local MessageText = string.format("%s - %s", Callsign, Message ) + return MessageText + end + + return nil +end + + +--- Returns a message with the callsign embedded (if there is one). +-- @param #POSITIONABLE self +-- @param #string Message The message text +-- @param DCS#Duration Duration The duration of the message. +-- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +-- @return Core.Message#MESSAGE +function POSITIONABLE:GetMessage( Message, Duration, Name ) --R2.1 changed callsign and name and using GetMessageText + + local DCSObject = self:GetDCSObject() + if DCSObject then + local MessageText = self:GetMessageText( Message, Name ) + return MESSAGE:New( MessageText, Duration ) + end + + return nil +end + +--- Returns a message of a specified type with the callsign embedded (if there is one). +-- @param #POSITIONABLE self +-- @param #string Message The message text +-- @param Core.Message#MESSAGE MessageType MessageType The message type. +-- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +-- @return Core.Message#MESSAGE +function POSITIONABLE:GetMessageType( Message, MessageType, Name ) -- R2.2 changed callsign and name and using GetMessageText + + local DCSObject = self:GetDCSObject() + if DCSObject then + local MessageText = self:GetMessageText( Message, Name ) + return MESSAGE:NewType( MessageText, MessageType ) + end + + return nil +end + +--- Send a message to all coalitions. +-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. +-- @param #POSITIONABLE self +-- @param #string Message The message text +-- @param DCS#Duration Duration The duration of the message. +-- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +function POSITIONABLE:MessageToAll( Message, Duration, Name ) + self:F2( { Message, Duration } ) + + local DCSObject = self:GetDCSObject() + if DCSObject then + self:GetMessage( Message, Duration, Name ):ToAll() + end + + return nil +end + +--- Send a message to a coalition. +-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. +-- @param #POSITIONABLE self +-- @param #string Message The message text +-- @param DCS#Duration Duration The duration of the message. +-- @param DCS#coalition MessageCoalition The Coalition receiving the message. +-- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +function POSITIONABLE:MessageToCoalition( Message, Duration, MessageCoalition, Name ) + self:F2( { Message, Duration } ) + + local Name = Name or "" + + local DCSObject = self:GetDCSObject() + if DCSObject then + self:GetMessage( Message, Duration, Name ):ToCoalition( MessageCoalition ) + end + + return nil +end + + +--- Send a message to a coalition. +-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. +-- @param #POSITIONABLE self +-- @param #string Message The message text +-- @param Core.Message#MESSAGE.Type MessageType The message type that determines the duration. +-- @param DCS#coalition MessageCoalition The Coalition receiving the message. +-- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +function POSITIONABLE:MessageTypeToCoalition( Message, MessageType, MessageCoalition, Name ) + self:F2( { Message, MessageType } ) + + local Name = Name or "" + + local DCSObject = self:GetDCSObject() + if DCSObject then + self:GetMessageType( Message, MessageType, Name ):ToCoalition( MessageCoalition ) + end + + return nil +end + + +--- Send a message to the red coalition. +-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. +-- @param #POSITIONABLE self +-- @param #string Message The message text +-- @param DCS#Duration Duration The duration of the message. +-- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +function POSITIONABLE:MessageToRed( Message, Duration, Name ) + self:F2( { Message, Duration } ) + + local DCSObject = self:GetDCSObject() + if DCSObject then + self:GetMessage( Message, Duration, Name ):ToRed() + end + + return nil +end + +--- Send a message to the blue coalition. +-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. +-- @param #POSITIONABLE self +-- @param #string Message The message text +-- @param DCS#Duration Duration The duration of the message. +-- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +function POSITIONABLE:MessageToBlue( Message, Duration, Name ) + self:F2( { Message, Duration } ) + + local DCSObject = self:GetDCSObject() + if DCSObject then + self:GetMessage( Message, Duration, Name ):ToBlue() + end + + return nil +end + +--- Send a message to a client. +-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. +-- @param #POSITIONABLE self +-- @param #string Message The message text +-- @param DCS#Duration Duration The duration of the message. +-- @param Wrapper.Client#CLIENT Client The client object receiving the message. +-- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +function POSITIONABLE:MessageToClient( Message, Duration, Client, Name ) + self:F2( { Message, Duration } ) + + local DCSObject = self:GetDCSObject() + if DCSObject then + self:GetMessage( Message, Duration, Name ):ToClient( Client ) + end + + return nil +end + +--- Send a message to a @{Wrapper.Group}. +-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. +-- @param #POSITIONABLE self +-- @param #string Message The message text +-- @param DCS#Duration Duration The duration of the message. +-- @param Wrapper.Group#GROUP MessageGroup The GROUP object receiving the message. +-- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +function POSITIONABLE:MessageToGroup( Message, Duration, MessageGroup, Name ) + self:F2( { Message, Duration } ) + + local DCSObject = self:GetDCSObject() + if DCSObject then + if DCSObject:isExist() then + if MessageGroup:IsAlive() then + self:GetMessage( Message, Duration, Name ):ToGroup( MessageGroup ) + else + BASE:E( { "Message not sent to Group; Group is not alive...", Message = Message, MessageGroup = MessageGroup } ) + end + else + BASE:E( { "Message not sent to Group; Positionable is not alive ...", Message = Message, Positionable = self, MessageGroup = MessageGroup } ) + end + end + + + return nil +end + +--- Send a message of a message type to a @{Wrapper.Group}. +-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. +-- @param #POSITIONABLE self +-- @param #string Message The message text +-- @param Core.Message#MESSAGE.Type MessageType The message type that determines the duration. +-- @param Wrapper.Group#GROUP MessageGroup The GROUP object receiving the message. +-- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +function POSITIONABLE:MessageTypeToGroup( Message, MessageType, MessageGroup, Name ) + self:F2( { Message, MessageType } ) + + local DCSObject = self:GetDCSObject() + if DCSObject then + if DCSObject:isExist() then + self:GetMessageType( Message, MessageType, Name ):ToGroup( MessageGroup ) + end + end + + return nil +end + +--- Send a message to a @{Core.Set#SET_GROUP}. +-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. +-- @param #POSITIONABLE self +-- @param #string Message The message text +-- @param DCS#Duration Duration The duration of the message. +-- @param Core.Set#SET_GROUP MessageSetGroup The SET_GROUP collection receiving the message. +-- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +function POSITIONABLE:MessageToSetGroup( Message, Duration, MessageSetGroup, Name ) --R2.1 + self:F2( { Message, Duration } ) + + local DCSObject = self:GetDCSObject() + if DCSObject then + if DCSObject:isExist() then + MessageSetGroup:ForEachGroupAlive( + function( MessageGroup ) + self:GetMessage( Message, Duration, Name ):ToGroup( MessageGroup ) + end + ) + end + end + + return nil +end + +--- Send a message to the players in the @{Wrapper.Group}. +-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. +-- @param #POSITIONABLE self +-- @param #string Message The message text +-- @param DCS#Duration Duration The duration of the message. +-- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +function POSITIONABLE:Message( Message, Duration, Name ) + self:F2( { Message, Duration } ) + + local DCSObject = self:GetDCSObject() + if DCSObject then + self:GetMessage( Message, Duration, Name ):ToGroup( self ) + end + + return nil +end + +--- Create a @{Core.Radio#RADIO}, to allow radio transmission for this POSITIONABLE. +-- Set parameters with the methods provided, then use RADIO:Broadcast() to actually broadcast the message +-- @param #POSITIONABLE self +-- @return Core.Radio#RADIO Radio +function POSITIONABLE:GetRadio() --R2.1 + self:F2(self) + return RADIO:New(self) +end + +--- Create a @{Core.Radio#BEACON}, to allow this POSITIONABLE to broadcast beacon signals +-- @param #POSITIONABLE self +-- @return Core.Radio#RADIO Radio +function POSITIONABLE:GetBeacon() --R2.1 + self:F2(self) + return BEACON:New(self) +end + +--- Start Lasing a POSITIONABLE +-- @param #POSITIONABLE self +-- @param #POSITIONABLE Target The target to lase. +-- @param #number LaserCode Laser code or random number in [1000, 9999]. +-- @param #number Duration Duration of lasing in seconds. +-- @return Core.Spot#SPOT +function POSITIONABLE:LaseUnit( Target, LaserCode, Duration ) --R2.1 + self:F2() + + LaserCode = LaserCode or math.random( 1000, 9999 ) + + local RecceDcsUnit = self:GetDCSObject() + local TargetVec3 = Target:GetVec3() + + self:F("bulding spot") + self.Spot = SPOT:New( self ) -- Core.Spot#SPOT + self.Spot:LaseOn( Target, LaserCode, Duration) + self.LaserCode = LaserCode + + return self.Spot + +end + +--- Start Lasing a COORDINATE. +-- @param #POSITIONABLE self +-- @param Core.Point#COORDIUNATE Coordinate The coordinate where the lase is pointing at. +-- @param #number LaserCode Laser code or random number in [1000, 9999]. +-- @param #number Duration Duration of lasing in seconds. +-- @return Core.Spot#SPOT +function POSITIONABLE:LaseCoordinate(Coordinate, LaserCode, Duration) + self:F2() + + LaserCode = LaserCode or math.random(1000, 9999) + + self.Spot = SPOT:New(self) -- Core.Spot#SPOT + self.Spot:LaseOnCoordinate(Coordinate, LaserCode, Duration) + self.LaserCode = LaserCode + + return self.Spot +end + +--- Stop Lasing a POSITIONABLE +-- @param #POSITIONABLE self +-- @return #POSITIONABLE +function POSITIONABLE:LaseOff() --R2.1 + self:F2() + + if self.Spot then + self.Spot:LaseOff() + self.Spot = nil + end + + return self +end + +--- Check if the POSITIONABLE is lasing a target +-- @param #POSITIONABLE self +-- @return #boolean true if it is lasing a target +function POSITIONABLE:IsLasing() --R2.1 + self:F2() + + local Lasing = false + + if self.Spot then + Lasing = self.Spot:IsLasing() + end + + return Lasing +end + +--- Get the Spot +-- @param #POSITIONABLE self +-- @return Core.Spot#SPOT The Spot +function POSITIONABLE:GetSpot() --R2.1 + + return self.Spot +end + +--- Get the last assigned laser code +-- @param #POSITIONABLE self +-- @return #number The laser code +function POSITIONABLE:GetLaserCode() --R2.1 + + return self.LaserCode +end + +do -- Cargo + + --- Add cargo. + -- @param #POSITIONABLE self + -- @param Core.Cargo#CARGO Cargo + -- @return #POSITIONABLE + function POSITIONABLE:AddCargo( Cargo ) + self.__.Cargo[Cargo] = Cargo + return self + end + + --- Get all contained cargo. + -- @param #POSITIONABLE self + -- @return #POSITIONABLE + function POSITIONABLE:GetCargo() + return self.__.Cargo + end + + + + --- Remove cargo. + -- @param #POSITIONABLE self + -- @param Core.Cargo#CARGO Cargo + -- @return #POSITIONABLE + function POSITIONABLE:RemoveCargo( Cargo ) + self.__.Cargo[Cargo] = nil + return self + end + + --- Returns if carrier has given cargo. + -- @param #POSITIONABLE self + -- @return Core.Cargo#CARGO Cargo + function POSITIONABLE:HasCargo( Cargo ) + return self.__.Cargo[Cargo] + end + + --- Clear all cargo. + -- @param #POSITIONABLE self + function POSITIONABLE:ClearCargo() + self.__.Cargo = {} + end + + --- Is cargo bay empty. + -- @param #POSITIONABLE self + function POSITIONABLE:IsCargoEmpty() + local IsEmpty = true + for _, Cargo in pairs( self.__.Cargo ) do + IsEmpty = false + break + end + return IsEmpty + end + + --- Get cargo item count. + -- @param #POSITIONABLE self + -- @return Core.Cargo#CARGO Cargo + function POSITIONABLE:CargoItemCount() + local ItemCount = 0 + for CargoName, Cargo in pairs( self.__.Cargo ) do + ItemCount = ItemCount + Cargo:GetCount() + end + return ItemCount + end + +-- --- Get Cargo Bay Free Volume in m3. +-- -- @param #POSITIONABLE self +-- -- @return #number CargoBayFreeVolume +-- function POSITIONABLE:GetCargoBayFreeVolume() +-- local CargoVolume = 0 +-- for CargoName, Cargo in pairs( self.__.Cargo ) do +-- CargoVolume = CargoVolume + Cargo:GetVolume() +-- end +-- return self.__.CargoBayVolumeLimit - CargoVolume +-- end +-- + + --- Get Cargo Bay Free Weight in kg. + -- @param #POSITIONABLE self + -- @return #number CargoBayFreeWeight + function POSITIONABLE:GetCargoBayFreeWeight() + + -- When there is no cargo bay weight limit set, then calculate this for this positionable! + if not self.__.CargoBayWeightLimit then + self:SetCargoBayWeightLimit() + end + + local CargoWeight = 0 + for CargoName, Cargo in pairs( self.__.Cargo ) do + CargoWeight = CargoWeight + Cargo:GetWeight() + end + return self.__.CargoBayWeightLimit - CargoWeight + end + +-- --- Get Cargo Bay Volume Limit in m3. +-- -- @param #POSITIONABLE self +-- -- @param #number VolumeLimit +-- function POSITIONABLE:SetCargoBayVolumeLimit( VolumeLimit ) +-- self.__.CargoBayVolumeLimit = VolumeLimit +-- end + + --- Set Cargo Bay Weight Limit in kg. + -- @param #POSITIONABLE self + -- @param #number WeightLimit + function POSITIONABLE:SetCargoBayWeightLimit( WeightLimit ) + + if WeightLimit then + self.__.CargoBayWeightLimit = WeightLimit + elseif self.__.CargoBayWeightLimit~=nil then + -- Value already set ==> Do nothing! + else + -- If weightlimit is not provided, we will calculate it depending on the type of unit. + + -- When an airplane or helicopter, we calculate the weightlimit based on the descriptor. + if self:IsAir() then + local Desc = self:GetDesc() + self:F({Desc=Desc}) + + local Weights = { + ["C-17A"] = 35000, --77519 cannot be used, because it loads way too much apcs and infantry., + ["C-130"] = 22000 --The real value cannot be used, because it loads way too much apcs and infantry., + } + + self.__.CargoBayWeightLimit = Weights[Desc.typeName] or ( Desc.massMax - ( Desc.massEmpty + Desc.fuelMassMax ) ) + elseif self:IsShip() then + local Desc = self:GetDesc() + self:F({Desc=Desc}) + + local Weights = { + ["Type_071"] = 245000, + ["LHA_Tarawa"] = 500000, + ["Ropucha-class"] = 150000, + ["Dry-cargo ship-1"] = 70000, + ["Dry-cargo ship-2"] = 70000, + ["Higgins_boat"] = 3700, -- Higgins Boat can load 3700 kg of general cargo or 36 men (source wikipedia). + ["USS_Samuel_Chase"] = 25000, -- Let's say 25 tons for now. Wiki says 33 Higgins boats, which would be 264 tons (can't be right!) and/or 578 troops. + ["LST_Mk2"] =2100000, -- Can carry 2100 tons according to wiki source! + } + self.__.CargoBayWeightLimit = ( Weights[Desc.typeName] or 50000 ) + + else + local Desc = self:GetDesc() + + local Weights = { + ["AAV7"] = 25, + ["Bedford_MWD"] = 8, -- new by kappa + ["Blitz_36-6700A"] = 10, -- new by kappa + ["BMD-1"] = 9, -- IRL should be 4 passengers + ["BMP-1"] = 8, + ["BMP-2"] = 7, + ["BMP-3"] = 8, -- IRL should be 7+2 passengers + ["Boman"] = 25, + ["BTR-80"] = 9, -- IRL should be 7 passengers + ["BTR-82A"] = 9, -- new by kappa -- IRL should be 7 passengers + ["BTR_D"] = 12, -- IRL should be 10 passengers + ["Cobra"] = 8, + ["Land_Rover_101_FC"] = 11, -- new by kappa + ["Land_Rover_109_S3"] = 7, -- new by kappa + ["LAV-25"] = 6, + ["M-2 Bradley"] = 6, + ["M1043 HMMWV Armament"] = 4, + ["M1045 HMMWV TOW"] = 4, + ["M1126 Stryker ICV"] = 9, + ["M1134 Stryker ATGM"] = 9, + ["M2A1_halftrack"] = 9, + ["M-113"] = 9, -- IRL should be 11 passengers + ["Marder"] = 6, + ["MCV-80"] = 9, -- IRL should be 7 passengers + ["MLRS FDDM"] = 4, + ["MTLB"] = 25, -- IRL should be 11 passengers + ["GAZ-66"] = 8, + ["GAZ-3307"] = 12, + ["GAZ-3308"] = 14, + ["Grad_FDDM"] = 6, -- new by kappa + ["KAMAZ Truck"] = 12, + ["KrAZ6322"] = 12, + ["M 818"] = 12, + ["Tigr_233036"] = 6, + ["TPZ"] = 10, + ["UAZ-469"] = 4, -- new by kappa + ["Ural-375"] = 12, + ["Ural-4320-31"] = 14, + ["Ural-4320 APA-5D"] = 10, + ["Ural-4320T"] = 14, + ["ZBD04A"] = 7, -- new by kappa + ["VAB_Mephisto"] = 8, -- new by Apple + } + + local CargoBayWeightLimit = ( Weights[Desc.typeName] or 0 ) * 95 + self.__.CargoBayWeightLimit = CargoBayWeightLimit + end + end + self:F({CargoBayWeightLimit = self.__.CargoBayWeightLimit}) + end +end --- Cargo + +--- Signal a flare at the position of the POSITIONABLE. +-- @param #POSITIONABLE self +-- @param Utilities.Utils#FLARECOLOR FlareColor +function POSITIONABLE:Flare( FlareColor ) + self:F2() + trigger.action.signalFlare( self:GetVec3(), FlareColor , 0 ) +end + +--- Signal a white flare at the position of the POSITIONABLE. +-- @param #POSITIONABLE self +function POSITIONABLE:FlareWhite() + self:F2() + trigger.action.signalFlare( self:GetVec3(), trigger.flareColor.White , 0 ) +end + +--- Signal a yellow flare at the position of the POSITIONABLE. +-- @param #POSITIONABLE self +function POSITIONABLE:FlareYellow() + self:F2() + trigger.action.signalFlare( self:GetVec3(), trigger.flareColor.Yellow , 0 ) +end + +--- Signal a green flare at the position of the POSITIONABLE. +-- @param #POSITIONABLE self +function POSITIONABLE:FlareGreen() + self:F2() + trigger.action.signalFlare( self:GetVec3(), trigger.flareColor.Green , 0 ) +end + +--- Signal a red flare at the position of the POSITIONABLE. +-- @param #POSITIONABLE self +function POSITIONABLE:FlareRed() + self:F2() + local Vec3 = self:GetVec3() + if Vec3 then + trigger.action.signalFlare( Vec3, trigger.flareColor.Red, 0 ) + end +end + +--- Smoke the POSITIONABLE. +-- @param #POSITIONABLE self +-- @param Utilities.Utils#SMOKECOLOR SmokeColor The color to smoke to positionable. +-- @param #number Range The range in meters to randomize the smoking around the positionable. +-- @param #number AddHeight The height in meters to add to the altitude of the positionable. +function POSITIONABLE:Smoke( SmokeColor, Range, AddHeight ) + self:F2() + if Range then + local Vec3 = self:GetRandomVec3( Range ) + Vec3.y = Vec3.y + AddHeight or 0 + trigger.action.smoke( Vec3, SmokeColor ) + else + local Vec3 = self:GetVec3() + Vec3.y = Vec3.y + AddHeight or 0 + trigger.action.smoke( self:GetVec3(), SmokeColor ) + end + +end + +--- Smoke the POSITIONABLE Green. +-- @param #POSITIONABLE self +function POSITIONABLE:SmokeGreen() + self:F2() + trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Green ) +end + +--- Smoke the POSITIONABLE Red. +-- @param #POSITIONABLE self +function POSITIONABLE:SmokeRed() + self:F2() + trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Red ) +end + +--- Smoke the POSITIONABLE White. +-- @param #POSITIONABLE self +function POSITIONABLE:SmokeWhite() + self:F2() + trigger.action.smoke( self:GetVec3(), trigger.smokeColor.White ) +end + +--- Smoke the POSITIONABLE Orange. +-- @param #POSITIONABLE self +function POSITIONABLE:SmokeOrange() + self:F2() + trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Orange ) +end + +--- Smoke the POSITIONABLE Blue. +-- @param #POSITIONABLE self +function POSITIONABLE:SmokeBlue() + self:F2() + trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Blue ) +end + + +--- Returns true if the unit is within a @{Zone}. +-- @param #POSITIONABLE self +-- @param Core.Zone#ZONE_BASE Zone The zone to test. +-- @return #boolean Returns true if the unit is within the @{Core.Zone#ZONE_BASE} +function POSITIONABLE:IsInZone( Zone ) + self:F2( { self.PositionableName, Zone } ) + + if self:IsAlive() then + local IsInZone = Zone:IsVec3InZone( self:GetVec3() ) + + return IsInZone + end + return false +end + +--- Returns true if the unit is not within a @{Zone}. +-- @param #POSITIONABLE self +-- @param Core.Zone#ZONE_BASE Zone The zone to test. +-- @return #boolean Returns true if the unit is not within the @{Core.Zone#ZONE_BASE} +function POSITIONABLE:IsNotInZone( Zone ) + self:F2( { self.PositionableName, Zone } ) + + if self:IsAlive() then + local IsNotInZone = not Zone:IsVec3InZone( self:GetVec3() ) + + return IsNotInZone + else + return false + end +end + --- **Wrapper** -- CONTROLLABLE is an intermediate class wrapping Group and Unit classes "controllers". +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- ### Contributions: +-- +-- === +-- +-- @module Wrapper.Controllable +-- @image Wrapper_Controllable.JPG + + +--- @type CONTROLLABLE +-- @field DCS#Controllable DCSControllable The DCS controllable class. +-- @field #string ControllableName The name of the controllable. +-- @extends Wrapper.Positionable#POSITIONABLE + + + +--- Wrapper class to handle the "DCS Controllable objects", which are Groups and Units: +-- +-- * Support all DCS Controllable APIs. +-- * Enhance with Controllable specific APIs not in the DCS Controllable API set. +-- * Handle local Controllable Controller. +-- * Manage the "state" of the DCS Controllable. +-- +-- # 1) CONTROLLABLE constructor +-- +-- The CONTROLLABLE class provides the following functions to construct a CONTROLLABLE instance: +-- +-- * @{#CONTROLLABLE.New}(): Create a CONTROLLABLE instance. +-- +-- # 2) CONTROLLABLE Task methods +-- +-- Several controllable task methods are available that help you to prepare tasks. +-- These methods return a string consisting of the task description, which can then be given to either a @{Wrapper.Controllable#CONTROLLABLE.PushTask} or @{Wrapper.Controllable#SetTask} method to assign the task to the CONTROLLABLE. +-- Tasks are specific for the category of the CONTROLLABLE, more specific, for AIR, GROUND or AIR and GROUND. +-- Each task description where applicable indicates for which controllable category the task is valid. +-- There are 2 main subdivisions of tasks: Assigned tasks and EnRoute tasks. +-- +-- ## 2.1) Task assignment +-- +-- Assigned task methods make the controllable execute the task where the location of the (possible) targets of the task are known before being detected. +-- This is different from the EnRoute tasks, where the targets of the task need to be detected before the task can be executed. +-- +-- Find below a list of the **assigned task** methods: +-- +-- * @{#CONTROLLABLE.TaskAttackGroup}: (AIR) Attack a Controllable. +-- * @{#CONTROLLABLE.TaskAttackMapObject}: (AIR) Attacking the map object (building, structure, e.t.c). +-- * @{#CONTROLLABLE.TaskAttackUnit}: (AIR) Attack the Unit. +-- * @{#CONTROLLABLE.TaskBombing}: (AIR) Delivering weapon at the point on the ground. +-- * @{#CONTROLLABLE.TaskBombingRunway}: (AIR) Delivering weapon on the runway. +-- * @{#CONTROLLABLE.TaskEmbarking}: (AIR) Move the controllable to a Vec2 Point, wait for a defined duration and embark a controllable. +-- * @{#CONTROLLABLE.TaskEmbarkToTransport}: (GROUND) Embark to a Transport landed at a location. +-- * @{#CONTROLLABLE.TaskEscort}: (AIR) Escort another airborne controllable. +-- * @{#CONTROLLABLE.TaskFAC_AttackGroup}: (AIR + GROUND) The task makes the controllable/unit a FAC and orders the FAC to control the target (enemy ground controllable) destruction. +-- * @{#CONTROLLABLE.TaskFireAtPoint}: (GROUND) Fire some or all ammunition at a VEC2 point. +-- * @{#CONTROLLABLE.TaskFollow}: (AIR) Following another airborne controllable. +-- * @{#CONTROLLABLE.TaskHold}: (GROUND) Hold ground controllable from moving. +-- * @{#CONTROLLABLE.TaskHoldPosition}: (AIR) Hold position at the current position of the first unit of the controllable. +-- * @{#CONTROLLABLE.TaskLand}: (AIR HELICOPTER) Landing at the ground. For helicopters only. +-- * @{#CONTROLLABLE.TaskLandAtZone}: (AIR) Land the controllable at a @{Core.Zone#ZONE_RADIUS). +-- * @{#CONTROLLABLE.TaskOrbitCircle}: (AIR) Orbit at the current position of the first unit of the controllable at a specified alititude. +-- * @{#CONTROLLABLE.TaskOrbitCircleAtVec2}: (AIR) Orbit at a specified position at a specified alititude during a specified duration with a specified speed. +-- * @{#CONTROLLABLE.TaskRefueling}: (AIR) Refueling from the nearest tanker. No parameters. +-- * @{#CONTROLLABLE.TaskRoute}: (AIR + GROUND) Return a Misson task to follow a given route defined by Points. +-- * @{#CONTROLLABLE.TaskRouteToVec2}: (AIR + GROUND) Make the Controllable move to a given point. +-- * @{#CONTROLLABLE.TaskRouteToVec3}: (AIR + GROUND) Make the Controllable move to a given point. +-- * @{#CONTROLLABLE.TaskRouteToZone}: (AIR + GROUND) Route the controllable to a given zone. +-- * @{#CONTROLLABLE.TaskReturnToBase}: (AIR) Route the controllable to an airbase. +-- +-- ## 2.2) EnRoute assignment +-- +-- EnRoute tasks require the targets of the task need to be detected by the controllable (using its sensors) before the task can be executed: +-- +-- * @{#CONTROLLABLE.EnRouteTaskAWACS}: (AIR) Aircraft will act as an AWACS for friendly units (will provide them with information about contacts). No parameters. +-- * @{#CONTROLLABLE.EnRouteTaskEngageControllable}: (AIR) Engaging a controllable. The task does not assign the target controllable to the unit/controllable to attack now; it just allows the unit/controllable to engage the target controllable as well as other assigned targets. +-- * @{#CONTROLLABLE.EnRouteTaskEngageTargets}: (AIR) Engaging targets of defined types. +-- * @{#CONTROLLABLE.EnRouteTaskEngageTargetsInZone}: (AIR) Engaging a targets of defined types at circle-shaped zone. +-- * @{#CONTROLLABLE.EnRouteTaskEWR}: (AIR) Attack the Unit. +-- * @{#CONTROLLABLE.EnRouteTaskFAC}: (AIR + GROUND) The task makes the controllable/unit a FAC and lets the FAC to choose a targets (enemy ground controllable) around as well as other assigned targets. +-- * @{#CONTROLLABLE.EnRouteTaskFAC_EngageControllable}: (AIR + GROUND) The task makes the controllable/unit a FAC and lets the FAC to choose the target (enemy ground controllable) as well as other assigned targets. +-- * @{#CONTROLLABLE.EnRouteTaskTanker}: (AIR) Aircraft will act as a tanker for friendly units. No parameters. +-- +-- ## 2.3) Task preparation +-- +-- There are certain task methods that allow to tailor the task behaviour: +-- +-- * @{#CONTROLLABLE.TaskWrappedAction}: Return a WrappedAction Task taking a Command. +-- * @{#CONTROLLABLE.TaskCombo}: Return a Combo Task taking an array of Tasks. +-- * @{#CONTROLLABLE.TaskCondition}: Return a condition section for a controlled task. +-- * @{#CONTROLLABLE.TaskControlled}: Return a Controlled Task taking a Task and a TaskCondition. +-- +-- ## 2.4) Call a function as a Task +-- +-- A function can be called which is part of a Task. The method @{#CONTROLLABLE.TaskFunction}() prepares +-- a Task that can call a GLOBAL function from within the Controller execution. +-- This method can also be used to **embed a function call when a certain waypoint has been reached**. +-- See below the **Tasks at Waypoints** section. +-- +-- Demonstration Mission: [GRP-502 - Route at waypoint to random point](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/GRP - Group Commands/GRP-502 - Route at waypoint to random point) +-- +-- ## 2.5) Tasks at Waypoints +-- +-- Special Task methods are available to set tasks at certain waypoints. +-- The method @{#CONTROLLABLE.SetTaskWaypoint}() helps preparing a Route, embedding a Task at the Waypoint of the Route. +-- +-- This creates a Task element, with an action to call a function as part of a Wrapped Task. +-- +-- ## 2.6) Obtain the mission from controllable templates +-- +-- Controllable templates contain complete mission descriptions. Sometimes you want to copy a complete mission from a controllable and assign it to another: +-- +-- * @{#CONTROLLABLE.TaskMission}: (AIR + GROUND) Return a mission task from a mission template. +-- +-- # 3) Command methods +-- +-- Controllable **command methods** prepare the execution of commands using the @{#CONTROLLABLE.SetCommand} method: +-- +-- * @{#CONTROLLABLE.CommandDoScript}: Do Script command. +-- * @{#CONTROLLABLE.CommandSwitchWayPoint}: Perform a switch waypoint command. +-- +-- # 4) Routing of Controllables +-- +-- Different routing methods exist to route GROUPs and UNITs to different locations: +-- +-- * @{#CONTROLLABLE.Route}(): Make the Controllable to follow a given route. +-- * @{#CONTROLLABLE.RouteGroundTo}(): Make the GROUND Controllable to drive towards a specific coordinate. +-- * @{#CONTROLLABLE.RouteAirTo}(): Make the AIR Controllable to fly towards a specific coordinate. +-- * @{#CONTROLLABLE.RelocateGroundRandomInRadius}(): Relocate the GROUND controllable to a random point in a given radius. +-- +-- # 5) Option methods +-- +-- Controllable **Option methods** change the behaviour of the Controllable while being alive. +-- +-- ## 5.1) Rule of Engagement: +-- +-- * @{#CONTROLLABLE.OptionROEWeaponFree} +-- * @{#CONTROLLABLE.OptionROEOpenFire} +-- * @{#CONTROLLABLE.OptionROEReturnFire} +-- * @{#CONTROLLABLE.OptionROEEvadeFire} +-- +-- To check whether an ROE option is valid for a specific controllable, use: +-- +-- * @{#CONTROLLABLE.OptionROEWeaponFreePossible} +-- * @{#CONTROLLABLE.OptionROEOpenFirePossible} +-- * @{#CONTROLLABLE.OptionROEReturnFirePossible} +-- * @{#CONTROLLABLE.OptionROEEvadeFirePossible} +-- +-- ## 5.2) Reaction On Thread: +-- +-- * @{#CONTROLLABLE.OptionROTNoReaction} +-- * @{#CONTROLLABLE.OptionROTPassiveDefense} +-- * @{#CONTROLLABLE.OptionROTEvadeFire} +-- * @{#CONTROLLABLE.OptionROTVertical} +-- +-- To test whether an ROT option is valid for a specific controllable, use: +-- +-- * @{#CONTROLLABLE.OptionROTNoReactionPossible} +-- * @{#CONTROLLABLE.OptionROTPassiveDefensePossible} +-- * @{#CONTROLLABLE.OptionROTEvadeFirePossible} +-- * @{#CONTROLLABLE.OptionROTVerticalPossible} +-- +-- ## 5.3) Alarm state: +-- +-- * @{#CONTROLLABLE.OptionAlarmStateAuto} +-- * @{#CONTROLLABLE.OptionAlarmStateGreen} +-- * @{#CONTROLLABLE.OptionAlarmStateRed} +-- +-- ## 5.4) Jettison weapons: +-- +-- * @{#CONTROLLABLE.OptionAllowJettisonWeaponsOnThreat} +-- * @{#CONTROLLABLE.OptionKeepWeaponsOnThreat} +-- +-- ## 5.5) Air-2-Air missile attack range: +-- * @{#CONTROLLABLE.OptionAAAttackRange}(): Defines the usage of A2A missiles against possible targets . +-- +-- @field #CONTROLLABLE +CONTROLLABLE = { + ClassName = "CONTROLLABLE", + ControllableName = "", + WayPointFunctions = {}, +} + +--- Create a new CONTROLLABLE from a DCSControllable +-- @param #CONTROLLABLE self +-- @param #string ControllableName The DCS Controllable name +-- @return #CONTROLLABLE self +function CONTROLLABLE:New( ControllableName ) + local self = BASE:Inherit( self, POSITIONABLE:New( ControllableName ) ) -- #CONTROLLABLE + --self:F( ControllableName ) + self.ControllableName = ControllableName + + self.TaskScheduler = SCHEDULER:New( self ) + return self +end + +-- DCS Controllable methods support. + +--- Get the controller for the CONTROLLABLE. +-- @param #CONTROLLABLE self +-- @return DCS#Controller +function CONTROLLABLE:_GetController() + local DCSControllable = self:GetDCSObject() + + if DCSControllable then + local ControllableController = DCSControllable:getController() + return ControllableController + end + + return nil +end + +-- Get methods + + +--- Returns the health. Dead controllables have health <= 1.0. +-- @param #CONTROLLABLE self +-- @return #number The controllable health value (unit or group average). +-- @return #nil The controllable is not existing or alive. +function CONTROLLABLE:GetLife() + self:F2( self.ControllableName ) + + local DCSControllable = self:GetDCSObject() + + if DCSControllable then + local UnitLife = 0 + local Units = self:GetUnits() + if #Units == 1 then + local Unit = Units[1] -- Wrapper.Unit#UNIT + UnitLife = Unit:GetLife() + else + local UnitLifeTotal = 0 + for UnitID, Unit in pairs( Units ) do + local Unit = Unit -- Wrapper.Unit#UNIT + UnitLifeTotal = UnitLifeTotal + Unit:GetLife() + end + UnitLife = UnitLifeTotal / #Units + end + return UnitLife + end + + return nil +end + +--- Returns the initial health. +-- @param #CONTROLLABLE self +-- @return #number The controllable health value (unit or group average) or `nil` if the controllable does not exist. +function CONTROLLABLE:GetLife0() + self:F2( self.ControllableName ) + + local DCSControllable = self:GetDCSObject() + + if DCSControllable then + local UnitLife = 0 + local Units = self:GetUnits() + if #Units == 1 then + local Unit = Units[1] -- Wrapper.Unit#UNIT + UnitLife = Unit:GetLife0() + else + local UnitLifeTotal = 0 + for UnitID, Unit in pairs( Units ) do + local Unit = Unit -- Wrapper.Unit#UNIT + UnitLifeTotal = UnitLifeTotal + Unit:GetLife0() + end + UnitLife = UnitLifeTotal / #Units + end + return UnitLife + end + + return nil +end + +--- Returns relative minimum amount of fuel (from 0.0 to 1.0) a unit or group has in its internal tanks. +-- This method returns nil to ensure polymorphic behaviour! This method needs to be overridden by GROUP or UNIT. +-- @param #CONTROLLABLE self +-- @return #nil The CONTROLLABLE is not existing or alive. +function CONTROLLABLE:GetFuelMin() + self:F( self.ControllableName ) + + return nil +end + +--- Returns relative average amount of fuel (from 0.0 to 1.0) a unit or group has in its internal tanks. +-- This method returns nil to ensure polymorphic behaviour! This method needs to be overridden by GROUP or UNIT. +-- @param #CONTROLLABLE self +-- @return #nil The CONTROLLABLE is not existing or alive. +function CONTROLLABLE:GetFuelAve() + self:F( self.ControllableName ) + + return nil +end + +--- Returns relative amount of fuel (from 0.0 to 1.0) the unit has in its internal tanks. +-- This method returns nil to ensure polymorphic behaviour! This method needs to be overridden by GROUP or UNIT. +-- @param #CONTROLLABLE self +-- @return #nil The CONTROLLABLE is not existing or alive. +function CONTROLLABLE:GetFuel() + self:F( self.ControllableName ) + return nil +end + + +-- Tasks + +--- Clear all tasks from the controllable. +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE +function CONTROLLABLE:ClearTasks() + + local DCSControllable = self:GetDCSObject() + + if DCSControllable then + local Controller = self:_GetController() + Controller:resetTask() + return self + end + + return nil +end + + +--- Popping current Task from the controllable. +-- @param #CONTROLLABLE self +-- @return Wrapper.Controllable#CONTROLLABLE self +function CONTROLLABLE:PopCurrentTask() + self:F2() + + local DCSControllable = self:GetDCSObject() + + if DCSControllable then + local Controller = self:_GetController() + Controller:popTask() + return self + end + + return nil +end + +--- Pushing Task on the queue from the controllable. +-- @param #CONTROLLABLE self +-- @return Wrapper.Controllable#CONTROLLABLE self +function CONTROLLABLE:PushTask( DCSTask, WaitTime ) + self:F2() + + local DCSControllable = self:GetDCSObject() + + if DCSControllable then + + local DCSControllableName = self:GetName() + + -- When a controllable SPAWNs, it takes about a second to get the controllable in the simulator. Setting tasks to unspawned controllables provides unexpected results. + -- Therefore we schedule the functions to set the mission and options for the Controllable. + -- Controller:pushTask( DCSTask ) + + local function PushTask( Controller, DCSTask ) + if self and self:IsAlive() then + local Controller = self:_GetController() + Controller:pushTask( DCSTask ) + else + BASE:E( { DCSControllableName .. " is not alive anymore.", DCSTask = DCSTask } ) + end + end + + if not WaitTime or WaitTime == 0 then + PushTask( self, DCSTask ) + else + self.TaskScheduler:Schedule( self, PushTask, { DCSTask }, WaitTime ) + end + + return self + end + + return nil +end + +--- Clearing the Task Queue and Setting the Task on the queue from the controllable. +-- @param #CONTROLLABLE self +-- @param DCS#Task DCSTask DCS Task array. +-- @param #number WaitTime Time in seconds, before the task is set. +-- @return Wrapper.Controllable#CONTROLLABLE self +function CONTROLLABLE:SetTask( DCSTask, WaitTime ) + self:F( { "SetTask", WaitTime, DCSTask = DCSTask } ) + + local DCSControllable = self:GetDCSObject() + + if DCSControllable then + + local DCSControllableName = self:GetName() + + self:T2( "Controllable Name = " .. DCSControllableName ) + + -- When a controllable SPAWNs, it takes about a second to get the controllable in the simulator. Setting tasks to unspawned controllables provides unexpected results. + -- Therefore we schedule the functions to set the mission and options for the Controllable. + -- Controller.setTask( Controller, DCSTask ) + + local function SetTask( Controller, DCSTask ) + if self and self:IsAlive() then + local Controller = self:_GetController() + --self:I( "Before SetTask" ) + Controller:setTask( DCSTask ) + -- AI_FORMATION class (used by RESCUEHELO) calls SetTask twice per second! hence spamming the DCS log file ==> setting this to trace. + self:T( { ControllableName = self:GetName(), DCSTask = DCSTask } ) + else + BASE:E( { DCSControllableName .. " is not alive anymore.", DCSTask = DCSTask } ) + end + end + + if not WaitTime or WaitTime == 0 then + SetTask( self, DCSTask ) + -- See above. + self:T( { ControllableName = self:GetName(), DCSTask = DCSTask } ) + else + self.TaskScheduler:Schedule( self, SetTask, { DCSTask }, WaitTime ) + end + + return self + end + + return nil +end + +--- Checking the Task Queue of the controllable. Returns false if no task is on the queue. true if there is a task. +-- @param #CONTROLLABLE self +-- @return Wrapper.Controllable#CONTROLLABLE self +function CONTROLLABLE:HasTask() --R2.2 + + local HasTaskResult = false + + local DCSControllable = self:GetDCSObject() + + if DCSControllable then + + local Controller = self:_GetController() + HasTaskResult = Controller:hasTask() + end + + return HasTaskResult +end + + +--- Return a condition section for a controlled task. +-- @param #CONTROLLABLE self +-- @param DCS#Time time DCS mission time. +-- @param #string userFlag Name of the user flag. +-- @param #boolean userFlagValue User flag value *true* or *false*. Could also be numeric, i.e. either 0=*false* or 1=*true*. Other numeric values don't work! +-- @param #string condition Lua string. +-- @param DCS#Time duration Duration in seconds. +-- @param #number lastWayPoint Last waypoint. +-- return DCS#Task +function CONTROLLABLE:TaskCondition( time, userFlag, userFlagValue, condition, duration, lastWayPoint ) + +--[[ + StopCondition = { + time = Time, + userFlag = string, + userFlagValue = boolean, + condition = string, + duration = Time, + lastWaypoint = number, + } +--]] + + local DCSStopCondition = {} + DCSStopCondition.time = time + DCSStopCondition.userFlag = userFlag + DCSStopCondition.userFlagValue = userFlagValue + DCSStopCondition.condition = condition + DCSStopCondition.duration = duration + DCSStopCondition.lastWayPoint = lastWayPoint + + return DCSStopCondition +end + +--- Return a Controlled Task taking a Task and a TaskCondition. +-- @param #CONTROLLABLE self +-- @param DCS#Task DCSTask +-- @param DCS#DCSStopCondition DCSStopCondition +-- @return DCS#Task +function CONTROLLABLE:TaskControlled( DCSTask, DCSStopCondition ) + + local DCSTaskControlled = { + id = 'ControlledTask', + params = { + task = DCSTask, + stopCondition = DCSStopCondition + } + } + + return DCSTaskControlled +end + +--- Return a Combo Task taking an array of Tasks. +-- @param #CONTROLLABLE self +-- @param DCS#TaskArray DCSTasks Array of @{DCSTasking.Task#Task} +-- @return DCS#Task +function CONTROLLABLE:TaskCombo( DCSTasks ) + + local DCSTaskCombo = { + id = 'ComboTask', + params = { + tasks = DCSTasks + } + } + + return DCSTaskCombo +end + +--- Return a WrappedAction Task taking a Command. +-- @param #CONTROLLABLE self +-- @param DCS#Command DCSCommand +-- @return DCS#Task +function CONTROLLABLE:TaskWrappedAction( DCSCommand, Index ) + + local DCSTaskWrappedAction = { + id = "WrappedAction", + enabled = true, + number = Index or 1, + auto = false, + params = { + action = DCSCommand, + }, + } + + return DCSTaskWrappedAction +end + +--- Set a Task at a Waypoint using a Route list. +-- @param #CONTROLLABLE self +-- @param #table Waypoint The Waypoint! +-- @param DCS#Task Task The Task structure to be executed! +-- @return DCS#Task +function CONTROLLABLE:SetTaskWaypoint( Waypoint, Task ) + + Waypoint.task = self:TaskCombo( { Task } ) + + self:F( { Waypoint.task } ) + return Waypoint.task +end + + + + +--- Executes a command action for the CONTROLLABLE. +-- @param #CONTROLLABLE self +-- @param DCS#Command DCSCommand The command to be executed. +-- @return #CONTROLLABLE self +function CONTROLLABLE:SetCommand( DCSCommand ) + self:F2( DCSCommand ) + + local DCSControllable = self:GetDCSObject() + + if DCSControllable then + local Controller = self:_GetController() + Controller:setCommand( DCSCommand ) + return self + end + + return nil +end + +--- Perform a switch waypoint command +-- @param #CONTROLLABLE self +-- @param #number FromWayPoint +-- @param #number ToWayPoint +-- @return DCS#Task +-- @usage +-- --- This test demonstrates the use(s) of the SwitchWayPoint method of the GROUP class. +-- HeliGroup = GROUP:FindByName( "Helicopter" ) +-- +-- --- Route the helicopter back to the FARP after 60 seconds. +-- -- We use the SCHEDULER class to do this. +-- SCHEDULER:New( nil, +-- function( HeliGroup ) +-- local CommandRTB = HeliGroup:CommandSwitchWayPoint( 2, 8 ) +-- HeliGroup:SetCommand( CommandRTB ) +-- end, { HeliGroup }, 90 +-- ) +function CONTROLLABLE:CommandSwitchWayPoint( FromWayPoint, ToWayPoint ) + self:F2( { FromWayPoint, ToWayPoint } ) + + local CommandSwitchWayPoint = { + id = 'SwitchWaypoint', + params = { + fromWaypointIndex = FromWayPoint, + goToWaypointIndex = ToWayPoint, + }, + } + + self:T3( { CommandSwitchWayPoint } ) + return CommandSwitchWayPoint +end + +--- Create a stop route command, which returns a string containing the command. +-- Use the result in the method @{#CONTROLLABLE.SetCommand}(). +-- A value of true will make the ground group stop, a value of false will make it continue. +-- Note that this can only work on GROUP level, although individual UNITs can be commanded, the whole GROUP will react. +-- +-- Example missions: +-- +-- * GRP-310 +-- +-- @param #CONTROLLABLE self +-- @param #boolean StopRoute true if the ground unit needs to stop, false if it needs to continue to move. +-- @return DCS#Task +function CONTROLLABLE:CommandStopRoute( StopRoute ) + self:F2( { StopRoute } ) + + local CommandStopRoute = { + id = 'StopRoute', + params = { + value = StopRoute, + }, + } + + self:T3( { CommandStopRoute } ) + return CommandStopRoute +end + + +--- Give an uncontrolled air controllable the start command. +-- @param #CONTROLLABLE self +-- @param #number delay (Optional) Delay before start command in seconds. +-- @return #CONTROLLABLE self +function CONTROLLABLE:StartUncontrolled(delay) + if delay and delay>0 then + SCHEDULER:New(nil, CONTROLLABLE.StartUncontrolled, {self}, delay) + else + self:SetCommand({id='Start', params={}}) + end + return self +end + +--- Give the CONTROLLABLE the command to activate a beacon. See [DCS_command_activateBeacon](https://wiki.hoggitworld.com/view/DCS_command_activateBeacon) on Hoggit. +-- For specific beacons like TACAN use the more convenient @{#BEACON} class. +-- Note that a controllable can only have one beacon activated at a time with the execption of ICLS. +-- @param #CONTROLLABLE self +-- @param Core.Radio#BEACON.Type Type Beacon type (VOR, DME, TACAN, RSBN, ILS etc). +-- @param Core.Radio#BEACON.System System Beacon system (VOR, DME, TACAN, RSBN, ILS etc). +-- @param #number Frequency Frequency in Hz the beacon is running on. Use @{#UTILS.TACANToFrequency} to generate a frequency for TACAN beacons. +-- @param #number UnitID The ID of the unit the beacon is attached to. Usefull if more units are in one group. +-- @param #number Channel Channel the beacon is using. For, e.g. TACAN beacons. +-- @param #string ModeChannel The TACAN mode of the beacon, i.e. "X" or "Y". +-- @param #boolean AA If true, create and Air-Air beacon. IF nil, automatically set if CONTROLLABLE depending on whether unit is and aircraft or not. +-- @param #string Callsign Morse code identification callsign. +-- @param #boolean Bearing If true, beacon provides bearing information - if supported by the unit the beacon is attached to. +-- @param #number Delay (Optional) Delay in seconds before the beacon is activated. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandActivateBeacon(Type, System, Frequency, UnitID, Channel, ModeChannel, AA, Callsign, Bearing, Delay) + + AA=AA or self:IsAir() + UnitID=UnitID or self:GetID() + + -- Command + local CommandActivateBeacon= { + id = "ActivateBeacon", + params = { + ["type"] = Type, + ["system"] = System, + ["frequency"] = Frequency, + ["unitId"] = UnitID, + ["channel"] = Channel, + ["modeChannel"] = ModeChannel, + ["AA"] = AA, + ["callsign"] = Callsign, + ["bearing"] = Bearing, + } + } + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandActivateBeacon, {self, Type, System, Frequency, UnitID, Channel, ModeChannel, AA, Callsign, Bearing}, Delay) + else + self:SetCommand(CommandActivateBeacon) + end + + return self +end + +--- Activate ICLS system of the CONTROLLABLE. The controllable should be an aircraft carrier! +-- @param #CONTROLLABLE self +-- @param #number Channel ICLS channel. +-- @param #number UnitID The ID of the unit the ICLS system is attached to. Useful if more units are in one group. +-- @param #string Callsign Morse code identification callsign. +-- @param #number Delay (Optional) Delay in seconds before the ICLS is deactivated. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandActivateICLS(Channel, UnitID, Callsign, Delay) + + -- Command to activate ICLS system. + local CommandActivateICLS= { + id = "ActivateICLS", + params= { + ["type"] = BEACON.Type.ICLS, + ["channel"] = Channel, + ["unitId"] = UnitID, + ["callsign"] = Callsign, + } + } + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandActivateICLS, {self}, Delay) + else + self:SetCommand(CommandActivateICLS) + end + + return self +end + + +--- Deactivate the active beacon of the CONTROLLABLE. +-- @param #CONTROLLABLE self +-- @param #number Delay (Optional) Delay in seconds before the beacon is deactivated. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandDeactivateBeacon(Delay) + + -- Command to deactivate + local CommandDeactivateBeacon={id='DeactivateBeacon', params={}} + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandActivateBeacon, {self}, Delay) + else + self:SetCommand(CommandDeactivateBeacon) + end + + return self +end + +--- Deactivate the ICLS of the CONTROLLABLE. +-- @param #CONTROLLABLE self +-- @param #number Delay (Optional) Delay in seconds before the ICLS is deactivated. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandDeactivateICLS(Delay) + + -- Command to deactivate + local CommandDeactivateICLS={id='DeactivateICLS', params={}} + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandDeactivateICLS, {self}, Delay) + else + self:SetCommand(CommandDeactivateICLS) + end + + return self +end + +--- Set callsign of the CONTROLLABLE. See [DCS command setCallsign](https://wiki.hoggitworld.com/view/DCS_command_setCallsign) +-- @param #CONTROLLABLE self +-- @param DCS#CALLSIGN CallName Number corresponding the the callsign identifier you wish this group to be called. +-- @param #number CallNumber The number value the group will be referred to as. Only valid numbers are 1-9. For example Uzi **5**-1. Default 1. +-- @param #number Delay (Optional) Delay in seconds before the callsign is set. Default is immediately. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandSetCallsign(CallName, CallNumber, Delay) + + -- Command to set the callsign. + local CommandSetCallsign={id='SetCallsign', params={callname=CallName, number=CallNumber or 1}} + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandSetCallsign, {self, CallName, CallNumber}, Delay) + else + self:SetCommand(CommandSetCallsign) + end + + return self +end + +--- Set EPLRS of the CONTROLLABLE on/off. See [DCS command EPLRS](https://wiki.hoggitworld.com/view/DCS_command_eplrs) +-- @param #CONTROLLABLE self +-- @param #boolean SwitchOnOff If true (or nil) switch EPLRS on. If false switch off. +-- @param #number Delay (Optional) Delay in seconds before the callsign is set. Default is immediately. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandEPLRS(SwitchOnOff, Delay) + + if SwitchOnOff==nil then + SwitchOnOff=true + end + + -- Command to set the callsign. + local CommandEPLRS={ + id='EPLRS', + params={ + value=SwitchOnOff, + groupId=self:GetID() + } + } + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandEPLRS, {self, SwitchOnOff}, Delay) + else + self:T(string.format("EPLRS=%s for controllable %s (id=%s)", tostring(SwitchOnOff), tostring(self:GetName()), tostring(self:GetID()))) + self:SetCommand(CommandEPLRS) + end + + return self +end + +--- Set radio frequency. See [DCS command EPLRS](https://wiki.hoggitworld.com/view/DCS_command_setFrequency) +-- @param #CONTROLLABLE self +-- @param #number Frequency Radio frequency in MHz. +-- @param #number Modulation Radio modulation. Default `radio.modulation.AM`. +-- @param #number Delay (Optional) Delay in seconds before the frequncy is set. Default is immediately. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandSetFrequency(Frequency, Modulation, Delay) + + local CommandSetFrequency = { + id = 'SetFrequency', + params = { + frequency = Frequency*1000000, + modulation = Modulation or radio.modulation.AM, + } + } + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandSetFrequency, {self, Frequency, Modulation}, Delay) + else + self:SetCommand(CommandSetFrequency) + end + + return self +end + + +--- Set EPLRS data link on/off. +-- @param #CONTROLLABLE self +-- @param #boolean SwitchOnOff If true (or nil) switch EPLRS on. If false switch off. +-- @param #number idx Task index. Default 1. +-- @return #table Task wrapped action. +function CONTROLLABLE:TaskEPLRS(SwitchOnOff, idx) + + if SwitchOnOff==nil then + SwitchOnOff=true + end + + -- Command to set the callsign. + local CommandEPLRS={ + id='EPLRS', + params={ + value=SwitchOnOff, + groupId=self:GetID() + } + } + + return self:TaskWrappedAction(CommandEPLRS, idx or 1) +end + + +-- TASKS FOR AIR CONTROLLABLES + +--- (AIR) Attack a Controllable. +-- @param #CONTROLLABLE self +-- @param Wrapper.Group#GROUP AttackGroup The Group to be attacked. +-- @param #number WeaponType (optional) Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. +-- @param DCS#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. +-- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. +-- @param DCS#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. +-- @param DCS#Distance Altitude (optional) Desired attack start altitude. Controllable/aircraft will make its attacks from the altitude. If the altitude is too low or too high to use weapon aircraft/controllable will choose closest altitude to the desired attack start altitude. If the desired altitude is defined controllable/aircraft will not attack from safe altitude. +-- @param #boolean AttackQtyLimit (optional) The flag determines how to interpret attackQty parameter. If the flag is true then attackQty is a limit on maximal attack quantity for "AttackGroup" and "AttackUnit" tasks. If the flag is false then attackQty is a desired attack quantity for "Bombing" and "BombingRunway" tasks. +-- @param #boolean GroupAttack (Optional) If true, attack as group. +-- @return DCS#Task The DCS task structure. +function CONTROLLABLE:TaskAttackGroup( AttackGroup, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit, GroupAttack ) + --self:F2( { self.ControllableName, AttackGroup, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit } ) + + -- AttackGroup = { + -- id = 'AttackGroup', + -- params = { + -- groupId = Group.ID, + -- weaponType = number, + -- expend = enum AI.Task.WeaponExpend, + -- attackQty = number, + -- directionEnabled = boolean, + -- direction = Azimuth, + -- altitudeEnabled = boolean, + -- altitude = Distance, + -- attackQtyLimit = boolean, + -- } + -- } + + + local DCSTask = { id = 'AttackGroup', + params = { + groupId = AttackGroup:GetID(), + weaponType = WeaponType or 1073741822, + expend = WeaponExpend or "Auto", + attackQtyLimit = AttackQty and true or false, + attackQty = AttackQty or 1, + directionEnabled = Direction and true or false, + direction = Direction and math.rad(Direction) or 0, + altitudeEnabled = Altitude and true or false, + altitude = Altitude, + groupAttack = GroupAttack and true or false, + }, + } + + return DCSTask +end + +--- (AIR) Attack the Unit. +-- @param #CONTROLLABLE self +-- @param Wrapper.Unit#UNIT AttackUnit The UNIT to be attacked +-- @param #boolean GroupAttack (Optional) If true, all units in the group will attack the Unit when found. Default false. +-- @param DCS#AI.Task.WeaponExpend WeaponExpend (Optional) Determines how many weapons will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. +-- @param #number AttackQty (Optional) Limits maximal quantity of attack. The aicraft/controllable will not make more attacks than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. +-- @param DCS#Azimuth Direction (Optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. +-- @param #number Altitude (Optional) The (minimum) altitude in meters from where to attack. Default is altitude of unit to attack but at least 1000 m. +-- @param #number WeaponType (optional) The WeaponType. See [DCS Enumerator Weapon Type](https://wiki.hoggitworld.com/view/DCS_enum_weapon_flag) on Hoggit. +-- @return DCS#Task The DCS task structure. +function CONTROLLABLE:TaskAttackUnit(AttackUnit, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType) + + local DCSTask = { + id = 'AttackUnit', + params = { + unitId = AttackUnit:GetID(), + groupAttack = GroupAttack and GroupAttack or false, + expend = WeaponExpend or "Auto", + directionEnabled = Direction and true or false, + direction = Direction and math.rad(Direction) or 0, + altitudeEnabled = Altitude and true or false, + altitude = Altitude, + attackQtyLimit = AttackQty and true or false, + attackQty = AttackQty, + weaponType = WeaponType or 1073741822, + } + } + + return DCSTask +end + + +--- (AIR) Delivering weapon at the point on the ground. +-- @param #CONTROLLABLE self +-- @param DCS#Vec2 Vec2 2D-coordinates of the point to deliver weapon at. +-- @param #boolean GroupAttack (optional) If true, all units in the group will attack the Unit when found. +-- @param DCS#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. +-- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. +-- @param DCS#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. +-- @param #number Altitude (optional) The altitude from where to attack. +-- @param #number WeaponType (optional) The WeaponType. +-- @param #boolean Divebomb (optional) Perform dive bombing. Default false. +-- @return DCS#Task The DCS task structure. +function CONTROLLABLE:TaskBombing( Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType, Divebomb ) + + local DCSTask = { + id = 'Bombing', + params = { + point = Vec2, + x = Vec2.x, + y = Vec2.y, + groupAttack = GroupAttack and GroupAttack or false, + expend = WeaponExpend or "Auto", + attackQtyLimit = AttackQty and true or false, + attackQty = AttackQty or 1, + directionEnabled = Direction and true or false, + direction = Direction and math.rad(Direction) or 0, + altitudeEnabled = Altitude and true or false, + altitude = Altitude or 2000, + weaponType = WeaponType or 1073741822, + attackType = Divebomb and "Dive" or nil, + }, + } + + return DCSTask +end + +--- (AIR) Attacking the map object (building, structure, etc). +-- @param #CONTROLLABLE self +-- @param DCS#Vec2 Vec2 2D-coordinates of the point to deliver weapon at. +-- @param #boolean GroupAttack (Optional) If true, all units in the group will attack the Unit when found. +-- @param DCS#AI.Task.WeaponExpend WeaponExpend (Optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit will choose expend on its own discretion. +-- @param #number AttackQty (Optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. +-- @param DCS#Azimuth Direction (Optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. +-- @param #number Altitude (Optional) The altitude [meters] from where to attack. Default 30 m. +-- @param #number WeaponType (Optional) The WeaponType. Default Auto=1073741822. +-- @return DCS#Task The DCS task structure. +function CONTROLLABLE:TaskAttackMapObject( Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType ) + + local DCSTask = { + id = 'AttackMapObject', + params = { + point = Vec2, + x = Vec2.x, + y = Vec2.y, + groupAttack = GroupAttack or false, + expend = WeaponExpend or "Auto", + attackQtyLimit = AttackQty and true or false, + attackQty = AttackQty, + directionEnabled = Direction and true or false, + direction = Direction and math.rad(Direction) or 0, + altitudeEnabled = Altitude and true or false, + altitude = Altitude, + weaponType = WeaponType or 1073741822, + }, + } + + return DCSTask +end + + +--- (AIR) Delivering weapon via CarpetBombing (all bombers in formation release at same time) at the point on the ground. +-- @param #CONTROLLABLE self +-- @param DCS#Vec2 Vec2 2D-coordinates of the point to deliver weapon at. +-- @param #boolean GroupAttack (optional) If true, all units in the group will attack the Unit when found. +-- @param DCS#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit will choose expend on its own discretion. +-- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. +-- @param DCS#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. +-- @param #number Altitude (optional) The altitude from where to attack. +-- @param #number WeaponType (optional) The WeaponType. +-- @param #number CarpetLength (optional) default to 500 m. +-- @return DCS#Task The DCS task structure. +function CONTROLLABLE:TaskCarpetBombing(Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType, CarpetLength) + + -- Build Task Structure + local DCSTask = { + id = 'CarpetBombing', + params = { + attackType = "Carpet", + x = Vec2.x, + y = Vec2.y, + groupAttack = GroupAttack and GroupAttack or false, + carpetLength = CarpetLength or 500, + weaponType = WeaponType or ENUMS.WeaponFlag.AnyBomb, + expend = WeaponExpend or "All", + attackQtyLimit = AttackQty and true or false, + attackQty = AttackQty or 1, + directionEnabled = Direction and true or false, + direction = Direction and math.rad(Direction) or 0, + altitudeEnabled = Altitude and true or false, + altitude = Altitude, + } + } + + return DCSTask +end + + + +--- (AIR) Following another airborne controllable. +-- The unit / controllable will follow lead unit of another controllable, wingmens of both controllables will continue following their leaders. +-- Used to support CarpetBombing Task +-- @param #CONTROLLABLE self +-- @param #CONTROLLABLE FollowControllable The controllable to be followed. +-- @param DCS#Vec3 Vec3 Position of the unit / lead unit of the controllable relative lead unit of another controllable in frame reference oriented by course of lead unit of another controllable. If another controllable is on land the unit / controllable will orbit around. +-- @param #number LastWaypointIndex Detach waypoint of another controllable. Once reached the unit / controllable Follow task is finished. +-- @return DCS#Task The DCS task structure. +function CONTROLLABLE:TaskFollowBigFormation(FollowControllable, Vec3, LastWaypointIndex ) + + local DCSTask = { + id = 'FollowBigFormation', + params = { + groupId = FollowControllable:GetID(), + pos = Vec3, + lastWptIndexFlag = LastWaypointIndex and true or false, + lastWptIndex = LastWaypointIndex + } + } + + return DCSTask +end + + +--- (AIR HELICOPTER) Move the controllable to a Vec2 Point, wait for a defined duration and embark infantry groups. +-- @param #CONTROLLABLE self +-- @param Core.Point#COORDINATE Coordinate The point where to pickup the troops. +-- @param Core.Set#SET_GROUP GroupSetForEmbarking Set of groups to embark. +-- @param #number Duration (Optional) The maximum duration in seconds to wait until all groups have embarked. +-- @param #table Distribution (Optional) Distribution used to put the infantry groups into specific carrier units. +-- @return DCS#Task The DCS task structure. +function CONTROLLABLE:TaskEmbarking(Coordinate, GroupSetForEmbarking, Duration, Distribution) + + -- Table of group IDs for embarking. + local g4e={} + + if GroupSetForEmbarking then + for _,_group in pairs(GroupSetForEmbarking:GetSet()) do + local group=_group --Wrapper.Group#GROUP + table.insert(g4e, group:GetID()) + end + else + self:E("ERROR: No groups for embarking specified!") + return nil + end + + -- Table of group IDs for embarking. + --local Distribution={} + + -- Distribution + --local distribution={} + --distribution[id]=gids + + local groupID=self and self:GetID() + + local DCSTask = { + id = 'Embarking', + params = { + selectedTransport = groupID, + x = Coordinate.x, + y = Coordinate.z, + groupsForEmbarking = g4e, + durationFlag = Duration and true or false, + duration = Duration, + distributionFlag = Distribution and true or false, + distribution = Distribution, + } + } + + return DCSTask +end + + +--- Used in conjunction with the embarking task for a transport helicopter group. The Ground units will move to the specified location and wait to be picked up by a helicopter. +-- The helicopter will then fly them to their dropoff point defined by another task for the ground forces; DisembarkFromTransport task. +-- The controllable has to be an infantry group! +-- @param #CONTROLLABLE self +-- @param Core.Point#COORDINATE Coordinate Coordinates where AI is expecting to be picked up. +-- @param #number Radius Radius in meters. Default 200 m. +-- @param #string UnitType The unit type name of the carrier, e.g. "UH-1H". Must not be specified. +-- @return DCS#Task Embark to transport task. +function CONTROLLABLE:TaskEmbarkToTransport(Coordinate, Radius, UnitType) + + local EmbarkToTransport = { + id = "EmbarkToTransport", + params={ + x = Coordinate.x, + y = Coordinate.z, + zoneRadius = Radius or 200, + selectedType = UnitType, + } + } + + return EmbarkToTransport +end + + +--- Specifies the location infantry groups that is being transported by helicopters will be unloaded at. Used in conjunction with the EmbarkToTransport task. +-- @param #CONTROLLABLE self +-- @param Core.Point#COORDINATE Coordinate Coordinates where AI is expecting to be picked up. +-- @return DCS#Task Embark to transport task. +function CONTROLLABLE:TaskDisembarking(Coordinate, GroupSetToDisembark) + + -- Table of group IDs for disembarking. + local g4e={} + + if GroupSetToDisembark then + for _,_group in pairs(GroupSetToDisembark:GetSet()) do + local group=_group --Wrapper.Group#GROUP + table.insert(g4e, group:GetID()) + end + else + self:E("ERROR: No groups for disembarking specified!") + return nil + end + + local Disembarking={ + id = "Disembarking", + params = { + x = Coordinate.x, + y = Coordinate.z, + groupsForEmbarking = g4e, -- This is no bug, the entry is really "groupsForEmbarking" even if we disembark the troops. + } + } + + return Disembarking +end + + +--- (AIR) Orbit at a specified position at a specified alititude during a specified duration with a specified speed. +-- @param #CONTROLLABLE self +-- @param DCS#Vec2 Point The point to hold the position. +-- @param #number Altitude The altitude AGL in meters to hold the position. +-- @param #number Speed The speed [m/s] flying when holding the position. +-- @return #CONTROLLABLE self +function CONTROLLABLE:TaskOrbitCircleAtVec2( Point, Altitude, Speed ) + self:F2( { self.ControllableName, Point, Altitude, Speed } ) + + local DCSTask = { + id = 'Orbit', + params = { + pattern = AI.Task.OrbitPattern.CIRCLE, + point = Point, + speed = Speed, + altitude = Altitude + land.getHeight( Point ) + } + } + + return DCSTask +end + +--- (AIR) Orbit at a position with at a given altitude and speed. Optionally, a race track pattern can be specified. +-- @param #CONTROLLABLE self +-- @param Core.Point#COORDINATE Coord Coordinate at which the CONTROLLABLE orbits. +-- @param #number Altitude Altitude in meters of the orbit pattern. Default y component of Coord. +-- @param #number Speed Speed [m/s] flying the orbit pattern. Default 128 m/s = 250 knots. +-- @param Core.Point#COORDINATE CoordRaceTrack (Optional) If this coordinate is specified, the CONTROLLABLE will fly a race-track pattern using this and the initial coordinate. +-- @return #CONTROLLABLE self +function CONTROLLABLE:TaskOrbit(Coord, Altitude, Speed, CoordRaceTrack) + + local Pattern=AI.Task.OrbitPattern.CIRCLE + + local P1=Coord:GetVec2() + local P2=nil + if CoordRaceTrack then + Pattern=AI.Task.OrbitPattern.RACE_TRACK + P2=CoordRaceTrack:GetVec2() + end + + local Task = { + id = 'Orbit', + params = { + pattern = Pattern, + point = P1, + point2 = P2, + speed = Speed or UTILS.KnotsToMps(250), + altitude = Altitude or Coord.y, + } + } + + return Task +end + +--- (AIR) Orbit at the current position of the first unit of the controllable at a specified alititude. +-- @param #CONTROLLABLE self +-- @param #number Altitude The altitude [m] to hold the position. +-- @param #number Speed The speed [m/s] flying when holding the position. +-- @param Core.Point#COORDINATE Coordinate (Optional) The coordinate where to orbit. If the coordinate is not given, then the current position of the controllable is used. +-- @return #CONTROLLABLE self +function CONTROLLABLE:TaskOrbitCircle( Altitude, Speed, Coordinate ) + self:F2( { self.ControllableName, Altitude, Speed } ) + + local DCSControllable = self:GetDCSObject() + + if DCSControllable then + local OrbitVec2 = Coordinate and Coordinate:GetVec2() or self:GetVec2() + return self:TaskOrbitCircleAtVec2( OrbitVec2, Altitude, Speed ) + end + + return nil +end + + + +--- (AIR) Hold position at the current position of the first unit of the controllable. +-- @param #CONTROLLABLE self +-- @param #number Duration The maximum duration in seconds to hold the position. +-- @return #CONTROLLABLE self +function CONTROLLABLE:TaskHoldPosition() + self:F2( { self.ControllableName } ) + + return self:TaskOrbitCircle( 30, 10 ) +end + + +--- (AIR) Delivering weapon on the runway. See [hoggit](https://wiki.hoggitworld.com/view/DCS_task_bombingRunway) +-- +-- Make sure the aircraft has the following role: +-- +-- * CAS +-- * Ground Attack +-- * Runway Attack +-- * Anti-Ship Strike +-- * AFAC +-- * Pinpoint Strike +-- +-- @param #CONTROLLABLE self +-- @param Wrapper.Airbase#AIRBASE Airbase Airbase to attack. +-- @param #number WeaponType (optional) Bitmask of weapon types those allowed to use. See [DCS enum weapon flag](https://wiki.hoggitworld.com/view/DCS_enum_weapon_flag). Default 2147485694 = AnyBomb (GuidedBomb + AnyUnguidedBomb). +-- @param DCS#AI.Task.WeaponExpend WeaponExpend Enum AI.Task.WeaponExpend that defines how much munitions the AI will expend per attack run. Default "ALL". +-- @param #number AttackQty Number of times the group will attack if the target. Default 1. +-- @param DCS#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. +-- @param #boolean GroupAttack (optional) Flag indicates that the target must be engaged by all aircrafts of the controllable. Has effect only if the task is assigned to a group and not to a single aircraft. +-- @return DCS#Task The DCS task structure. +function CONTROLLABLE:TaskBombingRunway(Airbase, WeaponType, WeaponExpend, AttackQty, Direction, GroupAttack) + + local DCSTask = { + id = 'BombingRunway', + params = { + runwayId = Airbase:GetID(), + weaponType = WeaponType or ENUMS.WeaponFlag.AnyBomb, + expend = WeaponExpend or AI.Task.WeaponExpend.ALL, + attackQty = AttackQty or 1, + direction = Direction and math.rad(Direction) or 0, + groupAttack = GroupAttack and true or false, + }, + } + + return DCSTask +end + + +--- (AIR) Refueling from the nearest tanker. No parameters. +-- @param #CONTROLLABLE self +-- @return DCS#Task The DCS task structure. +function CONTROLLABLE:TaskRefueling() + + local DCSTask={ + id='Refueling', + params={} + } + + return DCSTask +end + + +--- (AIR HELICOPTER) Landing at the ground. For helicopters only. +-- @param #CONTROLLABLE self +-- @param DCS#Vec2 Vec2 The point where to land. +-- @param #number Duration The duration in seconds to stay on the ground. +-- @return #CONTROLLABLE self +function CONTROLLABLE:TaskLandAtVec2(Vec2, Duration) + + local DCSTask = { + id = 'Land', + params = { + point = Vec2, + durationFlag = Duration and true or false, + duration = Duration, + }, + } + + return DCSTask +end + +--- (AIR) Land the controllable at a @{Core.Zone#ZONE_RADIUS). +-- @param #CONTROLLABLE self +-- @param Core.Zone#ZONE Zone The zone where to land. +-- @param #number Duration The duration in seconds to stay on the ground. +-- @return #CONTROLLABLE self +function CONTROLLABLE:TaskLandAtZone( Zone, Duration, RandomPoint ) + + -- Get landing point + local Point=RandomPoint and Zone:GetRandomVec2() or Zone:GetVec2() + + local DCSTask = CONTROLLABLE.TaskLandAtVec2( self, Point, Duration ) + + return DCSTask +end + + + +--- (AIR) Following another airborne controllable. +-- The unit / controllable will follow lead unit of another controllable, wingmens of both controllables will continue following their leaders. +-- If another controllable is on land the unit / controllable will orbit around. +-- @param #CONTROLLABLE self +-- @param Wrapper.Controllable#CONTROLLABLE FollowControllable The controllable to be followed. +-- @param DCS#Vec3 Vec3 Position of the unit / lead unit of the controllable relative lead unit of another controllable in frame reference oriented by course of lead unit of another controllable. If another controllable is on land the unit / controllable will orbit around. +-- @param #number LastWaypointIndex Detach waypoint of another controllable. Once reached the unit / controllable Follow task is finished. +-- @return DCS#Task The DCS task structure. +function CONTROLLABLE:TaskFollow( FollowControllable, Vec3, LastWaypointIndex ) + self:F2( { self.ControllableName, FollowControllable, Vec3, LastWaypointIndex } ) + +-- Follow = { +-- id = 'Follow', +-- params = { +-- groupId = Group.ID, +-- pos = Vec3, +-- lastWptIndexFlag = boolean, +-- lastWptIndex = number +-- } +-- } + + local LastWaypointIndexFlag = false + local lastWptIndexFlagChangedManually = false + if LastWaypointIndex then + LastWaypointIndexFlag = true + lastWptIndexFlagChangedManually = true + end + + local DCSTask = { + id = 'Follow', + params = { + groupId = FollowControllable:GetID(), + pos = Vec3, + lastWptIndexFlag = LastWaypointIndexFlag, + lastWptIndex = LastWaypointIndex, + lastWptIndexFlagChangedManually = lastWptIndexFlagChangedManually, + } + } + + self:T3( { DCSTask } ) + return DCSTask +end + + +--- (AIR) Escort another airborne controllable. +-- The unit / controllable will follow lead unit of another controllable, wingmens of both controllables will continue following their leaders. +-- The unit / controllable will also protect that controllable from threats of specified types. +-- @param #CONTROLLABLE self +-- @param Wrapper.Controllable#CONTROLLABLE FollowControllable The controllable to be escorted. +-- @param DCS#Vec3 Vec3 Position of the unit / lead unit of the controllable relative lead unit of another controllable in frame reference oriented by course of lead unit of another controllable. If another controllable is on land the unit / controllable will orbit around. +-- @param #number LastWaypointIndex Detach waypoint of another controllable. Once reached the unit / controllable Follow task is finished. +-- @param #number EngagementDistance Maximal distance from escorted controllable to threat. If the threat is already engaged by escort escort will disengage if the distance becomes greater than 1.5 * engagementDistMax. +-- @param DCS#AttributeNameArray TargetTypes Array of AttributeName that is contains threat categories allowed to engage. Default {"Air"}. +-- @return DCS#Task The DCS task structure. +function CONTROLLABLE:TaskEscort( FollowControllable, Vec3, LastWaypointIndex, EngagementDistance, TargetTypes ) + +-- Escort = { +-- id = 'Escort', +-- params = { +-- groupId = Group.ID, +-- pos = Vec3, +-- lastWptIndexFlag = boolean, +-- lastWptIndex = number, +-- engagementDistMax = Distance, +-- targetTypes = array of AttributeName, +-- } +-- } + + local DCSTask + DCSTask = { + id = 'Escort', + params = { + groupId = FollowControllable:GetID(), + pos = Vec3, + lastWptIndexFlag = LastWaypointIndex and true or false, + lastWptIndex = LastWaypointIndex, + engagementDistMax = EngagementDistance, + targetTypes = TargetTypes or {"Air"}, + }, + } + + return DCSTask +end + + +-- GROUND TASKS + +--- (GROUND) Fire at a VEC2 point until ammunition is finished. +-- @param #CONTROLLABLE self +-- @param DCS#Vec2 Vec2 The point to fire at. +-- @param DCS#Distance Radius The radius of the zone to deploy the fire at. +-- @param #number AmmoCount (optional) Quantity of ammunition to expand (omit to fire until ammunition is depleted). +-- @param #number WeaponType (optional) Enum for weapon type ID. This value is only required if you want the group firing to use a specific weapon, for instance using the task on a ship to force it to fire guided missiles at targets within cannon range. See http://wiki.hoggit.us/view/DCS_enum_weapon_flag +-- @param #number Altitude (Optional) Altitude in meters. +-- @param #number ASL Altitude is above mean sea level. Default is above ground level. +-- @return DCS#Task The DCS task structure. +function CONTROLLABLE:TaskFireAtPoint( Vec2, Radius, AmmoCount, WeaponType, Altitude, ASL ) + + local DCSTask = { + id = 'FireAtPoint', + params = { + point = Vec2, + x=Vec2.x, + y=Vec2.y, + zoneRadius = Radius, + radius = Radius, + expendQty = 100, -- dummy value + expendQtyEnabled = false, + alt_type = ASL and 0 or 1 + } + } + + if AmmoCount then + DCSTask.params.expendQty = AmmoCount + DCSTask.params.expendQtyEnabled = true + end + + if Altitude then + DCSTask.params.altitude=Altitude + end + + if WeaponType then + DCSTask.params.weaponType=WeaponType + end + + --self:I(DCSTask) + + return DCSTask +end + +--- (GROUND) Hold ground controllable from moving. +-- @param #CONTROLLABLE self +-- @return DCS#Task The DCS task structure. +function CONTROLLABLE:TaskHold() + local DCSTask = {id = 'Hold', params = {}} + return DCSTask +end + + +-- TASKS FOR AIRBORNE AND GROUND UNITS/CONTROLLABLES + +--- (AIR + GROUND) The task makes the controllable/unit a FAC and orders the FAC to control the target (enemy ground controllable) destruction. +-- The killer is player-controlled allied CAS-aircraft that is in contact with the FAC. +-- If the task is assigned to the controllable lead unit will be a FAC. +-- It's important to note that depending on the type of unit that is being assigned the task (AIR or GROUND), you must choose the correct type of callsign enumerator. For airborne controllables use CALLSIGN.Aircraft and for ground based use CALLSIGN.JTAC enumerators. +-- @param #CONTROLLABLE self +-- @param Wrapper.Group#GROUP AttackGroup Target GROUP object. +-- @param #number WeaponType Bitmask of weapon types, which are allowed to use. +-- @param DCS#AI.Task.Designation Designation (Optional) Designation type. +-- @param #boolean Datalink (Optional) Allows to use datalink to send the target information to attack aircraft. Enabled by default. +-- @param #number Frequency Frequency in MHz used to communicate with the FAC. Default 133 MHz. +-- @param #number Modulation Modulation of radio for communication. Default 0=AM. +-- @param #number CallsignName Callsign enumerator name of the FAC. (CALLSIGN.Aircraft.{name} for airborne controllables, CALLSIGN.JTACS.{name} for ground units) +-- @param #number CallsignNumber Callsign number, e.g. Axeman-**1**. +-- @return DCS#Task The DCS task structure. +function CONTROLLABLE:TaskFAC_AttackGroup( AttackGroup, WeaponType, Designation, Datalink, Frequency, Modulation, CallsignName, CallsignNumber ) + + local DCSTask = { + id = 'FAC_AttackGroup', + params = { + groupId = AttackGroup:GetID(), + weaponType = WeaponType or ENUMS.WeaponFlag.AutoDCS, + designation = Designation or "Auto", + datalink = Datalink and Datalink or true, + frequency = (Frequency or 133)*1000000, + modulation = Modulation or radio.modulation.AM, + callname = CallsignName, + number = CallsignNumber, + } + } + + return DCSTask +end + +-- EN-ACT_ROUTE TASKS FOR AIRBORNE CONTROLLABLES + +--- (AIR) Engaging targets of defined types. +-- @param #CONTROLLABLE self +-- @param DCS#Distance Distance Maximal distance from the target to a route leg. If the target is on a greater distance it will be ignored. +-- @param DCS#AttributeNameArray TargetTypes Array of target categories allowed to engage. +-- @param #number Priority All enroute tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. Default 0. +-- @return DCS#Task The DCS task structure. +function CONTROLLABLE:EnRouteTaskEngageTargets( Distance, TargetTypes, Priority ) + + local DCSTask = { + id = 'EngageTargets', + params = { + maxDistEnabled = Distance and true or false, + maxDist = Distance, + targetTypes = TargetTypes or {"Air"}, + priority = Priority or 0, + } + } + + return DCSTask +end + + + +--- (AIR) Engaging a targets of defined types at circle-shaped zone. +-- @param #CONTROLLABLE self +-- @param DCS#Vec2 Vec2 2D-coordinates of the zone. +-- @param DCS#Distance Radius Radius of the zone. +-- @param DCS#AttributeNameArray TargetTypes (Optional) Array of target categories allowed to engage. Default {"Air"}. +-- @param #number Priority (Optional) All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. Default 0. +-- @return DCS#Task The DCS task structure. +function CONTROLLABLE:EnRouteTaskEngageTargetsInZone( Vec2, Radius, TargetTypes, Priority ) + + local DCSTask = { + id = 'EngageTargetsInZone', + params = { + point = Vec2, + zoneRadius = Radius, + targetTypes = TargetTypes or {"Air"}, + priority = Priority or 0 + } + } + + return DCSTask +end + + +--- (AIR) Engaging a controllable. The task does not assign the target controllable to the unit/controllable to attack now; it just allows the unit/controllable to engage the target controllable as well as other assigned targets. +-- @param #CONTROLLABLE self +-- @param Wrapper.Controllable#CONTROLLABLE AttackGroup The Controllable to be attacked. +-- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. +-- @param #number WeaponType (optional) Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. +-- @param DCS#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. +-- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. +-- @param DCS#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. +-- @param DCS#Distance Altitude (optional) Desired attack start altitude. Controllable/aircraft will make its attacks from the altitude. If the altitude is too low or too high to use weapon aircraft/controllable will choose closest altitude to the desired attack start altitude. If the desired altitude is defined controllable/aircraft will not attack from safe altitude. +-- @param #boolean AttackQtyLimit (optional) The flag determines how to interpret attackQty parameter. If the flag is true then attackQty is a limit on maximal attack quantity for "AttackGroup" and "AttackUnit" tasks. If the flag is false then attackQty is a desired attack quantity for "Bombing" and "BombingRunway" tasks. +-- @return DCS#Task The DCS task structure. +function CONTROLLABLE:EnRouteTaskEngageGroup( AttackGroup, Priority, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit ) + + -- EngageControllable = { + -- id = 'EngageControllable ', + -- params = { + -- groupId = Group.ID, + -- weaponType = number, + -- expend = enum AI.Task.WeaponExpend, + -- attackQty = number, + -- directionEnabled = boolean, + -- direction = Azimuth, + -- altitudeEnabled = boolean, + -- altitude = Distance, + -- attackQtyLimit = boolean, + -- priority = number, + -- } + -- } + + local DCSTask = { + id = 'EngageControllable', + params = { + groupId = AttackGroup:GetID(), + weaponType = WeaponType, + expend = WeaponExpend or "Auto", + directionEnabled = Direction and true or false, + direction = Direction, + altitudeEnabled = Altitude and true or false, + altitude = Altitude, + attackQtyLimit = AttackQty and true or false, + attackQty = AttackQty, + priority = Priority or 1, + }, + } + + return DCSTask +end + + +--- (AIR) Search and attack the Unit. +-- @param #CONTROLLABLE self +-- @param Wrapper.Unit#UNIT EngageUnit The UNIT. +-- @param #number Priority (optional) All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. +-- @param #boolean GroupAttack (optional) If true, all units in the group will attack the Unit when found. +-- @param DCS#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. +-- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. +-- @param DCS#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. +-- @param DCS#Distance Altitude (optional) Desired altitude to perform the unit engagement. +-- @param #boolean Visible (optional) Unit must be visible. +-- @param #boolean ControllableAttack (optional) Flag indicates that the target must be engaged by all aircrafts of the controllable. Has effect only if the task is assigned to a controllable, not to a single aircraft. +-- @return DCS#Task The DCS task structure. +function CONTROLLABLE:EnRouteTaskEngageUnit( EngageUnit, Priority, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, Visible, ControllableAttack ) + + local DCSTask = { + id = 'EngageUnit', + params = { + unitId = EngageUnit:GetID(), + priority = Priority or 1, + groupAttack = GroupAttack and GroupAttack or false, + visible = Visible and Visible or false, + expend = WeaponExpend or "Auto", + directionEnabled = Direction and true or false, + direction = Direction and math.rad(Direction) or nil, + altitudeEnabled = Altitude and true or false, + altitude = Altitude, + attackQtyLimit = AttackQty and true or false, + attackQty = AttackQty, + controllableAttack = ControllableAttack, + }, + } + + return DCSTask +end + + + +--- (AIR) Aircraft will act as an AWACS for friendly units (will provide them with information about contacts). No parameters. +-- @param #CONTROLLABLE self +-- @return DCS#Task The DCS task structure. +function CONTROLLABLE:EnRouteTaskAWACS( ) + + local DCSTask = { + id = 'AWACS', + params = {}, + } + + return DCSTask +end + + +--- (AIR) Aircraft will act as a tanker for friendly units. No parameters. +-- @param #CONTROLLABLE self +-- @return DCS#Task The DCS task structure. +function CONTROLLABLE:EnRouteTaskTanker( ) + + local DCSTask = { + id = 'Tanker', + params = {}, + } + + return DCSTask +end + + +-- En-route tasks for ground units/controllables + +--- (GROUND) Ground unit (EW-radar) will act as an EWR for friendly units (will provide them with information about contacts). No parameters. +-- @param #CONTROLLABLE self +-- @return DCS#Task The DCS task structure. +function CONTROLLABLE:EnRouteTaskEWR( ) + + local DCSTask = { + id = 'EWR', + params = {}, + } + + return DCSTask +end + + +-- En-route tasks for airborne and ground units/controllables + +--- (AIR + GROUND) The task makes the controllable/unit a FAC and lets the FAC to choose the target (enemy ground controllable) as well as other assigned targets. +-- The killer is player-controlled allied CAS-aircraft that is in contact with the FAC. +-- If the task is assigned to the controllable lead unit will be a FAC. +-- @param #CONTROLLABLE self +-- @param Wrapper.Controllable#CONTROLLABLE AttackGroup Target CONTROLLABLE. +-- @param #number Priority (Optional) All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. Default is 0. +-- @param #number WeaponType (Optional) Bitmask of weapon types those allowed to use. Default is "Auto". +-- @param DCS#AI.Task.Designation Designation (Optional) Designation type. +-- @param #boolean Datalink (optional) Allows to use datalink to send the target information to attack aircraft. Enabled by default. +-- @return DCS#Task The DCS task structure. +function CONTROLLABLE:EnRouteTaskFAC_EngageGroup( AttackGroup, Priority, WeaponType, Designation, Datalink ) + + local DCSTask = { + id = 'FAC_EngageControllable', + params = { + groupId = AttackGroup:GetID(), + weaponType = WeaponType or "Auto", + designation = Designation, + datalink = Datalink and Datalink or false, + priority = Priority or 0, + } + } + + return DCSTask +end + + +--- (AIR + GROUND) The task makes the controllable/unit a FAC and lets the FAC to choose a targets (enemy ground controllable) around as well as other assigned targets. +-- The killer is player-controlled allied CAS-aircraft that is in contact with the FAC. +-- If the task is assigned to the controllable lead unit will be a FAC. +-- @param #CONTROLLABLE self +-- @param DCS#Distance Radius The maximal distance from the FAC to a target. +-- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. +-- @return DCS#Task The DCS task structure. +function CONTROLLABLE:EnRouteTaskFAC( Radius, Priority ) + +-- FAC = { +-- id = 'FAC', +-- params = { +-- radius = Distance, +-- priority = number +-- } +-- } + + local DCSTask = { + id = 'FAC', + params = { + radius = Radius, + priority = Priority + } + } + + return DCSTask +end + + +--- This creates a Task element, with an action to call a function as part of a Wrapped Task. +-- This Task can then be embedded at a Waypoint by calling the method @{#CONTROLLABLE.SetTaskWaypoint}. +-- @param #CONTROLLABLE self +-- @param #string FunctionString The function name embedded as a string that will be called. +-- @param ... The variable arguments passed to the function when called! These arguments can be of any type! +-- @return #CONTROLLABLE +-- @usage +-- +-- local ZoneList = { +-- ZONE:New( "ZONE1" ), +-- ZONE:New( "ZONE2" ), +-- ZONE:New( "ZONE3" ), +-- ZONE:New( "ZONE4" ), +-- ZONE:New( "ZONE5" ) +-- } +-- +-- GroundGroup = GROUP:FindByName( "Vehicle" ) +-- +-- --- @param Wrapper.Group#GROUP GroundGroup +-- function RouteToZone( Vehicle, ZoneRoute ) +-- +-- local Route = {} +-- +-- Vehicle:E( { ZoneRoute = ZoneRoute } ) +-- +-- Vehicle:MessageToAll( "Moving to zone " .. ZoneRoute:GetName(), 10 ) +-- +-- -- Get the current coordinate of the Vehicle +-- local FromCoord = Vehicle:GetCoordinate() +-- +-- -- Select a random Zone and get the Coordinate of the new Zone. +-- local RandomZone = ZoneList[ math.random( 1, #ZoneList ) ] -- Core.Zone#ZONE +-- local ToCoord = RandomZone:GetCoordinate() +-- +-- -- Create a "ground route point", which is a "point" structure that can be given as a parameter to a Task +-- Route[#Route+1] = FromCoord:WaypointGround( 72 ) +-- Route[#Route+1] = ToCoord:WaypointGround( 60, "Vee" ) +-- +-- local TaskRouteToZone = Vehicle:TaskFunction( "RouteToZone", RandomZone ) +-- +-- Vehicle:SetTaskWaypoint( Route[#Route], TaskRouteToZone ) -- Set for the given Route at Waypoint 2 the TaskRouteToZone. +-- +-- Vehicle:Route( Route, math.random( 10, 20 ) ) -- Move after a random seconds to the Route. See the Route method for details. +-- +-- end +-- +-- RouteToZone( GroundGroup, ZoneList[1] ) +-- +function CONTROLLABLE:TaskFunction( FunctionString, ... ) + + -- Script + local DCSScript = {} + DCSScript[#DCSScript+1] = "local MissionControllable = GROUP:Find( ... ) " + if arg and arg.n > 0 then + local ArgumentKey = '_' .. tostring( arg ):match("table: (.*)") + self:SetState( self, ArgumentKey, arg ) + DCSScript[#DCSScript+1] = "local Arguments = MissionControllable:GetState( MissionControllable, '" .. ArgumentKey .. "' ) " + DCSScript[#DCSScript+1] = FunctionString .. "( MissionControllable, unpack( Arguments ) )" + else + DCSScript[#DCSScript+1] = FunctionString .. "( MissionControllable )" + end + + -- DCS task. + local DCSTask = self:TaskWrappedAction(self:CommandDoScript(table.concat( DCSScript ))) + + return DCSTask +end + + + +--- (AIR + GROUND) Return a mission task from a mission template. +-- @param #CONTROLLABLE self +-- @param #table TaskMission A table containing the mission task. +-- @return DCS#Task +function CONTROLLABLE:TaskMission( TaskMission ) + + local DCSTask = { + id = 'Mission', + params = { TaskMission, }, + } + + return DCSTask +end + + +do -- Patrol methods + + --- (GROUND) Patrol iteratively using the waypoints of the (parent) group. + -- @param #CONTROLLABLE self + -- @return #CONTROLLABLE + function CONTROLLABLE:PatrolRoute() + + local PatrolGroup = self -- Wrapper.Group#GROUP + + if not self:IsInstanceOf( "GROUP" ) then + PatrolGroup = self:GetGroup() -- Wrapper.Group#GROUP + end + + self:F( { PatrolGroup = PatrolGroup:GetName() } ) + + if PatrolGroup:IsGround() or PatrolGroup:IsShip() then + + local Waypoints = PatrolGroup:GetTemplateRoutePoints() + + -- Calculate the new Route. + local FromCoord = PatrolGroup:GetCoordinate() + + -- test for submarine + local depth = 0 + local IsSub = false + if PatrolGroup:IsShip() then + local navalvec3 = FromCoord:GetVec3() + if navalvec3.y < 0 then + depth = navalvec3.y + IsSub = true + end + end + + + local Waypoint = Waypoints[1] + local Speed = Waypoint.speed or (20 / 3.6) + local From = FromCoord:WaypointGround( Speed ) + + if IsSub then + From = FromCoord:WaypointNaval( Speed, Waypoint.alt ) + end + + table.insert( Waypoints, 1, From ) + + local TaskRoute = PatrolGroup:TaskFunction( "CONTROLLABLE.PatrolRoute" ) + + self:F({Waypoints = Waypoints}) + local Waypoint = Waypoints[#Waypoints] + PatrolGroup:SetTaskWaypoint( Waypoint, TaskRoute ) -- Set for the given Route at Waypoint 2 the TaskRouteToZone. + + PatrolGroup:Route( Waypoints ) -- Move after a random seconds to the Route. See the Route method for details. + end + end + + --- (GROUND) Patrol randomly to the waypoints the for the (parent) group. + -- A random waypoint will be picked and the group will move towards that point. + -- @param #CONTROLLABLE self + -- @param #number Speed Speed in km/h. + -- @param #string Formation The formation the group uses. + -- @param Core.Point#COORDINATE ToWaypoint The waypoint where the group should move to. + -- @return #CONTROLLABLE + function CONTROLLABLE:PatrolRouteRandom( Speed, Formation, ToWaypoint ) + + local PatrolGroup = self -- Wrapper.Group#GROUP + + if not self:IsInstanceOf( "GROUP" ) then + PatrolGroup = self:GetGroup() -- Wrapper.Group#GROUP + end + + self:F( { PatrolGroup = PatrolGroup:GetName() } ) + + if PatrolGroup:IsGround() or PatrolGroup:IsShip() then + + local Waypoints = PatrolGroup:GetTemplateRoutePoints() + + -- Calculate the new Route. + local FromCoord = PatrolGroup:GetCoordinate() + local FromWaypoint = 1 + if ToWaypoint then + FromWaypoint = ToWaypoint + end + -- test for submarine + local depth = 0 + local IsSub = false + if PatrolGroup:IsShip() then + local navalvec3 = FromCoord:GetVec3() + if navalvec3.y < 0 then + depth = navalvec3.y + IsSub = true + end + end + -- Loop until a waypoint has been found that is not the same as the current waypoint. + -- Otherwise the object zon't move or drive in circles and the algorithm would not do exactly + -- what it is supposed to do, which is making groups drive around. + local ToWaypoint + repeat + -- Select a random waypoint and check if it is not the same waypoint as where the object is about. + ToWaypoint = math.random( 1, #Waypoints ) + until( ToWaypoint ~= FromWaypoint ) + self:F( { FromWaypoint = FromWaypoint, ToWaypoint = ToWaypoint } ) + + local Waypoint = Waypoints[ToWaypoint] -- Select random waypoint. + local ToCoord = COORDINATE:NewFromVec2( { x = Waypoint.x, y = Waypoint.y } ) + -- Create a "ground route point", which is a "point" structure that can be given as a parameter to a Task + local Route = {} + if IsSub then + Route[#Route+1] = FromCoord:WaypointNaval( Speed, depth ) + Route[#Route+1] = ToCoord:WaypointNaval( Speed, Waypoint.alt ) + else + Route[#Route+1] = FromCoord:WaypointGround( Speed, Formation ) + Route[#Route+1] = ToCoord:WaypointGround( Speed, Formation ) + end + + local TaskRouteToZone = PatrolGroup:TaskFunction( "CONTROLLABLE.PatrolRouteRandom", Speed, Formation, ToWaypoint ) + + PatrolGroup:SetTaskWaypoint( Route[#Route], TaskRouteToZone ) -- Set for the given Route at Waypoint 2 the TaskRouteToZone. + + PatrolGroup:Route( Route, 1 ) -- Move after a random seconds to the Route. See the Route method for details. + end + end + + --- (GROUND) Patrol randomly to the waypoints the for the (parent) group. + -- A random waypoint will be picked and the group will move towards that point. + -- @param #CONTROLLABLE self + -- @param #table ZoneList Table of zones. + -- @param #number Speed Speed in km/h the group moves at. + -- @param #string Formation (Optional) Formation the group should use. + -- @param #number DelayMin Delay in seconds before the group progresses to the next route point. Default 1 sec. + -- @param #number DelayMax Max. delay in seconds. Actual delay is randomly chosen between DelayMin and DelayMax. Default equal to DelayMin. + -- @return #CONTROLLABLE + function CONTROLLABLE:PatrolZones( ZoneList, Speed, Formation, DelayMin, DelayMax ) + + if not type( ZoneList ) == "table" then + ZoneList = { ZoneList } + end + + local PatrolGroup = self -- Wrapper.Group#GROUP + + if not self:IsInstanceOf( "GROUP" ) then + PatrolGroup = self:GetGroup() -- Wrapper.Group#GROUP + end + + DelayMin=DelayMin or 1 + if not DelayMax or DelayMax LengthDirect*10) or (LengthRoad/LengthOnRoad*100<5)) + + -- Debug info. + self:T(string.format("Length on road = %.3f km", LengthOnRoad/1000)) + self:T(string.format("Length directly = %.3f km", LengthDirect/1000)) + self:T(string.format("Length fraction = %.3f km", LengthOnRoad/LengthDirect)) + self:T(string.format("Length only road = %.3f km", LengthRoad/1000)) + self:T(string.format("Length off road = %.3f km", LengthOffRoad/1000)) + self:T(string.format("Percent on road = %.1f", LengthRoad/LengthOnRoad*100)) + + end + + -- Route, ground waypoints along road. + local route={} + local canroad=false + + -- Check if a valid path on road could be found. + if GotPath and LengthRoad and LengthDirect > 2000 then -- if the length of the movement is less than 1 km, drive directly. + -- Check whether the road is very long compared to direct path. + if LongRoad and Shortcut then + + -- Road is long ==> we take the short cut. + + table.insert(route, FromCoordinate:WaypointGround(Speed, OffRoadFormation)) + table.insert(route, ToCoordinate:WaypointGround(Speed, OffRoadFormation)) + + else + + -- Create waypoints. + table.insert(route, FromCoordinate:WaypointGround(Speed, OffRoadFormation)) + table.insert(route, PathOnRoad[2]:WaypointGround(Speed, "On Road")) + table.insert(route, PathOnRoad[#PathOnRoad-1]:WaypointGround(Speed, "On Road")) + + -- Add the final coordinate because the final might not be on the road. + local dist=ToCoordinate:Get2DDistance(PathOnRoad[#PathOnRoad-1]) + if dist>10 then + table.insert(route, ToCoordinate:WaypointGround(Speed, OffRoadFormation)) + table.insert(route, ToCoordinate:GetRandomCoordinateInRadius(10,5):WaypointGround(5, OffRoadFormation)) + table.insert(route, ToCoordinate:GetRandomCoordinateInRadius(10,5):WaypointGround(5, OffRoadFormation)) + end + + end + + canroad=true + else + + -- No path on road could be found (can happen!) ==> Route group directly from A to B. + table.insert(route, FromCoordinate:WaypointGround(Speed, OffRoadFormation)) + table.insert(route, ToCoordinate:WaypointGround(Speed, OffRoadFormation)) + + end + + -- Add passing waypoint function. + if WaypointFunction then + local N=#route + for n,waypoint in pairs(route) do + waypoint.task = {} + waypoint.task.id = "ComboTask" + waypoint.task.params = {} + waypoint.task.params.tasks = {self:TaskFunction("CONTROLLABLE.___PassingWaypoint", n, N, WaypointFunction, unpack(WaypointFunctionArguments or {}))} + end + end + + return route, canroad + end + + --- Make a task for a TRAIN Controllable to drive towards a specific point using railroad. + -- @param #CONTROLLABLE self + -- @param Core.Point#COORDINATE ToCoordinate A Coordinate to drive to. + -- @param #number Speed (Optional) Speed in km/h. The default speed is 20 km/h. + -- @param #function WaypointFunction (Optional) Function called when passing a waypoint. First parameters of the function are the @{CONTROLLABLE} object, the number of the waypoint and the total number of waypoints. + -- @param #table WaypointFunctionArguments (Optional) List of parameters passed to the *WaypointFunction*. + -- @return Task + function CONTROLLABLE:TaskGroundOnRailRoads(ToCoordinate, Speed, WaypointFunction, WaypointFunctionArguments ) + self:F2({ToCoordinate=ToCoordinate, Speed=Speed}) + + -- Defaults. + Speed=Speed or 20 + + -- Current coordinate. + local FromCoordinate = self:GetCoordinate() + + -- Get path and path length on railroad. + local PathOnRail, LengthOnRail=FromCoordinate:GetPathOnRoad(ToCoordinate, false, true) + + -- Debug info. + self:T(string.format("Length on railroad = %.3f km", LengthOnRail/1000)) + + -- Route, ground waypoints along road. + local route={} + + -- Check if a valid path on railroad could be found. + if PathOnRail then + + table.insert(route, PathOnRail[1]:WaypointGround(Speed, "On Railroad")) + table.insert(route, PathOnRail[2]:WaypointGround(Speed, "On Railroad")) + + end + + -- Add passing waypoint function. + if WaypointFunction then + local N=#route + for n,waypoint in pairs(route) do + waypoint.task = {} + waypoint.task.id = "ComboTask" + waypoint.task.params = {} + waypoint.task.params.tasks = {self:TaskFunction("CONTROLLABLE.___PassingWaypoint", n, N, WaypointFunction, unpack(WaypointFunctionArguments or {}))} + end + end + + return route + end + + --- Task function when controllable passes a waypoint. + -- @param #CONTROLLABLE controllable The controllable object. + -- @param #number n Current waypoint number passed. + -- @param #number N Total number of waypoints. + -- @param #function waypointfunction Function called when a waypoint is passed. + function CONTROLLABLE.___PassingWaypoint(controllable, n, N, waypointfunction, ...) + waypointfunction(controllable, n, N, ...) + end + + + --- Make the AIR Controllable fly towards a specific point. + -- @param #CONTROLLABLE self + -- @param Core.Point#COORDINATE ToCoordinate A Coordinate to drive to. + -- @param Core.Point#COORDINATE.RoutePointAltType AltType The altitude type. + -- @param Core.Point#COORDINATE.RoutePointType Type The route point type. + -- @param Core.Point#COORDINATE.RoutePointAction Action The route point action. + -- @param #number Speed (optional) Speed in km/h. The default speed is 500 km/h. + -- @param #number DelaySeconds Wait for the specified seconds before executing the Route. + -- @return #CONTROLLABLE The CONTROLLABLE. + function CONTROLLABLE:RouteAirTo( ToCoordinate, AltType, Type, Action, Speed, DelaySeconds ) + + local FromCoordinate = self:GetCoordinate() + local FromWP = FromCoordinate:WaypointAir() + + local ToWP = ToCoordinate:WaypointAir( AltType, Type, Action, Speed ) + + self:Route( { FromWP, ToWP }, DelaySeconds ) + + return self + end + + + --- (AIR + GROUND) Route the controllable to a given zone. + -- The controllable final destination point can be randomized. + -- A speed can be given in km/h. + -- A given formation can be given. + -- @param #CONTROLLABLE self + -- @param Core.Zone#ZONE Zone The zone where to route to. + -- @param #boolean Randomize Defines whether to target point gets randomized within the Zone. + -- @param #number Speed The speed in m/s. Default is 5.555 m/s = 20 km/h. + -- @param Base#FORMATION Formation The formation string. + function CONTROLLABLE:TaskRouteToZone( Zone, Randomize, Speed, Formation ) + self:F2( Zone ) + + local DCSControllable = self:GetDCSObject() + + if DCSControllable then + + local ControllablePoint = self:GetVec2() + + local PointFrom = {} + PointFrom.x = ControllablePoint.x + PointFrom.y = ControllablePoint.y + PointFrom.type = "Turning Point" + PointFrom.action = Formation or "Cone" + PointFrom.speed = 20 / 3.6 + + + local PointTo = {} + local ZonePoint + + if Randomize then + ZonePoint = Zone:GetRandomVec2() + else + ZonePoint = Zone:GetVec2() + end + + PointTo.x = ZonePoint.x + PointTo.y = ZonePoint.y + PointTo.type = "Turning Point" + + if Formation then + PointTo.action = Formation + else + PointTo.action = "Cone" + end + + if Speed then + PointTo.speed = Speed + else + PointTo.speed = 20 / 3.6 + end + + local Points = { PointFrom, PointTo } + + self:T3( Points ) + + self:Route( Points ) + + return self + end + + return nil + end + + --- (GROUND) Route the controllable to a given Vec2. + -- A speed can be given in km/h. + -- A given formation can be given. + -- @param #CONTROLLABLE self + -- @param DCS#Vec2 Vec2 The Vec2 where to route to. + -- @param #number Speed The speed in m/s. Default is 5.555 m/s = 20 km/h. + -- @param Base#FORMATION Formation The formation string. + function CONTROLLABLE:TaskRouteToVec2( Vec2, Speed, Formation ) + + local DCSControllable = self:GetDCSObject() + + if DCSControllable then + + local ControllablePoint = self:GetVec2() + + local PointFrom = {} + PointFrom.x = ControllablePoint.x + PointFrom.y = ControllablePoint.y + PointFrom.type = "Turning Point" + PointFrom.action = Formation or "Cone" + PointFrom.speed = 20 / 3.6 + + + local PointTo = {} + + PointTo.x = Vec2.x + PointTo.y = Vec2.y + PointTo.type = "Turning Point" + + if Formation then + PointTo.action = Formation + else + PointTo.action = "Cone" + end + + if Speed then + PointTo.speed = Speed + else + PointTo.speed = 20 / 3.6 + end + + local Points = { PointFrom, PointTo } + + self:T3( Points ) + + self:Route( Points ) + + return self + end + + return nil + end + +end -- Route methods + +-- Commands + +--- Do Script command +-- @param #CONTROLLABLE self +-- @param #string DoScript +-- @return DCS#DCSCommand +function CONTROLLABLE:CommandDoScript( DoScript ) + + local DCSDoScript = { + id = "Script", + params = { + command = DoScript, + }, + } + + self:T3( DCSDoScript ) + return DCSDoScript +end + + +--- Return the mission template of the controllable. +-- @param #CONTROLLABLE self +-- @return #table The MissionTemplate +-- TODO: Rework the method how to retrieve a template ... +function CONTROLLABLE:GetTaskMission() + self:F2( self.ControllableName ) + + return routines.utils.deepCopy( _DATABASE.Templates.Controllables[self.ControllableName].Template ) +end + +--- Return the mission route of the controllable. +-- @param #CONTROLLABLE self +-- @return #table The mission route defined by points. +function CONTROLLABLE:GetTaskRoute() + self:F2( self.ControllableName ) + + return routines.utils.deepCopy( _DATABASE.Templates.Controllables[self.ControllableName].Template.route.points ) +end + + + +--- Return the route of a controllable by using the @{Core.Database#DATABASE} class. +-- @param #CONTROLLABLE self +-- @param #number Begin The route point from where the copy will start. The base route point is 0. +-- @param #number End The route point where the copy will end. The End point is the last point - the End point. The last point has base 0. +-- @param #boolean Randomize Randomization of the route, when true. +-- @param #number Radius When randomization is on, the randomization is within the radius. +function CONTROLLABLE:CopyRoute( Begin, End, Randomize, Radius ) + self:F2( { Begin, End } ) + + local Points = {} + + -- Could be a Spawned Controllable + local ControllableName = string.match( self:GetName(), ".*#" ) + if ControllableName then + ControllableName = ControllableName:sub( 1, -2 ) + else + ControllableName = self:GetName() + end + + self:T3( { ControllableName } ) + + local Template = _DATABASE.Templates.Controllables[ControllableName].Template + + if Template then + if not Begin then + Begin = 0 + end + if not End then + End = 0 + end + + for TPointID = Begin + 1, #Template.route.points - End do + if Template.route.points[TPointID] then + Points[#Points+1] = routines.utils.deepCopy( Template.route.points[TPointID] ) + if Randomize then + if not Radius then + Radius = 500 + end + Points[#Points].x = Points[#Points].x + math.random( Radius * -1, Radius ) + Points[#Points].y = Points[#Points].y + math.random( Radius * -1, Radius ) + end + end + end + return Points + else + error( "Template not found for Controllable : " .. ControllableName ) + end + + return nil +end + + +--- Return the detected targets of the controllable. +-- 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 Wrapper.Controllable#CONTROLLABLE self +-- @param #boolean DetectVisual (optional) +-- @param #boolean DetectOptical (optional) +-- @param #boolean DetectRadar (optional) +-- @param #boolean DetectIRST (optional) +-- @param #boolean DetectRWR (optional) +-- @param #boolean DetectDLINK (optional) +-- @return #table DetectedTargets +function CONTROLLABLE:GetDetectedTargets( DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK ) + self:F2( self.ControllableName ) + + local DCSControllable = self:GetDCSObject() + + if DCSControllable then + + local DetectionVisual = ( DetectVisual and DetectVisual == true ) and Controller.Detection.VISUAL or nil + local DetectionOptical = ( DetectOptical and DetectOptical == true ) and Controller.Detection.OPTICAL or nil + local DetectionRadar = ( DetectRadar and DetectRadar == true ) and Controller.Detection.RADAR or nil + local DetectionIRST = ( DetectIRST and DetectIRST == true ) and Controller.Detection.IRST or nil + local DetectionRWR = ( DetectRWR and DetectRWR == true ) and Controller.Detection.RWR or nil + local DetectionDLINK = ( DetectDLINK and DetectDLINK == true ) and Controller.Detection.DLINK or nil + + + local Params = {} + if DetectionVisual then + Params[#Params+1] = DetectionVisual + end + if DetectionOptical then + Params[#Params+1] = DetectionOptical + end + if DetectionRadar then + Params[#Params+1] = DetectionRadar + end + if DetectionIRST then + Params[#Params+1] = DetectionIRST + end + if DetectionRWR then + Params[#Params+1] = DetectionRWR + end + if DetectionDLINK then + Params[#Params+1] = DetectionDLINK + end + + + self:T2( { DetectionVisual, DetectionOptical, DetectionRadar, DetectionIRST, DetectionRWR, DetectionDLINK } ) + + return self:_GetController():getDetectedTargets( Params[1], Params[2], Params[3], Params[4], Params[5], Params[6] ) + end + + return nil +end + +--- Check if a target is detected. +-- 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. +-- If **at least one** detection method is specified, only the methods set to *true* will be used. +-- @param Wrapper.Controllable#CONTROLLABLE self +-- @param DCS#Object DCSObject The DCS object that is checked. +-- @param Wrapper.Controllable#CONTROLLABLE self +-- @param #boolean DetectVisual (Optional) If *false*, do not include visually detected targets. +-- @param #boolean DetectOptical (Optional) If *false*, do not include optically detected targets. +-- @param #boolean DetectRadar (Optional) If *false*, do not include targets detected by radar. +-- @param #boolean DetectIRST (Optional) If *false*, do not include targets detected by IRST. +-- @param #boolean DetectRWR (Optional) If *false*, do not include targets detected by RWR. +-- @param #boolean DetectDLINK (Optional) If *false*, do not include targets detected by data link. +-- @return #boolean True if target is detected. +-- @return #boolean True if target is visible by line of sight. +-- @return #number Mission time when target was detected. +-- @return #boolean True if target type is known. +-- @return #boolean True if distance to target is known. +-- @return DCS#Vec3 Last known position vector of the target. +-- @return DCS#Vec3 Last known velocity vector of the target. +function CONTROLLABLE:IsTargetDetected( DCSObject, DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK ) + self:F2( self.ControllableName ) + + local DCSControllable = self:GetDCSObject() + + if DCSControllable then + + local DetectionVisual = ( DetectVisual and DetectVisual == true ) and Controller.Detection.VISUAL or nil + local DetectionOptical = ( DetectOptical and DetectOptical == true ) and Controller.Detection.OPTICAL or nil + local DetectionRadar = ( DetectRadar and DetectRadar == true ) and Controller.Detection.RADAR or nil + local DetectionIRST = ( DetectIRST and DetectIRST == true ) and Controller.Detection.IRST or nil + local DetectionRWR = ( DetectRWR and DetectRWR == true ) and Controller.Detection.RWR or nil + local DetectionDLINK = ( DetectDLINK and DetectDLINK == true ) and Controller.Detection.DLINK or nil + + local Controller = self:_GetController() + + local TargetIsDetected, TargetIsVisible, TargetLastTime, TargetKnowType, TargetKnowDistance, TargetLastPos, TargetLastVelocity + = Controller:isTargetDetected( DCSObject, DetectionVisual, DetectionOptical, DetectionRadar, DetectionIRST, DetectionRWR, DetectionDLINK ) + + return TargetIsDetected, TargetIsVisible, TargetLastTime, TargetKnowType, TargetKnowDistance, TargetLastPos, TargetLastVelocity + end + + return nil +end + +--- Check if a certain UNIT is detected by the controllable. +-- 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. +-- If **at least one** detection method is specified, only the methods set to *true* will be used. +-- @param #CONTROLLABLE self +-- @param Wrapper.Unit#UNIT Unit The unit that is supposed to be detected. +-- @param #boolean DetectVisual (Optional) If *false*, do not include visually detected targets. +-- @param #boolean DetectOptical (Optional) If *false*, do not include optically detected targets. +-- @param #boolean DetectRadar (Optional) If *false*, do not include targets detected by radar. +-- @param #boolean DetectIRST (Optional) If *false*, do not include targets detected by IRST. +-- @param #boolean DetectRWR (Optional) If *false*, do not include targets detected by RWR. +-- @param #boolean DetectDLINK (Optional) If *false*, do not include targets detected by data link. +-- @return #boolean True if target is detected. +-- @return #boolean True if target is visible by line of sight. +-- @return #number Mission time when target was detected. +-- @return #boolean True if target type is known. +-- @return #boolean True if distance to target is known. +-- @return DCS#Vec3 Last known position vector of the target. +-- @return DCS#Vec3 Last known velocity vector of the target. +function CONTROLLABLE:IsUnitDetected( Unit, DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK ) + self:F2( self.ControllableName ) + + if Unit and Unit:IsAlive() then + return self:IsTargetDetected(Unit:GetDCSObject(), DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) + end + + return nil +end + +--- Check if a certain GROUP is detected by the controllable. +-- 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. +-- If **at least one** detection method is specified, only the methods set to *true* will be used. +-- @param #CONTROLLABLE self +-- @param Wrapper.Group#GROUP Group The group that is supposed to be detected. +-- @param #boolean DetectVisual (Optional) If *false*, do not include visually detected targets. +-- @param #boolean DetectOptical (Optional) If *false*, do not include optically detected targets. +-- @param #boolean DetectRadar (Optional) If *false*, do not include targets detected by radar. +-- @param #boolean DetectIRST (Optional) If *false*, do not include targets detected by IRST. +-- @param #boolean DetectRWR (Optional) If *false*, do not include targets detected by RWR. +-- @param #boolean DetectDLINK (Optional) If *false*, do not include targets detected by data link. +-- @return #boolean True if any unit of the group is detected. +function CONTROLLABLE:IsGroupDetected( Group, DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK ) + self:F2( self.ControllableName ) + + if Group and Group:IsAlive() then + for _,_unit in pairs(Group:GetUnits()) do + local unit=_unit --Wrapper.Unit#UNIT + if unit and unit:IsAlive() then + + local isdetected=self:IsUnitDetected(unit, DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) + + if isdetected then + return true + end + end + end + return false + end + + return nil +end + + +--- Return the detected targets of the controllable. +-- 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. +-- If **at least one** detection method is specified, only the methods set to *true* will be used. +-- @param Wrapper.Controllable#CONTROLLABLE self +-- @param #boolean DetectVisual (Optional) If *false*, do not include visually detected targets. +-- @param #boolean DetectOptical (Optional) If *false*, do not include optically detected targets. +-- @param #boolean DetectRadar (Optional) If *false*, do not include targets detected by radar. +-- @param #boolean DetectIRST (Optional) If *false*, do not include targets detected by IRST. +-- @param #boolean DetectRWR (Optional) If *false*, do not include targets detected by RWR. +-- @param #boolean DetectDLINK (Optional) If *false*, do not include targets detected by data link. +-- @return Core.Set#SET_UNIT Set of detected units. +function CONTROLLABLE:GetDetectedUnitSet(DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) + + -- Get detected DCS units. + local detectedtargets=self:GetDetectedTargets(DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) + + local unitset=SET_UNIT:New() + + for DetectionObjectID, Detection in pairs(detectedtargets or {}) do + local DetectedObject=Detection.object -- DCS#Object + + if DetectedObject and DetectedObject:isExist() and DetectedObject.id_<50000000 then + local unit=UNIT:Find(DetectedObject) + + if unit and unit:IsAlive() then + + if not unitset:FindUnit(unit:GetName()) then + unitset:AddUnit(unit) + end + + end + end + end + + return unitset +end + +--- Return the detected target groups of the controllable as a @{Core.Set#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 Wrapper.Controllable#CONTROLLABLE self +-- @param #boolean DetectVisual (Optional) If *false*, do not include visually detected targets. +-- @param #boolean DetectOptical (Optional) If *false*, do not include optically detected targets. +-- @param #boolean DetectRadar (Optional) If *false*, do not include targets detected by radar. +-- @param #boolean DetectIRST (Optional) If *false*, do not include targets detected by IRST. +-- @param #boolean DetectRWR (Optional) If *false*, do not include targets detected by RWR. +-- @param #boolean DetectDLINK (Optional) If *false*, do not include targets detected by data link. +-- @return Core.Set#SET_GROUP Set of detected groups. +function CONTROLLABLE:GetDetectedGroupSet(DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) + + -- Get detected DCS units. + local detectedtargets=self:GetDetectedTargets(DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) + + local groupset=SET_GROUP:New() + + for DetectionObjectID, Detection in pairs(detectedtargets or {}) do + local DetectedObject=Detection.object -- DCS#Object + + if DetectedObject and DetectedObject:isExist() and DetectedObject.id_<50000000 then + local unit=UNIT:Find(DetectedObject) + + if unit and unit:IsAlive() then + local group=unit:GetGroup() + + if group and not groupset:FindGroup(group:GetName()) then + groupset:AddGroup(group) + end + + end + end + end + + return groupset +end + + +-- Options + +--- Set option. +-- @param #CONTROLLABLE self +-- @param #number OptionID ID/Type of the option. +-- @param #number OptionValue Value of the option +-- @return #CONTROLLABLE self +function CONTROLLABLE:SetOption(OptionID, OptionValue) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local Controller = self:_GetController() + + Controller:setOption( OptionID, OptionValue ) + + return self + end + + return nil +end + +--- Set option for Rules of Engagement (ROE). +-- @param Wrapper.Controllable#CONTROLLABLE self +-- @param #number ROEvalue ROE value. See ENUMS.ROE. +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionROE(ROEvalue) + + local DCSControllable = self:GetDCSObject() + + if DCSControllable then + + local Controller = self:_GetController() + + if self:IsAir() then + Controller:setOption(AI.Option.Air.id.ROE, ROEvalue ) + elseif self:IsGround() then + Controller:setOption(AI.Option.Ground.id.ROE, ROEvalue ) + elseif self:IsShip() then + Controller:setOption(AI.Option.Naval.id.ROE, ROEvalue ) + end + + return self + end + + return nil +end + +--- Can the CONTROLLABLE hold their weapons? +-- @param #CONTROLLABLE self +-- @return #boolean +function CONTROLLABLE:OptionROEHoldFirePossible() + self:F2( { self.ControllableName } ) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + if self:IsAir() or self:IsGround() or self:IsShip() then + return true + end + + return false + end + + return nil +end + +--- Weapons Hold: AI will hold fire under all circumstances. +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionROEHoldFire() + self:F2( { self.ControllableName } ) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local Controller = self:_GetController() + + if self:IsAir() then + Controller:setOption( AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD ) + elseif self:IsGround() then + Controller:setOption( AI.Option.Ground.id.ROE, AI.Option.Ground.val.ROE.WEAPON_HOLD ) + elseif self:IsShip() then + Controller:setOption( AI.Option.Naval.id.ROE, AI.Option.Naval.val.ROE.WEAPON_HOLD ) + end + + return self + end + + return nil +end + +--- Can the CONTROLLABLE attack returning on enemy fire? +-- @param #CONTROLLABLE self +-- @return #boolean +function CONTROLLABLE:OptionROEReturnFirePossible() + self:F2( { self.ControllableName } ) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + if self:IsAir() or self:IsGround() or self:IsShip() then + return true + end + + return false + end + + return nil +end + +--- Return Fire: AI will only engage threats that shoot first. +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionROEReturnFire() + self:F2( { self.ControllableName } ) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local Controller = self:_GetController() + + if self:IsAir() then + Controller:setOption( AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.RETURN_FIRE ) + elseif self:IsGround() then + Controller:setOption( AI.Option.Ground.id.ROE, AI.Option.Ground.val.ROE.RETURN_FIRE ) + elseif self:IsShip() then + Controller:setOption( AI.Option.Naval.id.ROE, AI.Option.Naval.val.ROE.RETURN_FIRE ) + end + + return self + end + + return nil +end + +--- Can the CONTROLLABLE attack designated targets? +-- @param #CONTROLLABLE self +-- @return #boolean +function CONTROLLABLE:OptionROEOpenFirePossible() + self:F2( { self.ControllableName } ) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + if self:IsAir() or self:IsGround() or self:IsShip() then + return true + end + + return false + end + + return nil +end + +--- Open Fire (Only Designated): AI will engage only targets specified in its taskings. +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionROEOpenFire() + self:F2( { self.ControllableName } ) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local Controller = self:_GetController() + + if self:IsAir() then + Controller:setOption( AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE ) + elseif self:IsGround() then + Controller:setOption( AI.Option.Ground.id.ROE, AI.Option.Ground.val.ROE.OPEN_FIRE ) + elseif self:IsShip() then + Controller:setOption( AI.Option.Naval.id.ROE, AI.Option.Naval.val.ROE.OPEN_FIRE ) + end + + return self + end + + return nil +end + +--- Can the CONTROLLABLE attack priority designated targets? Only for AIR! +-- @param #CONTROLLABLE self +-- @return #boolean +function CONTROLLABLE:OptionROEOpenFireWeaponFreePossible() + self:F2( { self.ControllableName } ) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + if self:IsAir() then + return true + end + + return false + end + + return nil +end + +--- Open Fire, Weapons Free (Priority Designated): AI will engage any enemy group it detects, but will prioritize targets specified in the groups tasking. +-- **Only for AIR units!** +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionROEOpenFireWeaponFree() + self:F2( { self.ControllableName } ) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local Controller = self:_GetController() + + if self:IsAir() then + Controller:setOption( AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE_WEAPON_FREE ) + end + + return self + end + + return nil +end + +--- Can the CONTROLLABLE attack targets of opportunity? +-- @param #CONTROLLABLE self +-- @return #boolean +function CONTROLLABLE:OptionROEWeaponFreePossible() + self:F2( { self.ControllableName } ) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + if self:IsAir() then + return true + end + + return false + end + + return nil +end + +--- Weapon free. +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionROEWeaponFree() + self:F2( { self.ControllableName } ) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local Controller = self:_GetController() + + if self:IsAir() then + Controller:setOption( AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_FREE ) + end + + return self + end + + return nil +end + +--- Can the CONTROLLABLE ignore enemy fire? +-- @param #CONTROLLABLE self +-- @return #boolean +function CONTROLLABLE:OptionROTNoReactionPossible() + self:F2( { self.ControllableName } ) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + if self:IsAir() then + return true + end + + return false + end + + return nil +end + + +--- No evasion on enemy threats. +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionROTNoReaction() + self:F2( { self.ControllableName } ) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local Controller = self:_GetController() + + if self:IsAir() then + Controller:setOption( AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.NO_REACTION ) + end + + return self + end + + return nil +end + +--- Set Reation On Threat behaviour. +-- @param #CONTROLLABLE self +-- @param #number ROTvalue ROT value. See ENUMS.ROT. +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionROT(ROTvalue) + self:F2( { self.ControllableName } ) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local Controller = self:_GetController() + + if self:IsAir() then + Controller:setOption( AI.Option.Air.id.REACTION_ON_THREAT, ROTvalue ) + end + + return self + end + + return nil +end + +--- Can the CONTROLLABLE evade using passive defenses? +-- @param #CONTROLLABLE self +-- @return #boolean +function CONTROLLABLE:OptionROTPassiveDefensePossible() + self:F2( { self.ControllableName } ) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + if self:IsAir() then + return true + end + + return false + end + + return nil +end + +--- Evasion passive defense. +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionROTPassiveDefense() + self:F2( { self.ControllableName } ) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local Controller = self:_GetController() + + if self:IsAir() then + Controller:setOption( AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.PASSIVE_DEFENCE ) + end + + return self + end + + return nil +end + +--- Can the CONTROLLABLE evade on enemy fire? +-- @param #CONTROLLABLE self +-- @return #boolean +function CONTROLLABLE:OptionROTEvadeFirePossible() + self:F2( { self.ControllableName } ) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + if self:IsAir() then + return true + end + + return false + end + + return nil +end + + +--- Evade on fire. +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionROTEvadeFire() + self:F2( { self.ControllableName } ) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local Controller = self:_GetController() + + if self:IsAir() then + Controller:setOption( AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) + end + + return self + end + + return nil +end + +--- Can the CONTROLLABLE evade on fire using vertical manoeuvres? +-- @param #CONTROLLABLE self +-- @return #boolean +function CONTROLLABLE:OptionROTVerticalPossible() + self:F2( { self.ControllableName } ) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + if self:IsAir() then + return true + end + + return false + end + + return nil +end + + +--- Evade on fire using vertical manoeuvres. +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionROTVertical() + self:F2( { self.ControllableName } ) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local Controller = self:_GetController() + + if self:IsAir() then + Controller:setOption( AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.BYPASS_AND_ESCAPE ) + end + + return self + end + + return nil +end + +--- Alarm state to Auto: AI will automatically switch alarm states based on the presence of threats. The AI kind of cheats in this regard. +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionAlarmStateAuto() + self:F2( { self.ControllableName } ) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local Controller = self:_GetController() + + if self:IsGround() then + Controller:setOption(AI.Option.Ground.id.ALARM_STATE, AI.Option.Ground.val.ALARM_STATE.AUTO) + elseif self:IsShip() then + --Controller:setOption(AI.Option.Naval.id.ALARM_STATE, AI.Option.Naval.val.ALARM_STATE.AUTO) + Controller:setOption(9, 0) + end + + return self + end + + return nil +end + +--- Alarm state to Green: Group is not combat ready. Sensors are stowed if possible. +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionAlarmStateGreen() + self:F2( { self.ControllableName } ) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local Controller = self:_GetController() + + if self:IsGround() then + Controller:setOption( AI.Option.Ground.id.ALARM_STATE, AI.Option.Ground.val.ALARM_STATE.GREEN ) + elseif self:IsShip() then + -- AI.Option.Naval.id.ALARM_STATE does not seem to exist! + --Controller:setOption( AI.Option.Naval.id.ALARM_STATE, AI.Option.Naval.val.ALARM_STATE.GREEN ) + Controller:setOption(9, 1) + end + + return self + end + + return nil +end + +--- Alarm state to Red: Group is combat ready and actively searching for targets. +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionAlarmStateRed() + self:F2( { self.ControllableName } ) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local Controller = self:_GetController() + + if self:IsGround() then + Controller:setOption(AI.Option.Ground.id.ALARM_STATE, AI.Option.Ground.val.ALARM_STATE.RED) + elseif self:IsShip() then + --Controller:setOption(AI.Option.Naval.id.ALARM_STATE, AI.Option.Naval.val.ALARM_STATE.RED) + Controller:setOption(9, 2) + end + + return self + end + + return nil +end + + +--- Set RTB on bingo fuel. +-- @param #CONTROLLABLE self +-- @param #boolean RTB true if RTB on bingo fuel (default), false if no RTB on bingo fuel. +-- Warning! When you switch this option off, the airborne group will continue to fly until all fuel has been consumed, and will crash. +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionRTBBingoFuel( RTB ) --R2.2 + self:F2( { self.ControllableName } ) + + --RTB = RTB or true + if RTB==nil then + RTB=true + end + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local Controller = self:_GetController() + + if self:IsAir() then + Controller:setOption( AI.Option.Air.id.RTB_ON_BINGO, RTB ) + end + + return self + end + + return nil +end + + +--- Set RTB on ammo. +-- @param #CONTROLLABLE self +-- @param #boolean WeaponsFlag Weapons.flag enumerator. +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionRTBAmmo( WeaponsFlag ) + self:F2( { self.ControllableName } ) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local Controller = self:_GetController() + + if self:IsAir() then + Controller:setOption( AI.Option.Air.id.RTB_ON_OUT_OF_AMMO, WeaponsFlag ) + end + + return self + end + + return nil +end + + +--- Allow to Jettison of weapons upon threat. +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionAllowJettisonWeaponsOnThreat() + self:F2( { self.ControllableName } ) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local Controller = self:_GetController() + + if self:IsAir() then + Controller:setOption( AI.Option.Air.id.PROHIBIT_JETT, false ) + end + + return self + end + + return nil +end + + +--- Keep weapons upon threat. +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionKeepWeaponsOnThreat() + self:F2( { self.ControllableName } ) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local Controller = self:_GetController() + + if self:IsAir() then + Controller:setOption( AI.Option.Air.id.PROHIBIT_JETT, true ) + end + + return self + end + + return nil +end + +--- Prohibit Afterburner. +-- @param #CONTROLLABLE self +-- @param #boolean Prohibit If true or nil, prohibit. If false, do not prohibit. +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionProhibitAfterburner(Prohibit) + self:F2( { self.ControllableName } ) + + if Prohibit==nil then + Prohibit=true + end + + if self:IsAir() then + self:SetOption(AI.Option.Air.id.PROHIBIT_AB, Prohibit) + end + + return self +end + +--- Defines the usage of Electronic Counter Measures by airborne forces. Disables the ability for AI to use their ECM. +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionECM_Never() + self:F2( { self.ControllableName } ) + + if self:IsAir() then + self:SetOption(AI.Option.Air.id.ECM_USING, 0) + end + + return self +end + +--- Defines the usage of Electronic Counter Measures by airborne forces. If the AI is actively being locked by an enemy radar they will enable their ECM jammer. +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionECM_OnlyLockByRadar() + self:F2( { self.ControllableName } ) + + if self:IsAir() then + self:SetOption(AI.Option.Air.id.ECM_USING, 1) + end + + return self +end + + +--- Defines the usage of Electronic Counter Measures by airborne forces. If the AI is being detected by a radar they will enable their ECM. +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionECM_DetectedLockByRadar() + self:F2( { self.ControllableName } ) + + if self:IsAir() then + self:SetOption(AI.Option.Air.id.ECM_USING, 2) + end + + return self +end + +--- Defines the usage of Electronic Counter Measures by airborne forces. AI will leave their ECM on all the time. +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionECM_AlwaysOn() + self:F2( { self.ControllableName } ) + + if self:IsAir() then + self:SetOption(AI.Option.Air.id.ECM_USING, 3) + end + + return self +end + +--- Retrieve the controllable mission and allow to place function hooks within the mission waypoint plan. +-- Use the method @{Wrapper.Controllable#CONTROLLABLE:WayPointFunction} to define the hook functions for specific waypoints. +-- Use the method @{Controllable@CONTROLLABLE:WayPointExecute) to start the execution of the new mission plan. +-- Note that when WayPointInitialize is called, the Mission of the controllable is RESTARTED! +-- @param #CONTROLLABLE self +-- @param #table WayPoints If WayPoints is given, then use the route. +-- @return #CONTROLLABLE self +function CONTROLLABLE:WayPointInitialize( WayPoints ) + self:F( { WayPoints } ) + + if WayPoints then + self.WayPoints = WayPoints + else + self.WayPoints = self:GetTaskRoute() + end + + return self +end + +--- Get the current WayPoints set with the WayPoint functions( Note that the WayPoints can be nil, although there ARE waypoints). +-- @param #CONTROLLABLE self +-- @return #table WayPoints If WayPoints is given, then return the WayPoints structure. +function CONTROLLABLE:GetWayPoints() + self:F( ) + + if self.WayPoints then + return self.WayPoints + end + + return nil +end + +--- Registers a waypoint function that will be executed when the controllable moves over the WayPoint. +-- @param #CONTROLLABLE self +-- @param #number WayPoint The waypoint number. Note that the start waypoint on the route is WayPoint 1! +-- @param #number WayPointIndex When defining multiple WayPoint functions for one WayPoint, use WayPointIndex to set the sequence of actions. +-- @param #function WayPointFunction The waypoint function to be called when the controllable moves over the waypoint. The waypoint function takes variable parameters. +-- @return #CONTROLLABLE self +function CONTROLLABLE:WayPointFunction( WayPoint, WayPointIndex, WayPointFunction, ... ) + self:F2( { WayPoint, WayPointIndex, WayPointFunction } ) + + table.insert( self.WayPoints[WayPoint].task.params.tasks, WayPointIndex ) + self.WayPoints[WayPoint].task.params.tasks[WayPointIndex] = self:TaskFunction( WayPointFunction, arg ) + return self +end + + +--- Executes the WayPoint plan. +-- The function gets a WayPoint parameter, that you can use to restart the mission at a specific WayPoint. +-- Note that when the WayPoint parameter is used, the new start mission waypoint of the controllable will be 1! +-- @param #CONTROLLABLE self +-- @param #number WayPoint The WayPoint from where to execute the mission. +-- @param #number WaitTime The amount seconds to wait before initiating the mission. +-- @return #CONTROLLABLE self +function CONTROLLABLE:WayPointExecute( WayPoint, WaitTime ) + self:F( { WayPoint, WaitTime } ) + + if not WayPoint then + WayPoint = 1 + end + + -- When starting the mission from a certain point, the TaskPoints need to be deleted before the given WayPoint. + for TaskPointID = 1, WayPoint - 1 do + table.remove( self.WayPoints, 1 ) + end + + self:T3( self.WayPoints ) + + self:SetTask( self:TaskRoute( self.WayPoints ), WaitTime ) + + return self +end + +--- Returns if the Controllable contains AirPlanes. +-- @param #CONTROLLABLE self +-- @return #boolean true if Controllable contains AirPlanes. +function CONTROLLABLE:IsAirPlane() + self:F2() + + local DCSObject = self:GetDCSObject() + + if DCSObject then + local Category = DCSObject:getDesc().category + return Category == Unit.Category.AIRPLANE + end + + return nil +end + +--- Returns if the Controllable contains Helicopters. +-- @param #CONTROLLABLE self +-- @return #boolean true if Controllable contains Helicopters. +function CONTROLLABLE:IsHelicopter() + self:F2() + + local DCSObject = self:GetDCSObject() + + if DCSObject then + local Category = DCSObject:getDesc().category + return Category == Unit.Category.HELICOPTER + end + + return nil +end + +--- Sets Controllable Option for Restriction of Afterburner. +-- @param #CONTROLLABLE self +-- @param #boolean RestrictBurner If true, restrict burner. If false or nil, allow (unrestrict) burner. +function CONTROLLABLE:OptionRestrictBurner(RestrictBurner) + self:F2({self.ControllableName}) + + local DCSControllable = self:GetDCSObject() + + if DCSControllable then + local Controller = self:_GetController() + + if Controller then + + -- Issue https://github.com/FlightControl-Master/MOOSE/issues/1216 + if RestrictBurner == true then + if self:IsAir() then + Controller:setOption(16, true) + end + else + if self:IsAir() then + Controller:setOption(16, false) + end + end + + end + end + +end + +--- Sets Controllable Option for A2A attack range for AIR FIGHTER units. +-- @param #CONTROLLABLE self +-- @param #number range Defines the range +-- @return #CONTROLLABLE self +-- @usage Range can be one of MAX_RANGE = 0, NEZ_RANGE = 1, HALF_WAY_RMAX_NEZ = 2, TARGET_THREAT_EST = 3, RANDOM_RANGE = 4. Defaults to 3. See: https://wiki.hoggitworld.com/view/DCS_option_missileAttack +function CONTROLLABLE:OptionAAAttackRange(range) + self:F2( { self.ControllableName } ) + -- defaults to 3 + local range = range or 3 + if range < 0 or range > 4 then + range = 3 + end + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local Controller = self:_GetController() + if Controller then + if self:IsAir() then + self:SetOption(AI.Option.Air.val.MISSILE_ATTACK, range) + end + end + return self + end + return nil +end + +--- Defines the range at which a GROUND unit/group is allowed to use its weapons automatically. +-- @param #CONTROLLABLE self +-- @param #number EngageRange Engage range limit in percent (a number between 0 and 100). Default 100. +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionEngageRange(EngageRange) + self:F2( { self.ControllableName } ) + -- Set default if not specified. + EngageRange=EngageRange or 100 + if EngageRange < 0 or EngageRange > 100 then + EngageRange = 100 + end + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local Controller = self:_GetController() + if Controller then + if self:IsGround() then + self:SetOption(AI.Option.Ground.id.AC_ENGAGEMENT_RANGE_RESTRICTION, EngageRange) + end + end + return self + end + return nil +end + +--- (GROUND) Relocate controllable to a random point within a given radius; use e.g.for evasive actions; Note that not all ground controllables can actually drive, also the alarm state of the controllable might stop it from moving. +-- @param #CONTROLLABLE self +-- @param #number speed Speed of the controllable, default 20 +-- @param #number radius Radius of the relocation zone, default 500 +-- @param #boolean onroad If true, route on road (less problems with AI way finding), default true +-- @param #boolean shortcut If true and onroad is set, take a shorter route - if available - off road, default false +-- @param #string formation Formation string as in the mission editor, e.g. "Vee", "Diamond", "Line abreast", etc. Defaults to "Off Road" +-- @return #CONTROLLABLE self +function CONTROLLABLE:RelocateGroundRandomInRadius(speed, radius, onroad, shortcut, formation) + self:F2( { self.ControllableName } ) + + local _coord = self:GetCoordinate() + local _radius = radius or 500 + local _speed = speed or 20 + local _tocoord = _coord:GetRandomCoordinateInRadius(_radius,100) + local _onroad = onroad or true + local _grptsk = {} + local _candoroad = false + local _shortcut = shortcut or false + local _formation = formation or "Off Road" + + -- create a DCS Task an push it on the group + if onroad then + _grptsk, _candoroad = self:TaskGroundOnRoad(_tocoord,_speed,_formation,_shortcut) + self:Route(_grptsk,5) + else + self:TaskRouteToVec2(_tocoord:GetVec2(),_speed,_formation) + end + + return self +end + +--- Defines how long a GROUND unit/group will move to avoid an ongoing attack. +-- @param #CONTROLLABLE self +-- @param #number Seconds Any positive number: AI will disperse, but only for the specified time before continuing their route. 0: AI will not disperse. +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionDisperseOnAttack(Seconds) + self:F2( { self.ControllableName } ) + -- Set default if not specified. + local seconds = Seconds or 0 + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local Controller = self:_GetController() + if Controller then + if self:IsGround() then + self:SetOption(AI.Option.Ground.id.DISPERSE_ON_ATTACK, seconds) + end + end + return self + end + return nil +end + +--- Returns if the unit is a submarine. +-- @param #POSITIONABLE self +-- @return #boolean Submarines attributes result. +function POSITIONABLE:IsSubmarine() + self:F2() + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitDescriptor = DCSUnit:getDesc() + if UnitDescriptor.attributes["Submarines"] == true then + return true + else + return false + end + end + + return nil +end +--- **Wrapper** -- GROUP wraps the DCS Class Group objects. +-- +-- === +-- +-- The @{#GROUP} class is a wrapper class to handle the DCS Group objects. +-- +-- ## Features: +-- +-- * Support all DCS Group APIs. +-- * Enhance with Group specific APIs not in the DCS Group API set. +-- * Handle local Group Controller. +-- * Manage the "state" of the DCS Group. +-- +-- **IMPORTANT: ONE SHOULD NEVER SANATIZE these GROUP OBJECT REFERENCES! (make the GROUP object references nil).** +-- +-- === +-- +-- For each DCS Group object alive within a running mission, a GROUP wrapper object (instance) will be created within the _@{DATABASE} object. +-- This is done at the beginning of the mission (when the mission starts), and dynamically when new DCS Group objects are spawned (using the @{SPAWN} class). +-- +-- The GROUP class does not contain a :New() method, rather it provides :Find() methods to retrieve the object reference +-- using the DCS Group or the DCS GroupName. +-- +-- The GROUP methods will reference the DCS Group object by name when it is needed during API execution. +-- If the DCS Group object does not exist or is nil, the GROUP methods will return nil and may log an exception in the DCS.log file. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- ### Contributions: +-- +-- * [**Entropy**](https://forums.eagle.ru/member.php?u=111471), **Afinegan**: Came up with the requirement for AIOnOff(). +-- +-- === +-- +-- @module Wrapper.Group +-- @image Wrapper_Group.JPG + + +--- @type GROUP +-- @extends Wrapper.Controllable#CONTROLLABLE +-- @field #string GroupName The name of the group. + + +--- Wrapper class of the DCS world Group object. +-- +-- The GROUP class provides the following functions to retrieve quickly the relevant GROUP instance: +-- +-- * @{#GROUP.Find}(): Find a GROUP instance from the _DATABASE object using a DCS Group object. +-- * @{#GROUP.FindByName}(): Find a GROUP instance from the _DATABASE object using a DCS Group name. +-- +-- # 1. Tasking of groups +-- +-- A GROUP is derived from the wrapper class CONTROLLABLE (@{Wrapper.Controllable#CONTROLLABLE}). +-- See the @{Wrapper.Controllable} task methods section for a description of the task methods. +-- +-- But here is an example how a group can be assigned a task. +-- +-- This test demonstrates the use(s) of the SwitchWayPoint method of the GROUP class. +-- +-- First we look up the objects. We create a GROUP object `HeliGroup`, using the @{#GROUP:FindByName}() method, looking up the `"Helicopter"` group object. +-- Same for the `"AttackGroup"`. +-- +-- local HeliGroup = GROUP:FindByName( "Helicopter" ) +-- local AttackGroup = GROUP:FindByName( "AttackGroup" ) +-- +-- Now we retrieve the @{Wrapper.Unit#UNIT} objects of the `AttackGroup` object, using the method `:GetUnits()`. +-- +-- local AttackUnits = AttackGroup:GetUnits() +-- +-- Tasks are actually text strings that we build using methods of GROUP. +-- So first, we declare an list of `Tasks`. +-- +-- local Tasks = {} +-- +-- Now we loop over the `AttackUnits` using a for loop. +-- We retrieve the `AttackUnit` using the `AttackGroup:GetUnit()` method. +-- Each `AttackUnit` found, will be attacked by `HeliGroup`, using the method `HeliGroup:TaskAttackUnit()`. +-- This method returns a string containing a command line to execute the task to the `HeliGroup`. +-- The code will assign the task string command to the next element in the `Task` list, using `Tasks[#Tasks+1]`. +-- This little code will take the count of `Task` using `#` operator, and will add `1` to the count. +-- This result will be the index of the `Task` element. +-- +-- for i = 1, #AttackUnits do +-- local AttackUnit = AttackGroup:GetUnit( i ) +-- Tasks[#Tasks+1] = HeliGroup:TaskAttackUnit( AttackUnit ) +-- end +-- +-- Once these tasks have been executed, a function `_Resume` will be called ... +-- +-- Tasks[#Tasks+1] = HeliGroup:TaskFunction( "_Resume", { "''" } ) +-- +-- --- @param Wrapper.Group#GROUP HeliGroup +-- function _Resume( HeliGroup ) +-- env.info( '_Resume' ) +-- +-- HeliGroup:MessageToAll( "Resuming",10,"Info") +-- end +-- +-- Now here is where the task gets assigned! +-- Using `HeliGroup:PushTask`, the task is pushed onto the task queue of the group `HeliGroup`. +-- Since `Tasks` is an array of tasks, we use the `HeliGroup:TaskCombo` method to execute the tasks. +-- The `HeliGroup:PushTask` method can receive a delay parameter in seconds. +-- In the example, `30` is given as a delay. +-- +-- +-- HeliGroup:PushTask( +-- HeliGroup:TaskCombo( +-- Tasks +-- ), 30 +-- ) +-- +-- That's it! +-- But again, please refer to the @{Wrapper.Controllable} task methods section for a description of the different task methods that are available. +-- +-- +-- +-- ### Obtain the mission from group templates +-- +-- Group templates contain complete mission descriptions. Sometimes you want to copy a complete mission from a group and assign it to another: +-- +-- * @{Wrapper.Controllable#CONTROLLABLE.TaskMission}: (AIR + GROUND) Return a mission task from a mission template. +-- +-- ## GROUP Command methods +-- +-- A GROUP is a @{Wrapper.Controllable}. See the @{Wrapper.Controllable} command methods section for a description of the command methods. +-- +-- ## GROUP option methods +-- +-- A GROUP is a @{Wrapper.Controllable}. See the @{Wrapper.Controllable} option methods section for a description of the option methods. +-- +-- ## GROUP Zone validation methods +-- +-- The group can be validated whether it is completely, partly or not within a @{Zone}. +-- Use the following Zone validation methods on the group: +-- +-- * @{#GROUP.IsCompletelyInZone}: Returns true if all units of the group are within a @{Zone}. +-- * @{#GROUP.IsPartlyInZone}: Returns true if some units of the group are within a @{Zone}. +-- * @{#GROUP.IsNotInZone}: Returns true if none of the group units of the group are within a @{Zone}. +-- +-- The zone can be of any @{Zone} class derived from @{Core.Zone#ZONE_BASE}. So, these methods are polymorphic to the zones tested on. +-- +-- ## GROUP AI methods +-- +-- A GROUP has AI methods to control the AI activation. +-- +-- * @{#GROUP.SetAIOnOff}(): Turns the GROUP AI On or Off. +-- * @{#GROUP.SetAIOn}(): Turns the GROUP AI On. +-- * @{#GROUP.SetAIOff}(): Turns the GROUP AI Off. +-- +-- @field #GROUP GROUP +GROUP = { + ClassName = "GROUP", +} + + +--- Enumerator for location at airbases +-- @type GROUP.Takeoff +GROUP.Takeoff = { + Air = 1, + Runway = 2, + Hot = 3, + Cold = 4, +} + +GROUPTEMPLATE = {} + +GROUPTEMPLATE.Takeoff = { + [GROUP.Takeoff.Air] = { "Turning Point", "Turning Point" }, + [GROUP.Takeoff.Runway] = { "TakeOff", "From Runway" }, + [GROUP.Takeoff.Hot] = { "TakeOffParkingHot", "From Parking Area Hot" }, + [GROUP.Takeoff.Cold] = { "TakeOffParking", "From Parking Area" } +} + +--- Generalized group attributes. See [DCS attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes) on hoggit. +-- @type GROUP.Attribute +-- @field #string AIR_TRANSPORTPLANE Airplane with transport capability. This can be used to transport other assets. +-- @field #string AIR_AWACS Airborne Early Warning and Control System. +-- @field #string AIR_FIGHTER Fighter, interceptor, ... airplane. +-- @field #string AIR_BOMBER Aircraft which can be used for strategic bombing. +-- @field #string AIR_TANKER Airplane which can refuel other aircraft. +-- @field #string AIR_TRANSPORTHELO Helicopter with transport capability. This can be used to transport other assets. +-- @field #string AIR_ATTACKHELO Attack helicopter. +-- @field #string AIR_UAV Unpiloted Aerial Vehicle, e.g. drones. +-- @field #string AIR_OTHER Any airborne unit that does not fall into any other airborne category. +-- @field #string GROUND_APC Infantry carriers, in particular Amoured Personell Carrier. This can be used to transport other assets. +-- @field #string GROUND_TRUCK Unarmed ground vehicles, which has the DCS "Truck" attribute. +-- @field #string GROUND_INFANTRY Ground infantry assets. +-- @field #string GROUND_ARTILLERY Artillery assets. +-- @field #string GROUND_TANK Tanks (modern or old). +-- @field #string GROUND_TRAIN Trains. Not that trains are **not** yet properly implemented in DCS and cannot be used currently. +-- @field #string GROUND_EWR Early Warning Radar. +-- @field #string GROUND_AAA Anti-Aircraft Artillery. +-- @field #string GROUND_SAM Surface-to-Air Missile system or components. +-- @field #string GROUND_OTHER Any ground unit that does not fall into any other ground category. +-- @field #string NAVAL_AIRCRAFTCARRIER Aircraft carrier. +-- @field #string NAVAL_WARSHIP War ship, i.e. cruisers, destroyers, firgates and corvettes. +-- @field #string NAVAL_ARMEDSHIP Any armed ship that is not an aircraft carrier, a cruiser, destroyer, firgatte or corvette. +-- @field #string NAVAL_UNARMEDSHIP Any unarmed naval vessel. +-- @field #string NAVAL_OTHER Any naval unit that does not fall into any other naval category. +-- @field #string OTHER_UNKNOWN Anything that does not fall into any other category. +GROUP.Attribute = { + AIR_TRANSPORTPLANE="Air_TransportPlane", + AIR_AWACS="Air_AWACS", + AIR_FIGHTER="Air_Fighter", + AIR_BOMBER="Air_Bomber", + AIR_TANKER="Air_Tanker", + AIR_TRANSPORTHELO="Air_TransportHelo", + AIR_ATTACKHELO="Air_AttackHelo", + AIR_UAV="Air_UAV", + AIR_OTHER="Air_OtherAir", + GROUND_APC="Ground_APC", + GROUND_TRUCK="Ground_Truck", + GROUND_INFANTRY="Ground_Infantry", + GROUND_ARTILLERY="Ground_Artillery", + GROUND_TANK="Ground_Tank", + GROUND_TRAIN="Ground_Train", + GROUND_EWR="Ground_EWR", + GROUND_AAA="Ground_AAA", + GROUND_SAM="Ground_SAM", + GROUND_OTHER="Ground_OtherGround", + NAVAL_AIRCRAFTCARRIER="Naval_AircraftCarrier", + NAVAL_WARSHIP="Naval_WarShip", + NAVAL_ARMEDSHIP="Naval_ArmedShip", + NAVAL_UNARMEDSHIP="Naval_UnarmedShip", + NAVAL_OTHER="Naval_OtherNaval", + OTHER_UNKNOWN="Other_Unknown", +} + + +--- Create a new GROUP from a given GroupTemplate as a parameter. +-- Note that the GroupTemplate is NOT spawned into the mission. +-- It is merely added to the @{Core.Database}. +-- @param #GROUP self +-- @param #table GroupTemplate The GroupTemplate Structure exactly as defined within the mission editor. +-- @param DCS#coalition.side CoalitionSide The coalition.side of the group. +-- @param DCS#Group.Category CategoryID The Group.Category of the group. +-- @param DCS#country.id CountryID the country.id of the group. +-- @return #GROUP self +function GROUP:NewTemplate( GroupTemplate, CoalitionSide, CategoryID, CountryID ) + local GroupName = GroupTemplate.name + + _DATABASE:_RegisterGroupTemplate( GroupTemplate, CoalitionSide, CategoryID, CountryID, GroupName ) + + local self = BASE:Inherit( self, CONTROLLABLE:New( GroupName ) ) + self.GroupName = GroupName + + if not _DATABASE.GROUPS[GroupName] then + _DATABASE.GROUPS[GroupName] = self + end + + self:SetEventPriority( 4 ) + return self +end + + + +--- Create a new GROUP from an existing Group in the Mission. +-- @param #GROUP self +-- @param #string GroupName The Group name +-- @return #GROUP self +function GROUP:Register( GroupName ) + + local self = BASE:Inherit( self, CONTROLLABLE:New( GroupName ) ) -- #GROUP + + self.GroupName = GroupName + + self:SetEventPriority( 4 ) + return self +end + +-- Reference methods. + +--- Find the GROUP wrapper class instance using the DCS Group. +-- @param #GROUP self +-- @param DCS#Group DCSGroup The DCS Group. +-- @return #GROUP The GROUP. +function GROUP:Find( DCSGroup ) + + local GroupName = DCSGroup:getName() -- Wrapper.Group#GROUP + local GroupFound = _DATABASE:FindGroup( GroupName ) + return GroupFound +end + +--- Find the created GROUP using the DCS Group Name. +-- @param #GROUP self +-- @param #string GroupName The DCS Group Name. +-- @return #GROUP The GROUP. +function GROUP:FindByName( GroupName ) + + local GroupFound = _DATABASE:FindGroup( GroupName ) + return GroupFound +end + +-- DCS Group methods support. + +--- Returns the DCS Group. +-- @param #GROUP self +-- @return DCS#Group The DCS Group. +function GROUP:GetDCSObject() + local DCSGroup = Group.getByName( self.GroupName ) + + if DCSGroup then + return DCSGroup + end + + return nil +end + +--- Returns the @{DCS#Position3} position vectors indicating the point and direction vectors in 3D of the POSITIONABLE within the mission. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return DCS#Position The 3D position vectors of the POSITIONABLE. +-- @return #nil The POSITIONABLE is not existing or alive. +function GROUP:GetPositionVec3() -- Overridden from POSITIONABLE:GetPositionVec3() + self:F2( self.PositionableName ) + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + local unit = DCSPositionable:getUnits()[1] + if unit then + local PositionablePosition = unit:getPosition().p + self:T3( PositionablePosition ) + return PositionablePosition + end + end + + return nil +end + +--- Returns if the group is alive. +-- The Group must: +-- +-- * Exist at run-time. +-- * Has at least one unit. +-- +-- When the first @{Wrapper.Unit} of the group is active, it will return true. +-- If the first @{Wrapper.Unit} of the group is inactive, it will return false. +-- +-- @param #GROUP self +-- @return #boolean true if the group is alive and active. +-- @return #boolean false if the group is alive but inactive. +-- @return #nil if the group does not exist anymore. +function GROUP:IsAlive() + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSObject() -- DCS#Group + + if DCSGroup then + if DCSGroup:isExist() then + local DCSUnit = DCSGroup:getUnit(1) -- DCS#Unit + if DCSUnit then + local GroupIsAlive = DCSUnit:isActive() + self:T3( GroupIsAlive ) + return GroupIsAlive + end + end + end + + return nil +end + +--- Returns if the group is activated. +-- @param #GROUP self +-- @return #boolean true if group is activated. +-- @return #nil The group is not existing or alive. +function GROUP:IsActive() + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSObject() -- DCS#Group + + if DCSGroup then + local unit = DCSGroup:getUnit(1) + if unit then + local GroupIsActive = unit:isActive() + return GroupIsActive + end + end + + return nil +end + + + +--- Destroys the DCS Group and all of its DCS Units. +-- Note that this destroy method also can raise a destroy event at run-time. +-- So all event listeners will catch the destroy event of this group for each unit in the group. +-- To raise these events, provide the `GenerateEvent` parameter. +-- @param #GROUP self +-- @param #boolean GenerateEvent If true, a crash or dead event for each unit is generated. If false, if no event is triggered. If nil, a RemoveUnit event is triggered. +-- @param #number delay Delay in seconds before despawning the group. +-- @usage +-- -- Air unit example: destroy the Helicopter and generate a S_EVENT_CRASH for each unit in the Helicopter group. +-- Helicopter = GROUP:FindByName( "Helicopter" ) +-- Helicopter:Destroy( true ) +-- @usage +-- -- Ground unit example: destroy the Tanks and generate a S_EVENT_DEAD for each unit in the Tanks group. +-- Tanks = GROUP:FindByName( "Tanks" ) +-- Tanks:Destroy( true ) +-- @usage +-- -- Ship unit example: destroy the Ship silently. +-- Ship = GROUP:FindByName( "Ship" ) +-- Ship:Destroy() +-- +-- @usage +-- -- Destroy without event generation example. +-- Ship = GROUP:FindByName( "Boat" ) +-- Ship:Destroy( false ) -- Don't generate an event upon destruction. +-- +function GROUP:Destroy( GenerateEvent, delay ) + self:F2( self.GroupName ) + + if delay and delay>0 then + --SCHEDULER:New(nil, GROUP.Destroy, {self, GenerateEvent}, delay) + self:ScheduleOnce(delay, GROUP.Destroy, self, GenerateEvent) + else + + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + for Index, UnitData in pairs( DCSGroup:getUnits() ) do + if GenerateEvent and GenerateEvent == true then + if self:IsAir() then + self:CreateEventCrash( timer.getTime(), UnitData ) + else + self:CreateEventDead( timer.getTime(), UnitData ) + end + elseif GenerateEvent == false then + -- Do nothing! + else + self:CreateEventRemoveUnit( timer.getTime(), UnitData ) + end + end + USERFLAG:New( self:GetName() ):Set( 100 ) + DCSGroup:destroy() + DCSGroup = nil + end + end + + return nil +end + + +--- Returns category of the DCS Group. Returns one of +-- +-- * Group.Category.AIRPLANE +-- * Group.Category.HELICOPTER +-- * Group.Category.GROUND +-- * Group.Category.SHIP +-- * Group.Category.TRAIN +-- +-- @param #GROUP self +-- @return DCS#Group.Category The category ID. +function GROUP:GetCategory() + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSObject() + if DCSGroup then + local GroupCategory = DCSGroup:getCategory() + self:T3( GroupCategory ) + return GroupCategory + end + + return nil +end + +--- Returns the category name of the #GROUP. +-- @param #GROUP self +-- @return #string Category name = Helicopter, Airplane, Ground Unit, Ship, Train. +function GROUP:GetCategoryName() + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSObject() + if DCSGroup then + local CategoryNames = { + [Group.Category.AIRPLANE] = "Airplane", + [Group.Category.HELICOPTER] = "Helicopter", + [Group.Category.GROUND] = "Ground Unit", + [Group.Category.SHIP] = "Ship", + [Group.Category.TRAIN] = "Train", + } + local GroupCategory = DCSGroup:getCategory() + self:T3( GroupCategory ) + + return CategoryNames[GroupCategory] + end + + return nil +end + + +--- Returns the coalition of the DCS Group. +-- @param #GROUP self +-- @return DCS#coalition.side The coalition side of the DCS Group. +function GROUP:GetCoalition() + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSObject() + if DCSGroup then + local GroupCoalition = DCSGroup:getCoalition() + self:T3( GroupCoalition ) + return GroupCoalition + end + + return nil +end + +--- Returns the country of the DCS Group. +-- @param #GROUP self +-- @return DCS#country.id The country identifier or nil if the DCS Group is not existing or alive. +function GROUP:GetCountry() + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSObject() + if DCSGroup then + local GroupCountry = DCSGroup:getUnit(1):getCountry() + self:T3( GroupCountry ) + return GroupCountry + end + + return nil +end + + +--- Check if at least one (or all) unit(s) has (have) a certain attribute. +-- See [hoggit documentation](https://wiki.hoggitworld.com/view/DCS_func_hasAttribute). +-- @param #GROUP self +-- @param #string attribute The name of the attribute the group is supposed to have. Valid attributes can be found in the "db_attributes.lua" file which is located at in "C:\Program Files\Eagle Dynamics\DCS World\Scripts\Database". +-- @param #boolean all If true, all units of the group must have the attribute in order to return true. Default is only one unit of a heterogenious group needs to have the attribute. +-- @return #boolean Group has this attribute. +function GROUP:HasAttribute(attribute, all) + + -- Get all units of the group. + local _units=self:GetUnits() + + local _allhave=true + local _onehas=false + + for _,_unit in pairs(_units) do + local _unit=_unit --Wrapper.Unit#UNIT + if _unit then + local _hastit=_unit:HasAttribute(attribute) + if _hastit==true then + _onehas=true + else + _allhave=false + end + end + end + + if all==true then + return _allhave + else + return _onehas + end +end + +--- Returns the maximum speed of the group. +-- If the group is heterogenious and consists of different units, the max speed of the slowest unit is returned. +-- @param #GROUP self +-- @return #number Speed in km/h. +function GROUP:GetSpeedMax() + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSObject() + if DCSGroup then + + local Units=self:GetUnits() + + local speedmax=nil + + for _,unit in pairs(Units) do + local unit=unit --Wrapper.Unit#UNIT + local speed=unit:GetSpeedMax() + if speedmax==nil then + speedmax=speed + elseif speed The list of @{Wrapper.Unit} objects of the @{Wrapper.Group}. +function GROUP:GetUnits() + self:F2( { self.GroupName } ) + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + local DCSUnits = DCSGroup:getUnits() + local Units = {} + for Index, UnitData in pairs( DCSUnits ) do + Units[#Units+1] = UNIT:Find( UnitData ) + end + self:T3( Units ) + return Units + end + + return nil +end + + +--- Returns a list of @{Wrapper.Unit} objects of the @{Wrapper.Group} that are occupied by a player. +-- @param #GROUP self +-- @return #list The list of player occupied @{Wrapper.Unit} objects of the @{Wrapper.Group}. +function GROUP:GetPlayerUnits() + self:F2( { self.GroupName } ) + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + local DCSUnits = DCSGroup:getUnits() + local Units = {} + for Index, UnitData in pairs( DCSUnits ) do + local PlayerUnit = UNIT:Find( UnitData ) + if PlayerUnit:GetPlayerName() then + Units[#Units+1] = PlayerUnit + end + end + self:T3( Units ) + return Units + end + + return nil +end + + +--- Returns the UNIT wrapper class with number UnitNumber. +-- If the underlying DCS Unit does not exist, the method will return nil. . +-- @param #GROUP self +-- @param #number UnitNumber The number of the UNIT wrapper class to be returned. +-- @return Wrapper.Unit#UNIT The UNIT wrapper class. +function GROUP:GetUnit( UnitNumber ) + + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + + local DCSUnit = DCSGroup:getUnit( UnitNumber ) + + local UnitFound = UNIT:Find(DCSUnit) + + return UnitFound + end + + return nil +end + +--- Returns the DCS Unit with number UnitNumber. +-- If the underlying DCS Unit does not exist, the method will return nil. . +-- @param #GROUP self +-- @param #number UnitNumber The number of the DCS Unit to be returned. +-- @return DCS#Unit The DCS Unit. +function GROUP:GetDCSUnit( UnitNumber ) + + local DCSGroup=self:GetDCSObject() + + if DCSGroup then + local DCSUnitFound=DCSGroup:getUnit( UnitNumber ) + return DCSUnitFound + end + + return nil +end + +--- Returns current size of the DCS Group. +-- If some of the DCS Units of the DCS Group are destroyed the size of the DCS Group is changed. +-- @param #GROUP self +-- @return #number The DCS Group size. +function GROUP:GetSize() + + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + + local GroupSize = DCSGroup:getSize() + + if GroupSize then + return GroupSize + else + return 0 + end + end + + return nil +end + +--- Count number of alive units in the group. +-- @param #GROUP self +-- @return #number Number of alive units. If DCS group is nil, 0 is returned. +function GROUP:CountAliveUnits() + self:F3( { self.GroupName } ) + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + local units=self:GetUnits() + local n=0 + for _,_unit in pairs(units) do + local unit=_unit --Wrapper.Unit#UNIT + if unit and unit:IsAlive() then + n=n+1 + end + end + return n + end + + return 0 +end + +--- Get the first unit of the group which is alive. +-- @param #GROUP self +-- @return Wrapper.Unit#UNIT First unit alive. +function GROUP:GetFirstUnitAlive() + self:F3({self.GroupName}) + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + local units=self:GetUnits() + for _,_unit in pairs(units) do + local unit=_unit --Wrapper.Unit#UNIT + if unit and unit:IsAlive() then + return unit + end + end + end + + return nil +end + + + +--- Returns the average velocity Vec3 vector. +-- @param Wrapper.Group#GROUP self +-- @return DCS#Vec3 The velocity Vec3 vector +-- @return #nil The GROUP is not existing or alive. +function GROUP:GetVelocityVec3() + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSObject() + + if DCSGroup and DCSGroup:isExist() then + local GroupUnits = DCSGroup:getUnits() + local GroupCount = #GroupUnits + + local VelocityVec3 = { x = 0, y = 0, z = 0 } + + for _, DCSUnit in pairs( GroupUnits ) do + local UnitVelocityVec3 = DCSUnit:getVelocity() + VelocityVec3.x = VelocityVec3.x + UnitVelocityVec3.x + VelocityVec3.y = VelocityVec3.y + UnitVelocityVec3.y + VelocityVec3.z = VelocityVec3.z + UnitVelocityVec3.z + end + + VelocityVec3.x = VelocityVec3.x / GroupCount + VelocityVec3.y = VelocityVec3.y / GroupCount + VelocityVec3.z = VelocityVec3.z / GroupCount + + return VelocityVec3 + end + + BASE:E( { "Cannot GetVelocityVec3", Group = self, Alive = self:IsAlive() } ) + + return nil +end + + +--- Returns the average group height in meters. +-- @param Wrapper.Group#GROUP self +-- @param #boolean FromGround Measure from the ground or from sea level. Provide **true** for measuring from the ground. **false** or **nil** if you measure from sea level. +-- @return DCS#Vec3 The height of the group or nil if is not existing or alive. +function GROUP:GetHeight( FromGround ) + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + local GroupUnits = DCSGroup:getUnits() + local GroupCount = #GroupUnits + + local GroupHeight = 0 + + for _, DCSUnit in pairs( GroupUnits ) do + local GroupPosition = DCSUnit:getPosition() + + if FromGround == true then + local LandHeight = land.getHeight( { x = GroupPosition.p.x, y = GroupPosition.p.z } ) + GroupHeight = GroupHeight + ( GroupPosition.p.y - LandHeight ) + else + GroupHeight = GroupHeight + GroupPosition.p.y + end + end + + return GroupHeight / GroupCount + end + + return nil +end + + + + +--- +--- Returns the initial size of the DCS Group. +-- If some of the DCS Units of the DCS Group are destroyed, the initial size of the DCS Group is unchanged. +-- @param #GROUP self +-- @return #number The DCS Group initial size. +function GROUP:GetInitialSize() + self:F3( { self.GroupName } ) + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + local GroupInitialSize = DCSGroup:getInitialSize() + self:T3( GroupInitialSize ) + return GroupInitialSize + end + + return nil +end + + +--- Returns the DCS Units of the DCS Group. +-- @param #GROUP self +-- @return #table The DCS Units. +function GROUP:GetDCSUnits() + self:F2( { self.GroupName } ) + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + local DCSUnits = DCSGroup:getUnits() + self:T3( DCSUnits ) + return DCSUnits + end + + return nil +end + + +--- Activates a late activated GROUP. +-- @param #GROUP self +-- @param #number delay Delay in seconds, before the group is activated. +-- @return #GROUP self +function GROUP:Activate(delay) + self:F2( { self.GroupName } ) + if delay and delay>0 then + self:ScheduleOnce(delay, GROUP.Activate, self) + else + trigger.action.activateGroup( self:GetDCSObject() ) + end + return self +end + + +--- Gets the type name of the group. +-- @param #GROUP self +-- @return #string The type name of the group. +function GROUP:GetTypeName() + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + local GroupTypeName = DCSGroup:getUnit(1):getTypeName() + self:T3( GroupTypeName ) + return( GroupTypeName ) + end + + return nil +end + +--- Gets the player name of the group. +-- @param #GROUP self +-- @return #string The player name of the group. +function GROUP:GetPlayerName() + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + local PlayerName = DCSGroup:getUnit(1):getPlayerName() + self:T3( PlayerName ) + return( PlayerName ) + end + + return nil +end + + +--- Gets the CallSign of the first DCS Unit of the DCS Group. +-- @param #GROUP self +-- @return #string The CallSign of the first DCS Unit of the DCS Group. +function GROUP:GetCallsign() + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + local GroupCallSign = DCSGroup:getUnit(1):getCallsign() + self:T3( GroupCallSign ) + return GroupCallSign + end + + BASE:E( { "Cannot GetCallsign", Positionable = self, Alive = self:IsAlive() } ) + + return nil +end + +--- Returns the current point (Vec2 vector) of the first DCS Unit in the DCS Group. +-- @param #GROUP self +-- @return DCS#Vec2 Current Vec2 point of the first DCS Unit of the DCS Group. +function GROUP:GetVec2() + + local Unit=self:GetUnit(1) + + if Unit then + local vec2=Unit:GetVec2() + return vec2 + end + +end + +--- Returns the current Vec3 vector of the first DCS Unit in the GROUP. +-- @param #GROUP self +-- @return DCS#Vec3 Current Vec3 of the first DCS Unit of the GROUP. +function GROUP:GetVec3() + + -- Get first unit. + local unit=self:GetUnit(1) + + if unit then + local vec3=unit:GetVec3() + return vec3 + end + + self:E("ERROR: Cannot get Vec3 of group "..tostring(self.GroupName)) + return nil +end + +--- Returns a POINT_VEC2 object indicating the point in 2D of the first UNIT of the GROUP within the mission. +-- @param #GROUP self +-- @return Core.Point#POINT_VEC2 The 2D point vector of the first DCS Unit of the GROUP. +-- @return #nil The first UNIT is not existing or alive. +function GROUP:GetPointVec2() + self:F2(self.GroupName) + + local FirstUnit = self:GetUnit(1) + + if FirstUnit then + local FirstUnitPointVec2 = FirstUnit:GetPointVec2() + self:T3(FirstUnitPointVec2) + return FirstUnitPointVec2 + end + + BASE:E( { "Cannot GetPointVec2", Group = self, Alive = self:IsAlive() } ) + + return nil +end + +--- Returns a COORDINATE object indicating the point of the first UNIT of the GROUP within the mission. +-- @param Wrapper.Group#GROUP self +-- @return Core.Point#COORDINATE The COORDINATE of the GROUP. +function GROUP:GetCoordinate() + + local FirstUnit = self:GetUnit(1) + + if FirstUnit then + local FirstUnitCoordinate = FirstUnit:GetCoordinate() + return FirstUnitCoordinate + end + + BASE:E( { "Cannot GetCoordinate", Group = self, Alive = self:IsAlive() } ) + + return nil +end + + +--- Returns a random @{DCS#Vec3} vector (point in 3D of the UNIT within the mission) within a range around the first UNIT of the GROUP. +-- @param #GROUP self +-- @param #number Radius +-- @return DCS#Vec3 The random 3D point vector around the first UNIT of the GROUP. +-- @return #nil The GROUP is invalid or empty +-- @usage +-- -- If Radius is ignored, returns the DCS#Vec3 of first UNIT of the GROUP +function GROUP:GetRandomVec3(Radius) + self:F2(self.GroupName) + + local FirstUnit = self:GetUnit(1) + + if FirstUnit then + local FirstUnitRandomPointVec3 = FirstUnit:GetRandomVec3(Radius) + self:T3(FirstUnitRandomPointVec3) + return FirstUnitRandomPointVec3 + end + + BASE:E( { "Cannot GetRandomVec3", Group = self, Alive = self:IsAlive() } ) + + return nil +end + +--- Returns the mean heading of every UNIT in the GROUP in degrees +-- @param #GROUP self +-- @return #number mean heading of the GROUP +-- @return #nil The first UNIT is not existing or alive. +function GROUP:GetHeading() + self:F2(self.GroupName) + + local GroupSize = self:GetSize() + local HeadingAccumulator = 0 + + local n=0 + if GroupSize then + for i = 1, GroupSize do + local unit=self:GetUnit(i) + if unit and unit:IsAlive() then + HeadingAccumulator = HeadingAccumulator + unit:GetHeading() + n=n+1 + end + end + return math.floor(HeadingAccumulator / n) + end + + BASE:E( { "Cannot GetHeading", Group = self, Alive = self:IsAlive() } ) + + return nil + +end + +--- Return the fuel state and unit reference for the unit with the least +-- amount of fuel in the group. +-- @param #GROUP self +-- @return #number The fuel state of the unit with the least amount of fuel +-- @return #Unit reference to #Unit object for further processing +function GROUP:GetFuelMin() + self:F3(self.ControllableName) + + if not self:GetDCSObject() then + BASE:E( { "Cannot GetFuel", Group = self, Alive = self:IsAlive() } ) + return 0 + end + + local min = 65535 -- some sufficiently large number to init with + local unit = nil + local tmp = nil + + for UnitID, UnitData in pairs( self:GetUnits() ) do + if UnitData and UnitData:IsAlive() then + tmp = UnitData:GetFuel() + if tmp < min then + min = tmp + unit = UnitData + end + end + end + + return min, unit +end + +--- Returns relative amount of fuel (from 0.0 to 1.0) the group has in its +-- internal tanks. If there are additional fuel tanks the value may be +-- greater than 1.0. +-- @param #GROUP self +-- @return #number The relative amount of fuel (from 0.0 to 1.0). +-- @return #nil The GROUP is not existing or alive. +function GROUP:GetFuelAvg() + self:F( self.ControllableName ) + + local DCSControllable = self:GetDCSObject() + + if DCSControllable then + local GroupSize = self:GetSize() + local TotalFuel = 0 + for UnitID, UnitData in pairs( self:GetUnits() ) do + local Unit = UnitData -- Wrapper.Unit#UNIT + local UnitFuel = Unit:GetFuel() + self:F( { Fuel = UnitFuel } ) + TotalFuel = TotalFuel + UnitFuel + end + local GroupFuel = TotalFuel / GroupSize + return GroupFuel + end + + BASE:E( { "Cannot GetFuel", Group = self, Alive = self:IsAlive() } ) + + return 0 +end + +--- Returns relative amount of fuel (from 0.0 to 1.0) the group has in its internal tanks. If there are additional fuel tanks the value may be greater than 1.0. +-- @param #GROUP self +-- @return #number The relative amount of fuel (from 0.0 to 1.0). +-- @return #nil The GROUP is not existing or alive. +function GROUP:GetFuel() + return self:GetFuelAvg() +end + + +--- Get the number of shells, rockets, bombs and missiles the whole group currently has. +-- @param #GROUP self +-- @return #number Total amount of ammo the group has left. This is the sum of shells, rockets, bombs and missiles of all units. +-- @return #number Number of shells left. +-- @return #number Number of rockets left. +-- @return #number Number of bombs left. +-- @return #number Number of missiles left. +function GROUP:GetAmmunition() + self:F( self.ControllableName ) + + local DCSControllable = self:GetDCSObject() + + local Ntot=0 + local Nshells=0 + local Nrockets=0 + local Nmissiles=0 + local Nbombs=0 + + if DCSControllable then + + -- Loop over units. + for UnitID, UnitData in pairs( self:GetUnits() ) do + local Unit = UnitData -- Wrapper.Unit#UNIT + + -- Get ammo of the unit + local ntot, nshells, nrockets, nbombs, nmissiles = Unit:GetAmmunition() + + Ntot=Ntot+ntot + Nshells=Nshells+nshells + Nrockets=Nrockets+nrockets + Nmissiles=Nmissiles+nmissiles + Nbombs=Nbombs+nbombs + + end + + end + + return Ntot, Nshells, Nrockets, Nbombs, Nmissiles +end + + +do -- Is Zone methods + + +--- Check if any unit of a group is inside a @{Zone}. +-- @param #GROUP self +-- @param Core.Zone#ZONE_BASE Zone The zone to test. +-- @return #boolean Returns `true` if *at least one unit* is inside the zone or `false` if *no* unit is inside. +function GROUP:IsInZone( Zone ) + + if self:IsAlive() then + + for UnitID, UnitData in pairs(self:GetUnits()) do + local Unit = UnitData -- Wrapper.Unit#UNIT + + -- Get 2D vector. That's all we need for the zone check. + local vec2=Unit:GetVec2() + + if Zone:IsVec2InZone(vec2) then + return true -- At least one unit is in the zone. That is enough. + else + -- This one is not but another could be. + end + + end + + return false + end + + return nil +end + +--- Returns true if all units of the group are within a @{Zone}. +-- @param #GROUP self +-- @param Core.Zone#ZONE_BASE Zone The zone to test. +-- @return #boolean Returns true if the Group is completely within the @{Core.Zone#ZONE_BASE} +function GROUP:IsCompletelyInZone( Zone ) + self:F2( { self.GroupName, Zone } ) + + if not self:IsAlive() then return false end + + for UnitID, UnitData in pairs( self:GetUnits() ) do + local Unit = UnitData -- Wrapper.Unit#UNIT + if Zone:IsVec3InZone( Unit:GetVec3() ) then + else + return false + end + end + + return true +end + +--- Returns true if some but NOT ALL units of the group are within a @{Zone}. +-- @param #GROUP self +-- @param Core.Zone#ZONE_BASE Zone The zone to test. +-- @return #boolean Returns true if the Group is partially within the @{Core.Zone#ZONE_BASE} +function GROUP:IsPartlyInZone( Zone ) + self:F2( { self.GroupName, Zone } ) + + local IsOneUnitInZone = false + local IsOneUnitOutsideZone = false + + if not self:IsAlive() then return false end + + for UnitID, UnitData in pairs( self:GetUnits() ) do + local Unit = UnitData -- Wrapper.Unit#UNIT + if Zone:IsVec3InZone( Unit:GetVec3() ) then + IsOneUnitInZone = true + else + IsOneUnitOutsideZone = true + end + end + + if IsOneUnitInZone and IsOneUnitOutsideZone then + return true + else + return false + end +end + +--- Returns true if part or all units of the group are within a @{Zone}. +-- @param #GROUP self +-- @param Core.Zone#ZONE_BASE Zone The zone to test. +-- @return #boolean Returns true if the Group is partially or completely within the @{Core.Zone#ZONE_BASE}. +function GROUP:IsPartlyOrCompletelyInZone( Zone ) + return self:IsPartlyInZone(Zone) or self:IsCompletelyInZone(Zone) +end + +--- Returns true if none of the group units of the group are within a @{Zone}. +-- @param #GROUP self +-- @param Core.Zone#ZONE_BASE Zone The zone to test. +-- @return #boolean Returns true if the Group is not within the @{Core.Zone#ZONE_BASE} +function GROUP:IsNotInZone( Zone ) + self:F2( { self.GroupName, Zone } ) + + if not self:IsAlive() then return true end + + for UnitID, UnitData in pairs( self:GetUnits() ) do + local Unit = UnitData -- Wrapper.Unit#UNIT + if Zone:IsVec3InZone( Unit:GetVec3() ) then + return false + end + end + + return true +end + +--- Returns true if any units of the group are within a @{Core.Zone}. +-- @param #GROUP self +-- @param Core.Zone#ZONE_BASE Zone The zone to test. +-- @return #boolean Returns true if any unit of the Group is within the @{Core.Zone#ZONE_BASE} +function GROUP:IsAnyInZone( Zone ) + + if not self:IsAlive() then return false end + + for UnitID, UnitData in pairs( self:GetUnits() ) do + local Unit = UnitData -- Wrapper.Unit#UNIT + if Zone:IsVec3InZone( Unit:GetVec3() ) then + return true + end + end + return false +end + +--- Returns the number of UNITs that are in the @{Zone} +-- @param #GROUP self +-- @param Core.Zone#ZONE_BASE Zone The zone to test. +-- @return #number The number of UNITs that are in the @{Zone} +function GROUP:CountInZone( Zone ) + self:F2( {self.GroupName, Zone} ) + local Count = 0 + + if not self:IsAlive() then return Count end + + for UnitID, UnitData in pairs( self:GetUnits() ) do + local Unit = UnitData -- Wrapper.Unit#UNIT + if Zone:IsVec3InZone( Unit:GetVec3() ) then + Count = Count + 1 + end + end + + return Count +end + +--- Returns if the group is of an air category. +-- If the group is a helicopter or a plane, then this method will return true, otherwise false. +-- @param #GROUP self +-- @return #boolean Air category evaluation result. +function GROUP:IsAir() + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + local IsAirResult = DCSGroup:getCategory() == Group.Category.AIRPLANE or DCSGroup:getCategory() == Group.Category.HELICOPTER + self:T3( IsAirResult ) + return IsAirResult + end + + return nil +end + +--- Returns if the DCS Group contains Helicopters. +-- @param #GROUP self +-- @return #boolean true if DCS Group contains Helicopters. +function GROUP:IsHelicopter() + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + local GroupCategory = DCSGroup:getCategory() + self:T2( GroupCategory ) + return GroupCategory == Group.Category.HELICOPTER + end + + return nil +end + +--- Returns if the DCS Group contains AirPlanes. +-- @param #GROUP self +-- @return #boolean true if DCS Group contains AirPlanes. +function GROUP:IsAirPlane() + self:F2() + + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + local GroupCategory = DCSGroup:getCategory() + self:T2( GroupCategory ) + return GroupCategory == Group.Category.AIRPLANE + end + + return nil +end + +--- Returns if the DCS Group contains Ground troops. +-- @param #GROUP self +-- @return #boolean true if DCS Group contains Ground troops. +function GROUP:IsGround() + self:F2() + + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + local GroupCategory = DCSGroup:getCategory() + self:T2( GroupCategory ) + return GroupCategory == Group.Category.GROUND + end + + return nil +end + +--- Returns if the DCS Group contains Ships. +-- @param #GROUP self +-- @return #boolean true if DCS Group contains Ships. +function GROUP:IsShip() + self:F2() + + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + local GroupCategory = DCSGroup:getCategory() + self:T2( GroupCategory ) + return GroupCategory == Group.Category.SHIP + end + + return nil +end + +--- Returns if all units of the group are on the ground or landed. +-- If all units of this group are on the ground, this function will return true, otherwise false. +-- @param #GROUP self +-- @return #boolean All units on the ground result. +function GROUP:AllOnGround() + self:F2() + + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + local AllOnGroundResult = true + + for Index, UnitData in pairs( DCSGroup:getUnits() ) do + if UnitData:inAir() then + AllOnGroundResult = false + end + end + + self:T3( AllOnGroundResult ) + return AllOnGroundResult + end + + return nil +end + +end + +do -- AI methods + + --- Turns the AI On or Off for the GROUP. + -- @param #GROUP self + -- @param #boolean AIOnOff The value true turns the AI On, the value false turns the AI Off. + -- @return #GROUP The GROUP. + function GROUP:SetAIOnOff( AIOnOff ) + + local DCSGroup = self:GetDCSObject() -- DCS#Group + + if DCSGroup then + local DCSController = DCSGroup:getController() -- DCS#Controller + if DCSController then + DCSController:setOnOff( AIOnOff ) + return self + end + end + + return nil + end + + --- Turns the AI On for the GROUP. + -- @param #GROUP self + -- @return #GROUP The GROUP. + function GROUP:SetAIOn() + + return self:SetAIOnOff( true ) + end + + --- Turns the AI Off for the GROUP. + -- @param #GROUP self + -- @return #GROUP The GROUP. + function GROUP:SetAIOff() + + return self:SetAIOnOff( false ) + end + +end + + + +--- Returns the current maximum velocity of the group. +-- Each unit within the group gets evaluated, and the maximum velocity (= the unit which is going the fastest) is returned. +-- @param #GROUP self +-- @return #number Maximum velocity found. +function GROUP:GetMaxVelocity() + self:F2() + + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + local GroupVelocityMax = 0 + + for Index, UnitData in pairs( DCSGroup:getUnits() ) do + + local UnitVelocityVec3 = UnitData:getVelocity() + local UnitVelocity = math.abs( UnitVelocityVec3.x ) + math.abs( UnitVelocityVec3.y ) + math.abs( UnitVelocityVec3.z ) + + if UnitVelocity > GroupVelocityMax then + GroupVelocityMax = UnitVelocity + end + end + + return GroupVelocityMax + end + + return nil +end + +--- Returns the current minimum height of the group. +-- Each unit within the group gets evaluated, and the minimum height (= the unit which is the lowest elevated) is returned. +-- @param #GROUP self +-- @return #number Minimum height found. +function GROUP:GetMinHeight() + self:F2() + + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + local GroupHeightMin = 999999999 + + for Index, UnitData in pairs( DCSGroup:getUnits() ) do + local UnitData = UnitData -- DCS#Unit + + local UnitHeight = UnitData:getPoint() + + if UnitHeight < GroupHeightMin then + GroupHeightMin = UnitHeight + end + end + + return GroupHeightMin + end + + return nil +end + +--- Returns the current maximum height of the group. +-- Each unit within the group gets evaluated, and the maximum height (= the unit which is the highest elevated) is returned. +-- @param #GROUP self +-- @return #number Maximum height found. +function GROUP:GetMaxHeight() + self:F2() + + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + local GroupHeightMax = -999999999 + + for Index, UnitData in pairs( DCSGroup:getUnits() ) do + local UnitData = UnitData -- DCS#Unit + + local UnitHeight = UnitData:getPoint() + + if UnitHeight > GroupHeightMax then + GroupHeightMax = UnitHeight + end + end + + return GroupHeightMax + end + + return nil +end + +-- RESPAWNING + +--- Returns the group template from the @{DATABASE} (_DATABASE object). +-- @param #GROUP self +-- @return #table +function GROUP:GetTemplate() + local GroupName = self:GetName() + return UTILS.DeepCopy( _DATABASE:GetGroupTemplate( GroupName ) ) +end + +--- Returns the group template route.points[] (the waypoints) from the @{DATABASE} (_DATABASE object). +-- @param #GROUP self +-- @return #table +function GROUP:GetTemplateRoutePoints() + local GroupName = self:GetName() + return UTILS.DeepCopy( _DATABASE:GetGroupTemplate( GroupName ).route.points ) +end + + + +--- Sets the controlled status in a Template. +-- @param #GROUP self +-- @param #boolean Controlled true is controlled, false is uncontrolled. +-- @return #table +function GROUP:SetTemplateControlled( Template, Controlled ) + Template.uncontrolled = not Controlled + return Template +end + +--- Sets the CountryID of the group in a Template. +-- @param #GROUP self +-- @param DCS#country.id CountryID The country ID. +-- @return #table +function GROUP:SetTemplateCountry( Template, CountryID ) + Template.CountryID = CountryID + return Template +end + +--- Sets the CoalitionID of the group in a Template. +-- @param #GROUP self +-- @param DCS#coalition.side CoalitionID The coalition ID. +-- @return #table +function GROUP:SetTemplateCoalition( Template, CoalitionID ) + Template.CoalitionID = CoalitionID + return Template +end + + +--- Set the heading for the units in degrees within the respawned group. +-- @param #GROUP self +-- @param #number Heading The heading in meters. +-- @return #GROUP self +function GROUP:InitHeading( Heading ) + self.InitRespawnHeading = Heading + return self +end + + +--- Set the height for the units in meters for the respawned group. (This is applicable for air units). +-- @param #GROUP self +-- @param #number Height The height in meters. +-- @return #GROUP self +function GROUP:InitHeight( Height ) + self.InitRespawnHeight = Height + return self +end + + +--- Set the respawn @{Zone} for the respawned group. +-- @param #GROUP self +-- @param Core.Zone#ZONE Zone The zone in meters. +-- @return #GROUP self +function GROUP:InitZone( Zone ) + self.InitRespawnZone = Zone + return self +end + + +--- Randomize the positions of the units of the respawned group within the @{Zone}. +-- When a Respawn happens, the units of the group will be placed at random positions within the Zone (selected). +-- @param #GROUP self +-- @param #boolean PositionZone true will randomize the positions within the Zone. +-- @return #GROUP self +function GROUP:InitRandomizePositionZone( PositionZone ) + + self.InitRespawnRandomizePositionZone = PositionZone + self.InitRespawnRandomizePositionInner = nil + self.InitRespawnRandomizePositionOuter = nil + + return self +end + + +--- Randomize the positions of the units of the respawned group in a circle band. +-- When a Respawn happens, the units of the group will be positioned at random places within the Outer and Inner radius. +-- Thus, a band is created around the respawn location where the units will be placed at random positions. +-- @param #GROUP self +-- @param #boolean OuterRadius Outer band in meters from the center. +-- @param #boolean InnerRadius Inner band in meters from the center. +-- @return #GROUP self +function GROUP:InitRandomizePositionRadius( OuterRadius, InnerRadius ) + + self.InitRespawnRandomizePositionZone = nil + self.InitRespawnRandomizePositionOuter = OuterRadius + self.InitRespawnRandomizePositionInner = InnerRadius + + return self +end + +--- Set respawn coordinate. +-- @param #GROUP self +-- @param Core.Point#COORDINATE coordinate Coordinate where the group should be respawned. +-- @return #GROUP self +function GROUP:InitCoordinate(coordinate) + self:F({coordinate=coordinate}) + self.InitCoord=coordinate + return self +end + +--- Sets the radio comms on or off when the group is respawned. Same as checking/unchecking the COMM box in the mission editor. +-- @param #GROUP self +-- @param #boolean switch If true (or nil), enables the radio comms. If false, disables the radio for the spawned group. +-- @return #GROUP self +function GROUP:InitRadioCommsOnOff(switch) + self:F({switch=switch}) + if switch==true or switch==nil then + self.InitRespawnRadio=true + else + self.InitRespawnRadio=false + end + return self +end + +--- Sets the radio frequency of the group when it is respawned. +-- @param #GROUP self +-- @param #number frequency The frequency in MHz. +-- @return #GROUP self +function GROUP:InitRadioFrequency(frequency) + self:F({frequency=frequency}) + + self.InitRespawnFreq=frequency + + return self +end + +--- Set radio modulation when the group is respawned. Default is AM. +-- @param #GROUP self +-- @param #string modulation Either "FM" or "AM". If no value is given, modulation is set to AM. +-- @return #GROUP self +function GROUP:InitRadioModulation(modulation) + self:F({modulation=modulation}) + if modulation and modulation:lower()=="fm" then + self.InitRespawnModu=radio.modulation.FM + else + self.InitRespawnModu=radio.modulation.AM + end + return self +end + +--- Sets the modex (tail number) of the first unit of the group. If more units are in the group, the number is increased with every unit. +-- @param #GROUP self +-- @param #string modex Tail number of the first unit. +-- @return #GROUP self +function GROUP:InitModex(modex) + self:F({modex=modex}) + if modex then + self.InitRespawnModex=tonumber(modex) + end + return self +end + +--- Respawn the @{Wrapper.Group} at a @{Point}. +-- The method will setup the new group template according the Init(Respawn) settings provided for the group. +-- These settings can be provided by calling the relevant Init...() methods of the Group. +-- +-- - @{#GROUP.InitHeading}: Set the heading for the units in degrees within the respawned group. +-- - @{#GROUP.InitHeight}: Set the height for the units in meters for the respawned group. (This is applicable for air units). +-- - @{#GROUP.InitRandomizeHeading}: Randomize the headings for the units within the respawned group. +-- - @{#GROUP.InitZone}: Set the respawn @{Zone} for the respawned group. +-- - @{#GROUP.InitRandomizeZones}: Randomize the respawn @{Zone} between one of the @{Zone}s given for the respawned group. +-- - @{#GROUP.InitRandomizePositionZone}: Randomize the positions of the units of the respawned group within the @{Zone}. +-- - @{#GROUP.InitRandomizePositionRadius}: Randomize the positions of the units of the respawned group in a circle band. +-- - @{#GROUP.InitRandomizeTemplates}: Randomize the Template for the respawned group. +-- +-- +-- Notes: +-- +-- - When InitZone or InitRandomizeZones is not used, the position of the respawned group will be its current position. +-- - The current alive group will always be destroyed and respawned using the template definition. +-- +-- @param Wrapper.Group#GROUP self +-- @param #table Template (optional) The template of the Group retrieved with GROUP:GetTemplate(). If the template is not provided, the template will be retrieved of the group itself. +-- @param #boolean Reset Reset positions if TRUE. +-- @return Wrapper.Group#GROUP self +function GROUP:Respawn( Template, Reset ) + + -- Given template or get old. + Template = Template or self:GetTemplate() + + -- Get correct heading. + local function _Heading(course) + local h + if course<=180 then + h=math.rad(course) + else + h=-math.rad(360-course) + end + return h + end + + -- First check if group is alive. + if self:IsAlive() then + + -- Respawn zone. + local Zone = self.InitRespawnZone -- Core.Zone#ZONE + + -- Zone position or current group position. + local Vec3 = Zone and Zone:GetVec3() or self:GetVec3() + + -- From point of the template. + local From = { x = Template.x, y = Template.y } + + -- X, Y + Template.x = Vec3.x + Template.y = Vec3.z + + --Template.x = nil + --Template.y = nil + + -- Debug number of units. + self:F( #Template.units ) + + -- Reset position etc? + if Reset == true then + + -- Loop over units in group. + for UnitID, UnitData in pairs( self:GetUnits() ) do + local GroupUnit = UnitData -- Wrapper.Unit#UNIT + self:F(GroupUnit:GetName()) + + if GroupUnit:IsAlive() then + self:I("FF Alive") + + -- Get unit position vector. + local GroupUnitVec3 = GroupUnit:GetVec3() + + -- Check if respawn zone is set. + if Zone then + if self.InitRespawnRandomizePositionZone then + GroupUnitVec3 = Zone:GetRandomVec3() + else + if self.InitRespawnRandomizePositionInner and self.InitRespawnRandomizePositionOuter then + GroupUnitVec3 = POINT_VEC3:NewFromVec2( From ):GetRandomPointVec3InRadius( self.InitRespawnRandomizePositionsOuter, self.InitRespawnRandomizePositionsInner ) + else + GroupUnitVec3 = Zone:GetVec3() + end + end + end + + -- Coordinate where the group should be respawned. + if self.InitCoord then + GroupUnitVec3=self.InitCoord:GetVec3() + end + + -- Altitude + Template.units[UnitID].alt = self.InitRespawnHeight and self.InitRespawnHeight or GroupUnitVec3.y + + -- Unit position. Why not simply take the current positon? + if Zone then + Template.units[UnitID].x = ( Template.units[UnitID].x - From.x ) + GroupUnitVec3.x -- Keep the original x position of the template and translate to the new position. + Template.units[UnitID].y = ( Template.units[UnitID].y - From.y ) + GroupUnitVec3.z -- Keep the original z position of the template and translate to the new position. + else + Template.units[UnitID].x=GroupUnitVec3.x + Template.units[UnitID].y=GroupUnitVec3.z + end + + -- Set heading. + Template.units[UnitID].heading = _Heading(self.InitRespawnHeading and self.InitRespawnHeading or GroupUnit:GetHeading()) + Template.units[UnitID].psi = -Template.units[UnitID].heading + + -- Debug. + self:F( { UnitID, Template.units[UnitID], Template.units[UnitID] } ) + end + end + + elseif Reset==false then -- Reset=false or nil + + -- Loop over template units. + for UnitID, TemplateUnitData in pairs( Template.units ) do + + self:F( "Reset" ) + + -- Position from template. + local GroupUnitVec3 = { x = TemplateUnitData.x, y = TemplateUnitData.alt, z = TemplateUnitData.y } + + -- Respawn zone position. + if Zone then + if self.InitRespawnRandomizePositionZone then + GroupUnitVec3 = Zone:GetRandomVec3() + else + if self.InitRespawnRandomizePositionInner and self.InitRespawnRandomizePositionOuter then + GroupUnitVec3 = POINT_VEC3:NewFromVec2( From ):GetRandomPointVec3InRadius( self.InitRespawnRandomizePositionsOuter, self.InitRespawnRandomizePositionsInner ) + else + GroupUnitVec3 = Zone:GetVec3() + end + end + end + + -- Coordinate where the group should be respawned. + if self.InitCoord then + GroupUnitVec3=self.InitCoord:GetVec3() + end + + -- Set altitude. + Template.units[UnitID].alt = self.InitRespawnHeight and self.InitRespawnHeight or GroupUnitVec3.y + + -- Unit position. + Template.units[UnitID].x = ( Template.units[UnitID].x - From.x ) + GroupUnitVec3.x -- Keep the original x position of the template and translate to the new position. + Template.units[UnitID].y = ( Template.units[UnitID].y - From.y ) + GroupUnitVec3.z -- Keep the original z position of the template and translate to the new position. + + -- Heading + Template.units[UnitID].heading = self.InitRespawnHeading and self.InitRespawnHeading or TemplateUnitData.heading + + -- Debug. + self:F( { UnitID, Template.units[UnitID], Template.units[UnitID] } ) + end + + else + + local units=self:GetUnits() + + -- Loop over template units. + for UnitID, Unit in pairs(Template.units) do + + for _,_unit in pairs(units) do + local unit=_unit --Wrapper.Unit#UNIT + + if unit:GetName()==Unit.name then + local coord=unit:GetCoordinate() + local heading=unit:GetHeading() + Unit.x=coord.x + Unit.y=coord.z + Unit.alt=coord.y + Unit.heading=math.rad(heading) + Unit.psi=-Unit.heading + end + end + + end + + end + + end + + -- Set tail number. + if self.InitRespawnModex then + for UnitID=1,#Template.units do + Template.units[UnitID].onboard_num=string.format("%03d", self.InitRespawnModex+(UnitID-1)) + end + end + + -- Set radio frequency and modulation. + if self.InitRespawnRadio then + Template.communication=self.InitRespawnRadio + end + if self.InitRespawnFreq then + Template.frequency=self.InitRespawnFreq + end + if self.InitRespawnModu then + Template.modulation=self.InitRespawnModu + end + + -- Destroy old group. Dont trigger any dead/crash events since this is a respawn. + self:Destroy(false) + + self:T({Template=Template}) + + -- Spawn new group. + _DATABASE:Spawn(Template) + + -- Reset events. + self:ResetEvents() + + return self +end + + +--- Respawn a group at an airbase. +-- Note that the group has to be on parking spots at the airbase already in order for this to work. +-- So each unit of the group is respawned at exactly the same parking spot as it currently occupies. +-- @param Wrapper.Group#GROUP self +-- @param #table SpawnTemplate (Optional) The spawn template for the group. If no template is given it is exacted from the group. +-- @param Core.Spawn#SPAWN.Takeoff Takeoff (Optional) Takeoff type. Sould be either SPAWN.Takeoff.Cold or SPAWN.Takeoff.Hot. Default is SPAWN.Takeoff.Hot. +-- @param #boolean Uncontrolled (Optional) If true, spawn in uncontrolled state. +-- @return Wrapper.Group#GROUP Group spawned at airbase or nil if group could not be spawned. +function GROUP:RespawnAtCurrentAirbase(SpawnTemplate, Takeoff, Uncontrolled) -- R2.4 + self:F2( { SpawnTemplate, Takeoff, Uncontrolled} ) + + if self and self:IsAlive() then + + -- Get closest airbase. Should be the one we are currently on. + local airbase=self:GetCoordinate():GetClosestAirbase() + + if airbase then + self:F2("Closest airbase = "..airbase:GetName()) + else + self:E("ERROR: could not find closest airbase!") + return nil + end + -- Takeoff type. Default hot. + Takeoff = Takeoff or SPAWN.Takeoff.Hot + + -- Coordinate of the airbase. + local AirbaseCoord=airbase:GetCoordinate() + + -- Spawn template. + SpawnTemplate = SpawnTemplate or self:GetTemplate() + + if SpawnTemplate then + + local SpawnPoint = SpawnTemplate.route.points[1] + + -- These are only for ships. + SpawnPoint.linkUnit = nil + SpawnPoint.helipadId = nil + SpawnPoint.airdromeId = nil + + -- Aibase id and category. + local AirbaseID = airbase:GetID() + local AirbaseCategory = airbase:GetAirbaseCategory() + + if AirbaseCategory == Airbase.Category.SHIP or AirbaseCategory == Airbase.Category.HELIPAD then + SpawnPoint.linkUnit = AirbaseID + SpawnPoint.helipadId = AirbaseID + elseif AirbaseCategory == Airbase.Category.AIRDROME then + SpawnPoint.airdromeId = AirbaseID + end + + + SpawnPoint.type = GROUPTEMPLATE.Takeoff[Takeoff][1] -- type + SpawnPoint.action = GROUPTEMPLATE.Takeoff[Takeoff][2] -- action + + -- Get the units of the group. + local units=self:GetUnits() + + local x + local y + for UnitID=1,#units do + + local unit=units[UnitID] --Wrapper.Unit#UNIT + + -- Get closest parking spot of current unit. Note that we look for occupied spots since the unit is currently sitting on it! + local Parkingspot, TermialID, Distance=unit:GetCoordinate():GetClosestParkingSpot(airbase) + + --Parkingspot:MarkToAll("parking spot") + self:T2(string.format("Closest parking spot distance = %s, terminal ID=%s", tostring(Distance), tostring(TermialID))) + + -- Get unit coordinates for respawning position. + local uc=unit:GetCoordinate() + --uc:MarkToAll(string.format("re-spawnplace %s terminal %d", unit:GetName(), TermialID)) + + SpawnTemplate.units[UnitID].x = uc.x --Parkingspot.x + SpawnTemplate.units[UnitID].y = uc.z --Parkingspot.z + SpawnTemplate.units[UnitID].alt = uc.y --Parkingspot.y + + SpawnTemplate.units[UnitID].parking = TermialID + SpawnTemplate.units[UnitID].parking_id = nil + + --SpawnTemplate.units[UnitID].unitId=nil + end + + --SpawnTemplate.groupId=nil + + SpawnPoint.x = SpawnTemplate.units[1].x --x --AirbaseCoord.x + SpawnPoint.y = SpawnTemplate.units[1].y --y --AirbaseCoord.z + SpawnPoint.alt = SpawnTemplate.units[1].alt --AirbaseCoord:GetLandHeight() + + SpawnTemplate.x = SpawnTemplate.units[1].x --x --AirbaseCoord.x + SpawnTemplate.y = SpawnTemplate.units[1].y --y --AirbaseCoord.z + + -- Set uncontrolled state. + SpawnTemplate.uncontrolled=Uncontrolled + + -- Set radio frequency and modulation. + if self.InitRespawnRadio then + SpawnTemplate.communication=self.InitRespawnRadio + end + if self.InitRespawnFreq then + SpawnTemplate.frequency=self.InitRespawnFreq + end + if self.InitRespawnModu then + SpawnTemplate.modulation=self.InitRespawnModu + end + + -- Destroy old group. + self:Destroy(false) + + -- Spawn new group. + _DATABASE:Spawn(SpawnTemplate) + + -- Reset events. + self:ResetEvents() + + return self + end + else + self:E("WARNING: GROUP is not alive!") + end + + return nil +end + + +--- Return the mission template of the group. +-- @param #GROUP self +-- @return #table The MissionTemplate +function GROUP:GetTaskMission() + self:F2( self.GroupName ) + + return routines.utils.deepCopy( _DATABASE.Templates.Groups[self.GroupName].Template ) +end + +--- Return the mission route of the group. +-- @param #GROUP self +-- @return #table The mission route defined by points. +function GROUP:GetTaskRoute() + self:F2( self.GroupName ) + + return routines.utils.deepCopy( _DATABASE.Templates.Groups[self.GroupName].Template.route.points ) +end + +--- Return the route of a group by using the @{Core.Database#DATABASE} class. +-- @param #GROUP self +-- @param #number Begin The route point from where the copy will start. The base route point is 0. +-- @param #number End The route point where the copy will end. The End point is the last point - the End point. The last point has base 0. +-- @param #boolean Randomize Randomization of the route, when true. +-- @param #number Radius When randomization is on, the randomization is within the radius. +function GROUP:CopyRoute( Begin, End, Randomize, Radius ) + self:F2( { Begin, End } ) + + local Points = {} + + -- Could be a Spawned Group + local GroupName = string.match( self:GetName(), ".*#" ) + if GroupName then + GroupName = GroupName:sub( 1, -2 ) + else + GroupName = self:GetName() + end + + self:T3( { GroupName } ) + + local Template = _DATABASE.Templates.Groups[GroupName].Template + + if Template then + if not Begin then + Begin = 0 + end + if not End then + End = 0 + end + + for TPointID = Begin + 1, #Template.route.points - End do + if Template.route.points[TPointID] then + Points[#Points+1] = routines.utils.deepCopy( Template.route.points[TPointID] ) + if Randomize then + if not Radius then + Radius = 500 + end + Points[#Points].x = Points[#Points].x + math.random( Radius * -1, Radius ) + Points[#Points].y = Points[#Points].y + math.random( Radius * -1, Radius ) + end + end + end + return Points + else + error( "Template not found for Group : " .. GroupName ) + end + + return nil +end + +--- Calculate the maxium A2G threat level of the Group. +-- @param #GROUP self +-- @return #number Number between 0 and 10. +function GROUP:CalculateThreatLevelA2G() + + local MaxThreatLevelA2G = 0 + for UnitName, UnitData in pairs( self:GetUnits() ) do + local ThreatUnit = UnitData -- Wrapper.Unit#UNIT + local ThreatLevelA2G = ThreatUnit:GetThreatLevel() + if ThreatLevelA2G > MaxThreatLevelA2G then + MaxThreatLevelA2G = ThreatLevelA2G + end + end + + self:T3( MaxThreatLevelA2G ) + return MaxThreatLevelA2G +end + +--- Get threat level of the group. +-- @param #GROUP self +-- @return #number Max threat level (a number between 0 and 10). +function GROUP:GetThreatLevel() + + local threatlevelMax = 0 + for UnitName, UnitData in pairs(self:GetUnits()) do + local ThreatUnit = UnitData -- Wrapper.Unit#UNIT + + local threatlevel = ThreatUnit:GetThreatLevel() + if threatlevel > threatlevelMax then + threatlevelMax=threatlevel + end + end + + return threatlevelMax +end + + +--- Returns true if the first unit of the GROUP is in the air. +-- @param Wrapper.Group#GROUP self +-- @return #boolean true if in the first unit of the group is in the air or #nil if the GROUP is not existing or not alive. +function GROUP:InAir() + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + local DCSUnit = DCSGroup:getUnit(1) + if DCSUnit then + local GroupInAir = DCSGroup:getUnit(1):inAir() + self:T3( GroupInAir ) + return GroupInAir + end + end + + return nil +end + +--- Checks whether any unit (or optionally) all units of a group is(are) airbore or not. +-- @param Wrapper.Group#GROUP self +-- @param #boolean AllUnits (Optional) If true, check whether all units of the group are airborne. +-- @return #boolean True if at least one (optionally all) unit(s) is(are) airborne or false otherwise. Nil if no unit exists or is alive. +function GROUP:IsAirborne(AllUnits) + self:F2( self.GroupName ) + + -- Get all units of the group. + local units=self:GetUnits() + + if units then + + if AllUnits then + + --- We want to know if ALL units are airborne. + + for _,_unit in pairs(units) do + local unit=_unit --Wrapper.Unit#UNIT + + if unit then + + -- Unit in air or not. + local inair=unit:InAir() + + -- At least one unit is not in air. + if not inair then + return false + end + end + + end + + -- All units are in air. + return true + + else + + --- We want to know if ANY unit is airborne. + + for _,_unit in pairs(units) do + local unit=_unit --Wrapper.Unit#UNIT + + if unit then + + -- Unit in air or not. + local inair=unit:InAir() + + if inair then + -- At least one unit is in air. + return true + end + + end + + -- No unit is in air. + return false + + end + end + end + + return nil +end + + + +--- Returns the DCS descriptor table of the nth unit of the group. +-- @param #GROUP self +-- @param #number n (Optional) The number of the unit for which the dscriptor is returned. +-- @return DCS#Object.Desc The descriptor of the first unit of the group or #nil if the group does not exist any more. +function GROUP:GetDCSDesc(n) + -- Default. + n=n or 1 + + local unit=self:GetUnit(n) + if unit and unit:IsAlive()~=nil then + local desc=unit:GetDesc() + return desc + end + + return nil +end + + +--- Get the generalized attribute of a self. +-- Note that for a heterogenious self, the attribute is determined from the attribute of the first unit! +-- @param #GROUP self +-- @return #string Generalized attribute of the self. +function GROUP:GetAttribute() + + -- Default + local attribute=GROUP.Attribute.OTHER_UNKNOWN --#GROUP.Attribute + + if self then + + ----------- + --- Air --- + ----------- + -- Planes + local transportplane=self:HasAttribute("Transports") and self:HasAttribute("Planes") + local awacs=self:HasAttribute("AWACS") + local fighter=self:HasAttribute("Fighters") or self:HasAttribute("Interceptors") or self:HasAttribute("Multirole fighters") or (self:HasAttribute("Bombers") and not self:HasAttribute("Strategic bombers")) + local bomber=self:HasAttribute("Strategic bombers") + local tanker=self:HasAttribute("Tankers") + local uav=self:HasAttribute("UAVs") + -- Helicopters + local transporthelo=self:HasAttribute("Transport helicopters") + local attackhelicopter=self:HasAttribute("Attack helicopters") + + -------------- + --- Ground --- + -------------- + -- Ground + local apc=self:HasAttribute("Infantry carriers") + local truck=self:HasAttribute("Trucks") and self:GetCategory()==Group.Category.GROUND + local infantry=self:HasAttribute("Infantry") + local artillery=self:HasAttribute("Artillery") + local tank=self:HasAttribute("Old Tanks") or self:HasAttribute("Modern Tanks") + local aaa=self:HasAttribute("AAA") + local ewr=self:HasAttribute("EWR") + local sam=self:HasAttribute("SAM elements") and (not self:HasAttribute("AAA")) + -- Train + local train=self:GetCategory()==Group.Category.TRAIN + + ------------- + --- Naval --- + ------------- + -- Ships + local aircraftcarrier=self:HasAttribute("Aircraft Carriers") + local warship=self:HasAttribute("Heavy armed ships") + local armedship=self:HasAttribute("Armed ships") + local unarmedship=self:HasAttribute("Unarmed ships") + + + -- Define attribute. Order is important. + if transportplane then + attribute=GROUP.Attribute.AIR_TRANSPORTPLANE + elseif awacs then + attribute=GROUP.Attribute.AIR_AWACS + elseif fighter then + attribute=GROUP.Attribute.AIR_FIGHTER + elseif bomber then + attribute=GROUP.Attribute.AIR_BOMBER + elseif tanker then + attribute=GROUP.Attribute.AIR_TANKER + elseif transporthelo then + attribute=GROUP.Attribute.AIR_TRANSPORTHELO + elseif attackhelicopter then + attribute=GROUP.Attribute.AIR_ATTACKHELO + elseif uav then + attribute=GROUP.Attribute.AIR_UAV + elseif apc then + attribute=GROUP.Attribute.GROUND_APC + elseif infantry then + attribute=GROUP.Attribute.GROUND_INFANTRY + elseif artillery then + attribute=GROUP.Attribute.GROUND_ARTILLERY + elseif tank then + attribute=GROUP.Attribute.GROUND_TANK + elseif aaa then + attribute=GROUP.Attribute.GROUND_AAA + elseif ewr then + attribute=GROUP.Attribute.GROUND_EWR + elseif sam then + attribute=GROUP.Attribute.GROUND_SAM + elseif truck then + attribute=GROUP.Attribute.GROUND_TRUCK + elseif train then + attribute=GROUP.Attribute.GROUND_TRAIN + elseif aircraftcarrier then + attribute=GROUP.Attribute.NAVAL_AIRCRAFTCARRIER + elseif warship then + attribute=GROUP.Attribute.NAVAL_WARSHIP + elseif armedship then + attribute=GROUP.Attribute.NAVAL_ARMEDSHIP + elseif unarmedship then + attribute=GROUP.Attribute.NAVAL_UNARMEDSHIP + else + if self:IsGround() then + attribute=GROUP.Attribute.GROUND_OTHER + elseif self:IsShip() then + attribute=GROUP.Attribute.NAVAL_OTHER + elseif self:IsAir() then + attribute=GROUP.Attribute.AIR_OTHER + else + attribute=GROUP.Attribute.OTHER_UNKNOWN + end + end + end + + return attribute +end + + +do -- Route methods + + --- (AIR) Return the Group to an @{Wrapper.Airbase#AIRBASE}. + -- The following things are to be taken into account: + -- + -- * The group is respawned to achieve the RTB, there may be side artefacts as a result of this. (Like weapons suddenly come back). + -- * A group consisting out of more than one unit, may rejoin formation when respawned. + -- * A speed can be given in km/h. If no speed is specified, the maximum speed of the first unit will be taken to return to base. + -- * When there is no @{Wrapper.Airbase} object specified, the group will return to the home base if the route of the group is pinned at take-off or at landing to a base. + -- * When there is no @{Wrapper.Airbase} object specified and the group route is not pinned to any airbase, it will return to the nearest airbase. + -- + -- @param #GROUP self + -- @param Wrapper.Airbase#AIRBASE RTBAirbase (optional) The @{Wrapper.Airbase} to return to. If blank, the controllable will return to the nearest friendly airbase. + -- @param #number Speed (optional) The Speed, if no Speed is given, 80% of maximum Speed of the group is selected. + -- @return #GROUP self + function GROUP:RouteRTB( RTBAirbase, Speed ) + self:F( { RTBAirbase:GetName(), Speed } ) + + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + + if RTBAirbase then + + -- If speed is not given take 80% of max speed. + local Speed=Speed or self:GetSpeedMax()*0.8 + + -- Curent (from) waypoint. + local coord=self:GetCoordinate() + local PointFrom=coord:WaypointAirTurningPoint(nil, Speed) + + -- Airbase coordinate. + --local PointAirbase=RTBAirbase:GetCoordinate():SetAltitude(coord.y):WaypointAirTurningPoint(nil ,Speed) + + -- Landing waypoint. More general than prev version since it should also work with FAPRS and ships. + local PointLanding=RTBAirbase:GetCoordinate():WaypointAirLanding(Speed, RTBAirbase) + + -- Waypoint table. + local Points={PointFrom, PointLanding} + --local Points={PointFrom, PointAirbase, PointLanding} + + -- Debug info. + self:T3(Points) + + -- Get group template. + local Template=self:GetTemplate() + + -- Set route points. + Template.route.points=Points + + -- Respawn the group. + self:Respawn(Template, true) + + -- Route the group or this will not work. + self:Route(Points) + else + + -- Clear all tasks. + self:ClearTasks() + + end + end + + return self + end + +end + +function GROUP:OnReSpawn( ReSpawnFunction ) + + self.ReSpawnFunction = ReSpawnFunction +end + +do -- Event Handling + + --- Subscribe to a DCS Event. + -- @param #GROUP self + -- @param Core.Event#EVENTS Event + -- @param #function EventFunction (optional) The function to be called when the event occurs for the GROUP. + -- @return #GROUP + function GROUP:HandleEvent( Event, EventFunction, ... ) + + self:EventDispatcher():OnEventForGroup( self:GetName(), EventFunction, self, Event, ... ) + + return self + end + + --- UnSubscribe to a DCS event. + -- @param #GROUP self + -- @param Core.Event#EVENTS Event + -- @return #GROUP + function GROUP:UnHandleEvent( Event ) + + self:EventDispatcher():RemoveEvent( self, Event ) + + return self + end + + --- Reset the subscriptions. + -- @param #GROUP self + -- @return #GROUP + function GROUP:ResetEvents() + + self:EventDispatcher():Reset( self ) + + for UnitID, UnitData in pairs( self:GetUnits() ) do + UnitData:ResetEvents() + end + + return self + end + +end + +do -- Players + + --- Get player names + -- @param #GROUP self + -- @return #table The group has players, an array of player names is returned. + -- @return #nil The group has no players + function GROUP:GetPlayerNames() + + local HasPlayers = false + + local PlayerNames = {} + + local Units = self:GetUnits() + for UnitID, UnitData in pairs( Units ) do + local Unit = UnitData -- Wrapper.Unit#UNIT + local PlayerName = Unit:GetPlayerName() + if PlayerName and PlayerName ~= "" then + PlayerNames = PlayerNames or {} + table.insert( PlayerNames, PlayerName ) + HasPlayers = true + end + end + + if HasPlayers == true then + self:F2( PlayerNames ) + return PlayerNames + end + + return nil + end + + + --- Get the active player count in the group. + -- @param #GROUP self + -- @return #number The amount of players. + function GROUP:GetPlayerCount() + + local PlayerCount = 0 + + local Units = self:GetUnits() + for UnitID, UnitData in pairs( Units or {} ) do + local Unit = UnitData -- Wrapper.Unit#UNIT + local PlayerName = Unit:GetPlayerName() + if PlayerName and PlayerName ~= "" then + PlayerCount = PlayerCount + 1 + end + end + + return PlayerCount + end + +end + +--- GROUND - Switch on/off radar emissions for the group. +-- @param #GROUP self +-- @param #boolean switch If true, emission is enabled. If false, emission is disabled. +-- @return #GROUP self +function GROUP:EnableEmission(switch) + self:F2( self.GroupName ) + local switch = switch or false + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + + DCSUnit:enableEmission(switch) + + end + + return self +end + +--- Switch on/off invisible flag for the group. +-- @param #GROUP self +-- @param #boolean switch If true, emission is enabled. If false, emission is disabled. +-- @return #GROUP self +function GROUP:SetCommandInvisible(switch) + self:F2( self.GroupName ) + if switch==nil then + switch=false + end + local SetInvisible = {id = 'SetInvisible', params = {value = switch}} + self:SetCommand(SetInvisible) + return self +end + +--- Switch on/off immortal flag for the group. +-- @param #GROUP self +-- @param #boolean switch If true, emission is enabled. If false, emission is disabled. +-- @return #GROUP self +function GROUP:SetCommandImmortal(switch) + self:F2( self.GroupName ) + if switch==nil then + switch=false + end + local SetImmortal = {id = 'SetImmortal', params = {value = switch}} + self:SetCommand(SetImmortal) + return self +end + +--- Get skill from Group. Effectively gets the skill from Unit 1 as the group holds no skill value. +-- @param #GROUP self +-- @return #string Skill String of skill name. +function GROUP:GetSkill() + self:F2( self.GroupName ) + local unit = self:GetUnit(1) + local name = unit:GetName() + local skill = _DATABASE.Templates.Units[name].Template.skill or "Random" + return skill +end + +--do -- Smoke +-- +----- Signal a flare at the position of the GROUP. +---- @param #GROUP self +---- @param Utilities.Utils#FLARECOLOR FlareColor +--function GROUP:Flare( FlareColor ) +-- self:F2() +-- trigger.action.signalFlare( self:GetVec3(), FlareColor , 0 ) +--end +-- +----- Signal a white flare at the position of the GROUP. +---- @param #GROUP self +--function GROUP:FlareWhite() +-- self:F2() +-- trigger.action.signalFlare( self:GetVec3(), trigger.flareColor.White , 0 ) +--end +-- +----- Signal a yellow flare at the position of the GROUP. +---- @param #GROUP self +--function GROUP:FlareYellow() +-- self:F2() +-- trigger.action.signalFlare( self:GetVec3(), trigger.flareColor.Yellow , 0 ) +--end +-- +----- Signal a green flare at the position of the GROUP. +---- @param #GROUP self +--function GROUP:FlareGreen() +-- self:F2() +-- trigger.action.signalFlare( self:GetVec3(), trigger.flareColor.Green , 0 ) +--end +-- +----- Signal a red flare at the position of the GROUP. +---- @param #GROUP self +--function GROUP:FlareRed() +-- self:F2() +-- local Vec3 = self:GetVec3() +-- if Vec3 then +-- trigger.action.signalFlare( Vec3, trigger.flareColor.Red, 0 ) +-- end +--end +-- +----- Smoke the GROUP. +---- @param #GROUP self +--function GROUP:Smoke( SmokeColor, Range ) +-- self:F2() +-- if Range then +-- trigger.action.smoke( self:GetRandomVec3( Range ), SmokeColor ) +-- else +-- trigger.action.smoke( self:GetVec3(), SmokeColor ) +-- end +-- +--end +-- +----- Smoke the GROUP Green. +---- @param #GROUP self +--function GROUP:SmokeGreen() +-- self:F2() +-- trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Green ) +--end +-- +----- Smoke the GROUP Red. +---- @param #GROUP self +--function GROUP:SmokeRed() +-- self:F2() +-- trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Red ) +--end +-- +----- Smoke the GROUP White. +---- @param #GROUP self +--function GROUP:SmokeWhite() +-- self:F2() +-- trigger.action.smoke( self:GetVec3(), trigger.smokeColor.White ) +--end +-- +----- Smoke the GROUP Orange. +---- @param #GROUP self +--function GROUP:SmokeOrange() +-- self:F2() +-- trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Orange ) +--end +-- +----- Smoke the GROUP Blue. +---- @param #GROUP self +--function GROUP:SmokeBlue() +-- self:F2() +-- trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Blue ) +--end +-- +-- +-- +--end +--- **Wrapper** - UNIT is a wrapper class for the DCS Class Unit. +-- +-- === +-- +-- The @{#UNIT} class is a wrapper class to handle the DCS Unit objects: +-- +-- * Support all DCS Unit APIs. +-- * Enhance with Unit specific APIs not in the DCS Unit API set. +-- * Handle local Unit Controller. +-- * Manage the "state" of the DCS Unit. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- ### Contributions: **funkyfranky** +-- +-- === +-- +-- @module Wrapper.Unit +-- @image Wrapper_Unit.JPG + + +--- @type UNIT +-- @field #string ClassName Name of the class. +-- @field #string UnitName Name of the unit. +-- @extends Wrapper.Controllable#CONTROLLABLE + +--- For each DCS Unit object alive within a running mission, a UNIT wrapper object (instance) will be created within the _@{DATABASE} object. +-- This is done at the beginning of the mission (when the mission starts), and dynamically when new DCS Unit objects are spawned (using the @{SPAWN} class). +-- +-- The UNIT class **does not contain a :New()** method, rather it provides **:Find()** methods to retrieve the object reference +-- using the DCS Unit or the DCS UnitName. +-- +-- Another thing to know is that UNIT objects do not "contain" the DCS Unit object. +-- The UNIT methods will reference the DCS Unit object by name when it is needed during API execution. +-- If the DCS Unit object does not exist or is nil, the UNIT methods will return nil and log an exception in the DCS.log file. +-- +-- The UNIT class provides the following functions to retrieve quickly the relevant UNIT instance: +-- +-- * @{#UNIT.Find}(): Find a UNIT instance from the _DATABASE object using a DCS Unit object. +-- * @{#UNIT.FindByName}(): Find a UNIT instance from the _DATABASE object using a DCS Unit name. +-- +-- IMPORTANT: ONE SHOULD NEVER SANATIZE these UNIT OBJECT REFERENCES! (make the UNIT object references nil). +-- +-- ## DCS UNIT APIs +-- +-- The DCS Unit APIs are used extensively within MOOSE. The UNIT class has for each DCS Unit API a corresponding method. +-- To be able to distinguish easily in your code the difference between a UNIT API call and a DCS Unit API call, +-- the first letter of the method is also capitalized. So, by example, the DCS Unit method @{DCS#Unit.getName}() +-- is implemented in the UNIT class as @{#UNIT.GetName}(). +-- +-- ## Smoke, Flare Units +-- +-- The UNIT class provides methods to smoke or flare units easily. +-- The @{#UNIT.SmokeBlue}(), @{#UNIT.SmokeGreen}(),@{#UNIT.SmokeOrange}(), @{#UNIT.SmokeRed}(), @{#UNIT.SmokeRed}() methods +-- will smoke the unit in the corresponding color. Note that smoking a unit is done at the current position of the DCS Unit. +-- When the DCS Unit moves for whatever reason, the smoking will still continue! +-- The @{#UNIT.FlareGreen}(), @{#UNIT.FlareRed}(), @{#UNIT.FlareWhite}(), @{#UNIT.FlareYellow}() +-- methods will fire off a flare in the air with the corresponding color. Note that a flare is a one-off shot and its effect is of very short duration. +-- +-- ## Location Position, Point +-- +-- The UNIT class provides methods to obtain the current point or position of the DCS Unit. +-- The @{#UNIT.GetPointVec2}(), @{#UNIT.GetVec3}() will obtain the current **location** of the DCS Unit in a Vec2 (2D) or a **point** in a Vec3 (3D) vector respectively. +-- If you want to obtain the complete **3D position** including orientation and direction vectors, consult the @{#UNIT.GetPositionVec3}() method respectively. +-- +-- ## Test if alive +-- +-- The @{#UNIT.IsAlive}(), @{#UNIT.IsActive}() methods determines if the DCS Unit is alive, meaning, it is existing and active. +-- +-- ## Test for proximity +-- +-- The UNIT class contains methods to test the location or proximity against zones or other objects. +-- +-- ### Zones range +-- +-- To test whether the Unit is within a **zone**, use the @{#UNIT.IsInZone}() or the @{#UNIT.IsNotInZone}() methods. Any zone can be tested on, but the zone must be derived from @{Core.Zone#ZONE_BASE}. +-- +-- ### Unit range +-- +-- * Test if another DCS Unit is within a given radius of the current DCS Unit, use the @{#UNIT.OtherUnitInRadius}() method. +-- +-- ## Test Line of Sight +-- +-- * Use the @{#UNIT.IsLOS}() method to check if the given unit is within line of sight. +-- +-- +-- @field #UNIT UNIT +UNIT = { + ClassName="UNIT", + UnitName=nil, +} + + +--- Unit.SensorType +-- @type Unit.SensorType +-- @field OPTIC +-- @field RADAR +-- @field IRST +-- @field RWR + + +-- Registration. + +--- Create a new UNIT from DCSUnit. +-- @param #UNIT self +-- @param #string UnitName The name of the DCS unit. +-- @return #UNIT self +function UNIT:Register( UnitName ) + + -- Inherit CONTROLLABLE. + local self = BASE:Inherit( self, CONTROLLABLE:New( UnitName ) ) + + -- Set unit name. + self.UnitName = UnitName + + -- Set event prio. + self:SetEventPriority( 3 ) + + return self +end + +-- Reference methods. + +--- Finds a UNIT from the _DATABASE using a DCSUnit object. +-- @param #UNIT self +-- @param DCS#Unit DCSUnit An existing DCS Unit object reference. +-- @return #UNIT self +function UNIT:Find( DCSUnit ) + if DCSUnit then + local UnitName = DCSUnit:getName() + local UnitFound = _DATABASE:FindUnit( UnitName ) + return UnitFound + end + return nil +end + +--- Find a UNIT in the _DATABASE using the name of an existing DCS Unit. +-- @param #UNIT self +-- @param #string UnitName The Unit Name. +-- @return #UNIT self +function UNIT:FindByName( UnitName ) + + local UnitFound = _DATABASE:FindUnit( UnitName ) + return UnitFound +end + +--- Return the name of the UNIT. +-- @param #UNIT self +-- @return #string The UNIT name. +function UNIT:Name() + + return self.UnitName +end + +--- Get the DCS unit object. +-- @param #UNIT self +-- @return DCS#Unit +function UNIT:GetDCSObject() + + local DCSUnit = Unit.getByName( self.UnitName ) + + if DCSUnit then + return DCSUnit + end + + return nil +end + + + + +--- Respawn the @{Wrapper.Unit} using a (tweaked) template of the parent Group. +-- +-- This function will: +-- +-- * Get the current position and heading of the group. +-- * When the unit is alive, it will tweak the template x, y and heading coordinates of the group and the embedded units to the current units positions. +-- * Then it will respawn the re-modelled group. +-- +-- @param #UNIT self +-- @param Core.Point#COORDINATE Coordinate The position where to Spawn the new Unit at. +-- @param #number Heading The heading of the unit respawn. +function UNIT:ReSpawnAt( Coordinate, Heading ) + + self:T( self:Name() ) + local SpawnGroupTemplate = UTILS.DeepCopy( _DATABASE:GetGroupTemplateFromUnitName( self:Name() ) ) + self:T( SpawnGroupTemplate ) + + local SpawnGroup = self:GetGroup() + self:T( { SpawnGroup = SpawnGroup } ) + + if SpawnGroup then + + local Vec3 = SpawnGroup:GetVec3() + SpawnGroupTemplate.x = Coordinate.x + SpawnGroupTemplate.y = Coordinate.z + + self:F( #SpawnGroupTemplate.units ) + for UnitID, UnitData in pairs( SpawnGroup:GetUnits() ) do + local GroupUnit = UnitData -- #UNIT + self:F( GroupUnit:GetName() ) + if GroupUnit:IsAlive() then + local GroupUnitVec3 = GroupUnit:GetVec3() + local GroupUnitHeading = GroupUnit:GetHeading() + SpawnGroupTemplate.units[UnitID].alt = GroupUnitVec3.y + SpawnGroupTemplate.units[UnitID].x = GroupUnitVec3.x + SpawnGroupTemplate.units[UnitID].y = GroupUnitVec3.z + SpawnGroupTemplate.units[UnitID].heading = GroupUnitHeading + self:F( { UnitID, SpawnGroupTemplate.units[UnitID], SpawnGroupTemplate.units[UnitID] } ) + end + end + end + + for UnitTemplateID, UnitTemplateData in pairs( SpawnGroupTemplate.units ) do + self:T( { UnitTemplateData.name, self:Name() } ) + SpawnGroupTemplate.units[UnitTemplateID].unitId = nil + if UnitTemplateData.name == self:Name() then + self:T("Adjusting") + SpawnGroupTemplate.units[UnitTemplateID].alt = Coordinate.y + SpawnGroupTemplate.units[UnitTemplateID].x = Coordinate.x + SpawnGroupTemplate.units[UnitTemplateID].y = Coordinate.z + SpawnGroupTemplate.units[UnitTemplateID].heading = Heading + self:F( { UnitTemplateID, SpawnGroupTemplate.units[UnitTemplateID], SpawnGroupTemplate.units[UnitTemplateID] } ) + else + self:F( SpawnGroupTemplate.units[UnitTemplateID].name ) + local GroupUnit = UNIT:FindByName( SpawnGroupTemplate.units[UnitTemplateID].name ) -- #UNIT + if GroupUnit and GroupUnit:IsAlive() then + local GroupUnitVec3 = GroupUnit:GetVec3() + local GroupUnitHeading = GroupUnit:GetHeading() + UnitTemplateData.alt = GroupUnitVec3.y + UnitTemplateData.x = GroupUnitVec3.x + UnitTemplateData.y = GroupUnitVec3.z + UnitTemplateData.heading = GroupUnitHeading + else + if SpawnGroupTemplate.units[UnitTemplateID].name ~= self:Name() then + self:T("nilling") + SpawnGroupTemplate.units[UnitTemplateID].delete = true + end + end + end + end + + -- Remove obscolete units from the group structure + local i = 1 + while i <= #SpawnGroupTemplate.units do + + local UnitTemplateData = SpawnGroupTemplate.units[i] + self:T( UnitTemplateData.name ) + + if UnitTemplateData.delete then + table.remove( SpawnGroupTemplate.units, i ) + else + i = i + 1 + end + end + + SpawnGroupTemplate.groupId = nil + + self:T( SpawnGroupTemplate ) + + _DATABASE:Spawn( SpawnGroupTemplate ) +end + + + +--- Returns if the unit is activated. +-- @param #UNIT self +-- @return #boolean `true` if Unit is activated. `nil` The DCS Unit is not existing or alive. +function UNIT:IsActive() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + + local UnitIsActive = DCSUnit:isActive() + return UnitIsActive + end + + return nil +end + +--- Returns if the Unit is alive. +-- If the Unit is not alive, nil is returned. +-- If the Unit is alive and active, true is returned. +-- If the Unit is alive but not active, false is returned. +-- @param #UNIT self +-- @return #boolean `true` if Unit is alive and active. `false` if Unit is alive but not active. `nil` if the Unit is not existing or is not alive. +function UNIT:IsAlive() + self:F3( self.UnitName ) + + local DCSUnit = self:GetDCSObject() -- DCS#Unit + + if DCSUnit then + local UnitIsAlive = DCSUnit:isExist() and DCSUnit:isActive() + return UnitIsAlive + end + + return nil +end + + + +--- Returns the Unit's callsign - the localized string. +-- @param #UNIT self +-- @return #string The Callsign of the Unit. +function UNIT:GetCallsign() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitCallSign = DCSUnit:getCallsign() + if UnitCallSign == "" then + UnitCallSign = DCSUnit:getName() + end + return UnitCallSign + end + + self:F( self.ClassName .. " " .. self.UnitName .. " not found!" ) + return nil +end + +--- Check if an (air) unit is a client or player slot. Information is retrieved from the group template. +-- @param #UNIT self +-- @return #boolean If true, unit is associated with a client or player slot. +function UNIT:IsPlayer() + + -- Get group. + local group=self:GetGroup() + + -- Units of template group. + local units=group:GetTemplate().units + + -- Get numbers. + for _,unit in pairs(units) do + + -- Check if unit name matach and skill is Client or Player. + if unit.name==self:GetName() and (unit.skill=="Client" or unit.skill=="Player") then + return true + end + + end + + return false +end + + +--- Returns name of the player that control the unit or nil if the unit is controlled by A.I. +-- @param #UNIT self +-- @return #string Player Name +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetPlayerName() + self:F( self.UnitName ) + + local DCSUnit = self:GetDCSObject() -- DCS#Unit + + if DCSUnit then + + local PlayerName = DCSUnit:getPlayerName() + -- TODO Workaround DCS-BUG-3 - https://github.com/FlightControl-Master/MOOSE/issues/696 +-- if PlayerName == nil or PlayerName == "" then +-- local PlayerCategory = DCSUnit:getDesc().category +-- if PlayerCategory == Unit.Category.GROUND_UNIT or PlayerCategory == Unit.Category.SHIP then +-- PlayerName = "Player" .. DCSUnit:getID() +-- end +-- end +-- -- Good code +-- if PlayerName == nil then +-- PlayerName = nil +-- else +-- if PlayerName == "" then +-- PlayerName = "Player" .. DCSUnit:getID() +-- end +-- end + return PlayerName + end + + return nil + +end + +--- Checks is the unit is a *Player* or *Client* slot. +-- @param #UNIT self +-- @return #boolean If true, unit is a player or client aircraft +function UNIT:IsClient() + + if _DATABASE.CLIENTS[self.UnitName] then + return true + end + + return false +end + +--- Get the CLIENT of the unit +-- @param #UNIT self +-- @return Wrapper.Client#CLIENT +function UNIT:GetClient() + + local client=_DATABASE.CLIENTS[self.UnitName] + + if client then + return client + end + + return nil +end + +--- Returns the unit's number in the group. +-- The number is the same number the unit has in ME. +-- It may not be changed during the mission. +-- If any unit in the group is destroyed, the numbers of another units will not be changed. +-- @param #UNIT self +-- @return #number The Unit number. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetNumber() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitNumber = DCSUnit:getNumber() + return UnitNumber + end + + return nil +end + + +--- Returns the unit's max speed in km/h derived from the DCS descriptors. +-- @param #UNIT self +-- @return #number Speed in km/h. +function UNIT:GetSpeedMax() + self:F2( self.UnitName ) + + local Desc = self:GetDesc() + + if Desc then + local SpeedMax = Desc.speedMax + return SpeedMax*3.6 + end + + return nil +end + +--- Returns the unit's max range in meters derived from the DCS descriptors. +-- For ground units it will return a range of 10,000 km as they have no real range. +-- @param #UNIT self +-- @return #number Range in meters. +function UNIT:GetRange() + self:F2( self.UnitName ) + + local Desc = self:GetDesc() + + if Desc then + local Range = Desc.range --This is in kilometers (not meters) for some reason. But should check again! + if Range then + Range=Range*1000 -- convert to meters. + else + Range=10000000 --10.000 km if no range + end + return Range + end + + return nil +end + +--- Check if the unit is refuelable. Also retrieves the refuelling system (boom or probe) if applicable. +-- @param #UNIT self +-- @return #boolean If true, unit is refuelable (checks for the attribute "Refuelable"). +-- @return #number Refueling system (if any): 0=boom, 1=probe. +function UNIT:IsRefuelable() + self:F2( self.UnitName ) + + local refuelable=self:HasAttribute("Refuelable") + + local system=nil + + local Desc=self:GetDesc() + if Desc and Desc.tankerType then + system=Desc.tankerType + end + + return refuelable, system +end + +--- Check if the unit is a tanker. Also retrieves the refuelling system (boom or probe) if applicable. +-- @param #UNIT self +-- @return #boolean If true, unit is refuelable (checks for the attribute "Refuelable"). +-- @return #number Refueling system (if any): 0=boom, 1=probe. +function UNIT:IsTanker() + self:F2( self.UnitName ) + + local tanker=self:HasAttribute("Tankers") + + local system=nil + + if tanker then + + local Desc=self:GetDesc() + if Desc and Desc.tankerType then + system=Desc.tankerType + end + + local typename=self:GetTypeName() + + -- Some hard coded data as this is not in the descriptors... + if typename=="IL-78M" then + system=1 --probe + elseif typename=="KC130" then + system=1 --probe + elseif typename=="KC135BDA" then + system=1 --probe + elseif typename=="KC135MPRS" then + system=1 --probe + elseif typename=="S-3B Tanker" then + system=1 --probe + end + + end + + return tanker, system +end + + +--- Returns the unit's group if it exist and nil otherwise. +-- @param Wrapper.Unit#UNIT self +-- @return Wrapper.Group#GROUP The Group of the Unit or `nil` if the unit does not exist. +function UNIT:GetGroup() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitGroup = GROUP:FindByName( DCSUnit:getGroup():getName() ) + return UnitGroup + end + + return nil +end + + +-- Need to add here functions to check if radar is on and which object etc. + +--- Returns the prefix name of the DCS Unit. A prefix name is a part of the name before a '#'-sign. +-- DCS Units spawned with the @{SPAWN} class contain a '#'-sign to indicate the end of the (base) DCS Unit name. +-- The spawn sequence number and unit number are contained within the name after the '#' sign. +-- @param #UNIT self +-- @return #string The name of the DCS Unit. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetPrefix() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitPrefix = string.match( self.UnitName, ".*#" ):sub( 1, -2 ) + self:T3( UnitPrefix ) + return UnitPrefix + end + + return nil +end + +--- Returns the Unit's ammunition. +-- @param #UNIT self +-- @return DCS#Unit.Ammo Table with ammuntion of the unit (or nil). This can be a complex table! +function UNIT:GetAmmo() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitAmmo = DCSUnit:getAmmo() + return UnitAmmo + end + + return nil +end + +--- Sets the Unit's Internal Cargo Mass, in kg +-- @param #UNIT self +-- @param #number mass to set cargo to +-- @return #UNIT self +function UNIT:SetUnitInternalCargo(mass) + local DCSUnit = self:GetDCSObject() + if DCSUnit then + trigger.action.setUnitInternalCargo(DCSUnit:getName(), mass) + end + return self +end + +--- Get the number of ammunition and in particular the number of shells, rockets, bombs and missiles a unit currently has. +-- @param #UNIT self +-- @return #number Total amount of ammo the unit has left. This is the sum of shells, rockets, bombs and missiles. +-- @return #number Number of shells left. +-- @return #number Number of rockets left. +-- @return #number Number of bombs left. +-- @return #number Number of missiles left. +function UNIT:GetAmmunition() + + -- Init counter. + local nammo=0 + local nshells=0 + local nrockets=0 + local nmissiles=0 + local nbombs=0 + + local unit=self + + -- Get ammo table. + local ammotable=unit:GetAmmo() + + if ammotable then + + local weapons=#ammotable + + -- Loop over all weapons. + for w=1,weapons do + + -- Number of current weapon. + local Nammo=ammotable[w]["count"] + + -- Type name of current weapon. + local Tammo=ammotable[w]["desc"]["typeName"] + + local _weaponString = UTILS.Split(Tammo,"%.") + local _weaponName = _weaponString[#_weaponString] + + -- Get the weapon category: shell=0, missile=1, rocket=2, bomb=3 + local Category=ammotable[w].desc.category + + -- Get missile category: Weapon.MissileCategory AAM=1, SAM=2, BM=3, ANTI_SHIP=4, CRUISE=5, OTHER=6 + local MissileCategory=nil + if Category==Weapon.Category.MISSILE then + MissileCategory=ammotable[w].desc.missileCategory + end + + -- We are specifically looking for shells or rockets here. + if Category==Weapon.Category.SHELL then + + -- Add up all shells. + nshells=nshells+Nammo + + elseif Category==Weapon.Category.ROCKET then + + -- Add up all rockets. + nrockets=nrockets+Nammo + + elseif Category==Weapon.Category.BOMB then + + -- Add up all rockets. + nbombs=nbombs+Nammo + + elseif Category==Weapon.Category.MISSILE then + + -- Add up all cruise missiles (category 5) + if MissileCategory==Weapon.MissileCategory.AAM then + nmissiles=nmissiles+Nammo + elseif MissileCategory==Weapon.MissileCategory.ANTI_SHIP then + nmissiles=nmissiles+Nammo + elseif MissileCategory==Weapon.MissileCategory.BM then + nmissiles=nmissiles+Nammo + elseif MissileCategory==Weapon.MissileCategory.OTHER then + nmissiles=nmissiles+Nammo + end + + end + + end + end + + -- Total amount of ammunition. + nammo=nshells+nrockets+nmissiles+nbombs + + return nammo, nshells, nrockets, nbombs, nmissiles +end + + + +--- Returns the unit sensors. +-- @param #UNIT self +-- @return DCS#Unit.Sensors Table of sensors. +function UNIT:GetSensors() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitSensors = DCSUnit:getSensors() + return UnitSensors + end + + return nil +end + +-- Need to add here a function per sensortype +-- unit:hasSensors(Unit.SensorType.RADAR, Unit.RadarType.AS) + +--- Returns if the unit has sensors of a certain type. +-- @param #UNIT self +-- @return #boolean returns true if the unit has specified types of sensors. This function is more preferable than Unit.getSensors() if you don't want to get information about all the unit's sensors, and just want to check if the unit has specified types of sensors. +function UNIT:HasSensors( ... ) + self:F2( arg ) + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local HasSensors = DCSUnit:hasSensors( unpack( arg ) ) + return HasSensors + end + + return nil +end + +--- Returns if the unit is SEADable. +-- @param #UNIT self +-- @return #boolean returns true if the unit is SEADable. +function UNIT:HasSEAD() + self:F2() + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitSEADAttributes = DCSUnit:getDesc().attributes + + local HasSEAD = false + if UnitSEADAttributes["RADAR_BAND1_FOR_ARM"] and UnitSEADAttributes["RADAR_BAND1_FOR_ARM"] == true or + UnitSEADAttributes["RADAR_BAND2_FOR_ARM"] and UnitSEADAttributes["RADAR_BAND2_FOR_ARM"] == true then + HasSEAD = true + end + return HasSEAD + end + + return nil +end + +--- Returns two values: +-- +-- * First value indicates if at least one of the unit's radar(s) is on. +-- * Second value is the object of the radar's interest. Not nil only if at least one radar of the unit is tracking a target. +-- @param #UNIT self +-- @return #boolean Indicates if at least one of the unit's radar(s) is on. +-- @return DCS#Object The object of the radar's interest. Not nil only if at least one radar of the unit is tracking a target. +function UNIT:GetRadar() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitRadarOn, UnitRadarObject = DCSUnit:getRadar() + return UnitRadarOn, UnitRadarObject + end + + return nil, nil +end + +--- Returns relative amount of fuel (from 0.0 to 1.0) the UNIT has in its internal tanks. If there are additional fuel tanks the value may be greater than 1.0. +-- @param #UNIT self +-- @return #number The relative amount of fuel (from 0.0 to 1.0) or *nil* if the DCS Unit is not existing or alive. +function UNIT:GetFuel() + self:F3( self.UnitName ) + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitFuel = DCSUnit:getFuel() + return UnitFuel + end + + return nil +end + + +--- Returns a list of one @{Wrapper.Unit}. +-- @param #UNIT self +-- @return #list A list of one @{Wrapper.Unit}. +function UNIT:GetUnits() + self:F3( { self.UnitName } ) + local DCSUnit = self:GetDCSObject() + + local Units = {} + + if DCSUnit then + Units[1] = UNIT:Find( DCSUnit ) + self:T3( Units ) + return Units + end + + return nil +end + + +--- Returns the unit's health. Dead units has health <= 1.0. +-- @param #UNIT self +-- @return #number The Unit's health value or -1 if unit does not exist any more. +function UNIT:GetLife() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitLife = DCSUnit:getLife() + return UnitLife + end + + return -1 +end + +--- Returns the Unit's initial health. +-- @param #UNIT self +-- @return #number The Unit's initial health value or 0 if unit does not exist any more. +function UNIT:GetLife0() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitLife0 = DCSUnit:getLife0() + return UnitLife0 + end + + return 0 +end + +--- Returns the unit's relative health. +-- @param #UNIT self +-- @return #number The Unit's relative health value, i.e. a number in [0,1] or -1 if unit does not exist any more. +function UNIT:GetLifeRelative() + self:F2(self.UnitName) + + if self and self:IsAlive() then + local life0=self:GetLife0() + local lifeN=self:GetLife() + return lifeN/life0 + end + + return -1 +end + +--- Returns the unit's relative damage, i.e. 1-life. +-- @param #UNIT self +-- @return #number The Unit's relative health value, i.e. a number in [0,1] or 1 if unit does not exist any more. +function UNIT:GetDamageRelative() + self:F2(self.UnitName) + + if self and self:IsAlive() then + return 1-self:GetLifeRelative() + end + + return 1 +end + +--- Returns the category of the #UNIT from descriptor. Returns one of +-- +-- * Unit.Category.AIRPLANE +-- * Unit.Category.HELICOPTER +-- * Unit.Category.GROUND_UNIT +-- * Unit.Category.SHIP +-- * Unit.Category.STRUCTURE +-- +-- @param #UNIT self +-- @return #number Unit category from `getDesc().category`. +function UNIT:GetUnitCategory() + self:F3( self.UnitName ) + + local DCSUnit = self:GetDCSObject() + if DCSUnit then + return DCSUnit:getDesc().category + end + + return nil +end + +--- Returns the category name of the #UNIT. +-- @param #UNIT self +-- @return #string Category name = Helicopter, Airplane, Ground Unit, Ship +function UNIT:GetCategoryName() + self:F3( self.UnitName ) + + local DCSUnit = self:GetDCSObject() + if DCSUnit then + local CategoryNames = { + [Unit.Category.AIRPLANE] = "Airplane", + [Unit.Category.HELICOPTER] = "Helicopter", + [Unit.Category.GROUND_UNIT] = "Ground Unit", + [Unit.Category.SHIP] = "Ship", + [Unit.Category.STRUCTURE] = "Structure", + } + local UnitCategory = DCSUnit:getDesc().category + self:T3( UnitCategory ) + + return CategoryNames[UnitCategory] + end + + return nil +end + + +--- Returns the Unit's A2G threat level on a scale from 1 to 10 ... +-- Depending on the era and the type of unit, the following threat levels are foreseen: +-- +-- **Modern**: +-- +-- * Threat level 0: Unit is unarmed. +-- * Threat level 1: Unit is infantry. +-- * Threat level 2: Unit is an infantry vehicle. +-- * Threat level 3: Unit is ground artillery. +-- * Threat level 4: Unit is a tank. +-- * Threat level 5: Unit is a modern tank or ifv with ATGM. +-- * Threat level 6: Unit is a AAA. +-- * Threat level 7: Unit is a SAM or manpad, IR guided. +-- * Threat level 8: Unit is a Short Range SAM, radar guided. +-- * Threat level 9: Unit is a Medium Range SAM, radar guided. +-- * Threat level 10: Unit is a Long Range SAM, radar guided. +-- +-- **Cold**: +-- +-- * Threat level 0: Unit is unarmed. +-- * Threat level 1: Unit is infantry. +-- * Threat level 2: Unit is an infantry vehicle. +-- * Threat level 3: Unit is ground artillery. +-- * Threat level 4: Unit is a tank. +-- * Threat level 5: Unit is a modern tank or ifv with ATGM. +-- * Threat level 6: Unit is a AAA. +-- * Threat level 7: Unit is a SAM or manpad, IR guided. +-- * Threat level 8: Unit is a Short Range SAM, radar guided. +-- * Threat level 10: Unit is a Medium Range SAM, radar guided. +-- +-- **Korea**: +-- +-- * Threat level 0: Unit is unarmed. +-- * Threat level 1: Unit is infantry. +-- * Threat level 2: Unit is an infantry vehicle. +-- * Threat level 3: Unit is ground artillery. +-- * Threat level 5: Unit is a tank. +-- * Threat level 6: Unit is a AAA. +-- * Threat level 7: Unit is a SAM or manpad, IR guided. +-- * Threat level 10: Unit is a Short Range SAM, radar guided. +-- +-- **WWII**: +-- +-- * Threat level 0: Unit is unarmed. +-- * Threat level 1: Unit is infantry. +-- * Threat level 2: Unit is an infantry vehicle. +-- * Threat level 3: Unit is ground artillery. +-- * Threat level 5: Unit is a tank. +-- * Threat level 7: Unit is FLAK. +-- * Threat level 10: Unit is AAA. +-- +-- +-- @param #UNIT self +-- @return #number Number between 0 (low threat level) and 10 (high threat level). +-- @return #string Some text. +function UNIT:GetThreatLevel() + + + local ThreatLevel = 0 + local ThreatText = "" + + local Descriptor = self:GetDesc() + + if Descriptor then + + local Attributes = Descriptor.attributes + + if self:IsGround() then + + local ThreatLevels = { + "Unarmed", + "Infantry", + "Old Tanks & APCs", + "Tanks & IFVs without ATGM", + "Tanks & IFV with ATGM", + "Modern Tanks", + "AAA", + "IR Guided SAMs", + "SR SAMs", + "MR SAMs", + "LR SAMs" + } + + + if Attributes["LR SAM"] then ThreatLevel = 10 + elseif Attributes["MR SAM"] then ThreatLevel = 9 + elseif Attributes["SR SAM"] and + not Attributes["IR Guided SAM"] then ThreatLevel = 8 + elseif ( Attributes["SR SAM"] or Attributes["MANPADS"] ) and + Attributes["IR Guided SAM"] then ThreatLevel = 7 + elseif Attributes["AAA"] then ThreatLevel = 6 + elseif Attributes["Modern Tanks"] then ThreatLevel = 5 + elseif ( Attributes["Tanks"] or Attributes["IFV"] ) and + Attributes["ATGM"] then ThreatLevel = 4 + elseif ( Attributes["Tanks"] or Attributes["IFV"] ) and + not Attributes["ATGM"] then ThreatLevel = 3 + elseif Attributes["Old Tanks"] or Attributes["APC"] or Attributes["Artillery"] then ThreatLevel = 2 + elseif Attributes["Infantry"] then ThreatLevel = 1 + end + + ThreatText = ThreatLevels[ThreatLevel+1] + end + + if self:IsAir() then + + local ThreatLevels = { + "Unarmed", + "Tanker", + "AWACS", + "Transport Helicopter", + "UAV", + "Bomber", + "Strategic Bomber", + "Attack Helicopter", + "Battleplane", + "Multirole Fighter", + "Fighter" + } + + + if Attributes["Fighters"] then ThreatLevel = 10 + elseif Attributes["Multirole fighters"] then ThreatLevel = 9 + elseif Attributes["Battleplanes"] then ThreatLevel = 8 + elseif Attributes["Attack helicopters"] then ThreatLevel = 7 + elseif Attributes["Strategic bombers"] then ThreatLevel = 6 + elseif Attributes["Bombers"] then ThreatLevel = 5 + elseif Attributes["UAVs"] then ThreatLevel = 4 + elseif Attributes["Transport helicopters"] then ThreatLevel = 3 + elseif Attributes["AWACS"] then ThreatLevel = 2 + elseif Attributes["Tankers"] then ThreatLevel = 1 + end + + ThreatText = ThreatLevels[ThreatLevel+1] + end + + if self:IsShip() then + + --["Aircraft Carriers"] = {"Heavy armed ships",}, + --["Cruisers"] = {"Heavy armed ships",}, + --["Destroyers"] = {"Heavy armed ships",}, + --["Frigates"] = {"Heavy armed ships",}, + --["Corvettes"] = {"Heavy armed ships",}, + --["Heavy armed ships"] = {"Armed ships", "Armed Air Defence", "HeavyArmoredUnits",}, + --["Light armed ships"] = {"Armed ships","NonArmoredUnits"}, + --["Armed ships"] = {"Ships"}, + --["Unarmed ships"] = {"Ships","HeavyArmoredUnits",}, + + local ThreatLevels = { + "Unarmed ship", + "Light armed ships", + "Corvettes", + "", + "Frigates", + "", + "Cruiser", + "", + "Destroyer", + "", + "Aircraft Carrier" + } + + + if Attributes["Aircraft Carriers"] then ThreatLevel = 10 + elseif Attributes["Destroyers"] then ThreatLevel = 8 + elseif Attributes["Cruisers"] then ThreatLevel = 6 + elseif Attributes["Frigates"] then ThreatLevel = 4 + elseif Attributes["Corvettes"] then ThreatLevel = 2 + elseif Attributes["Light armed ships"] then ThreatLevel = 1 + end + + ThreatText = ThreatLevels[ThreatLevel+1] + end + end + + return ThreatLevel, ThreatText + +end + +--- Triggers an explosion at the coordinates of the unit. +-- @param #UNIT self +-- @param #number power Power of the explosion in kg TNT. Default 100 kg TNT. +-- @param #number delay (Optional) Delay of explosion in seconds. +-- @return #UNIT self +function UNIT:Explode(power, delay) + + -- Default. + power=power or 100 + + local DCSUnit = self:GetDCSObject() + if DCSUnit then + + -- Check if delay or not. + if delay and delay>0 then + -- Delayed call. + SCHEDULER:New(nil, self.Explode, {self, power}, delay) + else + -- Create an explotion at the coordinate of the unit. + self:GetCoordinate():Explosion(power) + end + + return self + end + + return nil +end + +-- Is functions + + + +--- Returns true if there is an **other** DCS Unit within a radius of the current 2D point of the DCS Unit. +-- @param #UNIT self +-- @param #UNIT AwaitUnit The other UNIT wrapper object. +-- @param Radius The radius in meters with the DCS Unit in the centre. +-- @return true If the other DCS Unit is within the radius of the 2D point of the DCS Unit. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:OtherUnitInRadius( AwaitUnit, Radius ) + self:F2( { self.UnitName, AwaitUnit.UnitName, Radius } ) + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitVec3 = self:GetVec3() + local AwaitUnitVec3 = AwaitUnit:GetVec3() + + if (((UnitVec3.x - AwaitUnitVec3.x)^2 + (UnitVec3.z - AwaitUnitVec3.z)^2)^0.5 <= Radius) then + self:T3( "true" ) + return true + else + self:T3( "false" ) + return false + end + end + + return nil +end + + + + + + + +--- Returns if the unit is a friendly unit. +-- @param #UNIT self +-- @return #boolean IsFriendly evaluation result. +function UNIT:IsFriendly( FriendlyCoalition ) + self:F2() + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitCoalition = DCSUnit:getCoalition() + self:T3( { UnitCoalition, FriendlyCoalition } ) + + local IsFriendlyResult = ( UnitCoalition == FriendlyCoalition ) + + self:F( IsFriendlyResult ) + return IsFriendlyResult + end + + return nil +end + +--- Returns if the unit is of a ship category. +-- If the unit is a ship, this method will return true, otherwise false. +-- @param #UNIT self +-- @return #boolean Ship category evaluation result. +function UNIT:IsShip() + self:F2() + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitDescriptor = DCSUnit:getDesc() + self:T3( { UnitDescriptor.category, Unit.Category.SHIP } ) + + local IsShipResult = ( UnitDescriptor.category == Unit.Category.SHIP ) + + self:T3( IsShipResult ) + return IsShipResult + end + + return nil +end + +--- Returns true if the UNIT is in the air. +-- @param #UNIT self +-- @param #boolean NoHeloCheck If true, no additonal checks for helos are performed. +-- @return #boolean Return true if in the air or #nil if the UNIT is not existing or alive. +function UNIT:InAir(NoHeloCheck) + self:F2( self.UnitName ) + + -- Get DCS unit object. + local DCSUnit = self:GetDCSObject() --DCS#Unit + + if DCSUnit then + + -- Get DCS result of whether unit is in air or not. + local UnitInAir = DCSUnit:inAir() + + -- Get unit category. + local UnitCategory = DCSUnit:getDesc().category + + -- If DCS says that it is in air, check if this is really the case, since we might have landed on a building where inAir()=true but actually is not. + -- This is a workaround since DCS currently does not acknoledge that helos land on buildings. + -- Note however, that the velocity check will fail if the ground is moving, e.g. on an aircraft carrier! + if UnitInAir==true and UnitCategory == Unit.Category.HELICOPTER and (not NoHeloCheck) then + local VelocityVec3 = DCSUnit:getVelocity() + local Velocity = UTILS.VecNorm(VelocityVec3) + local Coordinate = DCSUnit:getPoint() + local LandHeight = land.getHeight( { x = Coordinate.x, y = Coordinate.z } ) + local Height = Coordinate.y - LandHeight + if Velocity < 1 and Height <= 60 then + UnitInAir = false + end + end + + self:T3( UnitInAir ) + return UnitInAir + end + + return nil +end + +do -- Event Handling + + --- Subscribe to a DCS Event. + -- @param #UNIT self + -- @param Core.Event#EVENTS EventID Event ID. + -- @param #function EventFunction (Optional) The function to be called when the event occurs for the unit. + -- @return #UNIT self + function UNIT:HandleEvent(EventID, EventFunction) + + self:EventDispatcher():OnEventForUnit(self:GetName(), EventFunction, self, EventID) + + return self + end + + --- UnSubscribe to a DCS event. + -- @param #UNIT self + -- @param Core.Event#EVENTS EventID Event ID. + -- @return #UNIT self + function UNIT:UnHandleEvent(EventID) + + --self:EventDispatcher():RemoveForUnit( self:GetName(), self, EventID ) + + -- Fixes issue #1365 https://github.com/FlightControl-Master/MOOSE/issues/1365 + self:EventDispatcher():RemoveEvent(self, EventID) + + return self + end + + --- Reset the subscriptions. + -- @param #UNIT self + -- @return #UNIT + function UNIT:ResetEvents() + + self:EventDispatcher():Reset( self ) + + return self + end + +end + +do -- Detection + + --- Returns if a unit is detecting the TargetUnit. + -- @param #UNIT self + -- @param #UNIT TargetUnit + -- @return #boolean true If the TargetUnit is detected by the unit, otherwise false. + function UNIT:IsDetected( TargetUnit ) --R2.1 + + local TargetIsDetected, TargetIsVisible, TargetLastTime, TargetKnowType, TargetKnowDistance, TargetLastPos, TargetLastVelocity = self:IsTargetDetected( TargetUnit:GetDCSObject() ) + + return TargetIsDetected + end + + --- Returns if a unit has Line of Sight (LOS) with the TargetUnit. + -- @param #UNIT self + -- @param #UNIT TargetUnit + -- @return #boolean true If the TargetUnit has LOS with the unit, otherwise false. + function UNIT:IsLOS( TargetUnit ) --R2.1 + + local IsLOS = self:GetPointVec3():IsLOS( TargetUnit:GetPointVec3() ) + + return IsLOS + end + + --- Forces the unit to become aware of the specified target, without the unit manually detecting the other unit itself. + -- Applies only to a Unit Controller. Cannot be used at the group level. + -- @param #UNIT self + -- @param #UNIT TargetUnit The unit to be known. + -- @param #boolean TypeKnown The target type is known. If *false*, the type is not known. + -- @param #boolean DistanceKnown The distance to the target is known. If *false*, distance is unknown. + function UNIT:KnowUnit(TargetUnit, TypeKnown, DistanceKnown) + + -- Defaults. + if TypeKnown~=false then + TypeKnown=true + end + if DistanceKnown~=false then + DistanceKnown=true + end + + local DCSControllable = self:GetDCSObject() + + if DCSControllable then + + local Controller = DCSControllable:getController() --self:_GetController() + + if Controller then + + local object=TargetUnit:GetDCSObject() + + if object then + + self:I(string.format("Unit %s now knows target unit %s. Type known=%s, distance known=%s", self:GetName(), TargetUnit:GetName(), tostring(TypeKnown), tostring(DistanceKnown))) + + Controller:knowTarget(object, TypeKnown, DistanceKnown) + + end + + end + + end + + end + +end + +--- Get the unit table from a unit's template. +-- @param #UNIT self +-- @return #table Table of the unit template (deep copy) or #nil. +function UNIT:GetTemplate() + + local group=self:GetGroup() + + local name=self:GetName() + + if group then + local template=group:GetTemplate() + + if template then + + for _,unit in pairs(template.units) do + + if unit.name==name then + return UTILS.DeepCopy(unit) + end + end + + end + end + + return nil +end + + +--- Get the payload table from a unit's template. +-- The payload table has elements: +-- +-- * pylons +-- * fuel +-- * chaff +-- * gun +-- +-- @param #UNIT self +-- @return #table Payload table (deep copy) or #nil. +function UNIT:GetTemplatePayload() + + local unit=self:GetTemplate() + + if unit then + return unit.payload + end + + return nil +end + +--- Get the pylons table from a unit's template. This can be a complex table depending on the weapons the unit is carrying. +-- @param #UNIT self +-- @return #table Table of pylons (deepcopy) or #nil. +function UNIT:GetTemplatePylons() + + local payload=self:GetTemplatePayload() + + if payload then + return payload.pylons + end + + return nil +end + +--- Get the fuel of the unit from its template. +-- @param #UNIT self +-- @return #number Fuel of unit in kg. +function UNIT:GetTemplateFuel() + + local payload=self:GetTemplatePayload() + + if payload then + return payload.fuel + end + + return nil +end + +--- GROUND - Switch on/off radar emissions of a unit. +-- @param #UNIT self +-- @param #boolean switch If true, emission is enabled. If false, emission is disabled. +-- @return #UNIT self +function UNIT:EnableEmission(switch) + self:F2( self.UnitName ) + + local switch = switch or false + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + + DCSUnit:enableEmission(switch) + + end + + return self +end + +--- Get skill from Unit. +-- @param #UNIT self +-- @return #string Skill String of skill name. +function UNIT:GetSkill() + self:F2( self.UnitName ) + local name = self.UnitName + local skill = _DATABASE.Templates.Units[name].Template.skill or "Random" + return skill +end +--- **Wrapper** -- CLIENT wraps DCS Unit objects acting as a __Client__ or __Player__ within a mission. +-- +-- === +-- +-- ### Author: **FlightControl** +-- ### Contributions: **funkyfranky** +-- +-- === +-- +-- @module Wrapper.Client +-- @image Wrapper_Client.JPG + + +--- The CLIENT class +-- @type CLIENT +-- @extends Wrapper.Unit#UNIT + + +--- Wrapper class of those **Units** defined within the Mission Editor that have the skillset defined as __Client__ or __Player__. +-- +-- Note that clients are NOT the same as Units, they are NOT necessarily alive. +-- The CLIENT class is a wrapper class to handle the DCS Unit objects that have the skillset defined as __Client__ or __Player__: +-- +-- * Wraps the DCS Unit objects with skill level set to Player or Client. +-- * Support all DCS Unit APIs. +-- * Enhance with Unit specific APIs not in the DCS Group API set. +-- * When player joins Unit, execute alive init logic. +-- * Handles messages to players. +-- * Manage the "state" of the DCS Unit. +-- +-- Clients are being used by the @{MISSION} class to follow players and register their successes. +-- +-- ## CLIENT reference methods +-- +-- For each DCS Unit having skill level Player or Client, a CLIENT wrapper object (instance) will be created within the _@{DATABASE} object. +-- This is done at the beginning of the mission (when the mission starts). +-- +-- The CLIENT class does not contain a :New() method, rather it provides :Find() methods to retrieve the object reference +-- using the DCS Unit or the DCS UnitName. +-- +-- Another thing to know is that CLIENT objects do not "contain" the DCS Unit object. +-- The CLIENT methods will reference the DCS Unit object by name when it is needed during API execution. +-- If the DCS Unit object does not exist or is nil, the CLIENT methods will return nil and log an exception in the DCS.log file. +-- +-- The CLIENT class provides the following functions to retrieve quickly the relevant CLIENT instance: +-- +-- * @{#CLIENT.Find}(): Find a CLIENT instance from the _DATABASE object using a DCS Unit object. +-- * @{#CLIENT.FindByName}(): Find a CLIENT instance from the _DATABASE object using a DCS Unit name. +-- +-- **IMPORTANT: ONE SHOULD NEVER SANATIZE these CLIENT OBJECT REFERENCES! (make the CLIENT object references nil).** +-- +-- @field #CLIENT +CLIENT = { + ClassName = "CLIENT", + ClientName = nil, + ClientAlive = false, + ClientTransport = false, + ClientBriefingShown = false, + _Menus = {}, + _Tasks = {}, + Messages = {}, + Players = {}, +} + + +--- Finds a CLIENT from the _DATABASE using the relevant DCS Unit. +-- @param #CLIENT self +-- @param DCS#Unit DCSUnit The DCS unit of the client. +-- @param #boolean Error Throw an error message. +-- @return #CLIENT The CLIENT found in the _DATABASE. +function CLIENT:Find(DCSUnit, Error) + + local ClientName = DCSUnit:getName() + + local ClientFound = _DATABASE:FindClient( ClientName ) + + if ClientFound then + ClientFound:F( ClientName ) + return ClientFound + end + + if not Error then + error( "CLIENT not found for: " .. ClientName ) + end +end + + +--- Finds a CLIENT from the _DATABASE using the relevant Client Unit Name. +-- As an optional parameter, a briefing text can be given also. +-- @param #CLIENT self +-- @param #string ClientName Name of the DCS **Unit** as defined within the Mission Editor. +-- @param #string ClientBriefing Text that describes the briefing of the mission when a Player logs into the Client. +-- @param #boolean Error A flag that indicates whether an error should be raised if the CLIENT cannot be found. By default an error will be raised. +-- @return #CLIENT +-- @usage +-- -- Create new Clients. +-- local Mission = MISSIONSCHEDULER.AddMission( 'Russia Transport Troops SA-6', 'Operational', 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', 'Russia' ) +-- Mission:AddGoal( DeploySA6TroopsGoal ) +-- +-- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 1' ):Transport() ) +-- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 3' ):Transport() ) +-- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 2' ):Transport() ) +-- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 4' ):Transport() ) +function CLIENT:FindByName( ClientName, ClientBriefing, Error ) + + -- Client + local ClientFound = _DATABASE:FindClient( ClientName ) + + if ClientFound then + ClientFound:F( { ClientName, ClientBriefing } ) + + ClientFound:AddBriefing(ClientBriefing) + + ClientFound.MessageSwitch = true + + return ClientFound + end + + if not Error then + error( "CLIENT not found for: " .. ClientName ) + end +end + +--- Transport defines that the Client is a Transport. Transports show cargo. +-- @param #CLIENT self +-- @param #string ClientName Name of the client unit. +-- @return #CLIENT self +function CLIENT:Register(ClientName) + + -- Inherit unit. + local self = BASE:Inherit( self, UNIT:Register(ClientName )) -- #CLIENT + + -- Set client name. + self.ClientName = ClientName + + -- Message switch. + self.MessageSwitch = true + + -- Alive2. + self.ClientAlive2 = false + + return self +end + + +--- Transport defines that the Client is a Transport. Transports show cargo. +-- @param #CLIENT self +-- @return #CLIENT self +function CLIENT:Transport() + self.ClientTransport = true + return self +end + +--- Adds a briefing to a CLIENT when a player joins a mission. +-- @param #CLIENT self +-- @param #string ClientBriefing is the text defining the Mission briefing. +-- @return #CLIENT self +function CLIENT:AddBriefing( ClientBriefing ) + + self.ClientBriefing = ClientBriefing + self.ClientBriefingShown = false + + return self +end + +--- Add player name. +-- @param #CLIENT self +-- @param #string PlayerName Name of the player. +-- @return #CLIENT self +function CLIENT:AddPlayer(PlayerName) + + table.insert(self.Players, PlayerName) + + return self +end + +--- Get player name(s). +-- @param #CLIENT self +-- @return #table List of player names or an empty table `{}`. +function CLIENT:GetPlayers() + return self.Players +end + +--- Get name of player. +-- @param #CLIENT self +-- @return #string Player name or `nil`. +function CLIENT:GetPlayer() + if #self.Players>0 then + return self.Players[1] + end + return nil +end + +--- Remove player. +-- @param #CLIENT self +-- @param #string PlayerName Name of the player. +-- @return #CLIENT self +function CLIENT:RemovePlayer(PlayerName) + + for i,playername in pairs(self.Players) do + if PlayerName==playername then + table.remove(self.Players, i) + break + end + end + + return self +end + +--- Remove all players. +-- @param #CLIENT self +-- @return #CLIENT self +function CLIENT:RemovePlayers() + self.Players={} + return self +end + + +--- Show the briefing of a CLIENT. +-- @param #CLIENT self +-- @return #CLIENT self +function CLIENT:ShowBriefing() + + if not self.ClientBriefingShown then + self.ClientBriefingShown = true + local Briefing = "" + if self.ClientBriefing and self.ClientBriefing ~= "" then + Briefing = Briefing .. self.ClientBriefing + self:Message( Briefing, 60, "Briefing" ) + end + end + + return self +end + +--- Show the mission briefing of a MISSION to the CLIENT. +-- @param #CLIENT self +-- @param #string MissionBriefing +-- @return #CLIENT self +function CLIENT:ShowMissionBriefing( MissionBriefing ) + self:F( { self.ClientName } ) + + if MissionBriefing then + self:Message( MissionBriefing, 60, "Mission Briefing" ) + end + + return self +end + +--- Resets a CLIENT. +-- @param #CLIENT self +-- @param #string ClientName Name of the Group as defined within the Mission Editor. The Group must have a Unit with the type Client. +function CLIENT:Reset( ClientName ) + self:F() + self._Menus = {} +end + +-- Is Functions + +--- Checks if the CLIENT is a multi-seated UNIT. +-- @param #CLIENT self +-- @return #boolean true if multi-seated. +function CLIENT:IsMultiSeated() + self:F( self.ClientName ) + + local ClientMultiSeatedTypes = { + ["Mi-8MT"] = "Mi-8MT", + ["UH-1H"] = "UH-1H", + ["P-51B"] = "P-51B" + } + + if self:IsAlive() then + local ClientTypeName = self:GetClientGroupUnit():GetTypeName() + if ClientMultiSeatedTypes[ClientTypeName] then + return true + end + end + + return false +end + +--- Checks for a client alive event and calls a function on a continuous basis. +-- @param #CLIENT self +-- @param #function CallBackFunction Create a function that will be called when a player joins the slot. +-- @param ... (Optional) Arguments for callback function as comma separated list. +-- @return #CLIENT +function CLIENT:Alive( CallBackFunction, ... ) + self:F() + + self.ClientCallBack = CallBackFunction + self.ClientParameters = arg + + self.AliveCheckScheduler = SCHEDULER:New( self, self._AliveCheckScheduler, { "Client Alive " .. self.ClientName }, 0.1, 5, 0.5 ) + self.AliveCheckScheduler:NoTrace() + + return self +end + +--- @param #CLIENT self +function CLIENT:_AliveCheckScheduler( SchedulerName ) + self:F3( { SchedulerName, self.ClientName, self.ClientAlive2, self.ClientBriefingShown, self.ClientCallBack } ) + + if self:IsAlive() then + + if self.ClientAlive2 == false then + + -- Show briefing. + self:ShowBriefing() + + -- Callback function. + if self.ClientCallBack then + self:T("Calling Callback function") + self.ClientCallBack( self, unpack( self.ClientParameters ) ) + end + + -- Alive. + self.ClientAlive2 = true + end + + else + + if self.ClientAlive2 == true then + self.ClientAlive2 = false + end + + end + + return true +end + +--- Return the DCSGroup of a Client. +-- This function is modified to deal with a couple of bugs in DCS 1.5.3 +-- @param #CLIENT self +-- @return DCS#Group The group of the Client. +function CLIENT:GetDCSGroup() + self:F3() + +-- local ClientData = Group.getByName( self.ClientName ) +-- if ClientData and ClientData:isExist() then +-- self:T( self.ClientName .. " : group found!" ) +-- return ClientData +-- else +-- return nil +-- end + + local ClientUnit = Unit.getByName( self.ClientName ) + + local CoalitionsData = { AlivePlayersRed = coalition.getPlayers( coalition.side.RED ), AlivePlayersBlue = coalition.getPlayers( coalition.side.BLUE ) } + + for CoalitionId, CoalitionData in pairs( CoalitionsData ) do + self:T3( { "CoalitionData:", CoalitionData } ) + for UnitId, UnitData in pairs( CoalitionData ) do + self:T3( { "UnitData:", UnitData } ) + if UnitData and UnitData:isExist() then + + --self:F(self.ClientName) + if ClientUnit then + + local ClientGroup = ClientUnit:getGroup() + + if ClientGroup then + self:T3( "ClientGroup = " .. self.ClientName ) + + if ClientGroup:isExist() and UnitData:getGroup():isExist() then + + if ClientGroup:getID() == UnitData:getGroup():getID() then + self:T3( "Normal logic" ) + self:T3( self.ClientName .. " : group found!" ) + self.ClientGroupID = ClientGroup:getID() + self.ClientGroupName = ClientGroup:getName() + return ClientGroup + end + + else + + -- Now we need to resolve the bugs in DCS 1.5 ... + -- Consult the database for the units of the Client Group. (ClientGroup:getUnits() returns nil) + self:T3( "Bug 1.5 logic" ) + + local ClientGroupTemplate = _DATABASE.Templates.Units[self.ClientName].GroupTemplate + + self.ClientGroupID = ClientGroupTemplate.groupId + + self.ClientGroupName = _DATABASE.Templates.Units[self.ClientName].GroupName + + self:T3( self.ClientName .. " : group found in bug 1.5 resolvement logic!" ) + return ClientGroup + + end + -- else + -- error( "Client " .. self.ClientName .. " not found!" ) + end + else + --self:F( { "Client not found!", self.ClientName } ) + end + end + end + end + + -- For non player clients + if ClientUnit then + local ClientGroup = ClientUnit:getGroup() + if ClientGroup then + self:T3( "ClientGroup = " .. self.ClientName ) + if ClientGroup:isExist() then + self:T3( "Normal logic" ) + self:T3( self.ClientName .. " : group found!" ) + return ClientGroup + end + end + end + + -- Nothing could be found :( + self.ClientGroupID = nil + self.ClientGroupName = nil + + return nil +end + + +--- Get the group ID of the client. +-- @param #CLIENT self +-- @return #number DCS#Group ID. +function CLIENT:GetClientGroupID() + + -- This updates the ID. + self:GetDCSGroup() + + return self.ClientGroupID +end + + +--- Get the name of the group of the client. +-- @param #CLIENT self +-- @return #string +function CLIENT:GetClientGroupName() + + -- This updates the group name. + self:GetDCSGroup() + + return self.ClientGroupName +end + +--- Returns the UNIT of the CLIENT. +-- @param #CLIENT self +-- @return Wrapper.Unit#UNIT The client UNIT or `nil`. +function CLIENT:GetClientGroupUnit() + self:F2() + + local ClientDCSUnit = Unit.getByName( self.ClientName ) + + self:T( self.ClientDCSUnit ) + + if ClientDCSUnit and ClientDCSUnit:isExist() then + local ClientUnit=_DATABASE:FindUnit( self.ClientName ) + return ClientUnit + end + + return nil +end + +--- Returns the DCSUnit of the CLIENT. +-- @param #CLIENT self +-- @return DCS#Unit +function CLIENT:GetClientGroupDCSUnit() + self:F2() + + local ClientDCSUnit = Unit.getByName( self.ClientName ) + + if ClientDCSUnit and ClientDCSUnit:isExist() then + self:T2( ClientDCSUnit ) + return ClientDCSUnit + end +end + + +--- Evaluates if the CLIENT is a transport. +-- @param #CLIENT self +-- @return #boolean true is a transport. +function CLIENT:IsTransport() + self:F() + return self.ClientTransport +end + +--- Shows the @{AI.AI_Cargo#CARGO} contained within the CLIENT to the player as a message. +-- The @{AI.AI_Cargo#CARGO} is shown using the @{Core.Message#MESSAGE} distribution system. +-- @param #CLIENT self +function CLIENT:ShowCargo() + self:F() + + local CargoMsg = "" + + for CargoName, Cargo in pairs( CARGOS ) do + if self == Cargo:IsLoadedInClient() then + CargoMsg = CargoMsg .. Cargo.CargoName .. " Type:" .. Cargo.CargoType .. " Weight: " .. Cargo.CargoWeight .. "\n" + end + end + + if CargoMsg == "" then + CargoMsg = "empty" + end + + self:Message( CargoMsg, 15, "Co-Pilot: Cargo Status", 30 ) + +end + + + +--- The main message driver for the CLIENT. +-- This function displays various messages to the Player logged into the CLIENT through the DCS World Messaging system. +-- @param #CLIENT self +-- @param #string Message is the text describing the message. +-- @param #number MessageDuration is the duration in seconds that the Message should be displayed. +-- @param #string MessageCategory is the category of the message (the title). +-- @param #number MessageInterval is the interval in seconds between the display of the @{Core.Message#MESSAGE} when the CLIENT is in the air. +-- @param #string MessageID is the identifier of the message when displayed with intervals. +function CLIENT:Message( Message, MessageDuration, MessageCategory, MessageInterval, MessageID ) + self:F( { Message, MessageDuration, MessageCategory, MessageInterval } ) + + if self.MessageSwitch == true then + if MessageCategory == nil then + MessageCategory = "Messages" + end + if MessageID ~= nil then + if self.Messages[MessageID] == nil then + self.Messages[MessageID] = {} + self.Messages[MessageID].MessageId = MessageID + self.Messages[MessageID].MessageTime = timer.getTime() + self.Messages[MessageID].MessageDuration = MessageDuration + if MessageInterval == nil then + self.Messages[MessageID].MessageInterval = 600 + else + self.Messages[MessageID].MessageInterval = MessageInterval + end + MESSAGE:New( Message, MessageDuration, MessageCategory ):ToClient( self ) + else + if self:GetClientGroupDCSUnit() and not self:GetClientGroupDCSUnit():inAir() then + if timer.getTime() - self.Messages[MessageID].MessageTime >= self.Messages[MessageID].MessageDuration + 10 then + MESSAGE:New( Message, MessageDuration , MessageCategory):ToClient( self ) + self.Messages[MessageID].MessageTime = timer.getTime() + end + else + if timer.getTime() - self.Messages[MessageID].MessageTime >= self.Messages[MessageID].MessageDuration + self.Messages[MessageID].MessageInterval then + MESSAGE:New( Message, MessageDuration, MessageCategory ):ToClient( self ) + self.Messages[MessageID].MessageTime = timer.getTime() + end + end + end + else + MESSAGE:New( Message, MessageDuration, MessageCategory ):ToClient( self ) + end + end +end +--- **Wrapper** -- STATIC wraps the DCS StaticObject class. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- ### Contributions: **funkyfranky** +-- +-- === +-- +-- @module Wrapper.Static +-- @image Wrapper_Static.JPG + + +--- @type STATIC +-- @extends Wrapper.Positionable#POSITIONABLE + +--- Wrapper class to handle Static objects. +-- +-- Note that Statics are almost the same as Units, but they don't have a controller. +-- The @{Wrapper.Static#STATIC} class is a wrapper class to handle the DCS Static objects: +-- +-- * Wraps the DCS Static objects. +-- * Support all DCS Static APIs. +-- * Enhance with Static specific APIs not in the DCS API set. +-- +-- ## STATIC reference methods +-- +-- For each DCS Static will have a STATIC wrapper object (instance) within the _@{DATABASE} object. +-- This is done at the beginning of the mission (when the mission starts). +-- +-- The STATIC class does not contain a :New() method, rather it provides :Find() methods to retrieve the object reference +-- using the Static Name. +-- +-- Another thing to know is that STATIC objects do not "contain" the DCS Static object. +-- The STATIc methods will reference the DCS Static object by name when it is needed during API execution. +-- If the DCS Static object does not exist or is nil, the STATIC methods will return nil and log an exception in the DCS.log file. +-- +-- The STATIc class provides the following functions to retrieve quickly the relevant STATIC instance: +-- +-- * @{#STATIC.FindByName}(): Find a STATIC instance from the _DATABASE object using a DCS Static name. +-- +-- IMPORTANT: ONE SHOULD NEVER SANATIZE these STATIC OBJECT REFERENCES! (make the STATIC object references nil). +-- +-- @field #STATIC +STATIC = { + ClassName = "STATIC", +} + + +--- Register a static object. +-- @param #STATIC self +-- @param #string StaticName Name of the static object. +-- @return #STATIC self +function STATIC:Register( StaticName ) + local self = BASE:Inherit( self, POSITIONABLE:New( StaticName ) ) + self.StaticName = StaticName + return self +end + + +--- Finds a STATIC from the _DATABASE using a DCSStatic object. +-- @param #STATIC self +-- @param DCS#StaticObject DCSStatic An existing DCS Static object reference. +-- @return #STATIC self +function STATIC:Find( DCSStatic ) + + local StaticName = DCSStatic:getName() + local StaticFound = _DATABASE:FindStatic( StaticName ) + return StaticFound +end + +--- Finds a STATIC from the _DATABASE using the relevant Static Name. +-- As an optional parameter, a briefing text can be given also. +-- @param #STATIC self +-- @param #string StaticName Name of the DCS **Static** as defined within the Mission Editor. +-- @param #boolean RaiseError Raise an error if not found. +-- @return #STATIC self or *nil* +function STATIC:FindByName( StaticName, RaiseError ) + + -- Find static in DB. + local StaticFound = _DATABASE:FindStatic( StaticName ) + + -- Set static name. + self.StaticName = StaticName + + if StaticFound then + return StaticFound + end + + if RaiseError == nil or RaiseError == true then + error( "STATIC not found for: " .. StaticName ) + end + + return nil +end + +--- Destroys the STATIC. +-- @param #STATIC self +-- @param #boolean GenerateEvent (Optional) true if you want to generate a crash or dead event for the static. +-- @return #nil The DCS StaticObject is not existing or alive. +-- @usage +-- -- Air static example: destroy the static Helicopter and generate a S_EVENT_CRASH. +-- Helicopter = STATIC:FindByName( "Helicopter" ) +-- Helicopter:Destroy( true ) +-- +-- @usage +-- -- Ground static example: destroy the static Tank and generate a S_EVENT_DEAD. +-- Tanks = UNIT:FindByName( "Tank" ) +-- Tanks:Destroy( true ) +-- +-- @usage +-- -- Ship static example: destroy the Ship silently. +-- Ship = STATIC:FindByName( "Ship" ) +-- Ship:Destroy() +-- +-- @usage +-- -- Destroy without event generation example. +-- Ship = STATIC:FindByName( "Boat" ) +-- Ship:Destroy( false ) -- Don't generate an event upon destruction. +-- +function STATIC:Destroy( GenerateEvent ) + self:F2( self.ObjectName ) + + local DCSObject = self:GetDCSObject() + + if DCSObject then + + local StaticName = DCSObject:getName() + self:F( { StaticName = StaticName } ) + + if GenerateEvent and GenerateEvent == true then + if self:IsAir() then + self:CreateEventCrash( timer.getTime(), DCSObject ) + else + self:CreateEventDead( timer.getTime(), DCSObject ) + end + elseif GenerateEvent == false then + -- Do nothing! + else + self:CreateEventRemoveUnit( timer.getTime(), DCSObject ) + end + + DCSObject:destroy() + return true + end + + return nil +end + + +--- Get DCS object of static of static. +-- @param #STATIC self +-- @return DCS static object +function STATIC:GetDCSObject() + local DCSStatic = StaticObject.getByName( self.StaticName ) + + if DCSStatic then + return DCSStatic + end + + return nil +end + +--- Returns a list of one @{Static}. +-- @param #STATIC self +-- @return #list A list of one @{Static}. +function STATIC:GetUnits() + self:F2( { self.StaticName } ) + local DCSStatic = self:GetDCSObject() + + local Statics = {} + + if DCSStatic then + Statics[1] = STATIC:Find( DCSStatic ) + self:T3( Statics ) + return Statics + end + + return nil +end + + +--- Get threat level of static. +-- @param #STATIC self +-- @return #number Threat level 1. +-- @return #string "Static" +function STATIC:GetThreatLevel() + return 1, "Static" +end + +--- Spawn the @{Wrapper.Static} at a specific coordinate and heading. +-- @param #STATIC self +-- @param Core.Point#COORDINATE Coordinate The coordinate where to spawn the new Static. +-- @param #number Heading The heading of the static respawn in degrees. Default is 0 deg. +-- @param #number Delay Delay in seconds before the static is spawned. +function STATIC:SpawnAt(Coordinate, Heading, Delay) + + Heading=Heading or 0 + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.SpawnAt, {self, Coordinate, Heading}, Delay) + else + + local SpawnStatic=SPAWNSTATIC:NewFromStatic(self.StaticName) + + SpawnStatic:SpawnFromPointVec2( Coordinate, Heading, self.StaticName ) + + end + + return self +end + + +--- Respawn the @{Wrapper.Unit} at the same location with the same properties. +-- This is useful to respawn a cargo after it has been destroyed. +-- @param #STATIC self +-- @param DCS#country.id CountryID (Optional) The country ID used for spawning the new static. Default is same as currently. +-- @param #number Delay (Optional) Delay in seconds before static is respawned. Default now. +function STATIC:ReSpawn(CountryID, Delay) + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.ReSpawn, {self, CountryID}, Delay) + else + + CountryID=CountryID or self:GetCountry() + + local SpawnStatic=SPAWNSTATIC:NewFromStatic(self.StaticName, CountryID) + + SpawnStatic:Spawn(nil, self.StaticName) + + end + + return self +end + + +--- Respawn the @{Wrapper.Unit} at a defined Coordinate with an optional heading. +-- @param #STATIC self +-- @param Core.Point#COORDINATE Coordinate The coordinate where to spawn the new Static. +-- @param #number Heading (Optional) The heading of the static respawn in degrees. Default the current heading. +-- @param #number Delay (Optional) Delay in seconds before static is respawned. Default now. +function STATIC:ReSpawnAt(Coordinate, Heading, Delay) + + --Heading=Heading or 0 + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.ReSpawnAt, {self, Coordinate, Heading}, Delay) + else + + local SpawnStatic=SPAWNSTATIC:NewFromStatic(self.StaticName, self:GetCountry()) + + SpawnStatic:SpawnFromCoordinate(Coordinate, Heading, self.StaticName) + + end + + return self +end + +--- **Wrapper** -- AIRBASE is a wrapper class to handle the DCS Airbase objects. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- ### Contributions: **funkyfranky** +-- +-- === +-- +-- @module Wrapper.Airbase +-- @image Wrapper_Airbase.JPG + + +--- @type AIRBASE +-- @field #string ClassName Name of the class, i.e. "AIRBASE". +-- @field #table CategoryName Names of airbase categories. +-- @field #string AirbaseName Name of the airbase. +-- @field #number AirbaseID Airbase ID. +-- @field Core.Zone#ZONE AirbaseZone Circular zone around the airbase with a radius of 2500 meters. For ships this is a ZONE_UNIT object. +-- @field #number category Airbase category. +-- @field #table descriptors DCS descriptors. +-- @field #boolean isAirdrome Airbase is an airdrome. +-- @field #boolean isHelipad Airbase is a helipad. +-- @field #boolean isShip Airbase is a ship. +-- @field #table parking Parking spot data. +-- @field #table parkingByID Parking spot data table with ID as key. +-- @field #number activerwyno Active runway number (forced). +-- @field #table parkingWhitelist List of parking spot terminal IDs considered for spawning. +-- @field #table parkingBlacklist List of parking spot terminal IDs **not** considered for spawning. +-- @extends Wrapper.Positionable#POSITIONABLE + +--- Wrapper class to handle the DCS Airbase objects: +-- +-- * Support all DCS Airbase APIs. +-- * Enhance with Airbase specific APIs not in the DCS Airbase API set. +-- +-- ## AIRBASE reference methods +-- +-- For each DCS Airbase object alive within a running mission, a AIRBASE wrapper object (instance) will be created within the _@{DATABASE} object. +-- This is done at the beginning of the mission (when the mission starts). +-- +-- The AIRBASE class **does not contain a :New()** method, rather it provides **:Find()** methods to retrieve the object reference +-- using the DCS Airbase or the DCS AirbaseName. +-- +-- Another thing to know is that AIRBASE objects do not "contain" the DCS Airbase object. +-- The AIRBASE methods will reference the DCS Airbase object by name when it is needed during API execution. +-- If the DCS Airbase object does not exist or is nil, the AIRBASE methods will return nil and log an exception in the DCS.log file. +-- +-- The AIRBASE class provides the following functions to retrieve quickly the relevant AIRBASE instance: +-- +-- * @{#AIRBASE.Find}(): Find a AIRBASE instance from the _DATABASE object using a DCS Airbase object. +-- * @{#AIRBASE.FindByName}(): Find a AIRBASE instance from the _DATABASE object using a DCS Airbase name. +-- +-- IMPORTANT: ONE SHOULD NEVER SANATIZE these AIRBASE OBJECT REFERENCES! (make the AIRBASE object references nil). +-- +-- ## DCS Airbase APIs +-- +-- The DCS Airbase APIs are used extensively within MOOSE. The AIRBASE class has for each DCS Airbase API a corresponding method. +-- To be able to distinguish easily in your code the difference between a AIRBASE API call and a DCS Airbase API call, +-- the first letter of the method is also capitalized. So, by example, the DCS Airbase method @{DCSWrapper.Airbase#Airbase.getName}() +-- is implemented in the AIRBASE class as @{#AIRBASE.GetName}(). +-- +-- @field #AIRBASE AIRBASE +AIRBASE = { + ClassName="AIRBASE", + CategoryName = { + [Airbase.Category.AIRDROME] = "Airdrome", + [Airbase.Category.HELIPAD] = "Helipad", + [Airbase.Category.SHIP] = "Ship", + }, + activerwyno=nil, + } + +--- Enumeration to identify the airbases in the Caucasus region. +-- +-- Airbases of the Caucasus map: +-- +-- * AIRBASE.Caucasus.Gelendzhik +-- * AIRBASE.Caucasus.Krasnodar_Pashkovsky +-- * AIRBASE.Caucasus.Sukhumi_Babushara +-- * AIRBASE.Caucasus.Gudauta +-- * AIRBASE.Caucasus.Batumi +-- * AIRBASE.Caucasus.Senaki_Kolkhi +-- * AIRBASE.Caucasus.Kobuleti +-- * AIRBASE.Caucasus.Kutaisi +-- * AIRBASE.Caucasus.Tbilisi_Lochini +-- * AIRBASE.Caucasus.Soganlug +-- * AIRBASE.Caucasus.Vaziani +-- * AIRBASE.Caucasus.Anapa_Vityazevo +-- * AIRBASE.Caucasus.Krasnodar_Center +-- * AIRBASE.Caucasus.Novorossiysk +-- * AIRBASE.Caucasus.Krymsk +-- * AIRBASE.Caucasus.Maykop_Khanskaya +-- * AIRBASE.Caucasus.Sochi_Adler +-- * AIRBASE.Caucasus.Mineralnye_Vody +-- * AIRBASE.Caucasus.Nalchik +-- * AIRBASE.Caucasus.Mozdok +-- * AIRBASE.Caucasus.Beslan +-- +-- @field Caucasus +AIRBASE.Caucasus = { + ["Gelendzhik"] = "Gelendzhik", + ["Krasnodar_Pashkovsky"] = "Krasnodar-Pashkovsky", + ["Sukhumi_Babushara"] = "Sukhumi-Babushara", + ["Gudauta"] = "Gudauta", + ["Batumi"] = "Batumi", + ["Senaki_Kolkhi"] = "Senaki-Kolkhi", + ["Kobuleti"] = "Kobuleti", + ["Kutaisi"] = "Kutaisi", + ["Tbilisi_Lochini"] = "Tbilisi-Lochini", + ["Soganlug"] = "Soganlug", + ["Vaziani"] = "Vaziani", + ["Anapa_Vityazevo"] = "Anapa-Vityazevo", + ["Krasnodar_Center"] = "Krasnodar-Center", + ["Novorossiysk"] = "Novorossiysk", + ["Krymsk"] = "Krymsk", + ["Maykop_Khanskaya"] = "Maykop-Khanskaya", + ["Sochi_Adler"] = "Sochi-Adler", + ["Mineralnye_Vody"] = "Mineralnye Vody", + ["Nalchik"] = "Nalchik", + ["Mozdok"] = "Mozdok", + ["Beslan"] = "Beslan", + } + +--- Airbases of the Nevada map: +-- +-- * AIRBASE.Nevada.Creech_AFB +-- * AIRBASE.Nevada.Groom_Lake_AFB +-- * AIRBASE.Nevada.McCarran_International_Airport +-- * AIRBASE.Nevada.Nellis_AFB +-- * AIRBASE.Nevada.Beatty_Airport +-- * AIRBASE.Nevada.Boulder_City_Airport +-- * AIRBASE.Nevada.Echo_Bay +-- * AIRBASE.Nevada.Henderson_Executive_Airport +-- * AIRBASE.Nevada.Jean_Airport +-- * AIRBASE.Nevada.Laughlin_Airport +-- * AIRBASE.Nevada.Lincoln_County +-- * AIRBASE.Nevada.Mesquite +-- * AIRBASE.Nevada.Mina_Airport_3Q0 +-- * AIRBASE.Nevada.North_Las_Vegas +-- * AIRBASE.Nevada.Pahute_Mesa_Airstrip +-- * AIRBASE.Nevada.Tonopah_Airport +-- * AIRBASE.Nevada.Tonopah_Test_Range_Airfield +-- +-- @field Nevada +AIRBASE.Nevada = { + ["Creech_AFB"] = "Creech AFB", + ["Groom_Lake_AFB"] = "Groom Lake AFB", + ["McCarran_International_Airport"] = "McCarran International Airport", + ["Nellis_AFB"] = "Nellis AFB", + ["Beatty_Airport"] = "Beatty Airport", + ["Boulder_City_Airport"] = "Boulder City Airport", + ["Echo_Bay"] = "Echo Bay", + ["Henderson_Executive_Airport"] = "Henderson Executive Airport", + ["Jean_Airport"] = "Jean Airport", + ["Laughlin_Airport"] = "Laughlin Airport", + ["Lincoln_County"] = "Lincoln County", + ["Mesquite"] = "Mesquite", + ["Mina_Airport_3Q0"] = "Mina Airport 3Q0", + ["North_Las_Vegas"] = "North Las Vegas", + ["Pahute_Mesa_Airstrip"] = "Pahute Mesa Airstrip", + ["Tonopah_Airport"] = "Tonopah Airport", + ["Tonopah_Test_Range_Airfield"] = "Tonopah Test Range Airfield", + } + +--- Airbases of the Normandy map: +-- +-- * AIRBASE.Normandy.Saint_Pierre_du_Mont +-- * AIRBASE.Normandy.Lignerolles +-- * AIRBASE.Normandy.Cretteville +-- * AIRBASE.Normandy.Maupertus +-- * AIRBASE.Normandy.Brucheville +-- * AIRBASE.Normandy.Meautis +-- * AIRBASE.Normandy.Cricqueville_en_Bessin +-- * AIRBASE.Normandy.Lessay +-- * AIRBASE.Normandy.Sainte_Laurent_sur_Mer +-- * AIRBASE.Normandy.Biniville +-- * AIRBASE.Normandy.Cardonville +-- * AIRBASE.Normandy.Deux_Jumeaux +-- * AIRBASE.Normandy.Chippelle +-- * AIRBASE.Normandy.Beuzeville +-- * AIRBASE.Normandy.Azeville +-- * AIRBASE.Normandy.Picauville +-- * AIRBASE.Normandy.Le_Molay +-- * AIRBASE.Normandy.Longues_sur_Mer +-- * AIRBASE.Normandy.Carpiquet +-- * AIRBASE.Normandy.Bazenville +-- * AIRBASE.Normandy.Sainte_Croix_sur_Mer +-- * AIRBASE.Normandy.Beny_sur_Mer +-- * AIRBASE.Normandy.Rucqueville +-- * AIRBASE.Normandy.Sommervieu +-- * AIRBASE.Normandy.Lantheuil +-- * AIRBASE.Normandy.Evreux +-- * AIRBASE.Normandy.Chailey +-- * AIRBASE.Normandy.Needs_Oar_Point +-- * AIRBASE.Normandy.Funtington +-- * AIRBASE.Normandy.Tangmere +-- * AIRBASE.Normandy.Ford_AF +-- +-- @field Normandy +AIRBASE.Normandy = { + ["Saint_Pierre_du_Mont"] = "Saint Pierre du Mont", + ["Lignerolles"] = "Lignerolles", + ["Cretteville"] = "Cretteville", + ["Maupertus"] = "Maupertus", + ["Brucheville"] = "Brucheville", + ["Meautis"] = "Meautis", + ["Cricqueville_en_Bessin"] = "Cricqueville-en-Bessin", + ["Lessay"] = "Lessay", + ["Sainte_Laurent_sur_Mer"] = "Sainte-Laurent-sur-Mer", + ["Biniville"] = "Biniville", + ["Cardonville"] = "Cardonville", + ["Deux_Jumeaux"] = "Deux Jumeaux", + ["Chippelle"] = "Chippelle", + ["Beuzeville"] = "Beuzeville", + ["Azeville"] = "Azeville", + ["Picauville"] = "Picauville", + ["Le_Molay"] = "Le Molay", + ["Longues_sur_Mer"] = "Longues-sur-Mer", + ["Carpiquet"] = "Carpiquet", + ["Bazenville"] = "Bazenville", + ["Sainte_Croix_sur_Mer"] = "Sainte-Croix-sur-Mer", + ["Beny_sur_Mer"] = "Beny-sur-Mer", + ["Rucqueville"] = "Rucqueville", + ["Sommervieu"] = "Sommervieu", + ["Lantheuil"] = "Lantheuil", + ["Evreux"] = "Evreux", + ["Chailey"] = "Chailey", + ["Needs_Oar_Point"] = "Needs Oar Point", + ["Funtington"] = "Funtington", + ["Tangmere"] = "Tangmere", + ["Ford_AF"] = "Ford_AF", + ["Goulet"] = "Goulet", + ["Argentan"] = "Argentan", + ["Vrigny"] = "Vrigny", + ["Essay"] = "Essay", + ["Hauterive"] = "Hauterive", + ["Barville"] = "Barville", + ["Conches"] = "Conches", +} + +--- Airbases of the Persion Gulf Map: +-- +-- * AIRBASE.PersianGulf.Abu_Dhabi_International_Airport +-- * AIRBASE.PersianGulf.Abu_Musa_Island_Airport +-- * AIRBASE.PersianGulf.Al-Bateen_Airport +-- * AIRBASE.PersianGulf.Al_Ain_International_Airport +-- * AIRBASE.PersianGulf.Al_Dhafra_AB +-- * AIRBASE.PersianGulf.Al_Maktoum_Intl +-- * AIRBASE.PersianGulf.Al_Minhad_AB +-- * AIRBASE.PersianGulf.Bandar_e_Jask_airfield +-- * AIRBASE.PersianGulf.Bandar_Abbas_Intl +-- * AIRBASE.PersianGulf.Bandar_Lengeh +-- * AIRBASE.PersianGulf.Dubai_Intl +-- * AIRBASE.PersianGulf.Fujairah_Intl +-- * AIRBASE.PersianGulf.Havadarya +-- * AIRBASE.PersianGulf.Jiroft_Airport +-- * AIRBASE.PersianGulf.Kerman_Airport +-- * AIRBASE.PersianGulf.Khasab +-- * AIRBASE.PersianGulf.Kish_International_Airport +-- * AIRBASE.PersianGulf.Lar_Airbase +-- * AIRBASE.PersianGulf.Lavan_Island_Airport +-- * AIRBASE.PersianGulf.Liwa_Airbase +-- * AIRBASE.PersianGulf.Qeshm_Island +-- * AIRBASE.PersianGulf.Ras_Al_Khaimah_International_Airport +-- * AIRBASE.PersianGulf.Sas_Al_Nakheel_Airport +-- * AIRBASE.PersianGulf.Sharjah_Intl +-- * AIRBASE.PersianGulf.Shiraz_International_Airport +-- * AIRBASE.PersianGulf.Sir_Abu_Nuayr +-- * AIRBASE.PersianGulf.Sirri_Island +-- * AIRBASE.PersianGulf.Tunb_Island_AFB +-- * AIRBASE.PersianGulf.Tunb_Kochak +-- +-- @field PersianGulf +AIRBASE.PersianGulf = { + ["Abu_Dhabi_International_Airport"] = "Abu Dhabi Intl", + ["Abu_Musa_Island_Airport"] = "Abu Musa Island", + ["Al_Ain_International_Airport"] = "Al Ain Intl", + ["Al_Bateen_Airport"] = "Al-Bateen", + ["Al_Dhafra_AB"] = "Al Dhafra AFB", + ["Al_Maktoum_Intl"] = "Al Maktoum Intl", + ["Al_Minhad_AB"] = "Al Minhad AFB", + ["Bandar_Abbas_Intl"] = "Bandar Abbas Intl", + ["Bandar_Lengeh"] = "Bandar Lengeh", + ["Bandar_e_Jask_airfield"] = "Bandar-e-Jask", + ["Dubai_Intl"] = "Dubai Intl", + ["Fujairah_Intl"] = "Fujairah Intl", + ["Havadarya"] = "Havadarya", + ["Jiroft_Airport"] = "Jiroft", + ["Kerman_Airport"] = "Kerman", + ["Khasab"] = "Khasab", + ["Kish_International_Airport"] = "Kish Intl", + ["Lar_Airbase"] = "Lar", + ["Lavan_Island_Airport"] = "Lavan Island", + ["Liwa_Airbase"] = "Liwa AFB", + ["Qeshm_Island"] = "Qeshm Island", + ["Ras_Al_Khaimah"] = "Ras Al Khaimah Intl", + ["Sas_Al_Nakheel_Airport"] = "Sas Al Nakheel", + ["Sharjah_Intl"] = "Sharjah Intl", + ["Shiraz_International_Airport"] = "Shiraz Intl", + ["Sir_Abu_Nuayr"] = "Sir Abu Nuayr", + ["Sirri_Island"] = "Sirri Island", + ["Tunb_Island_AFB"] = "Tunb Island AFB", + ["Tunb_Kochak"] = "Tunb Kochak", +} + +--- Airbases of The Channel Map: +-- +-- * AIRBASE.TheChannel.Abbeville_Drucat +-- * AIRBASE.TheChannel.Merville_Calonne +-- * AIRBASE.TheChannel.Saint_Omer_Longuenesse +-- * AIRBASE.TheChannel.Dunkirk_Mardyck +-- * AIRBASE.TheChannel.Manston +-- * AIRBASE.TheChannel.Hawkinge +-- * AIRBASE.TheChannel.Lympne +-- * AIRBASE.TheChannel.Detling +-- * AIRBASE.TheChannel.High_Halden +-- +-- @field TheChannel +AIRBASE.TheChannel = { + ["Abbeville_Drucat"] = "Abbeville Drucat", + ["Merville_Calonne"] = "Merville Calonne", + ["Saint_Omer_Longuenesse"] = "Saint Omer Longuenesse", + ["Dunkirk_Mardyck"] = "Dunkirk Mardyck", + ["Manston"] = "Manston", + ["Hawkinge"] = "Hawkinge", + ["Lympne"] = "Lympne", + ["Detling"] = "Detling", + ["High_Halden"] = "High Halden", +} + +--- Airbases of the Syria map: +-- +-- * AIRBASE.Syria.Kuweires +-- * AIRBASE.Syria.Marj_Ruhayyil +-- * AIRBASE.Syria.Kiryat_Shmona +-- * AIRBASE.Syria.Marj_as_Sultan_North +-- * AIRBASE.Syria.Eyn_Shemer +-- * AIRBASE.Syria.Incirlik +-- * AIRBASE.Syria.Damascus +-- * AIRBASE.Syria.Bassel_Al_Assad +-- * AIRBASE.Syria.Rosh_Pina +-- * AIRBASE.Syria.Aleppo +-- * AIRBASE.Syria.Al_Qusayr +-- * AIRBASE.Syria.Wujah_Al_Hajar +-- * AIRBASE.Syria.Al_Dumayr +-- * AIRBASE.Syria.Gazipasa +-- * AIRBASE.Syria.Ru_Convoy_4 +-- * AIRBASE.Syria.Hatay +-- * AIRBASE.Syria.Nicosia +-- * AIRBASE.Syria.Pinarbashi +-- * AIRBASE.Syria.Paphos +-- * AIRBASE.Syria.Kingsfield +-- * AIRBASE.Syria.Thalah +-- * AIRBASE.Syria.Haifa +-- * AIRBASE.Syria.Khalkhalah +-- * AIRBASE.Syria.Megiddo +-- * AIRBASE.Syria.Lakatamia +-- * AIRBASE.Syria.Rayak +-- * AIRBASE.Syria.Larnaca +-- * AIRBASE.Syria.Mezzeh +-- * AIRBASE.Syria.Gecitkale +-- * AIRBASE.Syria.Akrotiri +-- * AIRBASE.Syria.Naqoura +-- * AIRBASE.Syria.Gaziantep +-- * AIRBASE.Syria.CVN_71 +-- * AIRBASE.Syria.Sayqal +-- * AIRBASE.Syria.Tiyas +-- * AIRBASE.Syria.Shayrat +-- * AIRBASE.Syria.Taftanaz +-- * AIRBASE.Syria.H4 +-- * AIRBASE.Syria.King_Hussein_Air_College +-- * AIRBASE.Syria.Rene_Mouawad +-- * AIRBASE.Syria.Jirah +-- * AIRBASE.Syria.Ramat_David +-- * AIRBASE.Syria.Qabr_as_Sitt +-- * AIRBASE.Syria.Minakh +-- * AIRBASE.Syria.Adana_Sakirpasa +-- * AIRBASE.Syria.Palmyra +-- * AIRBASE.Syria.Hama +-- * AIRBASE.Syria.Ercan +-- * AIRBASE.Syria.Marj_as_Sultan_South +-- * AIRBASE.Syria.Tabqa +-- * AIRBASE.Syria.Beirut_Rafic_Hariri +-- * AIRBASE.Syria.An_Nasiriyah +-- * AIRBASE.Syria.Abu_al_Duhur +-- +--@field Syria +AIRBASE.Syria={ + ["Kuweires"]="Kuweires", + ["Marj_Ruhayyil"]="Marj Ruhayyil", + ["Kiryat_Shmona"]="Kiryat Shmona", + ["Marj_as_Sultan_North"]="Marj as Sultan North", + ["Eyn_Shemer"]="Eyn Shemer", + ["Incirlik"]="Incirlik", + ["Damascus"]="Damascus", + ["Bassel_Al_Assad"]="Bassel Al-Assad", + ["Rosh_Pina"]="Rosh Pina", + ["Aleppo"]="Aleppo", + ["Al_Qusayr"]="Al Qusayr", + ["Wujah_Al_Hajar"]="Wujah Al Hajar", + ["Al_Dumayr"]="Al-Dumayr", + ["Gazipasa"]="Gazipasa", + ["Ru_Convoy_4"]="Ru Convoy-4", + ["Hatay"]="Hatay", + ["Nicosia"]="Nicosia", + ["Pinarbashi"]="Pinarbashi", + ["Paphos"]="Paphos", + ["Kingsfield"]="Kingsfield", + ["Thalah"]="Tha'lah", + ["Haifa"]="Haifa", + ["Khalkhalah"]="Khalkhalah", + ["Megiddo"]="Megiddo", + ["Lakatamia"]="Lakatamia", + ["Rayak"]="Rayak", + ["Larnaca"]="Larnaca", + ["Mezzeh"]="Mezzeh", + ["Gecitkale"]="Gecitkale", + ["Akrotiri"]="Akrotiri", + ["Naqoura"]="Naqoura", + ["Gaziantep"]="Gaziantep", + ["Sayqal"]="Sayqal", + ["Tiyas"]="Tiyas", + ["Shayrat"]="Shayrat", + ["Taftanaz"]="Taftanaz", + ["H4"]="H4", + ["King_Hussein_Air_College"]="King Hussein Air College", + ["Rene_Mouawad"]="Rene Mouawad", + ["Jirah"]="Jirah", + ["Ramat_David"]="Ramat David", + ["Qabr_as_Sitt"]="Qabr as Sitt", + ["Minakh"]="Minakh", + ["Adana_Sakirpasa"]="Adana Sakirpasa", + ["Palmyra"]="Palmyra", + ["Hama"]="Hama", + ["Ercan"]="Ercan", + ["Marj_as_Sultan_South"]="Marj as Sultan South", + ["Tabqa"]="Tabqa", + ["Beirut_Rafic_Hariri"]="Beirut-Rafic Hariri", + ["An_Nasiriyah"]="An Nasiriyah", + ["Abu_al_Duhur"]="Abu al-Duhur", +} + + + +--- Airbases of the Mariana Islands map: +-- +-- * AIRBASE.MarianaIslands.Rota_Intl +-- * AIRBASE.MarianaIslands.Andersen_AFB +-- * AIRBASE.MarianaIslands.Antonio_B_Won_Pat_Intl +-- * AIRBASE.MarianaIslands.Saipan_Intl +-- * AIRBASE.MarianaIslands.Tinian_Intl +-- * AIRBASE.MarianaIslands.Olf_Orote +-- +--@field MarianaIslands +AIRBASE.MarianaIslands={ + ["Rota_Intl"]="Rota Intl", + ["Andersen_AFB"]="Andersen AFB", + ["Antonio_B_Won_Pat_Intl"]="Antonio B. Won Pat Intl", + ["Saipan_Intl"]="Saipan Intl", + ["Tinian_Intl"]="Tinian Intl", + ["Olf_Orote"]="Olf Orote", +} + + +--- AIRBASE.ParkingSpot ".Coordinate, ".TerminalID", ".TerminalType", ".TOAC", ".Free", ".TerminalID0", ".DistToRwy". +-- @type AIRBASE.ParkingSpot +-- @field Core.Point#COORDINATE Coordinate Coordinate of the parking spot. +-- @field #number TerminalID Terminal ID of the spot. Generally, this is not the same number as displayed in the mission editor. +-- @field #AIRBASE.TerminalType TerminalType Type of the spot, i.e. for which type of aircraft it can be used. +-- @field #boolean TOAC Takeoff or landing aircarft. I.e. this stop is occupied currently by an aircraft until it took of or until it landed. +-- @field #boolean Free This spot is currently free, i.e. there is no alive aircraft on it at the present moment. +-- @field #number TerminalID0 Unknown what this means. If you know, please tell us! +-- @field #number DistToRwy Distance to runway in meters. Currently bugged and giving the same number as the TerminalID. + +--- Terminal Types of parking spots. See also https://wiki.hoggitworld.com/view/DCS_func_getParking +-- +-- Supported types are: +-- +-- * AIRBASE.TerminalType.Runway = 16: Valid spawn points on runway. +-- * AIRBASE.TerminalType.HelicopterOnly = 40: Special spots for Helicopers. +-- * AIRBASE.TerminalType.Shelter = 68: Hardened Air Shelter. Currently only on Caucaus map. +-- * AIRBASE.TerminalType.OpenMed = 72: Open/Shelter air airplane only. +-- * AIRBASE.TerminalType.OpenBig = 104: Open air spawn points. Generally larger but does not guarantee large aircraft are capable of spawning there. +-- * AIRBASE.TerminalType.OpenMedOrBig = 176: Combines OpenMed and OpenBig spots. +-- * AIRBASE.TerminalType.HelicopterUsable = 216: Combines HelicopterOnly, OpenMed and OpenBig. +-- * AIRBASE.TerminalType.FighterAircraft = 244: Combines Shelter. OpenMed and OpenBig spots. So effectively all spots usable by fixed wing aircraft. +-- +-- @type AIRBASE.TerminalType +-- @field #number Runway 16: Valid spawn points on runway. +-- @field #number HelicopterOnly 40: Special spots for Helicopers. +-- @field #number Shelter 68: Hardened Air Shelter. Currently only on Caucaus map. +-- @field #number OpenMed 72: Open/Shelter air airplane only. +-- @field #number OpenBig 104: Open air spawn points. Generally larger but does not guarantee large aircraft are capable of spawning there. +-- @field #number OpenMedOrBig 176: Combines OpenMed and OpenBig spots. +-- @field #number HelicopterUsable 216: Combines HelicopterOnly, OpenMed and OpenBig. +-- @field #number FighterAircraft 244: Combines Shelter. OpenMed and OpenBig spots. So effectively all spots usable by fixed wing aircraft. +AIRBASE.TerminalType = { + Runway=16, + HelicopterOnly=40, + Shelter=68, + OpenMed=72, + OpenBig=104, + OpenMedOrBig=176, + HelicopterUsable=216, + FighterAircraft=244, +} + +--- Runway data. +-- @type AIRBASE.Runway +-- @field #number heading Heading of the runway in degrees. +-- @field #string idx Runway ID: heading 070° ==> idx="07". +-- @field #number length Length of runway in meters. +-- @field Core.Point#COORDINATE position Position of runway start. +-- @field Core.Point#COORDINATE endpoint End point of runway. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Registration +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new AIRBASE from DCSAirbase. +-- @param #AIRBASE self +-- @param #string AirbaseName The name of the airbase. +-- @return #AIRBASE self +function AIRBASE:Register(AirbaseName) + + -- Inherit everything from positionable. + local self=BASE:Inherit(self, POSITIONABLE:New(AirbaseName)) --#AIRBASE + + -- Set airbase name. + self.AirbaseName=AirbaseName + + -- Set airbase ID. + self.AirbaseID=self:GetID(true) + + -- Get descriptors. + self.descriptors=self:GetDesc() + + -- Category. + self.category=self.descriptors and self.descriptors.category or Airbase.Category.AIRDROME + + -- Set category. + if self.category==Airbase.Category.AIRDROME then + self.isAirdrome=true + elseif self.category==Airbase.Category.HELIPAD then + self.isHelipad=true + elseif self.category==Airbase.Category.SHIP then + self.isShip=true + -- DCS bug: Oil rigs and gas platforms have category=2 (ship). Also they cannot be retrieved by coalition.getStaticObjects() + if self.descriptors.typeName=="Oil rig" or self.descriptors.typeName=="Ga" then + self.isHelipad=true + self.isShip=false + self.category=Airbase.Category.HELIPAD + _DATABASE:AddStatic(AirbaseName) + end + else + self:E("ERROR: Unknown airbase category!") + end + + self:_InitParkingSpots() + + local vec2=self:GetVec2() + + -- Init coordinate. + self:GetCoordinate() + + if vec2 then + if self.isShip then + local unit=UNIT:FindByName(AirbaseName) + if unit then + self.AirbaseZone=ZONE_UNIT:New(AirbaseName, unit, 2500) + end + else + self.AirbaseZone=ZONE_RADIUS:New(AirbaseName, vec2, 2500) + end + else + self:E(string.format("ERROR: Cound not get position Vec2 of airbase %s", AirbaseName)) + end + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Reference methods +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Finds a AIRBASE from the _DATABASE using a DCSAirbase object. +-- @param #AIRBASE self +-- @param DCS#Airbase DCSAirbase An existing DCS Airbase object reference. +-- @return Wrapper.Airbase#AIRBASE self +function AIRBASE:Find( DCSAirbase ) + + local AirbaseName = DCSAirbase:getName() + local AirbaseFound = _DATABASE:FindAirbase( AirbaseName ) + return AirbaseFound +end + +--- Find a AIRBASE in the _DATABASE using the name of an existing DCS Airbase. +-- @param #AIRBASE self +-- @param #string AirbaseName The Airbase Name. +-- @return #AIRBASE self +function AIRBASE:FindByName( AirbaseName ) + + local AirbaseFound = _DATABASE:FindAirbase( AirbaseName ) + return AirbaseFound +end + +--- Find a AIRBASE in the _DATABASE by its ID. +-- @param #AIRBASE self +-- @param #number id Airbase ID. +-- @return #AIRBASE self +function AIRBASE:FindByID(id) + + for name,_airbase in pairs(_DATABASE.AIRBASES) do + local airbase=_airbase --#AIRBASE + + local aid=tonumber(airbase:GetID(true)) + + if aid==id then + return airbase + end + + end + + return nil +end + +--- Get the DCS object of an airbase +-- @param #AIRBASE self +-- @return DCS#Airbase DCS airbase object. +function AIRBASE:GetDCSObject() + + -- Get the DCS object. + local DCSAirbase = Airbase.getByName(self.AirbaseName) + + if DCSAirbase then + return DCSAirbase + end + + return nil +end + +--- Get the airbase zone. +-- @param #AIRBASE self +-- @return Core.Zone#ZONE_RADIUS The zone radius of the airbase. +function AIRBASE:GetZone() + return self.AirbaseZone +end + +--- Get all airbases of the current map. This includes ships and FARPS. +-- @param DCS#Coalition coalition (Optional) Return only airbases belonging to the specified coalition. By default, all airbases of the map are returned. +-- @param #number category (Optional) Return only airbases of a certain category, e.g. Airbase.Category.FARP +-- @return #table Table containing all airbase objects of the current map. +function AIRBASE.GetAllAirbases(coalition, category) + + local airbases={} + for _,_airbase in pairs(_DATABASE.AIRBASES) do + local airbase=_airbase --#AIRBASE + if coalition==nil or airbase:GetCoalition()==coalition then + if category==nil or category==airbase:GetAirbaseCategory() then + table.insert(airbases, airbase) + end + end + end + + return airbases +end + +--- Get all airbase names of the current map. This includes ships and FARPS. +-- @param DCS#Coalition coalition (Optional) Return only airbases belonging to the specified coalition. By default, all airbases of the map are returned. +-- @param #number category (Optional) Return only airbases of a certain category, e.g. `Airbase.Category.HELIPAD`. +-- @return #table Table containing all airbase names of the current map. +function AIRBASE.GetAllAirbaseNames(coalition, category) + + local airbases={} + for airbasename,_airbase in pairs(_DATABASE.AIRBASES) do + local airbase=_airbase --#AIRBASE + if coalition==nil or airbase:GetCoalition()==coalition then + if category==nil or category==airbase:GetAirbaseCategory() then + table.insert(airbases, airbasename) + end + end + end + + return airbases +end + +--- Get ID of the airbase. +-- @param #AIRBASE self +-- @param #boolean unique (Optional) If true, ships will get a negative sign as the unit ID might be the same as an airbase ID. Default off! +-- @return #number The airbase ID. +function AIRBASE:GetID(unique) + + if self.AirbaseID then + + return unique and self.AirbaseID or math.abs(self.AirbaseID) + + else + + for DCSAirbaseId, DCSAirbase in ipairs(world.getAirbases()) do + + -- Get the airbase name. + local AirbaseName = DCSAirbase:getName() + + -- This gives the incorrect value to be inserted into the airdromeID for DCS 2.5.6! + local airbaseID=tonumber(DCSAirbase:getID()) + + local airbaseCategory=self:GetAirbaseCategory() + + if AirbaseName==self.AirbaseName then + if airbaseCategory==Airbase.Category.SHIP or airbaseCategory==Airbase.Category.HELIPAD then + -- Ships get a negative sign as their unit number might be the same as the ID of another airbase. + return unique and -airbaseID or airbaseID + else + return airbaseID + end + end + + end + + end + + return nil +end + +--- Set parking spot whitelist. Only these spots will be considered for spawning. +-- Black listed spots overrule white listed spots. +-- **NOTE** that terminal IDs are not necessarily the same as those displayed in the mission editor! +-- @param #AIRBASE self +-- @param #table TerminalIdBlacklist Table of white listed terminal IDs. +-- @return #AIRBASE self +-- @usage AIRBASE:FindByName("Batumi"):SetParkingSpotWhitelist({2, 3, 4}) --Only allow terminal IDs 2, 3, 4 +function AIRBASE:SetParkingSpotWhitelist(TerminalIdWhitelist) + + if TerminalIdWhitelist==nil then + self.parkingWhitelist={} + return self + end + + -- Ensure we got a table. + if type(TerminalIdWhitelist)~="table" then + TerminalIdWhitelist={TerminalIdWhitelist} + end + + self.parkingWhitelist=TerminalIdWhitelist + + return self +end + +--- Set parking spot blacklist. These parking spots will *not* be used for spawning. +-- Black listed spots overrule white listed spots. +-- **NOTE** that terminal IDs are not necessarily the same as those displayed in the mission editor! +-- @param #AIRBASE self +-- @param #table TerminalIdBlacklist Table of black listed terminal IDs. +-- @return #AIRBASE self +-- @usage AIRBASE:FindByName("Batumi"):SetParkingSpotBlacklist({2, 3, 4}) --Forbit terminal IDs 2, 3, 4 +function AIRBASE:SetParkingSpotBlacklist(TerminalIdBlacklist) + + if TerminalIdBlacklist==nil then + self.parkingBlacklist={} + return self + end + + -- Ensure we got a table. + if type(TerminalIdBlacklist)~="table" then + TerminalIdBlacklist={TerminalIdBlacklist} + end + + self.parkingBlacklist=TerminalIdBlacklist + + return self +end + + +--- Get category of airbase. +-- @param #AIRBASE self +-- @return #number Category of airbase from GetDesc().category. +function AIRBASE:GetAirbaseCategory() + return self.category +end + +--- Check if airbase is an airdrome. +-- @param #AIRBASE self +-- @return #boolean If true, airbase is an airdrome. +function AIRBASE:IsAirdrome() + return self.isAirdrome +end + +--- Check if airbase is a helipad. +-- @param #AIRBASE self +-- @return #boolean If true, airbase is a helipad. +function AIRBASE:IsHelipad() + return self.isHelipad +end + +--- Check if airbase is a ship. +-- @param #AIRBASE self +-- @return #boolean If true, airbase is a ship. +function AIRBASE:IsShip() + return self.isShip +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Parking +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Returns a table of parking data for a given airbase. If the optional parameter *available* is true only available parking will be returned, otherwise all parking at the base is returned. Term types have the following enumerated values: +-- +-- * 16 : Valid spawn points on runway +-- * 40 : Helicopter only spawn +-- * 68 : Hardened Air Shelter +-- * 72 : Open/Shelter air airplane only +-- * 104: Open air spawn +-- +-- Note that only Caucuses will return 68 as it is the only map currently with hardened air shelters. +-- 104 are also generally larger, but does not guarantee a large aircraft like the B-52 or a C-130 are capable of spawning there. +-- +-- Table entries: +-- +-- * Term_index is the id for the parking +-- * vTerminal pos is its vec3 position in the world +-- * fDistToRW is the distance to the take-off position for the active runway from the parking. +-- +-- @param #AIRBASE self +-- @param #boolean available If true, only available parking spots will be returned. +-- @return #table Table with parking data. See https://wiki.hoggitworld.com/view/DCS_func_getParking +function AIRBASE:GetParkingData(available) + self:F2(available) + + -- Get DCS airbase object. + local DCSAirbase=self:GetDCSObject() + + -- Get parking data. + local parkingdata=nil + if DCSAirbase then + parkingdata=DCSAirbase:getParking(available) + end + + self:T2({parkingdata=parkingdata}) + return parkingdata +end + +--- Get number of parking spots at an airbase. Optionally, a specific terminal type can be requested. +-- @param #AIRBASE self +-- @param #AIRBASE.TerminalType termtype Terminal type of which the number of spots is counted. Default all spots but spawn points on runway. +-- @return #number Number of parking spots at this airbase. +function AIRBASE:GetParkingSpotsNumber(termtype) + + -- Get free parking spots data. + local parkingdata=self:GetParkingData(false) + + local nspots=0 + for _,parkingspot in pairs(parkingdata) do + if AIRBASE._CheckTerminalType(parkingspot.Term_Type, termtype) then + nspots=nspots+1 + end + end + + return nspots +end + +--- Get number of free parking spots at an airbase. +-- @param #AIRBASE self +-- @param #AIRBASE.TerminalType termtype Terminal type. +-- @param #boolean allowTOAC If true, spots are considered free even though TO_AC is true. Default is off which is saver to avoid spawning aircraft on top of each other. Option might be enabled for FARPS and ships. +-- @return #number Number of free parking spots at this airbase. +function AIRBASE:GetFreeParkingSpotsNumber(termtype, allowTOAC) + + -- Get free parking spots data. + local parkingdata=self:GetParkingData(true) + + local nfree=0 + for _,parkingspot in pairs(parkingdata) do + -- Spots on runway are not counted unless explicitly requested. + if AIRBASE._CheckTerminalType(parkingspot.Term_Type, termtype) then + if (allowTOAC and allowTOAC==true) or parkingspot.TO_AC==false then + nfree=nfree+1 + end + end + end + + return nfree +end + +--- Get the coordinates of free parking spots at an airbase. +-- @param #AIRBASE self +-- @param #AIRBASE.TerminalType termtype Terminal type. +-- @param #boolean allowTOAC If true, spots are considered free even though TO_AC is true. Default is off which is saver to avoid spawning aircraft on top of each other. Option might be enabled for FARPS and ships. +-- @return #table Table of coordinates of the free parking spots. +function AIRBASE:GetFreeParkingSpotsCoordinates(termtype, allowTOAC) + + -- Get free parking spots data. + local parkingdata=self:GetParkingData(true) + + -- Put coordinates of free spots into table. + local spots={} + for _,parkingspot in pairs(parkingdata) do + -- Coordinates on runway are not returned unless explicitly requested. + if AIRBASE._CheckTerminalType(parkingspot.Term_Type, termtype) then + if (allowTOAC and allowTOAC==true) or parkingspot.TO_AC==false then + table.insert(spots, COORDINATE:NewFromVec3(parkingspot.vTerminalPos)) + end + end + end + + return spots +end + +--- Get the coordinates of all parking spots at an airbase. Optionally only those of a specific terminal type. Spots on runways are excluded if not explicitly requested by terminal type. +-- @param #AIRBASE self +-- @param #AIRBASE.TerminalType termtype (Optional) Terminal type. Default all. +-- @return #table Table of coordinates of parking spots. +function AIRBASE:GetParkingSpotsCoordinates(termtype) + + -- Get all parking spots data. + local parkingdata=self:GetParkingData(false) + + -- Put coordinates of free spots into table. + local spots={} + for _,parkingspot in ipairs(parkingdata) do + + -- Coordinates on runway are not returned unless explicitly requested. + if AIRBASE._CheckTerminalType(parkingspot.Term_Type, termtype) then + + -- Get coordinate from Vec3 terminal position. + local _coord=COORDINATE:NewFromVec3(parkingspot.vTerminalPos) + + -- Add to table. + table.insert(spots, _coord) + end + + end + + return spots +end + +--- Get a table containing the coordinates, terminal index and terminal type of free parking spots at an airbase. +-- @param #AIRBASE self +-- @return#AIRBASE self +function AIRBASE:_InitParkingSpots() + + -- Get parking data of all spots (free or occupied) + local parkingdata=self:GetParkingData(false) + + -- Init table. + self.parking={} + self.parkingByID={} + + self.NparkingTotal=0 + self.NparkingTerminal={} + for _,terminalType in pairs(AIRBASE.TerminalType) do + self.NparkingTerminal[terminalType]=0 + end + + -- Put coordinates of parking spots into table. + for _,spot in pairs(parkingdata) do + + -- New parking spot. + local park={} --#AIRBASE.ParkingSpot + park.Vec3=spot.vTerminalPos + park.Coordinate=COORDINATE:NewFromVec3(spot.vTerminalPos) + park.DistToRwy=spot.fDistToRW + park.Free=nil + park.TerminalID=spot.Term_Index + park.TerminalID0=spot.Term_Index_0 + park.TerminalType=spot.Term_Type + park.TOAC=spot.TO_AC + + self.NparkingTotal=self.NparkingTotal+1 + + for _,terminalType in pairs(AIRBASE.TerminalType) do + if self._CheckTerminalType(terminalType, park.TerminalType) then + self.NparkingTerminal[terminalType]=self.NparkingTerminal[terminalType]+1 + end + end + + self.parkingByID[park.TerminalID]=park + table.insert(self.parking, park) + end + + return self +end + +--- Get a table containing the coordinates, terminal index and terminal type of free parking spots at an airbase. +-- @param #AIRBASE self +-- @param #number TerminalID Terminal ID. +-- @return #AIRBASE.ParkingSpot Parking spot. +function AIRBASE:_GetParkingSpotByID(TerminalID) + return self.parkingByID[TerminalID] +end + +--- Get a table containing the coordinates, terminal index and terminal type of free parking spots at an airbase. +-- @param #AIRBASE self +-- @param #AIRBASE.TerminalType termtype Terminal type. +-- @return #table Table free parking spots. Table has the elements ".Coordinate, ".TerminalID", ".TerminalType", ".TOAC", ".Free", ".TerminalID0", ".DistToRwy". +function AIRBASE:GetParkingSpotsTable(termtype) + + -- Get parking data of all spots (free or occupied) + local parkingdata=self:GetParkingData(false) + + -- Get parking data of all free spots. + local parkingfree=self:GetParkingData(true) + + -- Function to ckeck if any parking spot is free. + local function _isfree(_tocheck) + for _,_spot in pairs(parkingfree) do + if _spot.Term_Index==_tocheck.Term_Index then + return true + end + end + return false + end + + -- Put coordinates of parking spots into table. + local spots={} + for _,_spot in pairs(parkingdata) do + + if AIRBASE._CheckTerminalType(_spot.Term_Type, termtype) then + + local spot=self:_GetParkingSpotByID(_spot.Term_Index) + + if spot then + + spot.Free=_isfree(_spot) -- updated + spot.TOAC=_spot.TO_AC -- updated + + table.insert(spots, spot) + + else + + self:E(string.format("ERROR: Parking spot %s is nil!", tostring(_spot.Term_Index))) + + end + + end + + end + + return spots +end + +--- Get a table containing the coordinates, terminal index and terminal type of free parking spots at an airbase. +-- @param #AIRBASE self +-- @param #AIRBASE.TerminalType termtype Terminal type. +-- @param #boolean allowTOAC If true, spots are considered free even though TO_AC is true. Default is off which is saver to avoid spawning aircraft on top of each other. Option might be enabled for FARPS and ships. +-- @return #table Table free parking spots. Table has the elements ".Coordinate, ".TerminalID", ".TerminalType", ".TOAC", ".Free", ".TerminalID0", ".DistToRwy". +function AIRBASE:GetFreeParkingSpotsTable(termtype, allowTOAC) + + -- Get parking data of all free spots. + local parkingfree=self:GetParkingData(true) + + -- Put coordinates of free spots into table. + local freespots={} + for _,_spot in pairs(parkingfree) do + if AIRBASE._CheckTerminalType(_spot.Term_Type, termtype) and _spot.Term_Index>0 then + if (allowTOAC and allowTOAC==true) or _spot.TO_AC==false then + + local spot=self:_GetParkingSpotByID(_spot.Term_Index) + + spot.Free=true -- updated + spot.TOAC=_spot.TO_AC -- updated + + table.insert(freespots, spot) + + end + end + end + + return freespots +end + +--- Get a table containing the coordinates, terminal index and terminal type of free parking spots at an airbase. +-- @param #AIRBASE self +-- @param #number TerminalID The terminal ID of the parking spot. +-- @return #AIRBASE.ParkingSpot Table free parking spots. Table has the elements ".Coordinate, ".TerminalID", ".TerminalType", ".TOAC", ".Free", ".TerminalID0", ".DistToRwy". +function AIRBASE:GetParkingSpotData(TerminalID) + + -- Get parking data. + local parkingdata=self:GetParkingSpotsTable() + + for _,_spot in pairs(parkingdata) do + local spot=_spot --#AIRBASE.ParkingSpot + self:T({TerminalID=spot.TerminalID,TerminalType=spot.TerminalType}) + if TerminalID==spot.TerminalID then + return spot + end + end + + self:E("ERROR: Could not find spot with Terminal ID="..tostring(TerminalID)) + return nil +end + +--- Place markers of parking spots on the F10 map. +-- @param #AIRBASE self +-- @param #AIRBASE.TerminalType termtype Terminal type for which marks should be placed. +-- @param #boolean mark If false, do not place markers but only give output to DCS.log file. Default true. +function AIRBASE:MarkParkingSpots(termtype, mark) + + -- Default is true. + if mark==nil then + mark=true + end + + -- Get parking data from getParking() wrapper function. + local parkingdata=self:GetParkingSpotsTable(termtype) + + -- Get airbase name. + local airbasename=self:GetName() + self:E(string.format("Parking spots at %s for terminal type %s:", airbasename, tostring(termtype))) + + for _,_spot in pairs(parkingdata) do + + -- Mark text. + local _text=string.format("Term Index=%d, Term Type=%d, Free=%s, TOAC=%s, Term ID0=%d, Dist2Rwy=%.1f m", + _spot.TerminalID, _spot.TerminalType,tostring(_spot.Free),tostring(_spot.TOAC),_spot.TerminalID0,_spot.DistToRwy) + + -- Create mark on the F10 map. + if mark then + _spot.Coordinate:MarkToAll(_text) + end + + -- Info to DCS.log file. + local _text=string.format("%s, Term Index=%3d, Term Type=%03d, Free=%5s, TOAC=%5s, Term ID0=%3d, Dist2Rwy=%.1f m", + airbasename, _spot.TerminalID, _spot.TerminalType,tostring(_spot.Free),tostring(_spot.TOAC),_spot.TerminalID0,_spot.DistToRwy) + self:E(_text) + end +end + +--- Seach unoccupied parking spots at the airbase for a specific group of aircraft. The routine also optionally checks for other unit, static and scenery options in a certain radius around the parking spot. +-- The dimension of the spawned aircraft and of the potential obstacle are taken into account. Note that the routine can only return so many spots that are free. +-- @param #AIRBASE self +-- @param Wrapper.Group#GROUP group Aircraft group for which the parking spots are requested. +-- @param #AIRBASE.TerminalType terminaltype (Optional) Only search spots at a specific terminal type. Default is all types execpt on runway. +-- @param #number scanradius (Optional) Radius in meters around parking spot to scan for obstacles. Default 50 m. +-- @param #boolean scanunits (Optional) Scan for units as obstacles. Default true. +-- @param #boolean scanstatics (Optional) Scan for statics as obstacles. Default true. +-- @param #boolean scanscenery (Optional) Scan for scenery as obstacles. Default false. Can cause problems with e.g. shelters. +-- @param #boolean verysafe (Optional) If true, wait until an aircraft has taken off until the parking spot is considered to be free. Defaul false. +-- @param #number nspots (Optional) Number of freeparking spots requested. Default is the number of aircraft in the group. +-- @param #table parkingdata (Optional) Parking spots data table. If not given it is automatically derived from the GetParkingSpotsTable() function. +-- @return #table Table of coordinates and terminal IDs of free parking spots. Each table entry has the elements .Coordinate and .TerminalID. +function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, scanunits, scanstatics, scanscenery, verysafe, nspots, parkingdata) + + -- Init default + scanradius=scanradius or 50 + if scanunits==nil then + scanunits=true + end + if scanstatics==nil then + scanstatics=true + end + if scanscenery==nil then + scanscenery=false + end + if verysafe==nil then + verysafe=false + end + + -- Function calculating the overlap of two (square) objects. + local function _overlap(object1, object2, dist) + local pos1=object1 --Wrapper.Positionable#POSITIONABLE + local pos2=object2 --Wrapper.Positionable#POSITIONABLE + local r1=pos1:GetBoundingRadius() + local r2=pos2:GetBoundingRadius() + if r1 and r2 then + local safedist=(r1+r2)*1.1 + local safe = (dist > safedist) + self:T2(string.format("r1=%.1f r2=%.1f s=%.1f d=%.1f ==> safe=%s", r1, r2, safedist, dist, tostring(safe))) + return safe + else + return true + end + end + + -- Get airport name. + local airport=self:GetName() + + -- Get parking spot data table. This contains free and "non-free" spots. + -- Note that there are three major issues with the DCS getParking() function: + -- 1. A spot is considered as NOT free until an aircraft that is present has finally taken off. This might be a bit long especiall at smaller airports. + -- 2. A "free" spot does not take the aircraft size into accound. So if two big aircraft are spawned on spots next to each other, they might overlap and get destroyed. + -- 3. The routine return a free spot, if there a static objects placed on the spot. + parkingdata=parkingdata or self:GetParkingSpotsTable(terminaltype) + + -- Get the aircraft size, i.e. it's longest side of x,z. + local aircraft = nil -- fix local problem below + local _aircraftsize, ax,ay,az + if group and group.ClassName == "GROUP" then + aircraft=group:GetUnit(1) + _aircraftsize, ax,ay,az=aircraft:GetObjectSize() + else + -- SU27 dimensions + _aircraftsize = 23 + ax = 23 -- length + ay = 7 -- height + az = 17 -- width + end + + + -- Number of spots we are looking for. Note that, e.g. grouping can require a number different from the group size! + local _nspots=nspots or group:GetSize() + + -- Debug info. + self:E(string.format("%s: Looking for %d parking spot(s) for aircraft of size %.1f m (x=%.1f,y=%.1f,z=%.1f) at terminal type %s.", airport, _nspots, _aircraftsize, ax, ay, az, tostring(terminaltype))) + + -- Table of valid spots. + local validspots={} + local nvalid=0 + + -- Test other stuff if no parking spot is available. + local _test=false + if _test then + return validspots + end + + -- Mark all found obstacles on F10 map for debugging. + local markobstacles=false + + -- Loop over all known parking spots + for _,parkingspot in pairs(parkingdata) do + + -- Coordinate of the parking spot. + local _spot=parkingspot.Coordinate -- Core.Point#COORDINATE + local _termid=parkingspot.TerminalID + + -- Check terminal type and black/white listed parking spots. + if AIRBASE._CheckTerminalType(parkingspot.TerminalType, terminaltype) and self:_CheckParkingLists(_termid) then + + -- Very safe uses the DCS getParking() info to check if a spot is free. Unfortunately, the function returns free=false until the aircraft has actually taken-off. + if verysafe and (parkingspot.Free==false or parkingspot.TOAC==true) then + + -- DCS getParking() routine returned that spot is not free. + self:T(string.format("%s: Parking spot id %d NOT free (or aircraft has not taken off yet). Free=%s, TOAC=%s.", airport, parkingspot.TerminalID, tostring(parkingspot.Free), tostring(parkingspot.TOAC))) + + else + + -- Scan a radius of 50 meters around the spot. + local _,_,_,_units,_statics,_sceneries=_spot:ScanObjects(scanradius, scanunits, scanstatics, scanscenery) + + -- Loop over objects within scan radius. + local occupied=false + + -- Check all units. + for _,unit in pairs(_units) do + local _coord=unit:GetCoordinate() + local _dist=_coord:Get2DDistance(_spot) + local _safe=_overlap(aircraft, unit, _dist) + + if markobstacles then + local l,x,y,z=unit:GetObjectSize() + _coord:MarkToAll(string.format("Unit %s\nx=%.1f y=%.1f z=%.1f\nl=%.1f d=%.1f\nspot %d safe=%s", unit:GetName(),x,y,z,l,_dist, _termid, tostring(_safe))) + end + + if scanunits and not _safe then + occupied=true + end + end + + -- Check all statics. + for _,static in pairs(_statics) do + local _static=STATIC:Find(static) + local _vec3=static:getPoint() + local _coord=COORDINATE:NewFromVec3(_vec3) + local _dist=_coord:Get2DDistance(_spot) + local _safe=_overlap(aircraft,_static,_dist) + + if markobstacles then + local l,x,y,z=_static:GetObjectSize() + _coord:MarkToAll(string.format("Static %s\nx=%.1f y=%.1f z=%.1f\nl=%.1f d=%.1f\nspot %d safe=%s", static:getName(),x,y,z,l,_dist, _termid, tostring(_safe))) + end + + if scanstatics and not _safe then + occupied=true + end + end + + -- Check all scenery. + for _,scenery in pairs(_sceneries) do + local _scenery=SCENERY:Register(scenery:getTypeName(), scenery) + local _vec3=scenery:getPoint() + local _coord=COORDINATE:NewFromVec3(_vec3) + local _dist=_coord:Get2DDistance(_spot) + local _safe=_overlap(aircraft,_scenery,_dist) + + if markobstacles then + local l,x,y,z=scenery:GetObjectSize(scenery) + _coord:MarkToAll(string.format("Scenery %s\nx=%.1f y=%.1f z=%.1f\nl=%.1f d=%.1f\nspot %d safe=%s", scenery:getTypeName(),x,y,z,l,_dist, _termid, tostring(_safe))) + end + + if scanscenery and not _safe then + occupied=true + end + end + + -- Now check the already given spots so that we do not put a large aircraft next to one we already assigned a nearby spot. + for _,_takenspot in pairs(validspots) do + local _dist=_takenspot.Coordinate:Get2DDistance(_spot) + local _safe=_overlap(aircraft, aircraft, _dist) + if not _safe then + occupied=true + end + end + + --_spot:MarkToAll(string.format("Parking spot %d free=%s", parkingspot.TerminalID, tostring(not occupied))) + if occupied then + self:I(string.format("%s: Parking spot id %d occupied.", airport, _termid)) + else + self:I(string.format("%s: Parking spot id %d free.", airport, _termid)) + if nvalid<_nspots then + table.insert(validspots, {Coordinate=_spot, TerminalID=_termid}) + end + nvalid=nvalid+1 + self:I(string.format("%s: Parking spot id %d free. Nfree=%d/%d.", airport, _termid, nvalid,_nspots)) + end + + end -- loop over units + + -- We found enough spots. + if nvalid>=_nspots then + return validspots + end + end -- check terminal type + end + + -- Retrun spots we found, even if there were not enough. + return validspots + +end + +--- Check black and white lists. +-- @param #AIRBASE self +-- @param #number TerminalID Terminal ID to check. +-- @return #boolean `true` if this is a valid spot. +function AIRBASE:_CheckParkingLists(TerminalID) + + -- First check the black list. If we find a match, this spot is forbidden! + if self.parkingBlacklist and #self.parkingBlacklist>0 then + for _,terminalID in pairs(self.parkingBlacklist or {}) do + if terminalID==TerminalID then + -- This is a invalid spot. + return false + end + end + end + + + -- Check if a whitelist was defined. + if self.parkingWhitelist and #self.parkingWhitelist>0 then + for _,terminalID in pairs(self.parkingWhitelist or {}) do + if terminalID==TerminalID then + -- This is a valid spot. + return true + end + end + -- No match ==> invalid spot + return false + end + + -- Neither black nor white lists were defined or spot is not in black list. + return true +end + +--- Helper function to check for the correct terminal type including "artificial" ones. +-- @param #number Term_Type Termial type from getParking routine. +-- @param #AIRBASE.TerminalType termtype Terminal type from AIRBASE.TerminalType enumerator. +-- @return #boolean True if terminal types match. +function AIRBASE._CheckTerminalType(Term_Type, termtype) + + -- Nill check for Term_Type. + if Term_Type==nil then + return false + end + + -- If no terminal type is requested, we return true. BUT runways are excluded unless explicitly requested. + if termtype==nil then + if Term_Type==AIRBASE.TerminalType.Runway then + return false + else + return true + end + end + + -- Init no match. + local match=false + + -- Standar case. + if Term_Type==termtype then + match=true + end + + -- Artificial cases. Combination of terminal types. + if termtype==AIRBASE.TerminalType.OpenMedOrBig then + if Term_Type==AIRBASE.TerminalType.OpenMed or Term_Type==AIRBASE.TerminalType.OpenBig then + match=true + end + elseif termtype==AIRBASE.TerminalType.HelicopterUsable then + if Term_Type==AIRBASE.TerminalType.OpenMed or Term_Type==AIRBASE.TerminalType.OpenBig or Term_Type==AIRBASE.TerminalType.HelicopterOnly then + match=true + end + elseif termtype==AIRBASE.TerminalType.FighterAircraft then + if Term_Type==AIRBASE.TerminalType.OpenMed or Term_Type==AIRBASE.TerminalType.OpenBig or Term_Type==AIRBASE.TerminalType.Shelter then + match=true + end + end + + return match +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Runway +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get runways data. Only for airdromes! +-- @param #AIRBASE self +-- @param #number magvar (Optional) Magnetic variation in degrees. +-- @param #boolean mark (Optional) Place markers with runway data on F10 map. +-- @return #table Runway data. +function AIRBASE:GetRunwayData(magvar, mark) + + -- Runway table. + local runways={} + + if self:GetAirbaseCategory()~=Airbase.Category.AIRDROME then + return {} + end + + -- Get spawn points on runway. These can be used to determine the runway heading. + local runwaycoords=self:GetParkingSpotsCoordinates(AIRBASE.TerminalType.Runway) + + -- Debug: For finding the numbers of the spawn points belonging to each runway. + if false then + for i,_coord in pairs(runwaycoords) do + local coord=_coord --Core.Point#COORDINATE + coord:Translate(100, 0):MarkToAll("Runway i="..i) + end + end + + -- Magnetic declination. + magvar=magvar or UTILS.GetMagneticDeclination() + + -- Number of runways. + local N=#runwaycoords + local N2=N/2 + local exception=false + + -- Airbase name. + local name=self:GetName() + + + -- Exceptions + if name==AIRBASE.Nevada.Jean_Airport or + name==AIRBASE.Nevada.Creech_AFB or + name==AIRBASE.PersianGulf.Abu_Dhabi_International_Airport or + name==AIRBASE.PersianGulf.Dubai_Intl or + name==AIRBASE.PersianGulf.Shiraz_International_Airport or + name==AIRBASE.PersianGulf.Kish_International_Airport or + name==AIRBASE.MarianaIslands.Andersen_AFB then + + -- 1-->4, 2-->3, 3-->2, 4-->1 + exception=1 + + elseif UTILS.GetDCSMap()==DCSMAP.Syria and N>=2 and + name~=AIRBASE.Syria.Minakh and + name~=AIRBASE.Syria.Damascus and + name~=AIRBASE.Syria.Khalkhalah and + name~=AIRBASE.Syria.Marj_Ruhayyil and + name~=AIRBASE.Syria.Beirut_Rafic_Hariri then + + -- 1-->3, 2-->4, 3-->1, 4-->2 + exception=2 + + end + + --- Function returning the index of the runway coordinate belonding to the given index i. + local function f(i) + + local j + + if exception==1 then + + j=N-(i-1) -- 1-->4, 2-->3 + + elseif exception==2 then + + if i<=N2 then + j=i+N2 -- 1-->3, 2-->4 + else + j=i-N2 -- 3-->1, 4-->3 + end + + else + + if i%2==0 then + j=i-1 -- even 2-->1, 4-->3 + else + j=i+1 -- odd 1-->2, 3-->4 + end + + end + + -- Special case where there is no obvious order. + if name==AIRBASE.Syria.Beirut_Rafic_Hariri then + if i==1 then + j=3 + elseif i==2 then + j=6 + elseif i==3 then + j=1 + elseif i==4 then + j=5 + elseif i==5 then + j=4 + elseif i==6 then + j=2 + end + end + + if name==AIRBASE.Syria.Ramat_David then + if i==1 then + j=4 + elseif i==2 then + j=6 + elseif i==3 then + j=5 + elseif i==4 then + j=1 + elseif i==5 then + j=3 + elseif i==6 then + j=2 + end + end + + return j + end + + + for i=1,N do + + -- Get the other spawn point coordinate. + local j=f(i) + + -- Debug info. + --env.info(string.format("Runway i=%s j=%s (N=%d #runwaycoord=%d)", tostring(i), tostring(j), N, #runwaycoords)) + + -- Coordinates of the two runway points. + local c1=runwaycoords[i] --Core.Point#COORDINATE + local c2=runwaycoords[j] --Core.Point#COORDINATE + + -- Heading of runway. + local hdg=c1:HeadingTo(c2) + + -- Runway ID: heading=070° ==> idx="07" + local idx=string.format("%02d", UTILS.Round((hdg-magvar)/10, 0)) + + -- Runway table. + local runway={} --#AIRBASE.Runway + runway.heading=hdg + runway.idx=idx + runway.length=c1:Get2DDistance(c2) + runway.position=c1 + runway.endpoint=c2 + + -- Debug info. + --self:I(string.format("Airbase %s: Adding runway id=%s, heading=%03d, length=%d m i=%d j=%d", self:GetName(), runway.idx, runway.heading, runway.length, i, j)) + + -- Debug mark + if mark then + runway.position:MarkToAll(string.format("Runway %s: true heading=%03d (magvar=%d), length=%d m, i=%d, j=%d", runway.idx, runway.heading, magvar, runway.length, i, j)) + end + + -- Add runway. + table.insert(runways, runway) + + end + + return runways +end + +--- Set the active runway in case it cannot be determined by the wind direction. +-- @param #AIRBASE self +-- @param #number iactive Number of the active runway in the runway data table. +function AIRBASE:SetActiveRunway(iactive) + self.activerwyno=iactive +end + +--- Get the active runway based on current wind direction. +-- @param #AIRBASE self +-- @param #number magvar (Optional) Magnetic variation in degrees. +-- @return #AIRBASE.Runway Active runway data table. +function AIRBASE:GetActiveRunway(magvar) + + -- Get runways data (initialize if necessary). + local runways=self:GetRunwayData(magvar) + + -- Return user forced active runway if it was set. + if self.activerwyno then + return runways[self.activerwyno] + end + + -- Get wind vector. + local Vwind=self:GetCoordinate():GetWindWithTurbulenceVec3() + local norm=UTILS.VecNorm(Vwind) + + -- Active runway number. + local iact=1 + + -- Check if wind is blowing (norm>0). + if norm>0 then + + -- Normalize wind (not necessary). + Vwind.x=Vwind.x/norm + Vwind.y=0 + Vwind.z=Vwind.z/norm + + -- Loop over runways. + local dotmin=nil + for i,_runway in pairs(runways) do + local runway=_runway --#AIRBASE.Runway + + -- Angle in rad. + local alpha=math.rad(runway.heading) + + -- Runway vector. + local Vrunway={x=math.cos(alpha), y=0, z=math.sin(alpha)} + + -- Dot product: parallel component of the two vectors. + local dot=UTILS.VecDot(Vwind, Vrunway) + + -- Debug. + --env.info(string.format("runway=%03d° dot=%.3f", runway.heading, dot)) + + -- New min? + if dotmin==nil or dot radius %.1f m. Despawn = %s.", self:GetName(), unit:GetName(), group:GetName(),_i, dist, radius, tostring(despawn))) + --unit:FlareGreen() + end + + end + else + self:T(string.format("%s, checking if unit %s of group %s is on runway. Unit is NOT alive.",self:GetName(), unit:GetName(), group:GetName())) + end + end + else + self:T(string.format("%s, checking if group %s is on runway. Group is NOT alive.",self:GetName(), group:GetName())) + end + + return false +end +--- **Wrapper** -- SCENERY models scenery within the DCS simulator. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- ### Contributions: +-- +-- === +-- +-- @module Wrapper.Scenery +-- @image Wrapper_Scenery.JPG + + + +--- @type SCENERY +-- @extends Wrapper.Positionable#POSITIONABLE + + +--- Wrapper class to handle Scenery objects that are defined on the map. +-- +-- The @{Wrapper.Scenery#SCENERY} class is a wrapper class to handle the DCS Scenery objects: +-- +-- * Wraps the DCS Scenery objects. +-- * Support all DCS Scenery APIs. +-- * Enhance with Scenery specific APIs not in the DCS API set. +-- +-- @field #SCENERY +SCENERY = { + ClassName = "SCENERY", +} + + +--- Register scenery object as POSITIONABLE. +--@param #SCENERY self +--@param #string SceneryName Scenery name. +--@param #DCS.Object SceneryObject DCS scenery object. +--@return #SCENERY Scenery object. +function SCENERY:Register( SceneryName, SceneryObject ) + local self = BASE:Inherit( self, POSITIONABLE:New( SceneryName ) ) + self.SceneryName = SceneryName + self.SceneryObject = SceneryObject + return self +end + +--- Register scenery object as POSITIONABLE. +--@param #SCENERY self +--@return #DCS.Object DCS scenery object. +function SCENERY:GetDCSObject() + return self.SceneryObject +end + +--- Register scenery object as POSITIONABLE. +--@param #SCENERY self +--@return #number Threat level 0. +--@return #string "Scenery". +function SCENERY:GetThreatLevel() + return 0, "Scenery" +end + +--- Find a SCENERY object by it's name/id. +--@param #SCENERY self +--@param #string name The name/id of the scenery object as taken from the ME. Ex. '595785449' +--@return #SCENERY Scenery Object or nil if not found. +function SCENERY:FindByName(name) + local findAirbase = function () + local airbases = AIRBASE.GetAllAirbases() + for index,airbase in pairs(airbases) do + local surftype = airbase:GetCoordinate():GetSurfaceType() + if surftype ~= land.SurfaceType.SHALLOW_WATER and surftype ~= land.SurfaceType.WATER then + return airbase:GetCoordinate() + end + end + return nil + end + + local sceneryScan = function (scancoord) + if scancoord ~= nil then + local _,_,sceneryfound,_,_,scenerylist = scancoord:ScanObjects(200, false, false, true) + if sceneryfound == true then + scenerylist[1].id_ = name + SCENERY.SceneryObject = SCENERY:Register(scenerylist[1].id_, scenerylist[1]) + return SCENERY.SceneryObject + end + end + return nil + end + + if SCENERY.SceneryObject then + SCENERY.SceneryObject.SceneryObject.id_ = name + SCENERY.SceneryObject.SceneryName = name + return SCENERY:Register(SCENERY.SceneryObject.SceneryObject.id_, SCENERY.SceneryObject.SceneryObject) + else + return sceneryScan(findAirbase()) + end +end +--- **Wrapper** - Markers On the F10 map. +-- +-- **Main Features:** +-- +-- * Convenient handling of markers via multiple user API functions. +-- * Update text and position of marker easily via scripting. +-- * Delay creation and removal of markers via (optional) parameters. +-- * Retrieve data such as text and coordinate. +-- * Marker specific FSM events when a marker is added, removed or changed. +-- * Additional FSM events when marker text or position is changed. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Wrapper.Marker +-- @image MOOSE_Core.JPG + + +--- Marker class. +-- @type MARKER +-- @field #string ClassName Name of the class. +-- @field #boolean Debug Debug mode. Messages to all about status. +-- @field #string lid Class id string for output to DCS log file. +-- @field #number mid Marker ID. +-- @field Core.Point#COORDINATE coordinate Coordinate of the mark. +-- @field #string text Text displayed in the mark panel. +-- @field #string message Message dispayed when the mark is added. +-- @field #boolean readonly Marker is read-only. +-- @field #number coalition Coalition to which the marker is displayed. +-- @extends Core.Fsm#FSM + +--- Just because... +-- +-- === +-- +-- ![Banner Image](..\Presentations\MARKER\Marker_Main.jpg) +-- +-- # The MARKER Class Idea +-- +-- The MARKER class simplifies creating, updating and removing of markers on the F10 map. +-- +-- # Create a Marker +-- +-- -- Create a MARKER object at Batumi with a trivial text. +-- local Coordinate=AIRBASE:FindByName("Batumi"):GetCoordinate() +-- mymarker=MARKER:New(Coordinate, "I am Batumi Airfield") +-- +-- Now this does **not** show the marker yet. We still need to specifiy to whom it is shown. There are several options, i.e. +-- show the marker to everyone, to a speficic coaliton only, or only to a specific group. +-- +-- ## For Everyone +-- +-- If the marker should be visible to everyone, you can use the :ToAll() function. +-- +-- mymarker=MARKER:New(Coordinate, "I am Batumi Airfield"):ToAll() +-- +-- ## For a Coaliton +-- +-- If the maker should be visible to a specific coalition, you can use the :ToCoalition() function. +-- +-- mymarker=MARKER:New(Coordinate, "I am Batumi Airfield"):ToCoaliton(coaliton.side.BLUE) +-- +-- ### To Blue Coaliton +-- +-- ### To Red Coalition +-- +-- This would show the marker only to the Blue coaliton. +-- +-- ## For a Group +-- +-- +-- # Removing a Marker +-- +-- +-- # Updating a Marker +-- +-- The marker text and coordinate can be updated easily as shown below. +-- +-- However, note that **updateing involves to remove and recreate the marker if either text or its coordinate is changed**. +-- *This is a DCS scripting engine limitation.* +-- +-- ## Update Text +-- +-- If you created a marker "mymarker" as shown above, you can update the dispayed test by +-- +-- mymarker:UpdateText("I am the new text at Batumi") +-- +-- The update can also be delayed by, e.g. 90 seconds, using +-- +-- mymarker:UpdateText("I am the new text at Batumi", 90) +-- +-- ## Update Coordinate +-- +-- If you created a marker "mymarker" as shown above, you can update its coordinate on the F10 map by +-- +-- mymarker:UpdateCoordinate(NewCoordinate) +-- +-- The update can also be delayed by, e.g. 60 seconds, using +-- +-- mymarker:UpdateCoordinate(NewCoordinate, 60) +-- +-- # Retrieve Data +-- +-- The important data as the displayed text and the coordinate of the marker can be retrieved easily. +-- +-- ## Text +-- +-- local text=mymarker:GetText() +-- env.info("Marker Text = " .. text) +-- +-- ## Coordinate +-- +-- local Coordinate=mymarker:GetCoordinate() +-- env.info("Marker Coordinate LL DSM = " .. Coordinate:ToStringLLDMS()) +-- +-- +-- # FSM Events +-- +-- Moose creates addditonal events, so called FSM event, when markers are added, changed, removed, and text or the coordianteis updated. +-- +-- These events can be captured and used for processing via OnAfter functions as shown below. +-- +-- ## Added +-- +-- ## Changed +-- +-- ## Removed +-- +-- ## TextUpdate +-- +-- ## CoordUpdate +-- +-- +-- # Examples +-- +-- +-- @field #MARKER +MARKER = { + ClassName = "MARKER", + Debug = false, + lid = nil, + mid = nil, + coordinate = nil, + text = nil, + message = nil, + readonly = nil, + coalition = nil, +} + +--- Marker ID. Running number. +_MARKERID=0 + +--- Marker class version. +-- @field #string version +MARKER.version="0.1.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: User "Get" functions. E.g., :GetCoordinate() +-- DONE: Add delay to user functions. +-- DONE: Handle events. +-- DONE: Create FSM events. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new MARKER class object. +-- @param #MARKER self +-- @param Core.Point#COORDINATE Coordinate Coordinate where to place the marker. +-- @param #string Text Text displayed on the mark panel. +-- @return #MARKER self +function MARKER:New(Coordinate, Text) + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #MARKER + + self.coordinate=Coordinate + + self.text=Text + + -- Defaults + self.readonly=false + self.message="" + + -- New marker ID. This is not the one of the actual marker. + _MARKERID=_MARKERID+1 + + self.myid=_MARKERID + + -- Log ID. + self.lid=string.format("Marker #%d | ", self.myid) + + -- Start State. + self:SetStartState("Invisible") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Invisible", "Added", "Visible") -- Marker was added. + self:AddTransition("Visible", "Removed", "Invisible") -- Marker was removed. + self:AddTransition("*", "Changed", "*") -- Marker was changed. + + self:AddTransition("*", "TextUpdate", "*") -- Text updated. + self:AddTransition("*", "CoordUpdate", "*") -- Coordinates updated. + + --- Triggers the FSM event "Added". + -- @function [parent=#MARKER] Added + -- @param #MARKER self + -- @param Core.Event#EVENTDATA EventData Event data table. + + --- Triggers the delayed FSM event "Added". + -- @function [parent=#MARKER] __Added + -- @param #MARKER self + -- @param Core.Event#EVENTDATA EventData Event data table. + + --- On after "Added" event user function. + -- @function [parent=#MARKER] OnAfterAdded + -- @param #MARKER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Event#EVENTDATA EventData Event data table. + + + --- Triggers the FSM event "Removed". + -- @function [parent=#MARKER] Removed + -- @param #MARKER self + -- @param Core.Event#EVENTDATA EventData Event data table. + + --- Triggers the delayed FSM event "Removed". + -- @function [parent=#MARKER] __Removed + -- @param #MARKER self + -- @param Core.Event#EVENTDATA EventData Event data table. + + --- On after "Removed" event user function. + -- @function [parent=#MARKER] OnAfterRemoved + -- @param #MARKER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Event#EVENTDATA EventData Event data table. + + + --- Triggers the FSM event "Changed". + -- @function [parent=#MARKER] Changed + -- @param #MARKER self + -- @param Core.Event#EVENTDATA EventData Event data table. + + --- Triggers the delayed FSM event "Changed". + -- @function [parent=#MARKER] __Changed + -- @param #MARKER self + -- @param Core.Event#EVENTDATA EventData Event data table. + + --- On after "Changed" event user function. + -- @function [parent=#MARKER] OnAfterChanged + -- @param #MARKER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Event#EVENTDATA EventData Event data table. + + + --- Triggers the FSM event "TextUpdate". + -- @function [parent=#MARKER] TextUpdate + -- @param #MARKER self + -- @param #string Text The new text. + + --- Triggers the delayed FSM event "TextUpdate". + -- @function [parent=#MARKER] __TextUpdate + -- @param #MARKER self + -- @param #string Text The new text. + + --- On after "TextUpdate" event user function. + -- @function [parent=#MARKER] OnAfterTextUpdate + -- @param #MARKER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string Text The new text. + + + --- Triggers the FSM event "CoordUpdate". + -- @function [parent=#MARKER] CoordUpdate + -- @param #MARKER self + -- @param Core.Point#COORDINATE Coordinate The new Coordinate. + + --- Triggers the delayed FSM event "CoordUpdate". + -- @function [parent=#MARKER] __CoordUpdate + -- @param #MARKER self + -- @param Core.Point#COORDINATE Coordinate The updated Coordinate. + + --- On after "CoordUpdate" event user function. + -- @function [parent=#MARKER] OnAfterCoordUpdate + -- @param #MARKER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Point#COORDINATE Coordinate The updated Coordinate. + + + -- Handle events. + self:HandleEvent(EVENTS.MarkAdded) + self:HandleEvent(EVENTS.MarkRemoved) + self:HandleEvent(EVENTS.MarkChange) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User API Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Marker is readonly. Text cannot be changed and marker cannot be removed. +-- @param #MARKER self +-- @return #MARKER self +function MARKER:ReadOnly() + + self.readonly=true + + return self +end + +--- Set message that is displayed on screen if the marker is added. +-- @param #MARKER self +-- @param #string Text Message displayed when the marker is added. +-- @return #MARKER self +function MARKER:Message(Text) + + self.message=Text or "" + + return self +end + +--- Place marker visible for everyone. +-- @param #MARKER self +-- @param #number Delay (Optional) Delay in seconds, before the marker is created. +-- @return #MARKER self +function MARKER:ToAll(Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, MARKER.ToAll, self) + else + + self.toall=true + self.tocoaliton=nil + self.coalition=nil + self.togroup=nil + self.groupname=nil + self.groupid=nil + + -- First remove an existing mark. + if self.shown then + self:Remove() + end + + self.mid=UTILS.GetMarkID() + + -- Call DCS function. + trigger.action.markToAll(self.mid, self.text, self.coordinate:GetVec3(), self.readonly, self.message) + + end + + return self +end + +--- Place marker visible for a specific coalition only. +-- @param #MARKER self +-- @param #number Coalition Coalition 1=Red, 2=Blue, 0=Neutral. See `coaliton.side.RED`. +-- @param #number Delay (Optional) Delay in seconds, before the marker is created. +-- @return #MARKER self +function MARKER:ToCoalition(Coalition, Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, MARKER.ToCoalition, self, Coalition) + else + + self.coalition=Coalition + + self.tocoaliton=true + self.toall=false + self.togroup=false + self.groupname=nil + self.groupid=nil + + -- First remove an existing mark. + if self.shown then + self:Remove() + end + + self.mid=UTILS.GetMarkID() + + -- Call DCS function. + trigger.action.markToCoalition(self.mid, self.text, self.coordinate:GetVec3(), self.coalition, self.readonly, self.message) + + end + + return self +end + +--- Place marker visible for the blue coalition only. +-- @param #MARKER self +-- @param #number Delay (Optional) Delay in seconds, before the marker is created. +-- @return #MARKER self +function MARKER:ToBlue(Delay) + self:ToCoalition(coalition.side.BLUE, Delay) + return self +end + +--- Place marker visible for the blue coalition only. +-- @param #MARKER self +-- @param #number Delay (Optional) Delay in seconds, before the marker is created. +-- @return #MARKER self +function MARKER:ToRed(Delay) + self:ToCoalition(coalition.side.RED, Delay) + return self +end + +--- Place marker visible for the neutral coalition only. +-- @param #MARKER self +-- @param #number Delay (Optional) Delay in seconds, before the marker is created. +-- @return #MARKER self +function MARKER:ToNeutral(Delay) + self:ToCoalition(coalition.side.NEUTRAL, Delay) + return self +end + + +--- Place marker visible for a specific group only. +-- @param #MARKER self +-- @param Wrapper.Group#GROUP Group The group to which the marker is displayed. +-- @param #number Delay (Optional) Delay in seconds, before the marker is created. +-- @return #MARKER self +function MARKER:ToGroup(Group, Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, MARKER.ToGroup, self, Group) + else + + -- Check if group exists. + if Group and Group:IsAlive()~=nil then + + self.groupid=Group:GetID() + + if self.groupid then + + self.groupname=Group:GetName() + + self.togroup=true + self.tocoaliton=nil + self.coalition=nil + self.toall=nil + + -- First remove an existing mark. + if self.shown then + self:Remove() + end + + self.mid=UTILS.GetMarkID() + + -- Call DCS function. + trigger.action.markToGroup(self.mid, self.text, self.coordinate:GetVec3(), self.groupid, self.readonly, self.message) + + end + + else + --TODO: Warning! + end + + end + + return self +end + +--- Update the text displayed on the mark panel. +-- @param #MARKER self +-- @param #string Text Updated text. +-- @param #number Delay (Optional) Delay in seconds, before the marker is created. +-- @return #MARKER self +function MARKER:UpdateText(Text, Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, MARKER.UpdateText, self, Text) + else + + self.text=tostring(Text) + + self:Refresh() + + self:TextUpdate(tostring(Text)) + + end + + return self +end + +--- Update the coordinate where the marker is displayed. +-- @param #MARKER self +-- @param Core.Point#COORDINATE Coordinate The new coordinate. +-- @param #number Delay (Optional) Delay in seconds, before the marker is created. +-- @return #MARKER self +function MARKER:UpdateCoordinate(Coordinate, Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, MARKER.UpdateCoordinate, self, Coordinate) + else + + self.coordinate=Coordinate + + self:Refresh() + + self:CoordUpdate(Coordinate) + + end + + return self +end + +--- Refresh the marker. +-- @param #MARKER self +-- @param #number Delay (Optional) Delay in seconds, before the marker is created. +-- @return #MARKER self +function MARKER:Refresh(Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, MARKER.Refresh, self) + else + + if self.toall then + + self:ToAll() + + elseif self.tocoaliton then + + self:ToCoalition(self.coalition) + + elseif self.togroup then + + local group=GROUP:FindByName(self.groupname) + + self:ToGroup(group) + + else + self:E(self.lid.."ERROR: unknown To in :Refresh()!") + end + + end + + return self +end + +--- Remove a marker. +-- @param #MARKER self +-- @param #number Delay (Optional) Delay in seconds, before the marker is removed. +-- @return #MARKER self +function MARKER:Remove(Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, MARKER.Remove, self) + else + + if self.shown then + + -- Call DCS function. + trigger.action.removeMark(self.mid) + + end + + end + + return self +end + +--- Get position of the marker. +-- @param #MARKER self +-- @return Core.Point#COORDINATE The coordinate of the marker. +function MARKER:GetCoordinate() + return self.coordinate +end + +--- Get text that is displayed in the marker panel. +-- @param #MARKER self +-- @return #string Marker text. +function MARKER:GetText() + return self.text +end + +--- Set text that is displayed in the marker panel. Note this does not show the marker. +-- @param #MARKER self +-- @param #string Text Marker text. Default is an empty sting "". +-- @return #MARKER self +function MARKER:SetText(Text) + self.text=Text and tostring(Text) or "" + return self +end + + +--- Check if marker is currently visible on the F10 map. +-- @param #MARKER self +-- @return #boolean True if the marker is currently visible. +function MARKER:IsVisible() + return self:Is("Visible") +end + +--- Check if marker is currently invisible on the F10 map. +-- @param #MARKER self +-- @return +function MARKER:IsInvisible() + return self:Is("Invisible") +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Event Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Event function when a MARKER is added. +-- @param #MARKER self +-- @param Core.Event#EVENTDATA EventData +function MARKER:OnEventMarkAdded(EventData) + + if EventData and EventData.MarkID then + + local MarkID=EventData.MarkID + + self:T3(self.lid..string.format("Captured event MarkAdded for Mark ID=%s", tostring(MarkID))) + + if MarkID==self.mid then + + self.shown=true + + self:Added(EventData) + + end + + end + +end + +--- Event function when a MARKER is removed. +-- @param #MARKER self +-- @param Core.Event#EVENTDATA EventData +function MARKER:OnEventMarkRemoved(EventData) + + if EventData and EventData.MarkID then + + local MarkID=EventData.MarkID + + self:T3(self.lid..string.format("Captured event MarkRemoved for Mark ID=%s", tostring(MarkID))) + + if MarkID==self.mid then + + self.shown=false + + self:Removed(EventData) + + end + + end + +end + +--- Event function when a MARKER changed. +-- @param #MARKER self +-- @param Core.Event#EVENTDATA EventData +function MARKER:OnEventMarkChange(EventData) + + if EventData and EventData.MarkID then + + local MarkID=EventData.MarkID + + self:T3(self.lid..string.format("Captured event MarkChange for Mark ID=%s", tostring(MarkID))) + + if MarkID==self.mid then + + self.text=tostring(EventData.MarkText) + + self:Changed(EventData) + + end + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Event Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "Added" event. +-- @param #MARKER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Event#EVENTDATA EventData Event data table. +function MARKER:onafterAdded(From, Event, To, EventData) + + -- Debug info. + local text=string.format("Captured event MarkAdded for myself:\n") + text=text..string.format("Marker ID = %s\n", tostring(EventData.MarkID)) + text=text..string.format("Coalition = %s\n", tostring(EventData.MarkCoalition)) + text=text..string.format("Group ID = %s\n", tostring(EventData.MarkGroupID)) + text=text..string.format("Initiator = %s\n", EventData.IniUnit and EventData.IniUnit:GetName() or "Nobody") + text=text..string.format("Coordinate = %s\n", EventData.MarkCoordinate and EventData.MarkCoordinate:ToStringLLDMS() or "Nowhere") + text=text..string.format("Text: \n%s", tostring(EventData.MarkText)) + self:T2(self.lid..text) + +end + +--- On after "Removed" event. +-- @param #MARKER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Event#EVENTDATA EventData Event data table. +function MARKER:onafterRemoved(From, Event, To, EventData) + + -- Debug info. + local text=string.format("Captured event MarkRemoved for myself:\n") + text=text..string.format("Marker ID = %s\n", tostring(EventData.MarkID)) + text=text..string.format("Coalition = %s\n", tostring(EventData.MarkCoalition)) + text=text..string.format("Group ID = %s\n", tostring(EventData.MarkGroupID)) + text=text..string.format("Initiator = %s\n", EventData.IniUnit and EventData.IniUnit:GetName() or "Nobody") + text=text..string.format("Coordinate = %s\n", EventData.MarkCoordinate and EventData.MarkCoordinate:ToStringLLDMS() or "Nowhere") + text=text..string.format("Text: \n%s", tostring(EventData.MarkText)) + self:T2(self.lid..text) + +end + +--- On after "Changed" event. +-- @param #MARKER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Event#EVENTDATA EventData Event data table. +function MARKER:onafterChanged(From, Event, To, EventData) + + -- Debug info. + local text=string.format("Captured event MarkChange for myself:\n") + text=text..string.format("Marker ID = %s\n", tostring(EventData.MarkID)) + text=text..string.format("Coalition = %s\n", tostring(EventData.MarkCoalition)) + text=text..string.format("Group ID = %s\n", tostring(EventData.MarkGroupID)) + text=text..string.format("Initiator = %s\n", EventData.IniUnit and EventData.IniUnit:GetName() or "Nobody") + text=text..string.format("Coordinate = %s\n", EventData.MarkCoordinate and EventData.MarkCoordinate:ToStringLLDMS() or "Nowhere") + text=text..string.format("Text: \n%s", tostring(EventData.MarkText)) + self:T2(self.lid..text) + +end + +--- On after "TextUpdate" event. +-- @param #MARKER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string Text The updated text, displayed in the mark panel. +function MARKER:onafterTextUpdate(From, Event, To, Text) + + self:T(self.lid..string.format("New Marker Text:\n%s", Text)) + +end + +--- On after "CoordUpdate" event. +-- @param #MARKER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Point#COORDINATE Coordinate The updated coordinates. +function MARKER:onafterCoordUpdate(From, Event, To, Coordinate) + + self:T(self.lid..string.format("New Marker Coordinate in LL DMS: %s", Coordinate:ToStringLLDMS())) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- **Cargo** - Management of CARGO logistics, that can be transported from and to transportation carriers. +-- +-- === +-- +-- # 1) MOOSE Cargo System. +-- +-- #### Those who have used the mission editor, know that the DCS mission editor provides cargo facilities. +-- However, these are merely static objects. Wouldn't it be nice if cargo could bring a new dynamism into your +-- simulations? Where various objects of various types could be treated also as cargo? +-- +-- This is what MOOSE brings to you, a complete new cargo object model that used the cargo capabilities of +-- DCS world, but enhances it. +-- +-- MOOSE Cargo introduces also a new concept, called a "carrier". These can be: +-- +-- - Helicopters +-- - Planes +-- - Ground Vehicles +-- - Ships +-- +-- With the MOOSE Cargo system, you can: +-- +-- - Take full control of the cargo as objects within your script (see below). +-- - Board/Unboard infantry into carriers. Also other objects can be boarded, like mortars. +-- - Load/Unload dcs world cargo objects into carriers. +-- - Load/Unload other static objects into carriers (like tires etc). +-- - Slingload cargo objects. +-- - Board units one by one... +-- +-- # 2) MOOSE Cargo Objects. +-- +-- In order to make use of the MOOSE cargo system, you need to **declare** the DCS objects as MOOSE cargo objects! +-- +-- This sounds complicated, but it is actually quite simple. +-- +-- See here an example: +-- +-- local EngineerCargoGroup = CARGO_GROUP:New( GROUP:FindByName( "Engineers" ), "Workmaterials", "Engineers", 250 ) +-- +-- The above code declares a MOOSE cargo object called `EngineerCargoGroup`. +-- It actually just refers to an infantry group created within the sim called `"Engineers"`. +-- The infantry group now becomes controlled by the MOOSE cargo object `EngineerCargoGroup`. +-- A MOOSE cargo object also has properties, like the type of cargo, the logical name, and the reporting range. +-- +-- There are 4 types of MOOSE cargo objects possible, each represented by its own class: +-- +-- - @{Cargo.CargoGroup#CARGO_GROUP}: A MOOSE cargo that is represented by a DCS world GROUP object. +-- - @{Cargo.CargoCrate#CARGO_CRATE}: A MOOSE cargo that is represented by a DCS world cargo object (static object). +-- - @{Cargo.CargoUnit#CARGO_UNIT}: A MOOSE cargo that is represented by a DCS world unit object or static object. +-- - @{Cargo.CargoSlingload#CARGO_SLINGLOAD}: A MOOSE cargo that is represented by a DCS world cargo object (static object), that can be slingloaded. +-- +-- Note that a CARGO crate is not meant to be slingloaded (it can, but it is not **meant** to be handled like that. +-- Instead, a CARGO_CRATE is able to load itself into the bays of a carrier. +-- +-- Each of these MOOSE cargo objects behave in its own way, and have methods to be handled. +-- +-- local InfantryGroup = GROUP:FindByName( "Infantry" ) +-- local InfantryCargo = CARGO_GROUP:New( InfantryGroup, "Engineers", "Infantry Engineers", 2000 ) +-- local CargoCarrier = UNIT:FindByName( "Carrier" ) +-- -- This call will make the Cargo run to the CargoCarrier. +-- -- Upon arrival at the CargoCarrier, the Cargo will be Loaded into the Carrier. +-- -- This process is now fully automated. +-- InfantryCargo:Board( CargoCarrier, 25 ) +-- +-- The above would create a MOOSE cargo object called `InfantryCargo`, and using that object, +-- you can board the cargo into the carrier `CargoCarrier`. +-- Simple, isn't it? Told you, and this is only the beginning. +-- +-- The boarding, unboarding, loading, unloading of cargo is however something that is not meant to be coded manualy by mission designers. +-- It would be too low-level and not end-user friendly to deal with cargo handling complexity. +-- Things can become really complex if you want to make cargo being handled and behave in multiple scenarios. +-- +-- # 3) Cargo Handling Classes, the main engines for mission designers! +-- +-- For this reason, the MOOSE Cargo System is heavily used by 3 important **cargo handling class hierarchies** within MOOSE, +-- that make cargo come "alive" within your mission in a full automatic manner! +-- +-- ## 3.1) AI Cargo handlers. +-- +-- - @{AI.AI_Cargo_APC} will create for you the capatility to make an APC group handle cargo. +-- - @{AI.AI_Cargo_Helicopter} will create for you the capatility to make a Helicopter group handle cargo. +-- +-- +-- ## 3.2) AI Cargo transportation dispatchers. +-- +-- There are also dispatchers that make AI work together to transport cargo automatically!!! +-- +-- - @{AI.AI_Cargo_Dispatcher_APC} derived classes will create for your dynamic cargo handlers controlled by AI ground vehicle groups (APCs) to transport cargo between sites. +-- - @{AI.AI_Cargo_Dispatcher_Helicopters} derived classes will create for your dynamic cargo handlers controlled by AI helicpter groups to transport cargo between sites. +-- +-- ## 3.3) Cargo transportation tasking. +-- +-- And there is cargo transportation tasking for human players. +-- +-- - @{Tasking.Task_CARGO} derived classes will create for you cargo transportation tasks, that allow human players to interact with MOOSE cargo objects to complete tasks. +-- +-- Please refer to the documentation reflected within these modules to understand the detailed capabilties. +-- +-- # 4) Cargo SETs. +-- +-- To make life a bit more easy, MOOSE cargo objects can be grouped into a @{Core.Set#SET_CARGO}. +-- This is a collection of MOOSE cargo objects. +-- +-- This would work as follows: +-- +-- -- Define the cargo set. +-- local CargoSetWorkmaterials = SET_CARGO:New():FilterTypes( "Workmaterials" ):FilterStart() +-- +-- -- Now add cargo the cargo set. +-- local EngineerCargoGroup = CARGO_GROUP:New( GROUP:FindByName( "Engineers" ), "Workmaterials", "Engineers", 250 ) +-- local ConcreteCargo = CARGO_SLINGLOAD:New( STATIC:FindByName( "Concrete" ), "Workmaterials", "Concrete", 150, 50 ) +-- local CrateCargo = CARGO_CRATE:New( STATIC:FindByName( "Crate" ), "Workmaterials", "Crate", 150, 50 ) +-- local EnginesCargo = CARGO_CRATE:New( STATIC:FindByName( "Engines" ), "Workmaterials", "Engines", 150, 50 ) +-- local MetalCargo = CARGO_CRATE:New( STATIC:FindByName( "Metal" ), "Workmaterials", "Metal", 150, 50 ) +-- +-- This is a very powerful concept! +-- Instead of having to deal with multiple MOOSE cargo objects yourself, the cargo set capability will group cargo objects into one set. +-- The key is the **cargo type** name given at each cargo declaration! +-- In the above example, the cargo type name is `"Workmaterials"`. Each cargo object declared is given that type name. (the 2nd parameter). +-- What happens now is that the cargo set `CargoSetWorkmaterials` will be added with each cargo object **dynamically** when the cargo object is created. +-- In other words, the cargo set `CargoSetWorkmaterials` will incorporate any `"Workmaterials"` dynamically into its set. +-- +-- The cargo sets are extremely important for the AI cargo transportation dispatchers and the cargo transporation tasking. +-- +-- # 5) Declare cargo directly in the mission editor! +-- +-- But I am not finished! There is something more, that is even more great! +-- Imagine the mission designers having to code all these lines every time it wants to embed cargo within a mission. +-- +-- -- Now add cargo the cargo set. +-- local EngineerCargoGroup = CARGO_GROUP:New( GROUP:FindByName( "Engineers" ), "Workmaterials", "Engineers", 250 ) +-- local ConcreteCargo = CARGO_SLINGLOAD:New( STATIC:FindByName( "Concrete" ), "Workmaterials", "Concrete", 150, 50 ) +-- local CrateCargo = CARGO_CRATE:New( STATIC:FindByName( "Crate" ), "Workmaterials", "Crate", 150, 50 ) +-- local EnginesCargo = CARGO_CRATE:New( STATIC:FindByName( "Engines" ), "Workmaterials", "Engines", 150, 50 ) +-- local MetalCargo = CARGO_CRATE:New( STATIC:FindByName( "Metal" ), "Workmaterials", "Metal", 150, 50 ) +-- +-- This would be extremely tiring and a huge overload. +-- However, the MOOSE framework allows to declare MOOSE cargo objects within the mission editor!!! +-- +-- So, at mission startup, MOOSE will search for objects following a special naming convention, and will **create** for you **dynamically +-- cargo objects** at **mission start**!!! -- These cargo objects can then be automatically incorporated within cargo set(s)!!! +-- In other words, your mission will be reduced to about a few lines of code, providing you with a full dynamic cargo handling mission! +-- +-- ## 5.1) Use \#CARGO tags in the mission editor: +-- +-- MOOSE can create automatically cargo objects, if the name of the cargo contains the **\#CARGO** tag. +-- When a mission starts, MOOSE will scan all group and static objects it found for the presence of the \#CARGO tag. +-- When found, MOOSE will declare the object as cargo (create in the background a CARGO_ object, like CARGO_GROUP, CARGO_CRATE or CARGO_SLINGLOAD. +-- The creation of these CARGO_ objects will allow to be filtered and automatically added in SET_CARGO objects. +-- In other words, with very minimal code as explained in the above code section, you are able to create vast amounts of cargo objects just from within the editor. +-- +-- What I talk about is this: +-- +-- -- BEFORE THIS SCRIPT STARTS, MOOSE WILL ALREADY HAVE SCANNED FOR OBJECTS WITH THE #CARGO TAG IN THE NAME. +-- -- FOR EACH OF THESE OBJECT, MOOSE WILL HAVE CREATED CARGO_ OBJECTS LIKE CARGO_GROUP, CARGO_CRATE AND CARGO_SLINGLOAD. +-- +-- HQ = GROUP:FindByName( "HQ", "Bravo" ) +-- +-- CommandCenter = COMMANDCENTER +-- :New( HQ, "Lima" ) +-- +-- Mission = MISSION +-- :New( CommandCenter, "Operation Cargo Fun", "Tactical", "Transport Cargo", coalition.side.RED ) +-- +-- TransportGroups = SET_GROUP:New():FilterCoalitions( "blue" ):FilterPrefixes( "Transport" ):FilterStart() +-- +-- TaskDispatcher = TASK_CARGO_DISPATCHER:New( Mission, TransportGroups ) +-- +-- -- This is the most important now. You setup a new SET_CARGO filtering the relevant type. +-- -- The actual cargo objects are now created by MOOSE in the background. +-- -- Each cargo is setup in the Mission Editor using the #CARGO tag in the group name. +-- -- This allows a truly dynamic setup. +-- local CargoSetWorkmaterials = SET_CARGO:New():FilterTypes( "Workmaterials" ):FilterStart() +-- +-- local WorkplaceTask = TaskDispatcher:AddTransportTask( "Build a Workplace", CargoSetWorkmaterials, "Transport the workers, engineers and the equipment near the Workplace." ) +-- TaskDispatcher:SetTransportDeployZone( WorkplaceTask, ZONE:New( "Workplace" ) ) +-- +-- The above code example has the `CargoSetWorkmaterials`, which is a SET_CARGO collection and will include the CARGO_ objects of the type "Workmaterials". +-- And there is NO cargo object actually declared within the script! However, if you would open the mission, there would be hundreds of cargo objects... +-- +-- The \#CARGO tag even allows for several options to be specified, which are important to learn. +-- +-- ## 5.2) The \#CARGO tag to create CARGO_GROUP objects: +-- +-- You can also use the \#CARGO tag on **group** objects of the mission editor. +-- +-- For example, the following #CARGO naming in the **group name** of the object, will create a CARGO_GROUP object when the mission starts. +-- +-- `Infantry #CARGO(T=Workmaterials,RR=500,NR=25)` +-- +-- This will create a CARGO_GROUP object: +-- +-- * with the group name `Infantry #CARGO` +-- * is of type `Workmaterials` +-- * will report when a carrier is within 500 meters +-- * will board to carriers when the carrier is within 500 meters from the cargo object +-- * will dissapear when the cargo is within 25 meters from the carrier during boarding +-- +-- So the overall syntax of the #CARGO naming tag and arguments are: +-- +-- `GroupName #CARGO(T=CargoTypeName,RR=Range,NR=Range)` +-- +-- * **T=** Provide a text that contains the type name of the cargo object. This type name can be used to filter cargo within a SET_CARGO object. +-- * **RR=** Provide the minimal range in meters when the report to the carrier, and board to the carrier. +-- Note that this option is optional, so can be omitted. The default value of the RR is 250 meters. +-- * **NR=** Provide the maximum range in meters when the cargo units will be boarded within the carrier during boarding. +-- Note that this option is optional, so can be omitted. The default value of the RR is 10 meters. +-- +-- ## 5.2) The \#CARGO tag to create CARGO_CRATE objects: +-- +-- You can also use the \#CARGO tag on **static** objects, including **static cargo** objects of the mission editor. +-- +-- For example, the following #CARGO naming in the **static name** of the object, will create a CARGO_CRATE object when the mission starts. +-- +-- `Static #CARGO(T=Workmaterials,RR=500,NR=25)` +-- +-- This will create a CARGO_CRATE object: +-- +-- * with the group name `Static #CARGO` +-- * is of type `Workmaterials` +-- * will report when a carrier is within 500 meters +-- * will board to carriers when the carrier is within 500 meters from the cargo object +-- * will dissapear when the cargo is within 25 meters from the carrier during boarding +-- +-- So the overall syntax of the #CARGO naming tag and arguments are: +-- +-- `StaticName #CARGO(T=CargoTypeName,RR=Range,NR=Range)` +-- +-- * **T=** Provide a text that contains the type name of the cargo object. This type name can be used to filter cargo within a SET_CARGO object. +-- * **RR=** Provide the minimal range in meters when the report to the carrier, and board to the carrier. +-- Note that this option is optional, so can be omitted. The default value of the RR is 250 meters. +-- * **NR=** Provide the maximum range in meters when the cargo units will be boarded within the carrier during boarding. +-- Note that this option is optional, so can be omitted. The default value of the RR is 10 meters. +-- +-- === +-- +-- ### Author: **FlightControl** +-- ### Contributions: +-- +-- === +-- +-- @module Cargo.Cargo +-- @image Cargo.JPG + +-- Events + +-- Board + +--- Boards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo to the Carrier. +-- The cargo must be in the **UnLoaded** state. +-- @function [parent=#CARGO] Board +-- @param #CARGO self +-- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. +-- @param #number NearRadius The radius when the cargo will board the Carrier (to avoid collision). + +--- Boards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo to the Carrier. +-- The cargo must be in the **UnLoaded** state. +-- @function [parent=#CARGO] __Board +-- @param #CARGO self +-- @param #number DelaySeconds The amount of seconds to delay the action. +-- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. +-- @param #number NearRadius The radius when the cargo will board the Carrier (to avoid collision). + + +-- UnBoard + +--- UnBoards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo from the Carrier. +-- The cargo must be in the **Loaded** state. +-- @function [parent=#CARGO] UnBoard +-- @param #CARGO self +-- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Core.Point#POINT_VEC2) to where the cargo should run after onboarding. If not provided, the cargo will run to 60 meters behind the Carrier location. + +--- UnBoards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo from the Carrier. +-- The cargo must be in the **Loaded** state. +-- @function [parent=#CARGO] __UnBoard +-- @param #CARGO self +-- @param #number DelaySeconds The amount of seconds to delay the action. +-- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Core.Point#POINT_VEC2) to where the cargo should run after onboarding. If not provided, the cargo will run to 60 meters behind the Carrier location. + + +-- Load + +--- Loads the cargo to a Carrier. The event will load the cargo into the Carrier regardless of its position. There will be no movement simulated of the cargo loading. +-- The cargo must be in the **UnLoaded** state. +-- @function [parent=#CARGO] Load +-- @param #CARGO self +-- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. + +--- Loads the cargo to a Carrier. The event will load the cargo into the Carrier regardless of its position. There will be no movement simulated of the cargo loading. +-- The cargo must be in the **UnLoaded** state. +-- @function [parent=#CARGO] __Load +-- @param #CARGO self +-- @param #number DelaySeconds The amount of seconds to delay the action. +-- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. + + +-- UnLoad + +--- UnLoads the cargo to a Carrier. The event will unload the cargo from the Carrier. There will be no movement simulated of the cargo loading. +-- The cargo must be in the **Loaded** state. +-- @function [parent=#CARGO] UnLoad +-- @param #CARGO self +-- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Core.Point#POINT_VEC2) to where the cargo will be placed after unloading. If not provided, the cargo will be placed 60 meters behind the Carrier location. + +--- UnLoads the cargo to a Carrier. The event will unload the cargo from the Carrier. There will be no movement simulated of the cargo loading. +-- The cargo must be in the **Loaded** state. +-- @function [parent=#CARGO] __UnLoad +-- @param #CARGO self +-- @param #number DelaySeconds The amount of seconds to delay the action. +-- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Core.Point#POINT_VEC2) to where the cargo will be placed after unloading. If not provided, the cargo will be placed 60 meters behind the Carrier location. + +-- State Transition Functions + +-- UnLoaded + +--- @function [parent=#CARGO] OnLeaveUnLoaded +-- @param #CARGO self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable +-- @return #boolean + +--- @function [parent=#CARGO] OnEnterUnLoaded +-- @param #CARGO self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable + +-- Loaded + +--- @function [parent=#CARGO] OnLeaveLoaded +-- @param #CARGO self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable +-- @return #boolean + +--- @function [parent=#CARGO] OnEnterLoaded +-- @param #CARGO self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable + +-- Boarding + +--- @function [parent=#CARGO] OnLeaveBoarding +-- @param #CARGO self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable +-- @return #boolean + +--- @function [parent=#CARGO] OnEnterBoarding +-- @param #CARGO self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable +-- @param #number NearRadius The radius when the cargo will board the Carrier (to avoid collision). + +-- UnBoarding + +--- @function [parent=#CARGO] OnLeaveUnBoarding +-- @param #CARGO self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable +-- @return #boolean + +--- @function [parent=#CARGO] OnEnterUnBoarding +-- @param #CARGO self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable + + +-- TODO: Find all Carrier objects and make the type of the Carriers Wrapper.Unit#UNIT in the documentation. + +CARGOS = {} + +do -- CARGO + + --- @type CARGO + -- @extends Core.Fsm#FSM_PROCESS + -- @field #string Type A string defining the type of the cargo. eg. Engineers, Equipment, Screwdrivers. + -- @field #string Name A string defining the name of the cargo. The name is the unique identifier of the cargo. + -- @field #number Weight A number defining the weight of the cargo. The weight is expressed in kg. + -- @field #number NearRadius (optional) A number defining the radius in meters when the cargo is near to a Carrier, so that it can be loaded. + -- @field Wrapper.Unit#UNIT CargoObject The alive DCS object representing the cargo. This value can be nil, meaning, that the cargo is not represented anywhere... + -- @field Wrapper.Client#CLIENT CargoCarrier The alive DCS object carrying the cargo. This value can be nil, meaning, that the cargo is not contained anywhere... + -- @field #boolean Slingloadable This flag defines if the cargo can be slingloaded. + -- @field #boolean Moveable This flag defines if the cargo is moveable. + -- @field #boolean Representable This flag defines if the cargo can be represented by a DCS Unit. + -- @field #boolean Containable This flag defines if the cargo can be contained within a DCS Unit. + + --- Defines the core functions that defines a cargo object within MOOSE. + -- + -- A cargo is a **logical object** defined that is available for transport, and has a life status within a simulation. + -- + -- CARGO is not meant to be used directly by mission designers, but provides a base class for **concrete cargo implementation classes** to handle: + -- + -- * Cargo **group objects**, implemented by the @{Cargo.CargoGroup#CARGO_GROUP} class. + -- * Cargo **Unit objects**, implemented by the @{Cargo.CargoUnit#CARGO_UNIT} class. + -- * Cargo **Crate objects**, implemented by the @{Cargo.CargoCrate#CARGO_CRATE} class. + -- * Cargo **Sling Load objects**, implemented by the @{Cargo.CargoSlingload#CARGO_SLINGLOAD} class. + -- + -- The above cargo classes are used by the AI\_CARGO\_ classes to allow AI groups to transport cargo: + -- + -- * AI Armoured Personnel Carriers to transport cargo and engage in battles, using the @{AI.AI_Cargo_APC#AI_CARGO_APC} class. + -- * AI Helicopters to transport cargo, using the @{AI.AI_Cargo_Helicopter#AI_CARGO_HELICOPTER} class. + -- * AI Planes to transport cargo, using the @{AI.AI_Cargo_Plane#AI_CARGO_PLANE} class. + -- * AI Ships is planned. + -- + -- The above cargo classes are also used by the TASK\_CARGO\_ classes to allow human players to transport cargo as part of a tasking: + -- + -- * @{Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT} to transport cargo by human players. + -- * @{Tasking.Task_Cargo_Transport#TASK_CARGO_CSAR} to transport downed pilots by human players. + -- + -- + -- The CARGO is a state machine: it manages the different events and states of the cargo. + -- All derived classes from CARGO follow the same state machine, expose the same cargo event functions, and provide the same cargo states. + -- + -- ## CARGO Events: + -- + -- * @{#CARGO.Board}( ToCarrier ): Boards the cargo to a carrier. + -- * @{#CARGO.Load}( ToCarrier ): Loads the cargo into a carrier, regardless of its position. + -- * @{#CARGO.UnBoard}( ToPointVec2 ): UnBoard the cargo from a carrier. This will trigger a movement of the cargo to the option ToPointVec2. + -- * @{#CARGO.UnLoad}( ToPointVec2 ): UnLoads the cargo from a carrier. + -- * @{#CARGO.Destroyed}( Controllable ): The cargo is dead. The cargo process will be ended. + -- + -- @field #CARGO + CARGO = { + ClassName = "CARGO", + Type = nil, + Name = nil, + Weight = nil, + CargoObject = nil, + CargoCarrier = nil, + Representable = false, + Slingloadable = false, + Moveable = false, + Containable = false, + Reported = {}, + } + + --- @type CARGO.CargoObjects + -- @map < #string, Wrapper.Positionable#POSITIONABLE > The alive POSITIONABLE objects representing the the cargo. + + + --- CARGO Constructor. This class is an abstract class and should not be instantiated. + -- @param #CARGO self + -- @param #string Type + -- @param #string Name + -- @param #number Weight + -- @param #number LoadRadius (optional) + -- @param #number NearRadius (optional) + -- @return #CARGO + function CARGO:New( Type, Name, Weight, LoadRadius, NearRadius ) --R2.1 + + local self = BASE:Inherit( self, FSM:New() ) -- #CARGO + self:F( { Type, Name, Weight, LoadRadius, NearRadius } ) + + self:SetStartState( "UnLoaded" ) + self:AddTransition( { "UnLoaded", "Boarding" }, "Board", "Boarding" ) + self:AddTransition( "Boarding" , "Boarding", "Boarding" ) + self:AddTransition( "Boarding", "CancelBoarding", "UnLoaded" ) + self:AddTransition( "Boarding", "Load", "Loaded" ) + self:AddTransition( "UnLoaded", "Load", "Loaded" ) + self:AddTransition( "Loaded", "UnBoard", "UnBoarding" ) + self:AddTransition( "UnBoarding", "UnBoarding", "UnBoarding" ) + self:AddTransition( "UnBoarding", "UnLoad", "UnLoaded" ) + self:AddTransition( "Loaded", "UnLoad", "UnLoaded" ) + self:AddTransition( "*", "Damaged", "Damaged" ) + self:AddTransition( "*", "Destroyed", "Destroyed" ) + self:AddTransition( "*", "Respawn", "UnLoaded" ) + self:AddTransition( "*", "Reset", "UnLoaded" ) + + self.Type = Type + self.Name = Name + self.Weight = Weight or 0 + self.CargoObject = nil + self.CargoCarrier = nil -- Wrapper.Client#CLIENT + self.Representable = false + self.Slingloadable = false + self.Moveable = false + self.Containable = false + + self.CargoLimit = 0 + + self.LoadRadius = LoadRadius or 500 + --self.NearRadius = NearRadius or 25 + + self:SetDeployed( false ) + + self.CargoScheduler = SCHEDULER:New() + + CARGOS[self.Name] = self + + + return self + end + + + --- Find a CARGO in the _DATABASE. + -- @param #CARGO self + -- @param #string CargoName The Cargo Name. + -- @return #CARGO self + function CARGO:FindByName( CargoName ) + + local CargoFound = _DATABASE:FindCargo( CargoName ) + return CargoFound + end + + --- Get the x position of the cargo. + -- @param #CARGO self + -- @return #number + function CARGO:GetX() + if self:IsLoaded() then + return self.CargoCarrier:GetCoordinate().x + else + return self.CargoObject:GetCoordinate().x + end + end + + --- Get the y position of the cargo. + -- @param #CARGO self + -- @return #number + function CARGO:GetY() + if self:IsLoaded() then + return self.CargoCarrier:GetCoordinate().z + else + return self.CargoObject:GetCoordinate().z + end + end + + --- Get the heading of the cargo. + -- @param #CARGO self + -- @return #number + function CARGO:GetHeading() + if self:IsLoaded() then + return self.CargoCarrier:GetHeading() + else + return self.CargoObject:GetHeading() + end + end + + + --- Check if the cargo can be Slingloaded. + -- @param #CARGO self + function CARGO:CanSlingload() + return false + end + + --- Check if the cargo can be Boarded. + -- @param #CARGO self + function CARGO:CanBoard() + return true + end + + --- Check if the cargo can be Unboarded. + -- @param #CARGO self + function CARGO:CanUnboard() + return true + end + + --- Check if the cargo can be Loaded. + -- @param #CARGO self + function CARGO:CanLoad() + return true + end + + --- Check if the cargo can be Unloaded. + -- @param #CARGO self + function CARGO:CanUnload() + return true + end + + + --- Destroy the cargo. + -- @param #CARGO self + function CARGO:Destroy() + if self.CargoObject then + self.CargoObject:Destroy() + end + self:Destroyed() + end + + --- Get the name of the Cargo. + -- @param #CARGO self + -- @return #string The name of the Cargo. + function CARGO:GetName() --R2.1 + return self.Name + end + + --- Get the current active object representing or being the Cargo. + -- @param #CARGO self + -- @return Wrapper.Positionable#POSITIONABLE The object representing or being the Cargo. + function CARGO:GetObject() + if self:IsLoaded() then + return self.CargoCarrier + else + return self.CargoObject + end + end + + --- Get the object name of the Cargo. + -- @param #CARGO self + -- @return #string The object name of the Cargo. + function CARGO:GetObjectName() --R2.1 + if self:IsLoaded() then + return self.CargoCarrier:GetName() + else + return self.CargoObject:GetName() + end + end + + --- Get the amount of Cargo. + -- @param #CARGO self + -- @return #number The amount of Cargo. + function CARGO:GetCount() + return 1 + end + + --- Get the type of the Cargo. + -- @param #CARGO self + -- @return #string The type of the Cargo. + function CARGO:GetType() + return self.Type + end + + + --- Get the transportation method of the Cargo. + -- @param #CARGO self + -- @return #string The transportation method of the Cargo. + function CARGO:GetTransportationMethod() + return self.TransportationMethod + end + + + --- Get the coalition of the Cargo. + -- @param #CARGO self + -- @return Coalition + function CARGO:GetCoalition() + if self:IsLoaded() then + return self.CargoCarrier:GetCoalition() + else + return self.CargoObject:GetCoalition() + end + end + + + --- Get the current coordinates of the Cargo. + -- @param #CARGO self + -- @return Core.Point#COORDINATE The coordinates of the Cargo. + function CARGO:GetCoordinate() + return self.CargoObject:GetCoordinate() + end + + --- Check if cargo is destroyed. + -- @param #CARGO self + -- @return #boolean true if destroyed + function CARGO:IsDestroyed() + return self:Is( "Destroyed" ) + end + + + --- Check if cargo is loaded. + -- @param #CARGO self + -- @return #boolean true if loaded + function CARGO:IsLoaded() + return self:Is( "Loaded" ) + end + + --- Check if cargo is loaded. + -- @param #CARGO self + -- @param Wrapper.Unit#UNIT Carrier + -- @return #boolean true if loaded + function CARGO:IsLoadedInCarrier( Carrier ) + return self.CargoCarrier and self.CargoCarrier:GetName() == Carrier:GetName() + end + + --- Check if cargo is unloaded. + -- @param #CARGO self + -- @return #boolean true if unloaded + function CARGO:IsUnLoaded() + return self:Is( "UnLoaded" ) + end + + --- Check if cargo is boarding. + -- @param #CARGO self + -- @return #boolean true if boarding + function CARGO:IsBoarding() + return self:Is( "Boarding" ) + end + + + --- Check if cargo is unboarding. + -- @param #CARGO self + -- @return #boolean true if unboarding + function CARGO:IsUnboarding() + return self:Is( "UnBoarding" ) + end + + + --- Check if cargo is alive. + -- @param #CARGO self + -- @return #boolean true if unloaded + function CARGO:IsAlive() + + if self:IsLoaded() then + return self.CargoCarrier:IsAlive() + else + return self.CargoObject:IsAlive() + end + end + + --- Set the cargo as deployed. + -- @param #CARGO self + -- @param #boolean Deployed true if the cargo is to be deployed. false or nil otherwise. + function CARGO:SetDeployed( Deployed ) + self.Deployed = Deployed + end + + --- Is the cargo deployed + -- @param #CARGO self + -- @return #boolean + function CARGO:IsDeployed() + return self.Deployed + end + + + + + --- Template method to spawn a new representation of the CARGO in the simulator. + -- @param #CARGO self + -- @return #CARGO + function CARGO:Spawn( PointVec2 ) + self:F() + + end + + --- Signal a flare at the position of the CARGO. + -- @param #CARGO self + -- @param Utilities.Utils#FLARECOLOR FlareColor + function CARGO:Flare( FlareColor ) + if self:IsUnLoaded() then + trigger.action.signalFlare( self.CargoObject:GetVec3(), FlareColor , 0 ) + end + end + + --- Signal a white flare at the position of the CARGO. + -- @param #CARGO self + function CARGO:FlareWhite() + self:Flare( trigger.flareColor.White ) + end + + --- Signal a yellow flare at the position of the CARGO. + -- @param #CARGO self + function CARGO:FlareYellow() + self:Flare( trigger.flareColor.Yellow ) + end + + --- Signal a green flare at the position of the CARGO. + -- @param #CARGO self + function CARGO:FlareGreen() + self:Flare( trigger.flareColor.Green ) + end + + --- Signal a red flare at the position of the CARGO. + -- @param #CARGO self + function CARGO:FlareRed() + self:Flare( trigger.flareColor.Red ) + end + + --- Smoke the CARGO. + -- @param #CARGO self + -- @param Utilities.Utils#SMOKECOLOR SmokeColor The color of the smoke. + -- @param #number Radius The radius of randomization around the center of the Cargo. + function CARGO:Smoke( SmokeColor, Radius ) + if self:IsUnLoaded() then + if Radius then + trigger.action.smoke( self.CargoObject:GetRandomVec3( Radius ), SmokeColor ) + else + trigger.action.smoke( self.CargoObject:GetVec3(), SmokeColor ) + end + end + end + + --- Smoke the CARGO Green. + -- @param #CARGO self + function CARGO:SmokeGreen() + self:Smoke( trigger.smokeColor.Green, Range ) + end + + --- Smoke the CARGO Red. + -- @param #CARGO self + function CARGO:SmokeRed() + self:Smoke( trigger.smokeColor.Red, Range ) + end + + --- Smoke the CARGO White. + -- @param #CARGO self + function CARGO:SmokeWhite() + self:Smoke( trigger.smokeColor.White, Range ) + end + + --- Smoke the CARGO Orange. + -- @param #CARGO self + function CARGO:SmokeOrange() + self:Smoke( trigger.smokeColor.Orange, Range ) + end + + --- Smoke the CARGO Blue. + -- @param #CARGO self + function CARGO:SmokeBlue() + self:Smoke( trigger.smokeColor.Blue, Range ) + end + + + --- Set the Load radius, which is the radius till when the Cargo can be loaded. + -- @param #CARGO self + -- @param #number LoadRadius The radius till Cargo can be loaded. + -- @return #CARGO + function CARGO:SetLoadRadius( LoadRadius ) + self.LoadRadius = LoadRadius or 150 + end + + --- Get the Load radius, which is the radius till when the Cargo can be loaded. + -- @param #CARGO self + -- @return #number The radius till Cargo can be loaded. + function CARGO:GetLoadRadius() + return self.LoadRadius + end + + + + --- Check if Cargo is in the LoadRadius for the Cargo to be Boarded or Loaded. + -- @param #CARGO self + -- @param Core.Point#COORDINATE Coordinate + -- @return #boolean true if the CargoGroup is within the loading radius. + function CARGO:IsInLoadRadius( Coordinate ) + self:F( { Coordinate, LoadRadius = self.LoadRadius } ) + + local Distance = 0 + if self:IsUnLoaded() then + local CargoCoordinate = self.CargoObject:GetCoordinate() + Distance = Coordinate:Get2DDistance( CargoCoordinate ) + self:T( Distance ) + if Distance <= self.LoadRadius then + return true + end + end + + return false + end + + + --- Check if the Cargo can report itself to be Boarded or Loaded. + -- @param #CARGO self + -- @param Core.Point#COORDINATE Coordinate + -- @return #boolean true if the Cargo can report itself. + function CARGO:IsInReportRadius( Coordinate ) + self:F( { Coordinate } ) + + local Distance = 0 + if self:IsUnLoaded() then + Distance = Coordinate:Get2DDistance( self.CargoObject:GetCoordinate() ) + self:T( Distance ) + if Distance <= self.LoadRadius then + return true + end + end + + return false + end + + + --- Check if CargoCarrier is near the coordinate within NearRadius. + -- @param #CARGO self + -- @param Core.Point#COORDINATE Coordinate + -- @param #number NearRadius The radius when the cargo will board the Carrier (to avoid collision). + -- @return #boolean + function CARGO:IsNear( Coordinate, NearRadius ) + --self:F( { PointVec2 = PointVec2, NearRadius = NearRadius } ) + + if self.CargoObject:IsAlive() then + --local Distance = PointVec2:Get2DDistance( self.CargoObject:GetPointVec2() ) + --self:F( { CargoObjectName = self.CargoObject:GetName() } ) + --self:F( { CargoObjectVec2 = self.CargoObject:GetVec2() } ) + --self:F( { PointVec2 = PointVec2:GetVec2() } ) + local Distance = Coordinate:Get2DDistance( self.CargoObject:GetCoordinate() ) + --self:F( { Distance = Distance, NearRadius = NearRadius or "nil" } ) + + if Distance <= NearRadius then + --self:F( { PointVec2 = PointVec2, NearRadius = NearRadius, IsNear = true } ) + return true + end + end + + --self:F( { PointVec2 = PointVec2, NearRadius = NearRadius, IsNear = false } ) + return false + end + + + + --- Check if Cargo is the given @{Zone}. + -- @param #CARGO self + -- @param Core.Zone#ZONE_BASE Zone + -- @return #boolean **true** if cargo is in the Zone, **false** if cargo is not in the Zone. + function CARGO:IsInZone( Zone ) + --self:F( { Zone } ) + + if self:IsLoaded() then + return Zone:IsPointVec2InZone( self.CargoCarrier:GetPointVec2() ) + else + --self:F( { Size = self.CargoObject:GetSize(), Units = self.CargoObject:GetUnits() } ) + if self.CargoObject:GetSize() ~= 0 then + return Zone:IsPointVec2InZone( self.CargoObject:GetPointVec2() ) + else + return false + end + end + + return nil + + end + + + --- Get the current PointVec2 of the cargo. + -- @param #CARGO self + -- @return Core.Point#POINT_VEC2 + function CARGO:GetPointVec2() + return self.CargoObject:GetPointVec2() + end + + --- Get the current Coordinate of the cargo. + -- @param #CARGO self + -- @return Core.Point#COORDINATE + function CARGO:GetCoordinate() + return self.CargoObject:GetCoordinate() + end + + --- Get the weight of the cargo. + -- @param #CARGO self + -- @return #number Weight The weight in kg. + function CARGO:GetWeight() + return self.Weight + end + + --- Set the weight of the cargo. + -- @param #CARGO self + -- @param #number Weight The weight in kg. + -- @return #CARGO + function CARGO:SetWeight( Weight ) + self.Weight = Weight + return self + end + + --- Get the volume of the cargo. + -- @param #CARGO self + -- @return #number Volume The volume in kg. + function CARGO:GetVolume() + return self.Volume + end + + --- Set the volume of the cargo. + -- @param #CARGO self + -- @param #number Volume The volume in kg. + -- @return #CARGO + function CARGO:SetVolume( Volume ) + self.Volume = Volume + return self + end + + --- Send a CC message to a @{Wrapper.Group}. + -- @param #CARGO self + -- @param #string Message + -- @param Wrapper.Group#GROUP CarrierGroup The Carrier Group. + -- @param #string Name (optional) The name of the Group used as a prefix for the message to the Group. If not provided, there will be nothing shown. + function CARGO:MessageToGroup( Message, CarrierGroup, Name ) + + MESSAGE:New( Message, 20, "Cargo " .. self:GetName() ):ToGroup( CarrierGroup ) + + end + + --- Report to a Carrier Group. + -- @param #CARGO self + -- @param #string Action The string describing the action for the cargo. + -- @param Wrapper.Group#GROUP CarrierGroup The Carrier Group to send the report to. + -- @return #CARGO + function CARGO:Report( ReportText, Action, CarrierGroup ) + + if not self.Reported[CarrierGroup] or not self.Reported[CarrierGroup][Action] then + self.Reported[CarrierGroup] = {} + self.Reported[CarrierGroup][Action] = true + self:MessageToGroup( ReportText, CarrierGroup ) + if self.ReportFlareColor then + if not self.Reported[CarrierGroup]["Flaring"] then + self:Flare( self.ReportFlareColor ) + self.Reported[CarrierGroup]["Flaring"] = true + end + end + if self.ReportSmokeColor then + if not self.Reported[CarrierGroup]["Smoking"] then + self:Smoke( self.ReportSmokeColor ) + self.Reported[CarrierGroup]["Smoking"] = true + end + end + end + end + + + --- Report to a Carrier Group with a Flaring signal. + -- @param #CARGO self + -- @param Utils#UTILS.FlareColor FlareColor the color of the flare. + -- @return #CARGO + function CARGO:ReportFlare( FlareColor ) + + self.ReportFlareColor = FlareColor + end + + + --- Report to a Carrier Group with a Smoking signal. + -- @param #CARGO self + -- @param Utils#UTILS.SmokeColor SmokeColor the color of the smoke. + -- @return #CARGO + function CARGO:ReportSmoke( SmokeColor ) + + self.ReportSmokeColor = SmokeColor + end + + + --- Reset the reporting for a Carrier Group. + -- @param #CARGO self + -- @param #string Action The string describing the action for the cargo. + -- @param Wrapper.Group#GROUP CarrierGroup The Carrier Group to send the report to. + -- @return #CARGO + function CARGO:ReportReset( Action, CarrierGroup ) + + self.Reported[CarrierGroup][Action] = nil + end + + --- Reset all the reporting for a Carrier Group. + -- @param #CARGO self + -- @param Wrapper.Group#GROUP CarrierGroup The Carrier Group to send the report to. + -- @return #CARGO + function CARGO:ReportResetAll( CarrierGroup ) + + self.Reported[CarrierGroup] = nil + end + + --- Respawn the cargo when destroyed + -- @param #CARGO self + -- @param #boolean RespawnDestroyed + function CARGO:RespawnOnDestroyed( RespawnDestroyed ) + + if RespawnDestroyed then + self.onenterDestroyed = function( self ) + self:Respawn() + end + else + self.onenterDestroyed = nil + end + + end + + + + +end -- CARGO + +do -- CARGO_REPRESENTABLE + + --- @type CARGO_REPRESENTABLE + -- @extends #CARGO + -- @field test + + --- Models CARGO that is representable by a Unit. + -- @field #CARGO_REPRESENTABLE CARGO_REPRESENTABLE + CARGO_REPRESENTABLE = { + ClassName = "CARGO_REPRESENTABLE" + } + + --- CARGO_REPRESENTABLE Constructor. + -- @param #CARGO_REPRESENTABLE self + -- @param Wrapper.Positionable#POSITIONABLE CargoObject The cargo object. + -- @param #string Type Type name + -- @param #string Name Name. + -- @param #number LoadRadius (optional) Radius in meters. + -- @param #number NearRadius (optional) Radius in meters when the cargo is loaded into the carrier. + -- @return #CARGO_REPRESENTABLE + function CARGO_REPRESENTABLE:New( CargoObject, Type, Name, LoadRadius, NearRadius ) + + -- Inherit CARGO. + local self = BASE:Inherit( self, CARGO:New( Type, Name, 0, LoadRadius, NearRadius ) ) -- #CARGO_REPRESENTABLE + self:F( { Type, Name, LoadRadius, NearRadius } ) + + -- Descriptors. + local Desc=CargoObject:GetDesc() + self:T({Desc=Desc}) + + -- Weight. + local Weight = math.random( 80, 120 ) + + -- Adjust weight.. + if Desc then + if Desc.typeName == "2B11 mortar" then + Weight = 210 + else + Weight = Desc.massEmpty + end + end + + -- Set weight. + self:SetWeight( Weight ) + + return self + end + + --- CARGO_REPRESENTABLE Destructor. + -- @param #CARGO_REPRESENTABLE self + -- @return #CARGO_REPRESENTABLE + function CARGO_REPRESENTABLE:Destroy() + + -- Cargo objects are deleted from the _DATABASE and SET_CARGO objects. + self:F( { CargoName = self:GetName() } ) + --_EVENTDISPATCHER:CreateEventDeleteCargo( self ) + + return self + end + + --- Route a cargo unit to a PointVec2. + -- @param #CARGO_REPRESENTABLE self + -- @param Core.Point#POINT_VEC2 ToPointVec2 + -- @param #number Speed + -- @return #CARGO_REPRESENTABLE + function CARGO_REPRESENTABLE:RouteTo( ToPointVec2, Speed ) + self:F2( ToPointVec2 ) + + local Points = {} + + local PointStartVec2 = self.CargoObject:GetPointVec2() + + Points[#Points+1] = PointStartVec2:WaypointGround( Speed ) + Points[#Points+1] = ToPointVec2:WaypointGround( Speed ) + + local TaskRoute = self.CargoObject:TaskRoute( Points ) + self.CargoObject:SetTask( TaskRoute, 2 ) + return self + end + + --- Send a message to a @{Wrapper.Group} through a communication channel near the cargo. + -- @param #CARGO_REPRESENTABLE self + -- @param #string Message + -- @param Wrapper.Group#GROUP TaskGroup + -- @param #string Name (optional) The name of the Group used as a prefix for the message to the Group. If not provided, there will be nothing shown. + function CARGO_REPRESENTABLE:MessageToGroup( Message, TaskGroup, Name ) + + local CoordinateZone = ZONE_RADIUS:New( "Zone" , self:GetCoordinate():GetVec2(), 500 ) + CoordinateZone:Scan( { Object.Category.UNIT } ) + for _, DCSUnit in pairs( CoordinateZone:GetScannedUnits() ) do + local NearUnit = UNIT:Find( DCSUnit ) + self:F({NearUnit=NearUnit}) + local NearUnitCoalition = NearUnit:GetCoalition() + local CargoCoalition = self:GetCoalition() + if NearUnitCoalition == CargoCoalition then + local Attributes = NearUnit:GetDesc() + self:F({Desc=Attributes}) + if NearUnit:HasAttribute( "Trucks" ) then + MESSAGE:New( Message, 20, NearUnit:GetCallsign() .. " reporting - Cargo " .. self:GetName() ):ToGroup( TaskGroup ) + break + end + end + end + + end + + +end -- CARGO_REPRESENTABLE + +do -- CARGO_REPORTABLE + + --- @type CARGO_REPORTABLE + -- @extends #CARGO + CARGO_REPORTABLE = { + ClassName = "CARGO_REPORTABLE" + } + + --- CARGO_REPORTABLE Constructor. + -- @param #CARGO_REPORTABLE self + -- @param #string Type + -- @param #string Name + -- @param #number Weight + -- @param #number LoadRadius (optional) + -- @param #number NearRadius (optional) + -- @return #CARGO_REPORTABLE + function CARGO_REPORTABLE:New( Type, Name, Weight, LoadRadius, NearRadius ) + local self = BASE:Inherit( self, CARGO:New( Type, Name, Weight, LoadRadius, NearRadius ) ) -- #CARGO_REPORTABLE + self:F( { Type, Name, Weight, LoadRadius, NearRadius } ) + + return self + end + + --- Send a CC message to a @{Wrapper.Group}. + -- @param #CARGO_REPORTABLE self + -- @param #string Message + -- @param Wrapper.Group#GROUP TaskGroup + -- @param #string Name (optional) The name of the Group used as a prefix for the message to the Group. If not provided, there will be nothing shown. + function CARGO_REPORTABLE:MessageToGroup( Message, TaskGroup, Name ) + + MESSAGE:New( Message, 20, "Cargo " .. self:GetName() .. " reporting" ):ToGroup( TaskGroup ) + + end + + + +end + + + + + + + +do -- CARGO_PACKAGE + + --- @type CARGO_PACKAGE + -- @extends #CARGO_REPRESENTABLE + CARGO_PACKAGE = { + ClassName = "CARGO_PACKAGE" + } + +--- CARGO_PACKAGE Constructor. +-- @param #CARGO_PACKAGE self +-- @param Wrapper.Unit#UNIT CargoCarrier The UNIT carrying the package. +-- @param #string Type +-- @param #string Name +-- @param #number Weight +-- @param #number LoadRadius (optional) +-- @param #number NearRadius (optional) +-- @return #CARGO_PACKAGE +function CARGO_PACKAGE:New( CargoCarrier, Type, Name, Weight, LoadRadius, NearRadius ) + local self = BASE:Inherit( self, CARGO_REPRESENTABLE:New( CargoCarrier, Type, Name, Weight, LoadRadius, NearRadius ) ) -- #CARGO_PACKAGE + self:F( { Type, Name, Weight, LoadRadius, NearRadius } ) + + self:T( CargoCarrier ) + self.CargoCarrier = CargoCarrier + + return self +end + +--- Board Event. +-- @param #CARGO_PACKAGE self +-- @param #string Event +-- @param #string From +-- @param #string To +-- @param Wrapper.Unit#UNIT CargoCarrier +-- @param #number Speed +-- @param #number BoardDistance +-- @param #number Angle +function CARGO_PACKAGE:onafterOnBoard( From, Event, To, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) + self:F() + + self.CargoInAir = self.CargoCarrier:InAir() + + self:T( self.CargoInAir ) + + -- Only move the CargoCarrier to the New CargoCarrier when the New CargoCarrier is not in the air. + if not self.CargoInAir then + + local Points = {} + + local StartPointVec2 = self.CargoCarrier:GetPointVec2() + local CargoCarrierHeading = CargoCarrier:GetHeading() -- Get Heading of object in degrees. + local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) + self:T( { CargoCarrierHeading, CargoDeployHeading } ) + local CargoDeployPointVec2 = CargoCarrier:GetPointVec2():Translate( BoardDistance, CargoDeployHeading ) + + Points[#Points+1] = StartPointVec2:WaypointGround( Speed ) + Points[#Points+1] = CargoDeployPointVec2:WaypointGround( Speed ) + + local TaskRoute = self.CargoCarrier:TaskRoute( Points ) + self.CargoCarrier:SetTask( TaskRoute, 1 ) + end + + self:Boarded( CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) + +end + +--- Check if CargoCarrier is near the Cargo to be Loaded. +-- @param #CARGO_PACKAGE self +-- @param Wrapper.Unit#UNIT CargoCarrier +-- @return #boolean +function CARGO_PACKAGE:IsNear( CargoCarrier ) + self:F() + + local CargoCarrierPoint = CargoCarrier:GetCoordinate() + + local Distance = CargoCarrierPoint:Get2DDistance( self.CargoCarrier:GetCoordinate() ) + self:T( Distance ) + + if Distance <= self.NearRadius then + return true + else + return false + end +end + +--- Boarded Event. +-- @param #CARGO_PACKAGE self +-- @param #string Event +-- @param #string From +-- @param #string To +-- @param Wrapper.Unit#UNIT CargoCarrier +-- @param #number Speed +-- @param #number BoardDistance +-- @param #number LoadDistance +-- @param #number Angle +function CARGO_PACKAGE:onafterOnBoarded( From, Event, To, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) + self:F() + + if self:IsNear( CargoCarrier ) then + self:__Load( 1, CargoCarrier, Speed, LoadDistance, Angle ) + else + self:__Boarded( 1, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) + end +end + +--- UnBoard Event. +-- @param #CARGO_PACKAGE self +-- @param #string Event +-- @param #string From +-- @param #string To +-- @param Wrapper.Unit#UNIT CargoCarrier +-- @param #number Speed +-- @param #number UnLoadDistance +-- @param #number UnBoardDistance +-- @param #number Radius +-- @param #number Angle +function CARGO_PACKAGE:onafterUnBoard( From, Event, To, CargoCarrier, Speed, UnLoadDistance, UnBoardDistance, Radius, Angle ) + self:F() + + self.CargoInAir = self.CargoCarrier:InAir() + + self:T( self.CargoInAir ) + + -- Only unboard the cargo when the carrier is not in the air. + -- (eg. cargo can be on a oil derrick, moving the cargo on the oil derrick will drop the cargo on the sea). + if not self.CargoInAir then + + self:_Next( self.FsmP.UnLoad, UnLoadDistance, Angle ) + + local Points = {} + + local StartPointVec2 = CargoCarrier:GetPointVec2() + local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. + local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) + self:T( { CargoCarrierHeading, CargoDeployHeading } ) + local CargoDeployPointVec2 = StartPointVec2:Translate( UnBoardDistance, CargoDeployHeading ) + + Points[#Points+1] = StartPointVec2:WaypointGround( Speed ) + Points[#Points+1] = CargoDeployPointVec2:WaypointGround( Speed ) + + local TaskRoute = CargoCarrier:TaskRoute( Points ) + CargoCarrier:SetTask( TaskRoute, 1 ) + end + + self:__UnBoarded( 1 , CargoCarrier, Speed ) + +end + +--- UnBoarded Event. +-- @param #CARGO_PACKAGE self +-- @param #string Event +-- @param #string From +-- @param #string To +-- @param Wrapper.Unit#UNIT CargoCarrier +-- @param #number Speed +function CARGO_PACKAGE:onafterUnBoarded( From, Event, To, CargoCarrier, Speed ) + self:F() + + if self:IsNear( CargoCarrier ) then + self:__UnLoad( 1, CargoCarrier, Speed ) + else + self:__UnBoarded( 1, CargoCarrier, Speed ) + end +end + +--- Load Event. +-- @param #CARGO_PACKAGE self +-- @param #string Event +-- @param #string From +-- @param #string To +-- @param Wrapper.Unit#UNIT CargoCarrier +-- @param #number Speed +-- @param #number LoadDistance +-- @param #number Angle +function CARGO_PACKAGE:onafterLoad( From, Event, To, CargoCarrier, Speed, LoadDistance, Angle ) + self:F() + + self.CargoCarrier = CargoCarrier + + local StartPointVec2 = self.CargoCarrier:GetPointVec2() + local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. + local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) + local CargoDeployPointVec2 = StartPointVec2:Translate( LoadDistance, CargoDeployHeading ) + + local Points = {} + Points[#Points+1] = StartPointVec2:WaypointGround( Speed ) + Points[#Points+1] = CargoDeployPointVec2:WaypointGround( Speed ) + + local TaskRoute = self.CargoCarrier:TaskRoute( Points ) + self.CargoCarrier:SetTask( TaskRoute, 1 ) + +end + +--- UnLoad Event. +-- @param #CARGO_PACKAGE self +-- @param #string Event +-- @param #string From +-- @param #string To +-- @param Wrapper.Unit#UNIT CargoCarrier +-- @param #number Speed +-- @param #number Distance +-- @param #number Angle +function CARGO_PACKAGE:onafterUnLoad( From, Event, To, CargoCarrier, Speed, Distance, Angle ) + self:F() + + local StartPointVec2 = self.CargoCarrier:GetPointVec2() + local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. + local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) + local CargoDeployPointVec2 = StartPointVec2:Translate( Distance, CargoDeployHeading ) + + self.CargoCarrier = CargoCarrier + + local Points = {} + Points[#Points+1] = StartPointVec2:WaypointGround( Speed ) + Points[#Points+1] = CargoDeployPointVec2:WaypointGround( Speed ) + + local TaskRoute = self.CargoCarrier:TaskRoute( Points ) + self.CargoCarrier:SetTask( TaskRoute, 1 ) + +end + + +end +--- **Cargo** - Management of single cargo logistics, which are based on a @{Wrapper.Unit} object. +-- +-- === +-- +-- ### [Demo Missions]() +-- +-- ### [YouTube Playlist]() +-- +-- === +-- +-- ### Author: **FlightControl** +-- ### Contributions: +-- +-- === +-- +-- @module Cargo.CargoUnit +-- @image Cargo_Units.JPG + +do -- CARGO_UNIT + + --- Models CARGO in the form of units, which can be boarded, unboarded, loaded, unloaded. + -- @type CARGO_UNIT + -- @extends Cargo.Cargo#CARGO_REPRESENTABLE + + --- Defines a cargo that is represented by a UNIT object within the simulator, and can be transported by a carrier. + -- Use the event functions as described above to Load, UnLoad, Board, UnBoard the CARGO_UNIT objects to and from carriers. + -- Note that ground forces behave in a group, and thus, act in formation, regardless if one unit is commanded to move. + -- + -- This class is used in CARGO_GROUP, and is not meant to be used by mission designers individually. + -- + -- === + -- + -- @field #CARGO_UNIT CARGO_UNIT + -- + CARGO_UNIT = { + ClassName = "CARGO_UNIT" + } + + --- CARGO_UNIT Constructor. + -- @param #CARGO_UNIT self + -- @param Wrapper.Unit#UNIT CargoUnit + -- @param #string Type + -- @param #string Name + -- @param #number Weight + -- @param #number LoadRadius (optional) + -- @param #number NearRadius (optional) + -- @return #CARGO_UNIT + function CARGO_UNIT:New( CargoUnit, Type, Name, LoadRadius, NearRadius ) + + -- Inherit CARGO_REPRESENTABLE. + local self = BASE:Inherit( self, CARGO_REPRESENTABLE:New( CargoUnit, Type, Name, LoadRadius, NearRadius ) ) -- #CARGO_UNIT + + -- Debug info. + self:T({Type=Type, Name=Name, LoadRadius=LoadRadius, NearRadius=NearRadius}) + + -- Set cargo object. + self.CargoObject = CargoUnit + + -- Set event prio. + self:SetEventPriority( 5 ) + + return self + end + + --- Enter UnBoarding State. + -- @param #CARGO_UNIT self + -- @param #string Event + -- @param #string From + -- @param #string To + -- @param Core.Point#POINT_VEC2 ToPointVec2 + -- @param #number NearRadius (optional) Defaut 25 m. + function CARGO_UNIT:onenterUnBoarding( From, Event, To, ToPointVec2, NearRadius ) + self:F( { From, Event, To, ToPointVec2, NearRadius } ) + + local Angle = 180 + local Speed = 60 + local DeployDistance = 9 + local RouteDistance = 60 + + if From == "Loaded" then + + if not self:IsDestroyed() then + + local CargoCarrier = self.CargoCarrier -- Wrapper.Controllable#CONTROLLABLE + + if CargoCarrier:IsAlive() then + + local CargoCarrierPointVec2 = CargoCarrier:GetPointVec2() + local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. + local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) + + + local CargoRoutePointVec2 = CargoCarrierPointVec2:Translate( RouteDistance, CargoDeployHeading ) + + + -- if there is no ToPointVec2 given, then use the CargoRoutePointVec2 + local FromDirectionVec3 = CargoCarrierPointVec2:GetDirectionVec3( ToPointVec2 or CargoRoutePointVec2 ) + local FromAngle = CargoCarrierPointVec2:GetAngleDegrees(FromDirectionVec3) + local FromPointVec2 = CargoCarrierPointVec2:Translate( DeployDistance, FromAngle ) + --local CargoDeployPointVec2 = CargoCarrierPointVec2:GetRandomCoordinateInRadius( 10, 5 ) + + ToPointVec2 = ToPointVec2 or CargoCarrierPointVec2:GetRandomCoordinateInRadius( NearRadius, DeployDistance ) + + -- Respawn the group... + if self.CargoObject then + if CargoCarrier:IsShip() then + -- If CargoCarrier is a ship, we don't want to spawn the units in the water next to the boat. Use destination coord instead. + self.CargoObject:ReSpawnAt( ToPointVec2, CargoDeployHeading ) + else + self.CargoObject:ReSpawnAt( FromPointVec2, CargoDeployHeading ) + end + self:F( { "CargoUnits:", self.CargoObject:GetGroup():GetName() } ) + self.CargoCarrier = nil + + local Points = {} + + -- From + Points[#Points+1] = FromPointVec2:WaypointGround( Speed, "Vee" ) + + -- To + Points[#Points+1] = ToPointVec2:WaypointGround( Speed, "Vee" ) + + local TaskRoute = self.CargoObject:TaskRoute( Points ) + self.CargoObject:SetTask( TaskRoute, 1 ) + + + self:__UnBoarding( 1, ToPointVec2, NearRadius ) + end + else + -- the Carrier is dead. This cargo is dead too! + self:Destroyed() + end + end + end + + end + + --- Leave UnBoarding State. + -- @param #CARGO_UNIT self + -- @param #string Event + -- @param #string From + -- @param #string To + -- @param Core.Point#POINT_VEC2 ToPointVec2 + -- @param #number NearRadius (optional) Defaut 100 m. + function CARGO_UNIT:onleaveUnBoarding( From, Event, To, ToPointVec2, NearRadius ) + self:F( { From, Event, To, ToPointVec2, NearRadius } ) + + local Angle = 180 + local Speed = 10 + local Distance = 5 + + if From == "UnBoarding" then + --if self:IsNear( ToPointVec2, NearRadius ) then + return true + --else + + --self:__UnBoarding( 1, ToPointVec2, NearRadius ) + --end + --return false + end + + end + + --- UnBoard Event. + -- @param #CARGO_UNIT self + -- @param #string Event + -- @param #string From + -- @param #string To + -- @param Core.Point#POINT_VEC2 ToPointVec2 + -- @param #number NearRadius (optional) Defaut 100 m. + function CARGO_UNIT:onafterUnBoarding( From, Event, To, ToPointVec2, NearRadius ) + self:F( { From, Event, To, ToPointVec2, NearRadius } ) + + self.CargoInAir = self.CargoObject:InAir() + + self:T( self.CargoInAir ) + + -- Only unboard the cargo when the carrier is not in the air. + -- (eg. cargo can be on a oil derrick, moving the cargo on the oil derrick will drop the cargo on the sea). + if not self.CargoInAir then + + end + + self:__UnLoad( 1, ToPointVec2, NearRadius ) + + end + + + + --- Enter UnLoaded State. + -- @param #CARGO_UNIT self + -- @param #string Event + -- @param #string From + -- @param #string To + -- @param Core.Point#POINT_VEC2 + function CARGO_UNIT:onenterUnLoaded( From, Event, To, ToPointVec2 ) + self:F( { ToPointVec2, From, Event, To } ) + + local Angle = 180 + local Speed = 10 + local Distance = 5 + + if From == "Loaded" then + local StartPointVec2 = self.CargoCarrier:GetPointVec2() + local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. + local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) + local CargoDeployCoord = StartPointVec2:Translate( Distance, CargoDeployHeading ) + + ToPointVec2 = ToPointVec2 or COORDINATE:New( CargoDeployCoord.x, CargoDeployCoord.z ) + + -- Respawn the group... + if self.CargoObject then + self.CargoObject:ReSpawnAt( ToPointVec2, 0 ) + self.CargoCarrier = nil + end + + end + + if self.OnUnLoadedCallBack then + self.OnUnLoadedCallBack( self, unpack( self.OnUnLoadedParameters ) ) + self.OnUnLoadedCallBack = nil + end + + end + + --- Board Event. + -- @param #CARGO_UNIT self + -- @param #string Event + -- @param #string From + -- @param #string To + -- @param Wrapper.Group#GROUP CargoCarrier + -- @param #number NearRadius + function CARGO_UNIT:onafterBoard( From, Event, To, CargoCarrier, NearRadius, ... ) + self:F( { From, Event, To, CargoCarrier, NearRadius = NearRadius } ) + + self.CargoInAir = self.CargoObject:InAir() + + local Desc = self.CargoObject:GetDesc() + local MaxSpeed = Desc.speedMaxOffRoad + local TypeName = Desc.typeName + + --self:F({Unit=self.CargoObject:GetName()}) + + -- A cargo unit can only be boarded if it is not dead + + -- Only move the group to the carrier when the cargo is not in the air + -- (eg. cargo can be on a oil derrick, moving the cargo on the oil derrick will drop the cargo on the sea). + if not self.CargoInAir then + -- If NearRadius is given, then use the given NearRadius, otherwise calculate the NearRadius + -- based upon the Carrier bounding radius, which is calculated from the bounding rectangle on the Y axis. + local NearRadius = NearRadius or CargoCarrier:GetBoundingRadius() + 5 + if self:IsNear( CargoCarrier:GetPointVec2(), NearRadius ) then + self:Load( CargoCarrier, NearRadius, ... ) + else + if MaxSpeed and MaxSpeed == 0 or TypeName and TypeName == "Stinger comm" then + self:Load( CargoCarrier, NearRadius, ... ) + else + + local Speed = 90 + local Angle = 180 + local Distance = 0 + + local CargoCarrierPointVec2 = CargoCarrier:GetPointVec2() + local CargoCarrierHeading = CargoCarrier:GetHeading() -- Get Heading of object in degrees. + local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) + local CargoDeployPointVec2 = CargoCarrierPointVec2:Translate( Distance, CargoDeployHeading ) + + -- Set the CargoObject to state Green to ensure it is boarding! + self.CargoObject:OptionAlarmStateGreen() + + local Points = {} + + local PointStartVec2 = self.CargoObject:GetPointVec2() + + Points[#Points+1] = PointStartVec2:WaypointGround( Speed ) + Points[#Points+1] = CargoDeployPointVec2:WaypointGround( Speed ) + + local TaskRoute = self.CargoObject:TaskRoute( Points ) + self.CargoObject:SetTask( TaskRoute, 2 ) + self:__Boarding( -5, CargoCarrier, NearRadius, ... ) + self.RunCount = 0 + end + end + end + end + + + --- Boarding Event. + -- @param #CARGO_UNIT self + -- @param #string Event + -- @param #string From + -- @param #string To + -- @param Wrapper.Client#CLIENT CargoCarrier + -- @param #number NearRadius Default 25 m. + function CARGO_UNIT:onafterBoarding( From, Event, To, CargoCarrier, NearRadius, ... ) + self:F( { From, Event, To, CargoCarrier:GetName(), NearRadius = NearRadius } ) + + self:F( { IsAlive=self.CargoObject:IsAlive() } ) + + if CargoCarrier and CargoCarrier:IsAlive() then -- and self.CargoObject and self.CargoObject:IsAlive() then + if (CargoCarrier:IsAir() and not CargoCarrier:InAir()) or true then + local NearRadius = NearRadius or CargoCarrier:GetBoundingRadius( NearRadius ) + 5 + if self:IsNear( CargoCarrier:GetPointVec2(), NearRadius ) then + self:__Load( -1, CargoCarrier, ... ) + else + if self:IsNear( CargoCarrier:GetPointVec2(), 20 ) then + self:__Boarding( -1, CargoCarrier, NearRadius, ... ) + self.RunCount = self.RunCount + 1 + else + self:__Boarding( -2, CargoCarrier, NearRadius, ... ) + self.RunCount = self.RunCount + 2 + end + if self.RunCount >= 40 then + self.RunCount = 0 + local Speed = 90 + local Angle = 180 + local Distance = 0 + + --self:F({Unit=self.CargoObject:GetName()}) + + local CargoCarrierPointVec2 = CargoCarrier:GetPointVec2() + local CargoCarrierHeading = CargoCarrier:GetHeading() -- Get Heading of object in degrees. + local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) + local CargoDeployPointVec2 = CargoCarrierPointVec2:Translate( Distance, CargoDeployHeading ) + + -- Set the CargoObject to state Green to ensure it is boarding! + self.CargoObject:OptionAlarmStateGreen() + + local Points = {} + + local PointStartVec2 = self.CargoObject:GetPointVec2() + + Points[#Points+1] = PointStartVec2:WaypointGround( Speed, "Off road" ) + Points[#Points+1] = CargoDeployPointVec2:WaypointGround( Speed, "Off road" ) + + local TaskRoute = self.CargoObject:TaskRoute( Points ) + self.CargoObject:SetTask( TaskRoute, 0.2 ) + end + end + else + self.CargoObject:MessageToGroup( "Cancelling Boarding... Get back on the ground!", 5, CargoCarrier:GetGroup(), self:GetName() ) + self:CancelBoarding( CargoCarrier, NearRadius, ... ) + self.CargoObject:SetCommand( self.CargoObject:CommandStopRoute( true ) ) + end + else + self:E("Something is wrong") + end + + end + + + --- Loaded State. + -- @param #CARGO_UNIT self + -- @param #string Event + -- @param #string From + -- @param #string To + -- @param Wrapper.Unit#UNIT CargoCarrier + function CARGO_UNIT:onenterLoaded( From, Event, To, CargoCarrier ) + self:F( { From, Event, To, CargoCarrier } ) + + self.CargoCarrier = CargoCarrier + + --self:F({Unit=self.CargoObject:GetName()}) + + -- Only destroy the CargoObject if there is a CargoObject (packages don't have CargoObjects). + if self.CargoObject then + self.CargoObject:Destroy( false ) + --self.CargoObject:ReSpawnAt( COORDINATE:NewFromVec2( {x=0,y=0} ), 0 ) + end + end + + --- Get the transportation method of the Cargo. + -- @param #CARGO_UNIT self + -- @return #string The transportation method of the Cargo. + function CARGO_UNIT:GetTransportationMethod() + if self:IsLoaded() then + return "for unboarding" + else + if self:IsUnLoaded() then + return "for boarding" + else + if self:IsDeployed() then + return "delivered" + end + end + end + return "" + end + +end -- CARGO_UNIT +--- **Cargo** -- Management of single cargo crates, which are based on a @{Static} object. The cargo can only be slingloaded. +-- +-- === +-- +-- ### [Demo Missions]() +-- +-- ### [YouTube Playlist]() +-- +-- === +-- +-- ### Author: **FlightControl** +-- ### Contributions: +-- +-- === +-- +-- @module Cargo.CargoSlingload +-- @image Cargo_Slingload.JPG + + +do -- CARGO_SLINGLOAD + + --- Models the behaviour of cargo crates, which can only be slingloaded. + -- @type CARGO_SLINGLOAD + -- @extends Cargo.Cargo#CARGO_REPRESENTABLE + + --- Defines a cargo that is represented by a UNIT object within the simulator, and can be transported by a carrier. + -- + -- The above cargo classes are also used by the TASK_CARGO_ classes to allow human players to transport cargo as part of a tasking: + -- + -- * @{Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT} to transport cargo by human players. + -- * @{Tasking.Task_Cargo_Transport#TASK_CARGO_CSAR} to transport downed pilots by human players. + -- + -- === + -- + -- @field #CARGO_SLINGLOAD + CARGO_SLINGLOAD = { + ClassName = "CARGO_SLINGLOAD" + } + + --- CARGO_SLINGLOAD Constructor. + -- @param #CARGO_SLINGLOAD self + -- @param Wrapper.Static#STATIC CargoStatic + -- @param #string Type + -- @param #string Name + -- @param #number LoadRadius (optional) + -- @param #number NearRadius (optional) + -- @return #CARGO_SLINGLOAD + function CARGO_SLINGLOAD:New( CargoStatic, Type, Name, LoadRadius, NearRadius ) + local self = BASE:Inherit( self, CARGO_REPRESENTABLE:New( CargoStatic, Type, Name, nil, LoadRadius, NearRadius ) ) -- #CARGO_SLINGLOAD + self:F( { Type, Name, NearRadius } ) + + self.CargoObject = CargoStatic + + -- Cargo objects are added to the _DATABASE and SET_CARGO objects. + _EVENTDISPATCHER:CreateEventNewCargo( self ) + + self:HandleEvent( EVENTS.Dead, self.OnEventCargoDead ) + self:HandleEvent( EVENTS.Crash, self.OnEventCargoDead ) + --self:HandleEvent( EVENTS.RemoveUnit, self.OnEventCargoDead ) + self:HandleEvent( EVENTS.PlayerLeaveUnit, self.OnEventCargoDead ) + + self:SetEventPriority( 4 ) + + self.NearRadius = NearRadius or 25 + + return self + end + + + --- @param #CARGO_SLINGLOAD self + -- @param Core.Event#EVENTDATA EventData + function CARGO_SLINGLOAD:OnEventCargoDead( EventData ) + + local Destroyed = false + + if self:IsDestroyed() or self:IsUnLoaded() then + if self.CargoObject:GetName() == EventData.IniUnitName then + if not self.NoDestroy then + Destroyed = true + end + end + end + + if Destroyed then + self:I( { "Cargo crate destroyed: " .. self.CargoObject:GetName() } ) + self:Destroyed() + end + + end + + + --- Check if the cargo can be Slingloaded. + -- @param #CARGO_SLINGLOAD self + function CARGO_SLINGLOAD:CanSlingload() + return true + end + + --- Check if the cargo can be Boarded. + -- @param #CARGO_SLINGLOAD self + function CARGO_SLINGLOAD:CanBoard() + return false + end + + --- Check if the cargo can be Unboarded. + -- @param #CARGO_SLINGLOAD self + function CARGO_SLINGLOAD:CanUnboard() + return false + end + + --- Check if the cargo can be Loaded. + -- @param #CARGO_SLINGLOAD self + function CARGO_SLINGLOAD:CanLoad() + return false + end + + --- Check if the cargo can be Unloaded. + -- @param #CARGO_SLINGLOAD self + function CARGO_SLINGLOAD:CanUnload() + return false + end + + + --- Check if Cargo Crate is in the radius for the Cargo to be reported. + -- @param #CARGO_SLINGLOAD self + -- @param Core.Point#COORDINATE Coordinate + -- @return #boolean true if the Cargo Crate is within the report radius. + function CARGO_SLINGLOAD:IsInReportRadius( Coordinate ) + --self:F( { Coordinate, LoadRadius = self.LoadRadius } ) + + local Distance = 0 + if self:IsUnLoaded() then + Distance = Coordinate:Get2DDistance( self.CargoObject:GetCoordinate() ) + if Distance <= self.LoadRadius then + return true + end + end + + return false + end + + + --- Check if Cargo Slingload is in the radius for the Cargo to be Boarded or Loaded. + -- @param #CARGO_SLINGLOAD self + -- @param Core.Point#COORDINATE Coordinate + -- @return #boolean true if the Cargo Slingload is within the loading radius. + function CARGO_SLINGLOAD:IsInLoadRadius( Coordinate ) + --self:F( { Coordinate } ) + + local Distance = 0 + if self:IsUnLoaded() then + Distance = Coordinate:Get2DDistance( self.CargoObject:GetCoordinate() ) + if Distance <= self.NearRadius then + return true + end + end + + return false + end + + + + --- Get the current Coordinate of the CargoGroup. + -- @param #CARGO_SLINGLOAD self + -- @return Core.Point#COORDINATE The current Coordinate of the first Cargo of the CargoGroup. + -- @return #nil There is no valid Cargo in the CargoGroup. + function CARGO_SLINGLOAD:GetCoordinate() + --self:F() + + return self.CargoObject:GetCoordinate() + end + + --- Check if the CargoGroup is alive. + -- @param #CARGO_SLINGLOAD self + -- @return #boolean true if the CargoGroup is alive. + -- @return #boolean false if the CargoGroup is dead. + function CARGO_SLINGLOAD:IsAlive() + + local Alive = true + + -- When the Cargo is Loaded, the Cargo is in the CargoCarrier, so we check if the CargoCarrier is alive. + -- When the Cargo is not Loaded, the Cargo is the CargoObject, so we check if the CargoObject is alive. + if self:IsLoaded() then + Alive = Alive == true and self.CargoCarrier:IsAlive() + else + Alive = Alive == true and self.CargoObject:IsAlive() + end + + return Alive + + end + + + --- Route Cargo to Coordinate and randomize locations. + -- @param #CARGO_SLINGLOAD self + -- @param Core.Point#COORDINATE Coordinate + function CARGO_SLINGLOAD:RouteTo( Coordinate ) + --self:F( {Coordinate = Coordinate } ) + + end + + + --- Check if Cargo is near to the Carrier. + -- The Cargo is near to the Carrier within NearRadius. + -- @param #CARGO_SLINGLOAD self + -- @param Wrapper.Group#GROUP CargoCarrier + -- @param #number NearRadius + -- @return #boolean The Cargo is near to the Carrier. + -- @return #nil The Cargo is not near to the Carrier. + function CARGO_SLINGLOAD:IsNear( CargoCarrier, NearRadius ) + --self:F( {NearRadius = NearRadius } ) + + return self:IsNear( CargoCarrier:GetCoordinate(), NearRadius ) + end + + + --- Respawn the CargoGroup. + -- @param #CARGO_SLINGLOAD self + function CARGO_SLINGLOAD:Respawn() + + --self:F( { "Respawning slingload " .. self:GetName() } ) + + + -- Respawn the group... + if self.CargoObject then + self.CargoObject:ReSpawn() -- A cargo destroy crates a DEAD event. + self:__Reset( -0.1 ) + end + + + end + + + --- Respawn the CargoGroup. + -- @param #CARGO_SLINGLOAD self + function CARGO_SLINGLOAD:onafterReset() + + --self:F( { "Reset slingload " .. self:GetName() } ) + + + -- Respawn the group... + if self.CargoObject then + self:SetDeployed( false ) + self:SetStartState( "UnLoaded" ) + self.CargoCarrier = nil + -- Cargo objects are added to the _DATABASE and SET_CARGO objects. + _EVENTDISPATCHER:CreateEventNewCargo( self ) + end + + + end + + --- Get the transportation method of the Cargo. + -- @param #CARGO_SLINGLOAD self + -- @return #string The transportation method of the Cargo. + function CARGO_SLINGLOAD:GetTransportationMethod() + if self:IsLoaded() then + return "for sling loading" + else + if self:IsUnLoaded() then + return "for sling loading" + else + if self:IsDeployed() then + return "delivered" + end + end + end + return "" + end + +end +--- **Cargo** -- Management of single cargo crates, which are based on a @{Static} object. +-- +-- === +-- +-- ### [Demo Missions]() +-- +-- ### [YouTube Playlist]() +-- +-- === +-- +-- ### Author: **FlightControl** +-- ### Contributions: +-- +-- === +-- +-- @module Cargo.CargoCrate +-- @image Cargo_Crates.JPG + +do -- CARGO_CRATE + + --- Models the behaviour of cargo crates, which can be slingloaded and boarded on helicopters. + -- @type CARGO_CRATE + -- @extends Cargo.Cargo#CARGO_REPRESENTABLE + + --- Defines a cargo that is represented by a UNIT object within the simulator, and can be transported by a carrier. + -- Use the event functions as described above to Load, UnLoad, Board, UnBoard the CARGO\_CRATE objects to and from carriers. + -- + -- The above cargo classes are used by the following AI_CARGO_ classes to allow AI groups to transport cargo: + -- + -- * AI Armoured Personnel Carriers to transport cargo and engage in battles, using the @{AI.AI_Cargo_APC} module. + -- * AI Helicopters to transport cargo, using the @{AI.AI_Cargo_Helicopter} module. + -- * AI Planes to transport cargo, using the @{AI.AI_Cargo_Airplane} module. + -- * AI Ships is planned. + -- + -- The above cargo classes are also used by the TASK_CARGO_ classes to allow human players to transport cargo as part of a tasking: + -- + -- * @{Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT} to transport cargo by human players. + -- * @{Tasking.Task_Cargo_Transport#TASK_CARGO_CSAR} to transport downed pilots by human players. + -- + -- === + -- + -- @field #CARGO_CRATE + CARGO_CRATE = { + ClassName = "CARGO_CRATE" + } + + --- CARGO_CRATE Constructor. + -- @param #CARGO_CRATE self + -- @param Wrapper.Static#STATIC CargoStatic + -- @param #string Type + -- @param #string Name + -- @param #number LoadRadius (optional) + -- @param #number NearRadius (optional) + -- @return #CARGO_CRATE + function CARGO_CRATE:New( CargoStatic, Type, Name, LoadRadius, NearRadius ) + local self = BASE:Inherit( self, CARGO_REPRESENTABLE:New( CargoStatic, Type, Name, nil, LoadRadius, NearRadius ) ) -- #CARGO_CRATE + self:F( { Type, Name, NearRadius } ) + + self.CargoObject = CargoStatic -- Wrapper.Static#STATIC + + -- Cargo objects are added to the _DATABASE and SET_CARGO objects. + _EVENTDISPATCHER:CreateEventNewCargo( self ) + + self:HandleEvent( EVENTS.Dead, self.OnEventCargoDead ) + self:HandleEvent( EVENTS.Crash, self.OnEventCargoDead ) + --self:HandleEvent( EVENTS.RemoveUnit, self.OnEventCargoDead ) + self:HandleEvent( EVENTS.PlayerLeaveUnit, self.OnEventCargoDead ) + + self:SetEventPriority( 4 ) + + self.NearRadius = NearRadius or 25 + + return self + end + + --- @param #CARGO_CRATE self + -- @param Core.Event#EVENTDATA EventData + function CARGO_CRATE:OnEventCargoDead( EventData ) + + local Destroyed = false + + if self:IsDestroyed() or self:IsUnLoaded() or self:IsBoarding() then + if self.CargoObject:GetName() == EventData.IniUnitName then + if not self.NoDestroy then + Destroyed = true + end + end + else + if self:IsLoaded() then + local CarrierName = self.CargoCarrier:GetName() + if CarrierName == EventData.IniDCSUnitName then + MESSAGE:New( "Cargo is lost from carrier " .. CarrierName, 15 ):ToAll() + Destroyed = true + self.CargoCarrier:ClearCargo() + end + end + end + + if Destroyed then + self:I( { "Cargo crate destroyed: " .. self.CargoObject:GetName() } ) + self:Destroyed() + end + + end + + + --- Enter UnLoaded State. + -- @param #CARGO_CRATE self + -- @param #string Event + -- @param #string From + -- @param #string To + -- @param Core.Point#POINT_VEC2 + function CARGO_CRATE:onenterUnLoaded( From, Event, To, ToPointVec2 ) + --self:F( { ToPointVec2, From, Event, To } ) + + local Angle = 180 + local Speed = 10 + local Distance = 10 + + if From == "Loaded" then + local StartCoordinate = self.CargoCarrier:GetCoordinate() + local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. + local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) + local CargoDeployCoord = StartCoordinate:Translate( Distance, CargoDeployHeading ) + + ToPointVec2 = ToPointVec2 or COORDINATE:NewFromVec2( { x= CargoDeployCoord.x, y = CargoDeployCoord.z } ) + + -- Respawn the group... + if self.CargoObject then + self.CargoObject:ReSpawnAt( ToPointVec2, 0 ) + self.CargoCarrier = nil + end + + end + + if self.OnUnLoadedCallBack then + self.OnUnLoadedCallBack( self, unpack( self.OnUnLoadedParameters ) ) + self.OnUnLoadedCallBack = nil + end + + end + + + --- Loaded State. + -- @param #CARGO_CRATE self + -- @param #string Event + -- @param #string From + -- @param #string To + -- @param Wrapper.Unit#UNIT CargoCarrier + function CARGO_CRATE:onenterLoaded( From, Event, To, CargoCarrier ) + --self:F( { From, Event, To, CargoCarrier } ) + + self.CargoCarrier = CargoCarrier + + -- Only destroy the CargoObject is if there is a CargoObject (packages don't have CargoObjects). + if self.CargoObject then + self:T("Destroying") + self.NoDestroy = true + self.CargoObject:Destroy( false ) -- Do not generate a remove unit event, because we want to keep the template for later respawn in the database. + --local Coordinate = self.CargoObject:GetCoordinate():GetRandomCoordinateInRadius( 50, 20 ) + --self.CargoObject:ReSpawnAt( Coordinate, 0 ) + end + end + + --- Check if the cargo can be Boarded. + -- @param #CARGO_CRATE self + function CARGO_CRATE:CanBoard() + return false + end + + --- Check if the cargo can be Unboarded. + -- @param #CARGO_CRATE self + function CARGO_CRATE:CanUnboard() + return false + end + + --- Check if the cargo can be sling loaded. + -- @param #CARGO_CRATE self + function CARGO_CRATE:CanSlingload() + return false + end + + --- Check if Cargo Crate is in the radius for the Cargo to be reported. + -- @param #CARGO_CRATE self + -- @param Core.Point#COORDINATE Coordinate + -- @return #boolean true if the Cargo Crate is within the report radius. + function CARGO_CRATE:IsInReportRadius( Coordinate ) + --self:F( { Coordinate, LoadRadius = self.LoadRadius } ) + + local Distance = 0 + if self:IsUnLoaded() then + Distance = Coordinate:Get2DDistance( self.CargoObject:GetCoordinate() ) + --self:T( Distance ) + if Distance <= self.LoadRadius then + return true + end + end + + return false + end + + + --- Check if Cargo Crate is in the radius for the Cargo to be Boarded or Loaded. + -- @param #CARGO_CRATE self + -- @param Core.Point#Coordinate Coordinate + -- @return #boolean true if the Cargo Crate is within the loading radius. + function CARGO_CRATE:IsInLoadRadius( Coordinate ) + --self:F( { Coordinate, LoadRadius = self.NearRadius } ) + + local Distance = 0 + if self:IsUnLoaded() then + Distance = Coordinate:Get2DDistance( self.CargoObject:GetCoordinate() ) + --self:T( Distance ) + if Distance <= self.NearRadius then + return true + end + end + + return false + end + + + + --- Get the current Coordinate of the CargoGroup. + -- @param #CARGO_CRATE self + -- @return Core.Point#COORDINATE The current Coordinate of the first Cargo of the CargoGroup. + -- @return #nil There is no valid Cargo in the CargoGroup. + function CARGO_CRATE:GetCoordinate() + --self:F() + + return self.CargoObject:GetCoordinate() + end + + --- Check if the CargoGroup is alive. + -- @param #CARGO_CRATE self + -- @return #boolean true if the CargoGroup is alive. + -- @return #boolean false if the CargoGroup is dead. + function CARGO_CRATE:IsAlive() + + local Alive = true + + -- When the Cargo is Loaded, the Cargo is in the CargoCarrier, so we check if the CargoCarrier is alive. + -- When the Cargo is not Loaded, the Cargo is the CargoObject, so we check if the CargoObject is alive. + if self:IsLoaded() then + Alive = Alive == true and self.CargoCarrier:IsAlive() + else + Alive = Alive == true and self.CargoObject:IsAlive() + end + + return Alive + + end + + + --- Route Cargo to Coordinate and randomize locations. + -- @param #CARGO_CRATE self + -- @param Core.Point#COORDINATE Coordinate + function CARGO_CRATE:RouteTo( Coordinate ) + self:F( {Coordinate = Coordinate } ) + + end + + + --- Check if Cargo is near to the Carrier. + -- The Cargo is near to the Carrier within NearRadius. + -- @param #CARGO_CRATE self + -- @param Wrapper.Group#GROUP CargoCarrier + -- @param #number NearRadius + -- @return #boolean The Cargo is near to the Carrier. + -- @return #nil The Cargo is not near to the Carrier. + function CARGO_CRATE:IsNear( CargoCarrier, NearRadius ) + self:F( {NearRadius = NearRadius } ) + + return self:IsNear( CargoCarrier:GetCoordinate(), NearRadius ) + end + + --- Respawn the CargoGroup. + -- @param #CARGO_CRATE self + function CARGO_CRATE:Respawn() + + self:F( { "Respawning crate " .. self:GetName() } ) + + + -- Respawn the group... + if self.CargoObject then + self.CargoObject:ReSpawn() -- A cargo destroy crates a DEAD event. + self:__Reset( -0.1 ) + end + + + end + + + --- Respawn the CargoGroup. + -- @param #CARGO_CRATE self + function CARGO_CRATE:onafterReset() + + self:F( { "Reset crate " .. self:GetName() } ) + + + -- Respawn the group... + if self.CargoObject then + self:SetDeployed( false ) + self:SetStartState( "UnLoaded" ) + self.CargoCarrier = nil + -- Cargo objects are added to the _DATABASE and SET_CARGO objects. + _EVENTDISPATCHER:CreateEventNewCargo( self ) + end + + + end + + --- Get the transportation method of the Cargo. + -- @param #CARGO_CRATE self + -- @return #string The transportation method of the Cargo. + function CARGO_CRATE:GetTransportationMethod() + if self:IsLoaded() then + return "for unloading" + else + if self:IsUnLoaded() then + return "for loading" + else + if self:IsDeployed() then + return "delivered" + end + end + end + return "" + end + +end + +--- **Cargo** - Management of grouped cargo logistics, which are based on a @{Wrapper.Group} object. +-- +-- === +-- +-- ### [Demo Missions]() +-- +-- ### [YouTube Playlist]() +-- +-- === +-- +-- ### Author: **FlightControl** +-- ### Contributions: +-- +-- === +-- +-- @module Cargo.CargoGroup +-- @image Cargo_Groups.JPG + + +do -- CARGO_GROUP + + --- @type CARGO_GROUP + -- @field Core.Set#SET_CARGO CargoSet The collection of derived CARGO objects. + -- @field #string GroupName The name of the CargoGroup. + -- @extends Cargo.Cargo#CARGO_REPORTABLE + + --- Defines a cargo that is represented by a @{Wrapper.Group} object within the simulator. + -- The cargo can be Loaded, UnLoaded, Boarded, UnBoarded to and from Carriers. + -- + -- The above cargo classes are used by the following AI_CARGO_ classes to allow AI groups to transport cargo: + -- + -- * AI Armoured Personnel Carriers to transport cargo and engage in battles, using the @{AI.AI_Cargo_APC} module. + -- * AI Helicopters to transport cargo, using the @{AI.AI_Cargo_Helicopter} module. + -- * AI Planes to transport cargo, using the @{AI.AI_Cargo_Airplane} module. + -- * AI Ships is planned. + -- + -- The above cargo classes are also used by the TASK_CARGO_ classes to allow human players to transport cargo as part of a tasking: + -- + -- * @{Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT} to transport cargo by human players. + -- * @{Tasking.Task_Cargo_Transport#TASK_CARGO_CSAR} to transport downed pilots by human players. + -- + -- @field #CARGO_GROUP CARGO_GROUP + -- + CARGO_GROUP = { + ClassName = "CARGO_GROUP", + } + + --- CARGO_GROUP constructor. + -- This make a new CARGO_GROUP from a @{Wrapper.Group} object. + -- It will "ungroup" the group object within the sim, and will create a @{Set} of individual Unit objects. + -- @param #CARGO_GROUP self + -- @param Wrapper.Group#GROUP CargoGroup Group to be transported as cargo. + -- @param #string Type Cargo type, e.g. "Infantry". This is the type used in SET_CARGO:New():FilterTypes("Infantry") to define the valid cargo groups of the set. + -- @param #string Name A user defined name of the cargo group. This name CAN be the same as the group object but can also have a different name. This name MUST be unique! + -- @param #number LoadRadius (optional) Distance in meters until which a cargo is loaded into the carrier. Cargo outside this radius has to be routed by other means to within the radius to be loaded. + -- @param #number NearRadius (optional) Once the units are within this radius of the carrier, they are actually loaded, i.e. disappear from the scene. + -- @return #CARGO_GROUP Cargo group object. + function CARGO_GROUP:New( CargoGroup, Type, Name, LoadRadius, NearRadius ) + + -- Inherit CAROG_REPORTABLE + local self = BASE:Inherit( self, CARGO_REPORTABLE:New( Type, Name, 0, LoadRadius, NearRadius ) ) -- #CARGO_GROUP + self:F( { Type, Name, LoadRadius } ) + + self.CargoSet = SET_CARGO:New() + self.CargoGroup = CargoGroup + self.Grouped = true + self.CargoUnitTemplate = {} + + self.NearRadius = NearRadius + + self:SetDeployed( false ) + + local WeightGroup = 0 + local VolumeGroup = 0 + + self.CargoGroup:Destroy() -- destroy and generate a unit removal event, so that the database gets cleaned, and the linked sets get properly cleaned. + + local GroupName = CargoGroup:GetName() + self.CargoName = Name + self.CargoTemplate = UTILS.DeepCopy( _DATABASE:GetGroupTemplate( GroupName ) ) + + -- Deactivate late activation. + self.CargoTemplate.lateActivation=false + + self.GroupTemplate = UTILS.DeepCopy( self.CargoTemplate ) + self.GroupTemplate.name = self.CargoName .. "#CARGO" + self.GroupTemplate.groupId = nil + + self.GroupTemplate.units = {} + + for UnitID, UnitTemplate in pairs( self.CargoTemplate.units ) do + UnitTemplate.name = UnitTemplate.name .. "#CARGO" + local CargoUnitName = UnitTemplate.name + self.CargoUnitTemplate[CargoUnitName] = UnitTemplate + + self.GroupTemplate.units[#self.GroupTemplate.units+1] = self.CargoUnitTemplate[CargoUnitName] + self.GroupTemplate.units[#self.GroupTemplate.units].unitId = nil + + -- And we register the spawned unit as part of the CargoSet. + local Unit = UNIT:Register( CargoUnitName ) + + end + + -- Then we register the new group in the database + self.CargoGroup = GROUP:NewTemplate( self.GroupTemplate, self.GroupTemplate.CoalitionID, self.GroupTemplate.CategoryID, self.GroupTemplate.CountryID ) + + -- Now we spawn the new group based on the template created. + self.CargoObject = _DATABASE:Spawn( self.GroupTemplate ) + + for CargoUnitID, CargoUnit in pairs( self.CargoObject:GetUnits() ) do + + + local CargoUnitName = CargoUnit:GetName() + + local Cargo = CARGO_UNIT:New( CargoUnit, Type, CargoUnitName, LoadRadius, NearRadius ) + self.CargoSet:Add( CargoUnitName, Cargo ) + + WeightGroup = WeightGroup + Cargo:GetWeight() + + end + + self:SetWeight( WeightGroup ) + + self:T( { "Weight Cargo", WeightGroup } ) + + -- Cargo objects are added to the _DATABASE and SET_CARGO objects. + _EVENTDISPATCHER:CreateEventNewCargo( self ) + + self:HandleEvent( EVENTS.Dead, self.OnEventCargoDead ) + self:HandleEvent( EVENTS.Crash, self.OnEventCargoDead ) + --self:HandleEvent( EVENTS.RemoveUnit, self.OnEventCargoDead ) + self:HandleEvent( EVENTS.PlayerLeaveUnit, self.OnEventCargoDead ) + + self:SetEventPriority( 4 ) + + return self + end + + + --- Respawn the CargoGroup. + -- @param #CARGO_GROUP self + function CARGO_GROUP:Respawn() + + self:F( { "Respawning" } ) + + for CargoID, CargoData in pairs( self.CargoSet:GetSet() ) do + local Cargo = CargoData -- Cargo.Cargo#CARGO + Cargo:Destroy() -- Destroy the cargo and generate a remove unit event to update the sets. + Cargo:SetStartState( "UnLoaded" ) + end + + -- Now we spawn the new group based on the template created. + _DATABASE:Spawn( self.GroupTemplate ) + + for CargoUnitID, CargoUnit in pairs( self.CargoObject:GetUnits() ) do + + local CargoUnitName = CargoUnit:GetName() + + local Cargo = CARGO_UNIT:New( CargoUnit, self.Type, CargoUnitName, self.LoadRadius ) + self.CargoSet:Add( CargoUnitName, Cargo ) + + end + + self:SetDeployed( false ) + self:SetStartState( "UnLoaded" ) + + end + + --- Ungroup the cargo group into individual groups with one unit. + -- This is required because by default a group will move in formation and this is really an issue for group control. + -- Therefore this method is made to be able to ungroup a group. + -- This works for ground only groups. + -- @param #CARGO_GROUP self + function CARGO_GROUP:Ungroup() + + if self.Grouped == true then + + self.Grouped = false + + self.CargoGroup:Destroy() + + for CargoUnitName, CargoUnit in pairs( self.CargoSet:GetSet() ) do + local CargoUnit = CargoUnit -- Cargo.CargoUnit#CARGO_UNIT + + if CargoUnit:IsUnLoaded() then + local GroupTemplate = UTILS.DeepCopy( self.CargoTemplate ) + --local GroupName = env.getValueDictByKey( GroupTemplate.name ) + + -- We create a new group object with one unit... + -- First we prepare the template... + GroupTemplate.name = self.CargoName .. "#CARGO#" .. CargoUnitName + GroupTemplate.groupId = nil + + if CargoUnit:IsUnLoaded() then + GroupTemplate.units = {} + GroupTemplate.units[1] = self.CargoUnitTemplate[CargoUnitName] + GroupTemplate.units[#GroupTemplate.units].unitId = nil + GroupTemplate.units[#GroupTemplate.units].x = CargoUnit:GetX() + GroupTemplate.units[#GroupTemplate.units].y = CargoUnit:GetY() + GroupTemplate.units[#GroupTemplate.units].heading = CargoUnit:GetHeading() + end + + + -- Then we register the new group in the database + local CargoGroup = GROUP:NewTemplate( GroupTemplate, GroupTemplate.CoalitionID, GroupTemplate.CategoryID, GroupTemplate.CountryID) + + -- Now we spawn the new group based on the template created. + _DATABASE:Spawn( GroupTemplate ) + end + end + + self.CargoObject = nil + end + + + end + + --- Regroup the cargo group into one group with multiple unit. + -- This is required because by default a group will move in formation and this is really an issue for group control. + -- Therefore this method is made to be able to regroup a group. + -- This works for ground only groups. + -- @param #CARGO_GROUP self + function CARGO_GROUP:Regroup() + + self:F("Regroup") + + if self.Grouped == false then + + self.Grouped = true + + local GroupTemplate = UTILS.DeepCopy( self.CargoTemplate ) + GroupTemplate.name = self.CargoName .. "#CARGO" + GroupTemplate.groupId = nil + GroupTemplate.units = {} + + for CargoUnitName, CargoUnit in pairs( self.CargoSet:GetSet() ) do + local CargoUnit = CargoUnit -- Cargo.CargoUnit#CARGO_UNIT + + self:F( { CargoUnit:GetName(), UnLoaded = CargoUnit:IsUnLoaded() } ) + + if CargoUnit:IsUnLoaded() then + + CargoUnit.CargoObject:Destroy() + + GroupTemplate.units[#GroupTemplate.units+1] = self.CargoUnitTemplate[CargoUnitName] + GroupTemplate.units[#GroupTemplate.units].unitId = nil + GroupTemplate.units[#GroupTemplate.units].x = CargoUnit:GetX() + GroupTemplate.units[#GroupTemplate.units].y = CargoUnit:GetY() + GroupTemplate.units[#GroupTemplate.units].heading = CargoUnit:GetHeading() + end + end + + -- Then we register the new group in the database + self.CargoGroup = GROUP:NewTemplate( GroupTemplate, GroupTemplate.CoalitionID, GroupTemplate.CategoryID, GroupTemplate.CountryID ) + + self:F( { "Regroup", GroupTemplate } ) + + -- Now we spawn the new group based on the template created. + self.CargoObject = _DATABASE:Spawn( GroupTemplate ) + end + + end + + + --- @param #CARGO_GROUP self + -- @param Core.Event#EVENTDATA EventData + function CARGO_GROUP:OnEventCargoDead( EventData ) + + self:E(EventData) + + local Destroyed = false + + if self:IsDestroyed() or self:IsUnLoaded() or self:IsBoarding() or self:IsUnboarding() then + Destroyed = true + for CargoID, CargoData in pairs( self.CargoSet:GetSet() ) do + local Cargo = CargoData -- Cargo.Cargo#CARGO + if Cargo:IsAlive() then + Destroyed = false + else + Cargo:Destroyed() + end + end + else + local CarrierName = self.CargoCarrier:GetName() + if CarrierName == EventData.IniDCSUnitName then + MESSAGE:New( "Cargo is lost from carrier " .. CarrierName, 15 ):ToAll() + Destroyed = true + self.CargoCarrier:ClearCargo() + end + end + + if Destroyed then + self:Destroyed() + self:E( { "Cargo group destroyed" } ) + end + + end + + --- After Board Event. + -- @param #CARGO_GROUP self + -- @param #string Event + -- @param #string From + -- @param #string To + -- @param Wrapper.Unit#UNIT CargoCarrier + -- @param #number NearRadius If distance is smaller than this number, cargo is loaded into the carrier. + function CARGO_GROUP:onafterBoard( From, Event, To, CargoCarrier, NearRadius, ... ) + self:F( { CargoCarrier.UnitName, From, Event, To, NearRadius = NearRadius } ) + + NearRadius = NearRadius or self.NearRadius + + -- For each Cargo object within the CARGO_GROUPED, route each object to the CargoLoadPointVec2 + self.CargoSet:ForEach( + function( Cargo, ... ) + self:F( { "Board Unit", Cargo:GetName( ), Cargo:IsDestroyed(), Cargo.CargoObject:IsAlive() } ) + local CargoGroup = Cargo.CargoObject --Wrapper.Group#GROUP + CargoGroup:OptionAlarmStateGreen() + Cargo:__Board( 1, CargoCarrier, NearRadius, ... ) + end, ... + ) + + self:__Boarding( -1, CargoCarrier, NearRadius, ... ) + + end + + --- Enter Loaded State. + -- @param #CARGO_GROUP self + -- @param #string Event + -- @param #string From + -- @param #string To + -- @param Wrapper.Unit#UNIT CargoCarrier + function CARGO_GROUP:onafterLoad( From, Event, To, CargoCarrier, ... ) + --self:F( { From, Event, To, CargoCarrier, ...} ) + + if From == "UnLoaded" then + -- For each Cargo object within the CARGO_GROUP, load each cargo to the CargoCarrier. + for CargoID, Cargo in pairs( self.CargoSet:GetSet() ) do + if not Cargo:IsDestroyed() then + Cargo:Load( CargoCarrier ) + end + end + end + + --self.CargoObject:Destroy() + self.CargoCarrier = CargoCarrier + self.CargoCarrier:AddCargo( self ) + + end + + --- Leave Boarding State. + -- @param #CARGO_GROUP self + -- @param #string Event + -- @param #string From + -- @param #string To + -- @param Wrapper.Unit#UNIT CargoCarrier + -- @param #number NearRadius If distance is smaller than this number, cargo is loaded into the carrier. + function CARGO_GROUP:onafterBoarding( From, Event, To, CargoCarrier, NearRadius, ... ) + --self:F( { CargoCarrier.UnitName, From, Event, To } ) + + local Boarded = true + local Cancelled = false + local Dead = true + + self.CargoSet:Flush() + + -- For each Cargo object within the CARGO_GROUP, route each object to the CargoLoadPointVec2 + for CargoID, Cargo in pairs( self.CargoSet:GetSet() ) do + --self:T( { Cargo:GetName(), Cargo.current } ) + + + if not Cargo:is( "Loaded" ) + and (not Cargo:is( "Destroyed" )) then -- If one or more units of a group defined as CARGO_GROUP died, the CARGO_GROUP:Board() command does not trigger the CARGO_GRUOP:OnEnterLoaded() function. + Boarded = false + end + + if Cargo:is( "UnLoaded" ) then + Cancelled = true + end + + if not Cargo:is( "Destroyed" ) then + Dead = false + end + + end + + if not Dead then + + if not Cancelled then + if not Boarded then + self:__Boarding( -5, CargoCarrier, NearRadius, ... ) + else + self:F("Group Cargo is loaded") + self:__Load( 1, CargoCarrier, ... ) + end + else + self:__CancelBoarding( 1, CargoCarrier, NearRadius, ... ) + end + else + self:__Destroyed( 1, CargoCarrier, NearRadius, ... ) + end + + end + + --- Enter UnBoarding State. + -- @param #CARGO_GROUP self + -- @param #string Event + -- @param #string From + -- @param #string To + -- @param Core.Point#POINT_VEC2 ToPointVec2 + -- @param #number NearRadius If distance is smaller than this number, cargo is loaded into the carrier. + function CARGO_GROUP:onafterUnBoard( From, Event, To, ToPointVec2, NearRadius, ... ) + self:F( {From, Event, To, ToPointVec2, NearRadius } ) + + NearRadius = NearRadius or 25 + + local Timer = 1 + + if From == "Loaded" then + + if self.CargoObject then + self.CargoObject:Destroy() + end + + -- For each Cargo object within the CARGO_GROUP, route each object to the CargoLoadPointVec2 + self.CargoSet:ForEach( + --- @param Cargo.Cargo#CARGO Cargo + function( Cargo, NearRadius ) + if not Cargo:IsDestroyed() then + local ToVec=nil + if ToPointVec2==nil then + ToVec=self.CargoCarrier:GetPointVec2():GetRandomPointVec2InRadius(2*NearRadius, NearRadius) + else + ToVec=ToPointVec2 + end + Cargo:__UnBoard( Timer, ToVec, NearRadius ) + Timer = Timer + 1 + end + end, { NearRadius } + ) + + + self:__UnBoarding( 1, ToPointVec2, NearRadius, ... ) + end + + end + + --- Leave UnBoarding State. + -- @param #CARGO_GROUP self + -- @param #string Event + -- @param #string From + -- @param #string To + -- @param Core.Point#POINT_VEC2 ToPointVec2 + -- @param #number NearRadius If distance is smaller than this number, cargo is loaded into the carrier. + function CARGO_GROUP:onafterUnBoarding( From, Event, To, ToPointVec2, NearRadius, ... ) + --self:F( { From, Event, To, ToPointVec2, NearRadius } ) + + --local NearRadius = NearRadius or 25 + + local Angle = 180 + local Speed = 10 + local Distance = 5 + + if From == "UnBoarding" then + local UnBoarded = true + + -- For each Cargo object within the CARGO_GROUP, route each object to the CargoLoadPointVec2 + for CargoID, Cargo in pairs( self.CargoSet:GetSet() ) do + self:T( { Cargo:GetName(), Cargo.current } ) + if not Cargo:is( "UnLoaded" ) and not Cargo:IsDestroyed() then + UnBoarded = false + end + end + + if UnBoarded then + self:__UnLoad( 1, ToPointVec2, ... ) + else + self:__UnBoarding( 1, ToPointVec2, NearRadius, ... ) + end + + return false + end + + end + + --- Enter UnLoaded State. + -- @param #CARGO_GROUP self + -- @param #string Event + -- @param #string From + -- @param #string To + -- @param Core.Point#POINT_VEC2 ToPointVec2 + function CARGO_GROUP:onafterUnLoad( From, Event, To, ToPointVec2, ... ) + --self:F( { From, Event, To, ToPointVec2 } ) + + if From == "Loaded" then + + -- For each Cargo object within the CARGO_GROUP, route each object to the CargoLoadPointVec2 + self.CargoSet:ForEach( + function( Cargo ) + --Cargo:UnLoad( ToPointVec2 ) + local RandomVec2=nil + if ToPointVec2 then + RandomVec2=ToPointVec2:GetRandomPointVec2InRadius(20, 10) + end + Cargo:UnBoard( RandomVec2 ) + end + ) + + end + + self.CargoCarrier:RemoveCargo( self ) + self.CargoCarrier = nil + + end + + + --- Get the current Coordinate of the CargoGroup. + -- @param #CARGO_GROUP self + -- @return Core.Point#COORDINATE The current Coordinate of the first Cargo of the CargoGroup. + -- @return #nil There is no valid Cargo in the CargoGroup. + function CARGO_GROUP:GetCoordinate() + local Cargo = self:GetFirstAlive() -- Cargo.Cargo#CARGO + + if Cargo then + return Cargo.CargoObject:GetCoordinate() + end + + return nil + end + + --- Get the x position of the cargo. + -- @param #CARGO_GROUP self + -- @return #number + function CARGO:GetX() + + local Cargo = self:GetFirstAlive() -- Cargo.Cargo#CARGO + + if Cargo then + return Cargo:GetCoordinate().x + end + + return nil + end + + --- Get the y position of the cargo. + -- @param #CARGO_GROUP self + -- @return #number + function CARGO:GetY() + + local Cargo = self:GetFirstAlive() -- Cargo.Cargo#CARGO + + if Cargo then + return Cargo:GetCoordinate().z + end + + return nil + end + + + + --- Check if the CargoGroup is alive. + -- @param #CARGO_GROUP self + -- @return #boolean true if the CargoGroup is alive. + -- @return #boolean false if the CargoGroup is dead. + function CARGO_GROUP:IsAlive() + + local Cargo = self:GetFirstAlive() -- Cargo.Cargo#CARGO + return Cargo ~= nil + + end + + + --- Get the first alive Cargo Unit of the Cargo Group. + -- @param #CARGO_GROUP self + -- @return #CARGO_GROUP + function CARGO_GROUP:GetFirstAlive() + + local CargoFirstAlive = nil + + for _, Cargo in pairs( self.CargoSet:GetSet() ) do + if not Cargo:IsDestroyed() then + CargoFirstAlive = Cargo + break + end + end + return CargoFirstAlive + end + + + --- Get the amount of cargo units in the group. + -- @param #CARGO_GROUP self + -- @return #CARGO_GROUP + function CARGO_GROUP:GetCount() + return self.CargoSet:Count() + end + + + --- Get the amount of cargo units in the group. + -- @param #CARGO_GROUP self + -- @return #CARGO_GROUP + function CARGO_GROUP:GetGroup( Cargo ) + local Cargo = Cargo or self:GetFirstAlive() -- Cargo.Cargo#CARGO + return Cargo.CargoObject:GetGroup() + end + + + --- Route Cargo to Coordinate and randomize locations. + -- @param #CARGO_GROUP self + -- @param Core.Point#COORDINATE Coordinate + function CARGO_GROUP:RouteTo( Coordinate ) + --self:F( {Coordinate = Coordinate } ) + + -- For each Cargo within the CargoSet, route each object to the Coordinate + self.CargoSet:ForEach( + function( Cargo ) + Cargo.CargoObject:RouteGroundTo( Coordinate, 10, "vee", 0 ) + end + ) + + end + + --- Check if Cargo is near to the Carrier. + -- The Cargo is near to the Carrier if the first unit of the Cargo Group is within NearRadius. + -- @param #CARGO_GROUP self + -- @param Wrapper.Group#GROUP CargoCarrier + -- @param #number NearRadius + -- @return #boolean The Cargo is near to the Carrier or #nil if the Cargo is not near to the Carrier. + function CARGO_GROUP:IsNear( CargoCarrier, NearRadius ) + self:F( {NearRadius = NearRadius } ) + + for _, Cargo in pairs( self.CargoSet:GetSet() ) do + local Cargo = Cargo -- Cargo.Cargo#CARGO + if Cargo:IsAlive() then + if Cargo:IsNear( CargoCarrier:GetCoordinate(), NearRadius ) then + self:F( "Near" ) + return true + end + end + end + + return nil + end + + --- Check if Cargo Group is in the radius for the Cargo to be Boarded. + -- @param #CARGO_GROUP self + -- @param Core.Point#COORDINATE Coordinate + -- @return #boolean true if the Cargo Group is within the load radius. + function CARGO_GROUP:IsInLoadRadius( Coordinate ) + --self:F( { Coordinate } ) + + local Cargo = self:GetFirstAlive() -- Cargo.Cargo#CARGO + + if Cargo then + local Distance = 0 + local CargoCoordinate + if Cargo:IsLoaded() then + CargoCoordinate = Cargo.CargoCarrier:GetCoordinate() + else + CargoCoordinate = Cargo.CargoObject:GetCoordinate() + end + + -- FF check if coordinate could be obtained. This was commented out for some (unknown) reason. But the check seems valid! + if CargoCoordinate then + Distance = Coordinate:Get2DDistance( CargoCoordinate ) + else + return false + end + + self:F( { Distance = Distance, LoadRadius = self.LoadRadius } ) + if Distance <= self.LoadRadius then + return true + else + return false + end + end + + return nil + + end + + + --- Check if Cargo Group is in the report radius. + -- @param #CARGO_GROUP self + -- @param Core.Point#Coordinate Coordinate + -- @return #boolean true if the Cargo Group is within the report radius. + function CARGO_GROUP:IsInReportRadius( Coordinate ) + --self:F( { Coordinate } ) + + local Cargo = self:GetFirstAlive() -- Cargo.Cargo#CARGO + + if Cargo then + self:F( { Cargo } ) + local Distance = 0 + if Cargo:IsUnLoaded() then + Distance = Coordinate:Get2DDistance( Cargo.CargoObject:GetCoordinate() ) + --self:T( Distance ) + if Distance <= self.LoadRadius then + return true + end + end + end + + return nil + + end + + + --- Signal a flare at the position of the CargoGroup. + -- @param #CARGO_GROUP self + -- @param Utilities.Utils#FLARECOLOR FlareColor + function CARGO_GROUP:Flare( FlareColor ) + + local Cargo = self.CargoSet:GetFirst() -- Cargo.Cargo#CARGO + if Cargo then + Cargo:Flare( FlareColor ) + end + end + + --- Smoke the CargoGroup. + -- @param #CARGO_GROUP self + -- @param Utilities.Utils#SMOKECOLOR SmokeColor The color of the smoke. + -- @param #number Radius The radius of randomization around the center of the first element of the CargoGroup. + function CARGO_GROUP:Smoke( SmokeColor, Radius ) + + local Cargo = self.CargoSet:GetFirst() -- Cargo.Cargo#CARGO + + if Cargo then + Cargo:Smoke( SmokeColor, Radius ) + end + end + + --- Check if the first element of the CargoGroup is the given @{Zone}. + -- @param #CARGO_GROUP self + -- @param Core.Zone#ZONE_BASE Zone + -- @return #boolean **true** if the first element of the CargoGroup is in the Zone + -- @return #boolean **false** if there is no element of the CargoGroup in the Zone. + function CARGO_GROUP:IsInZone( Zone ) + --self:F( { Zone } ) + + local Cargo = self.CargoSet:GetFirst() -- Cargo.Cargo#CARGO + + if Cargo then + return Cargo:IsInZone( Zone ) + end + + return nil + + end + + --- Get the transportation method of the Cargo. + -- @param #CARGO_GROUP self + -- @return #string The transportation method of the Cargo. + function CARGO_GROUP:GetTransportationMethod() + if self:IsLoaded() then + return "for unboarding" + else + if self:IsUnLoaded() then + return "for boarding" + else + if self:IsDeployed() then + return "delivered" + end + end + end + return "" + end + + + +end -- CARGO_GROUP +--- **Functional** - Administer the scoring of player achievements, and create a CSV file logging the scoring events for use at team or squadron websites. +-- +-- === +-- +-- ## Features: +-- +-- * Set the scoring scales based on threat level. +-- * Positive scores and negative scores. +-- * A contribution model to score achievements. +-- * Score goals. +-- * Score specific achievements. +-- * Score the hits and destroys of units. +-- * Score the hits and destroys of statics. +-- * Score the hits and destroys of scenery. +-- * Log scores into a CSV file. +-- * Connect to a remote server using JSON and IP. +-- +-- === +-- +-- ## Missions: +-- +-- [SCO - Scoring](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/SCO%20-%20Scoring) +-- +-- === +-- +-- Administers the scoring of player achievements, +-- and creates a CSV file logging the scoring events and results for use at team or squadron websites. +-- +-- SCORING automatically calculates the threat level of the objects hit and destroyed by players, +-- which can be @{Wrapper.Unit}, @{Static) and @{Scenery} objects. +-- +-- Positive score points are granted when enemy or neutral targets are destroyed. +-- Negative score points or penalties are given when a friendly target is hit or destroyed. +-- This brings a lot of dynamism in the scoring, where players need to take care to inflict damage on the right target. +-- By default, penalties weight heavier in the scoring, to ensure that players don't commit fratricide. +-- The total score of the player is calculated by **adding the scores minus the penalties**. +-- +-- ![Banner Image](..\Presentations\SCORING\Dia4.JPG) +-- +-- The score value is calculated based on the **threat level of the player** and the **threat level of the target**. +-- A calculated score takes the threat level of the target divided by a balanced threat level of the player unit. +-- As such, if the threat level of the target is high, and the player threat level is low, a higher score will be given than +-- if the threat level of the player would be high too. +-- +-- ![Banner Image](..\Presentations\SCORING\Dia5.JPG) +-- +-- When multiple players hit the same target, and finally succeed in destroying the target, then each player who contributed to the target +-- destruction, will receive a score. This is important for targets that require significant damage before it can be destroyed, like +-- ships or heavy planes. +-- +-- ![Banner Image](..\Presentations\SCORING\Dia13.JPG) +-- +-- Optionally, the score values can be **scaled** by a **scale**. Specific scales can be set for positive cores or negative penalties. +-- The default range of the scores granted is a value between 0 and 10. The default range of penalties given is a value between 0 and 30. +-- +-- ![Banner Image](..\Presentations\SCORING\Dia7.JPG) +-- +-- **Additional scores** can be granted to **specific objects**, when the player(s) destroy these objects. +-- +-- ![Banner Image](..\Presentations\SCORING\Dia9.JPG) +-- +-- Various @{Zone}s can be defined for which scores are also granted when objects in that @{Zone} are destroyed. +-- This is **specifically useful** to designate **scenery targets on the map** that will generate points when destroyed. +-- +-- With a small change in MissionScripting.lua, the scoring results can also be logged in a **CSV file**. +-- These CSV files can be used to: +-- +-- * Upload scoring to a database or a BI tool to publish the scoring results to the player community. +-- * Upload scoring in an (online) Excel like tool, using pivot tables and pivot charts to show mission results. +-- * Share scoring amoung players after the mission to discuss mission results. +-- +-- Scores can be **reported**. **Menu options** are automatically added to **each player group** when a player joins a client slot or a CA unit. +-- Use the radio menu F10 to consult the scores while running the mission. +-- Scores can be reported for your user, or an overall score can be reported of all players currently active in the mission. +-- +-- === +-- +-- ### Authors: **FlightControl** +-- +-- ### Contributions: +-- +-- * **Wingthor (TAW)**: Testing & Advice. +-- * **Dutch-Baron (TAW)**: Testing & Advice. +-- * **[Whisper](http://forums.eagle.ru/member.php?u=3829): Testing and Advice. +-- +-- === +-- +-- @module Functional.Scoring +-- @image Scoring.JPG + + +--- @type SCORING +-- @field Players A collection of the current players that have joined the game. +-- @extends Core.Base#BASE + +--- SCORING class +-- +-- # Constructor: +-- +-- local Scoring = SCORING:New( "Scoring File" ) +-- +-- +-- # Set the destroy score or penalty scale: +-- +-- Score scales can be set for scores granted when enemies or friendlies are destroyed. +-- Use the method @{#SCORING.SetScaleDestroyScore}() to set the scale of enemy destroys (positive destroys). +-- Use the method @{#SCORING.SetScaleDestroyPenalty}() to set the scale of friendly destroys (negative destroys). +-- +-- local Scoring = SCORING:New( "Scoring File" ) +-- Scoring:SetScaleDestroyScore( 10 ) +-- Scoring:SetScaleDestroyPenalty( 40 ) +-- +-- The above sets the scale for valid scores to 10. So scores will be given in a scale from 0 to 10. +-- The penalties will be given in a scale from 0 to 40. +-- +-- # Define special targets that will give extra scores: +-- +-- Special targets can be set that will give extra scores to the players when these are destroyed. +-- Use the methods @{#SCORING.AddUnitScore}() and @{#SCORING.RemoveUnitScore}() to specify a special additional score for a specific @{Wrapper.Unit}s. +-- Use the methods @{#SCORING.AddStaticScore}() and @{#SCORING.RemoveStaticScore}() to specify a special additional score for a specific @{Static}s. +-- Use the method @{#SCORING.SetGroupGroup}() to specify a special additional score for a specific @{Wrapper.Group}s. +-- +-- local Scoring = SCORING:New( "Scoring File" ) +-- Scoring:AddUnitScore( UNIT:FindByName( "Unit #001" ), 200 ) +-- Scoring:AddStaticScore( STATIC:FindByName( "Static #1" ), 100 ) +-- +-- The above grants an additional score of 200 points for Unit #001 and an additional 100 points of Static #1 if these are destroyed. +-- Note that later in the mission, one can remove these scores set, for example, when the a goal achievement time limit is over. +-- For example, this can be done as follows: +-- +-- Scoring:RemoveUnitScore( UNIT:FindByName( "Unit #001" ) ) +-- +-- # Define destruction zones that will give extra scores: +-- +-- Define zones of destruction. Any object destroyed within the zone of the given category will give extra points. +-- Use the method @{#SCORING.AddZoneScore}() to add a @{Zone} for additional scoring. +-- Use the method @{#SCORING.RemoveZoneScore}() to remove a @{Zone} for additional scoring. +-- There are interesting variations that can be achieved with this functionality. For example, if the @{Zone} is a @{Core.Zone#ZONE_UNIT}, +-- then the zone is a moving zone, and anything destroyed within that @{Zone} will generate points. +-- The other implementation could be to designate a scenery target (a building) in the mission editor surrounded by a @{Zone}, +-- just large enough around that building. +-- +-- # Add extra Goal scores upon an event or a condition: +-- +-- A mission has goals and achievements. The scoring system provides an API to set additional scores when a goal or achievement event happens. +-- Use the method @{#SCORING.AddGoalScore}() to add a score for a Player at any time in your mission. +-- +-- # (Decommissioned) Configure fratricide level. +-- +-- **This functionality is decomissioned until the DCS bug concerning Unit:destroy() not being functional in multi player for player units has been fixed by ED**. +-- +-- When a player commits too much damage to friendlies, his penalty score will reach a certain level. +-- Use the method @{#SCORING.SetFratricide}() to define the level when a player gets kicked. +-- By default, the fratricide level is the default penalty mutiplier * 2 for the penalty score. +-- +-- # Penalty score when a player changes the coalition. +-- +-- When a player changes the coalition, he can receive a penalty score. +-- Use the method @{#SCORING.SetCoalitionChangePenalty}() to define the penalty when a player changes coalition. +-- By default, the penalty for changing coalition is the default penalty scale. +-- +-- # Define output CSV files. +-- +-- The CSV file is given the name of the string given in the @{#SCORING.New}{} constructor, followed by the .csv extension. +-- The file is incrementally saved in the **\\Saved Games\\DCS\\Logs** folder, and has a time stamp indicating each mission run. +-- See the following example: +-- +-- local ScoringFirstMission = SCORING:New( "FirstMission" ) +-- local ScoringSecondMission = SCORING:New( "SecondMission" ) +-- +-- The above documents that 2 Scoring objects are created, ScoringFirstMission and ScoringSecondMission. +-- +-- ### **IMPORTANT!!!* +-- In order to allow DCS world to write CSV files, you need to adapt a configuration file in your DCS world installation **on the server**. +-- For this, browse to the **missionscripting.lua** file in your DCS world installation folder. +-- For me, this installation folder is in _D:\\Program Files\\Eagle Dynamics\\DCS World\Scripts_. +-- +-- Edit a few code lines in the MissionScripting.lua file. Comment out the lines **os**, **io** and **lfs**: +-- +-- do +-- --sanitizeModule('os') +-- --sanitizeModule('io') +-- --sanitizeModule('lfs') +-- require = nil +-- loadlib = nil +-- end +-- +-- When these lines are not sanitized, functions become available to check the time, and to write files to your system at the above specified location. +-- Note that the MissionScripting.lua file provides a warning. So please beware of this warning as outlined by Eagle Dynamics! +-- +-- --Sanitize Mission Scripting environment +-- --This makes unavailable some unsecure functions. +-- --Mission downloaded from server to client may contain potentialy harmful lua code that may use these functions. +-- --You can remove the code below and make availble these functions at your own risk. +-- +-- The MOOSE designer cannot take any responsibility of any damage inflicted as a result of the de-sanitization. +-- That being said, I hope that the SCORING class provides you with a great add-on to score your squad mates achievements. +-- +-- # Configure messages. +-- +-- When players hit or destroy targets, messages are sent. +-- Various methods exist to configure: +-- +-- * Which messages are sent upon the event. +-- * Which audience receives the message. +-- +-- ## Configure the messages sent upon the event. +-- +-- Use the following methods to configure when to send messages. By default, all messages are sent. +-- +-- * @{#SCORING.SetMessagesHit}(): Configure to send messages after a target has been hit. +-- * @{#SCORING.SetMessagesDestroy}(): Configure to send messages after a target has been destroyed. +-- * @{#SCORING.SetMessagesAddon}(): Configure to send messages for additional score, after a target has been destroyed. +-- * @{#SCORING.SetMessagesZone}(): Configure to send messages for additional score, after a target has been destroyed within a given zone. +-- +-- ## Configure the audience of the messages. +-- +-- Use the following methods to configure the audience of the messages. By default, the messages are sent to all players in the mission. +-- +-- * @{#SCORING.SetMessagesToAll}(): Configure to send messages to all players. +-- * @{#SCORING.SetMessagesToCoalition}(): Configure to send messages to only those players within the same coalition as the player. +-- +-- === +-- +-- @field #SCORING +SCORING = { + ClassName = "SCORING", + ClassID = 0, + Players = {}, +} + +local _SCORINGCoalition = + { + [1] = "Red", + [2] = "Blue", + } + +local _SCORINGCategory = + { + [Unit.Category.AIRPLANE] = "Plane", + [Unit.Category.HELICOPTER] = "Helicopter", + [Unit.Category.GROUND_UNIT] = "Vehicle", + [Unit.Category.SHIP] = "Ship", + [Unit.Category.STRUCTURE] = "Structure", + } + +--- Creates a new SCORING object to administer the scoring achieved by players. +-- @param #SCORING self +-- @param #string GameName The name of the game. This name is also logged in the CSV score file. +-- @return #SCORING self +-- @usage +-- +-- -- Define a new scoring object for the mission Gori Valley. +-- ScoringObject = SCORING:New( "Gori Valley" ) +-- +function SCORING:New( GameName ) + + -- Inherits from BASE + local self = BASE:Inherit( self, BASE:New() ) -- #SCORING + + if GameName then + self.GameName = GameName + else + error( "A game name must be given to register the scoring results" ) + end + + + -- Additional Object scores + self.ScoringObjects = {} + + -- Additional Zone scores. + self.ScoringZones = {} + + -- Configure Messages + self:SetMessagesToAll() + self:SetMessagesHit( false ) + self:SetMessagesDestroy( true ) + self:SetMessagesScore( true ) + self:SetMessagesZone( true ) + + -- Scales + self:SetScaleDestroyScore( 10 ) + self:SetScaleDestroyPenalty( 30 ) + + -- Default fratricide penalty level (maximum penalty that can be assigned to a player before he gets kicked). + self:SetFratricide( self.ScaleDestroyPenalty * 3 ) + + -- Default penalty when a player changes coalition. + self:SetCoalitionChangePenalty( self.ScaleDestroyPenalty ) + + self:SetDisplayMessagePrefix() + + -- Event handlers + self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) + self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) + self:HandleEvent( EVENTS.Hit, self._EventOnHit ) + self:HandleEvent( EVENTS.Birth ) + --self:HandleEvent( EVENTS.PlayerEnterUnit ) + self:HandleEvent( EVENTS.PlayerLeaveUnit ) + + -- During mission startup, especially for single player, + -- iterate the database for the player that has joined, and add him to the scoring, and set the menu. + -- But this can only be started one second after the mission has started, so i need to schedule this ... + self.ScoringPlayerScan = BASE:ScheduleOnce( 1, + function() + for PlayerName, PlayerUnit in pairs( _DATABASE:GetPlayerUnits() ) do + self:_AddPlayerFromUnit( PlayerUnit ) + self:SetScoringMenu( PlayerUnit:GetGroup() ) + end + end + ) + + + -- Create the CSV file. + self:OpenCSV( GameName ) + + return self + +end + +--- Set a prefix string that will be displayed at each scoring message sent. +-- @param #SCORING self +-- @param #string DisplayMessagePrefix (Default="Scoring: ") The scoring prefix string. +-- @return #SCORING +function SCORING:SetDisplayMessagePrefix( DisplayMessagePrefix ) + self.DisplayMessagePrefix = DisplayMessagePrefix or "" + return self +end + + +--- Set the scale for scoring valid destroys (enemy destroys). +-- A default calculated score is a value between 1 and 10. +-- The scale magnifies the scores given to the players. +-- @param #SCORING self +-- @param #number Scale The scale of the score given. +function SCORING:SetScaleDestroyScore( Scale ) + self.ScaleDestroyScore = Scale + return self +end + +--- Set the scale for scoring penalty destroys (friendly destroys). +-- A default calculated penalty is a value between 1 and 10. +-- The scale magnifies the scores given to the players. +-- @param #SCORING self +-- @param #number Scale The scale of the score given. +-- @return #SCORING +function SCORING:SetScaleDestroyPenalty( Scale ) + + self.ScaleDestroyPenalty = Scale + + return self +end + +--- Add a @{Wrapper.Unit} for additional scoring when the @{Wrapper.Unit} is destroyed. +-- Note that if there was already a @{Wrapper.Unit} declared within the scoring with the same name, +-- then the old @{Wrapper.Unit} will be replaced with the new @{Wrapper.Unit}. +-- @param #SCORING self +-- @param Wrapper.Unit#UNIT ScoreUnit The @{Wrapper.Unit} for which the Score needs to be given. +-- @param #number Score The Score value. +-- @return #SCORING +function SCORING:AddUnitScore( ScoreUnit, Score ) + + local UnitName = ScoreUnit:GetName() + + self.ScoringObjects[UnitName] = Score + + return self +end + +--- Removes a @{Wrapper.Unit} for additional scoring when the @{Wrapper.Unit} is destroyed. +-- @param #SCORING self +-- @param Wrapper.Unit#UNIT ScoreUnit The @{Wrapper.Unit} for which the Score needs to be given. +-- @return #SCORING +function SCORING:RemoveUnitScore( ScoreUnit ) + + local UnitName = ScoreUnit:GetName() + + self.ScoringObjects[UnitName] = nil + + return self +end + +--- Add a @{Static} for additional scoring when the @{Static} is destroyed. +-- Note that if there was already a @{Static} declared within the scoring with the same name, +-- then the old @{Static} will be replaced with the new @{Static}. +-- @param #SCORING self +-- @param Wrapper.Static#UNIT ScoreStatic The @{Static} for which the Score needs to be given. +-- @param #number Score The Score value. +-- @return #SCORING +function SCORING:AddStaticScore( ScoreStatic, Score ) + + local StaticName = ScoreStatic:GetName() + + self.ScoringObjects[StaticName] = Score + + return self +end + +--- Removes a @{Static} for additional scoring when the @{Static} is destroyed. +-- @param #SCORING self +-- @param Wrapper.Static#UNIT ScoreStatic The @{Static} for which the Score needs to be given. +-- @return #SCORING +function SCORING:RemoveStaticScore( ScoreStatic ) + + local StaticName = ScoreStatic:GetName() + + self.ScoringObjects[StaticName] = nil + + return self +end + + +--- Specify a special additional score for a @{Wrapper.Group}. +-- @param #SCORING self +-- @param Wrapper.Group#GROUP ScoreGroup The @{Wrapper.Group} for which each @{Wrapper.Unit} a Score is given. +-- @param #number Score The Score value. +-- @return #SCORING +function SCORING:AddScoreGroup( ScoreGroup, Score ) + + local ScoreUnits = ScoreGroup:GetUnits() + + for ScoreUnitID, ScoreUnit in pairs( ScoreUnits ) do + local UnitName = ScoreUnit:GetName() + self.ScoringObjects[UnitName] = Score + end + + return self +end + +--- Add a @{Zone} to define additional scoring when any object is destroyed in that zone. +-- Note that if a @{Zone} with the same name is already within the scoring added, the @{Zone} (type) and Score will be replaced! +-- This allows for a dynamic destruction zone evolution within your mission. +-- @param #SCORING self +-- @param Core.Zone#ZONE_BASE ScoreZone The @{Zone} which defines the destruction score perimeters. +-- Note that a zone can be a polygon or a moving zone. +-- @param #number Score The Score value. +-- @return #SCORING +function SCORING:AddZoneScore( ScoreZone, Score ) + + local ZoneName = ScoreZone:GetName() + + self.ScoringZones[ZoneName] = {} + self.ScoringZones[ZoneName].ScoreZone = ScoreZone + self.ScoringZones[ZoneName].Score = Score + + return self +end + +--- Remove a @{Zone} for additional scoring. +-- The scoring will search if any @{Zone} is added with the given name, and will remove that zone from the scoring. +-- This allows for a dynamic destruction zone evolution within your mission. +-- @param #SCORING self +-- @param Core.Zone#ZONE_BASE ScoreZone The @{Zone} which defines the destruction score perimeters. +-- Note that a zone can be a polygon or a moving zone. +-- @return #SCORING +function SCORING:RemoveZoneScore( ScoreZone ) + + local ZoneName = ScoreZone:GetName() + + self.ScoringZones[ZoneName] = nil + + return self +end + + +--- Configure to send messages after a target has been hit. +-- @param #SCORING self +-- @param #boolean OnOff If true is given, the messages are sent. +-- @return #SCORING +function SCORING:SetMessagesHit( OnOff ) + + self.MessagesHit = OnOff + return self +end + +--- If to send messages after a target has been hit. +-- @param #SCORING self +-- @return #boolean +function SCORING:IfMessagesHit() + + return self.MessagesHit +end + +--- Configure to send messages after a target has been destroyed. +-- @param #SCORING self +-- @param #boolean OnOff If true is given, the messages are sent. +-- @return #SCORING +function SCORING:SetMessagesDestroy( OnOff ) + + self.MessagesDestroy = OnOff + return self +end + +--- If to send messages after a target has been destroyed. +-- @param #SCORING self +-- @return #boolean +function SCORING:IfMessagesDestroy() + + return self.MessagesDestroy +end + +--- Configure to send messages after a target has been destroyed and receives additional scores. +-- @param #SCORING self +-- @param #boolean OnOff If true is given, the messages are sent. +-- @return #SCORING +function SCORING:SetMessagesScore( OnOff ) + + self.MessagesScore = OnOff + return self +end + +--- If to send messages after a target has been destroyed and receives additional scores. +-- @param #SCORING self +-- @return #boolean +function SCORING:IfMessagesScore() + + return self.MessagesScore +end + +--- Configure to send messages after a target has been hit in a zone, and additional score is received. +-- @param #SCORING self +-- @param #boolean OnOff If true is given, the messages are sent. +-- @return #SCORING +function SCORING:SetMessagesZone( OnOff ) + + self.MessagesZone = OnOff + return self +end + +--- If to send messages after a target has been hit in a zone, and additional score is received. +-- @param #SCORING self +-- @return #boolean +function SCORING:IfMessagesZone() + + return self.MessagesZone +end + +--- Configure to send messages to all players. +-- @param #SCORING self +-- @return #SCORING +function SCORING:SetMessagesToAll() + + self.MessagesAudience = 1 + return self +end + +--- If to send messages to all players. +-- @param #SCORING self +-- @return #boolean +function SCORING:IfMessagesToAll() + + return self.MessagesAudience == 1 +end + +--- Configure to send messages to only those players within the same coalition as the player. +-- @param #SCORING self +-- @return #SCORING +function SCORING:SetMessagesToCoalition() + + self.MessagesAudience = 2 + return self +end + +--- If to send messages to only those players within the same coalition as the player. +-- @param #SCORING self +-- @return #boolean +function SCORING:IfMessagesToCoalition() + + return self.MessagesAudience == 2 +end + + +--- When a player commits too much damage to friendlies, his penalty score will reach a certain level. +-- Use this method to define the level when a player gets kicked. +-- By default, the fratricide level is the default penalty mutiplier * 2 for the penalty score. +-- @param #SCORING self +-- @param #number Fratricide The amount of maximum penalty that may be inflicted by a friendly player before he gets kicked. +-- @return #SCORING +function SCORING:SetFratricide( Fratricide ) + + self.Fratricide = Fratricide + return self +end + + +--- When a player changes the coalition, he can receive a penalty score. +-- Use the method @{#SCORING.SetCoalitionChangePenalty}() to define the penalty when a player changes coalition. +-- By default, the penalty for changing coalition is the default penalty scale. +-- @param #SCORING self +-- @param #number CoalitionChangePenalty The amount of penalty that is given. +-- @return #SCORING +function SCORING:SetCoalitionChangePenalty( CoalitionChangePenalty ) + + self.CoalitionChangePenalty = CoalitionChangePenalty + return self +end + + +--- Sets the scoring menu. +-- @param #SCORING self +-- @return #SCORING +function SCORING:SetScoringMenu( ScoringGroup ) + local Menu = MENU_GROUP:New( ScoringGroup, 'Scoring and Statistics' ) + local ReportGroupSummary = MENU_GROUP_COMMAND:New( ScoringGroup, 'Summary report players in group', Menu, SCORING.ReportScoreGroupSummary, self, ScoringGroup ) + local ReportGroupDetailed = MENU_GROUP_COMMAND:New( ScoringGroup, 'Detailed report players in group', Menu, SCORING.ReportScoreGroupDetailed, self, ScoringGroup ) + local ReportToAllSummary = MENU_GROUP_COMMAND:New( ScoringGroup, 'Summary report all players', Menu, SCORING.ReportScoreAllSummary, self, ScoringGroup ) + self:SetState( ScoringGroup, "ScoringMenu", Menu ) + return self +end + + +--- Add a new player entering a Unit. +-- @param #SCORING self +-- @param Wrapper.Unit#UNIT UnitData +function SCORING:_AddPlayerFromUnit( UnitData ) + self:F( UnitData ) + + if UnitData:IsAlive() then + local UnitName = UnitData:GetName() + local PlayerName = UnitData:GetPlayerName() + local UnitDesc = UnitData:GetDesc() + local UnitCategory = UnitDesc.category + local UnitCoalition = UnitData:GetCoalition() + local UnitTypeName = UnitData:GetTypeName() + local UnitThreatLevel, UnitThreatType = UnitData:GetThreatLevel() + + self:T( { PlayerName, UnitName, UnitCategory, UnitCoalition, UnitTypeName } ) + + if self.Players[PlayerName] == nil then -- I believe this is the place where a Player gets a life in a mission when he enters a unit ... + self.Players[PlayerName] = {} + self.Players[PlayerName].Hit = {} + self.Players[PlayerName].Destroy = {} + self.Players[PlayerName].Goals = {} + self.Players[PlayerName].Mission = {} + + -- for CategoryID, CategoryName in pairs( SCORINGCategory ) do + -- self.Players[PlayerName].Hit[CategoryID] = {} + -- self.Players[PlayerName].Destroy[CategoryID] = {} + -- end + self.Players[PlayerName].HitPlayers = {} + self.Players[PlayerName].Score = 0 + self.Players[PlayerName].Penalty = 0 + self.Players[PlayerName].PenaltyCoalition = 0 + self.Players[PlayerName].PenaltyWarning = 0 + end + + if not self.Players[PlayerName].UnitCoalition then + self.Players[PlayerName].UnitCoalition = UnitCoalition + else + if self.Players[PlayerName].UnitCoalition ~= UnitCoalition then + self.Players[PlayerName].Penalty = self.Players[PlayerName].Penalty + 50 + self.Players[PlayerName].PenaltyCoalition = self.Players[PlayerName].PenaltyCoalition + 1 + MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' changed coalition from " .. _SCORINGCoalition[self.Players[PlayerName].UnitCoalition] .. " to " .. _SCORINGCoalition[UnitCoalition] .. + "(changed " .. self.Players[PlayerName].PenaltyCoalition .. " times the coalition). 50 Penalty points added.", + MESSAGE.Type.Information + ):ToAll() + self:ScoreCSV( PlayerName, "", "COALITION_PENALTY", 1, -50, self.Players[PlayerName].UnitName, _SCORINGCoalition[self.Players[PlayerName].UnitCoalition], _SCORINGCategory[self.Players[PlayerName].UnitCategory], self.Players[PlayerName].UnitType, + UnitName, _SCORINGCoalition[UnitCoalition], _SCORINGCategory[UnitCategory], UnitData:GetTypeName() ) + end + end + + self.Players[PlayerName].UnitName = UnitName + self.Players[PlayerName].UnitCoalition = UnitCoalition + self.Players[PlayerName].UnitCategory = UnitCategory + self.Players[PlayerName].UnitType = UnitTypeName + self.Players[PlayerName].UNIT = UnitData + self.Players[PlayerName].ThreatLevel = UnitThreatLevel + self.Players[PlayerName].ThreatType = UnitThreatType + + if self.Players[PlayerName].Penalty > self.Fratricide * 0.50 then + if self.Players[PlayerName].PenaltyWarning < 1 then + MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "': WARNING! If you continue to commit FRATRICIDE and have a PENALTY score higher than " .. self.Fratricide .. ", you will be COURT MARTIALED and DISMISSED from this mission! \nYour total penalty is: " .. self.Players[PlayerName].Penalty, + MESSAGE.Type.Information + ):ToAll() + self.Players[PlayerName].PenaltyWarning = self.Players[PlayerName].PenaltyWarning + 1 + end + end + + if self.Players[PlayerName].Penalty > self.Fratricide then + MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' committed FRATRICIDE, he will be COURT MARTIALED and is DISMISSED from this mission!", + MESSAGE.Type.Information + ):ToAll() + UnitData:GetGroup():Destroy() + end + end +end + + +--- Add a goal score for a player. +-- The method takes the Player name for which the Goal score needs to be set. +-- The GoalTag is a string or identifier that is taken into the CSV file scoring log to identify the goal. +-- A free text can be given that is shown to the players. +-- The Score can be both positive and negative. +-- @param #SCORING self +-- @param #string PlayerName The name of the Player. +-- @param #string GoalTag The string or identifier that is used in the CSV file to identify the goal (sort or group later in Excel). +-- @param #string Text A free text that is shown to the players. +-- @param #number Score The score can be both positive or negative ( Penalty ). +function SCORING:AddGoalScorePlayer( PlayerName, GoalTag, Text, Score ) + + self:F( { PlayerName, PlayerName, GoalTag, Text, Score } ) + + -- PlayerName can be nil, if the Unit with the player crashed or due to another reason. + if PlayerName then + local PlayerData = self.Players[PlayerName] + + PlayerData.Goals[GoalTag] = PlayerData.Goals[GoalTag] or { Score = 0 } + PlayerData.Goals[GoalTag].Score = PlayerData.Goals[GoalTag].Score + Score + PlayerData.Score = PlayerData.Score + Score + + MESSAGE:NewType( self.DisplayMessagePrefix .. Text, MESSAGE.Type.Information ):ToAll() + + self:ScoreCSV( PlayerName, "", "GOAL_" .. string.upper( GoalTag ), 1, Score, nil ) + end +end + + + +--- Add a goal score for a player. +-- The method takes the PlayerUnit for which the Goal score needs to be set. +-- The GoalTag is a string or identifier that is taken into the CSV file scoring log to identify the goal. +-- A free text can be given that is shown to the players. +-- The Score can be both positive and negative. +-- @param #SCORING self +-- @param Wrapper.Unit#UNIT PlayerUnit The @{Wrapper.Unit} of the Player. Other Properties for the scoring are taken from this PlayerUnit, like coalition, type etc. +-- @param #string GoalTag The string or identifier that is used in the CSV file to identify the goal (sort or group later in Excel). +-- @param #string Text A free text that is shown to the players. +-- @param #number Score The score can be both positive or negative ( Penalty ). +function SCORING:AddGoalScore( PlayerUnit, GoalTag, Text, Score ) + + local PlayerName = PlayerUnit:GetPlayerName() + + self:F( { PlayerUnit.UnitName, PlayerName, GoalTag, Text, Score } ) + + -- PlayerName can be nil, if the Unit with the player crashed or due to another reason. + if PlayerName then + local PlayerData = self.Players[PlayerName] + + PlayerData.Goals[GoalTag] = PlayerData.Goals[GoalTag] or { Score = 0 } + PlayerData.Goals[GoalTag].Score = PlayerData.Goals[GoalTag].Score + Score + PlayerData.Score = PlayerData.Score + Score + + MESSAGE:NewType( self.DisplayMessagePrefix .. Text, MESSAGE.Type.Information ):ToAll() + + self:ScoreCSV( PlayerName, "", "GOAL_" .. string.upper( GoalTag ), 1, Score, PlayerUnit:GetName() ) + end +end + + +--- Registers Scores the players completing a Mission Task. +-- @param #SCORING self +-- @param Tasking.Mission#MISSION Mission +-- @param Wrapper.Unit#UNIT PlayerUnit +-- @param #string Text +-- @param #number Score +function SCORING:_AddMissionTaskScore( Mission, PlayerUnit, Text, Score ) + + local PlayerName = PlayerUnit:GetPlayerName() + local MissionName = Mission:GetName() + + self:F( { Mission:GetName(), PlayerUnit.UnitName, PlayerName, Text, Score } ) + + -- PlayerName can be nil, if the Unit with the player crashed or due to another reason. + if PlayerName then + local PlayerData = self.Players[PlayerName] + + if not PlayerData.Mission[MissionName] then + PlayerData.Mission[MissionName] = {} + PlayerData.Mission[MissionName].ScoreTask = 0 + PlayerData.Mission[MissionName].ScoreMission = 0 + end + + self:T( PlayerName ) + self:T( PlayerData.Mission[MissionName] ) + + PlayerData.Score = self.Players[PlayerName].Score + Score + PlayerData.Mission[MissionName].ScoreTask = self.Players[PlayerName].Mission[MissionName].ScoreTask + Score + + MESSAGE:NewType( self.DisplayMessagePrefix .. Mission:GetText() .. " : " .. Text .. " Score: " .. Score, MESSAGE.Type.Information ):ToAll() + + self:ScoreCSV( PlayerName, "", "TASK_" .. MissionName:gsub( ' ', '_' ), 1, Score, PlayerUnit:GetName() ) + end +end + +--- Registers Scores the players completing a Mission Task. +-- @param #SCORING self +-- @param Tasking.Mission#MISSION Mission +-- @param #string PlayerName +-- @param #string Text +-- @param #number Score +function SCORING:_AddMissionGoalScore( Mission, PlayerName, Text, Score ) + + local MissionName = Mission:GetName() + + self:F( { Mission:GetName(), PlayerName, Text, Score } ) + + -- PlayerName can be nil, if the Unit with the player crashed or due to another reason. + if PlayerName then + local PlayerData = self.Players[PlayerName] + + if not PlayerData.Mission[MissionName] then + PlayerData.Mission[MissionName] = {} + PlayerData.Mission[MissionName].ScoreTask = 0 + PlayerData.Mission[MissionName].ScoreMission = 0 + end + + self:T( PlayerName ) + self:T( PlayerData.Mission[MissionName] ) + + PlayerData.Score = self.Players[PlayerName].Score + Score + PlayerData.Mission[MissionName].ScoreTask = self.Players[PlayerName].Mission[MissionName].ScoreTask + Score + + MESSAGE:NewType( string.format( "%s%s: %s! Player %s receives %d score!", self.DisplayMessagePrefix, Mission:GetText(), Text, PlayerName, Score ), MESSAGE.Type.Information ):ToAll() + + self:ScoreCSV( PlayerName, "", "TASK_" .. MissionName:gsub( ' ', '_' ), 1, Score ) + end +end + +--- Registers Mission Scores for possible multiple players that contributed in the Mission. +-- @param #SCORING self +-- @param Tasking.Mission#MISSION Mission +-- @param Wrapper.Unit#UNIT PlayerUnit +-- @param #string Text +-- @param #number Score +function SCORING:_AddMissionScore( Mission, Text, Score ) + + local MissionName = Mission:GetName() + + self:F( { Mission, Text, Score } ) + self:F( self.Players ) + + for PlayerName, PlayerData in pairs( self.Players ) do + + self:F( PlayerData ) + if PlayerData.Mission[MissionName] then + + PlayerData.Score = PlayerData.Score + Score + PlayerData.Mission[MissionName].ScoreMission = PlayerData.Mission[MissionName].ScoreMission + Score + + MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' has " .. Text .. " in " .. Mission:GetText() .. ". " .. + Score .. " mission score!", + MESSAGE.Type.Information ):ToAll() + + self:ScoreCSV( PlayerName, "", "MISSION_" .. MissionName:gsub( ' ', '_' ), 1, Score ) + end + end +end + + + +--- Handles the OnPlayerEnterUnit event for the scoring. +-- @param #SCORING self +-- @param Core.Event#EVENTDATA Event +--function SCORING:OnEventPlayerEnterUnit( Event ) +-- if Event.IniUnit then +-- self:_AddPlayerFromUnit( Event.IniUnit ) +-- self:SetScoringMenu( Event.IniGroup ) +-- end +--end + +--- Handles the OnBirth event for the scoring. +-- @param #SCORING self +-- @param Core.Event#EVENTDATA Event +function SCORING:OnEventBirth( Event ) + + if Event.IniUnit then + if Event.IniObjectCategory == 1 then + local PlayerName = Event.IniUnit:GetPlayerName() + if PlayerName then + self:_AddPlayerFromUnit( Event.IniUnit ) + self:SetScoringMenu( Event.IniGroup ) + end + end + end +end + +--- Handles the OnPlayerLeaveUnit event for the scoring. +-- @param #SCORING self +-- @param Core.Event#EVENTDATA Event +function SCORING:OnEventPlayerLeaveUnit( Event ) + if Event.IniUnit then + local Menu = self:GetState( Event.IniUnit:GetGroup(), "ScoringMenu" ) -- Core.Menu#MENU_GROUP + if Menu then + -- TODO: Check if this fixes #281. + --Menu:Remove() + end + end +end + + +--- Handles the OnHit event for the scoring. +-- @param #SCORING self +-- @param Core.Event#EVENTDATA Event +function SCORING:_EventOnHit( Event ) + self:F( { Event } ) + + local InitUnit = nil + local InitUNIT = nil + local InitUnitName = "" + local InitGroup = nil + local InitGroupName = "" + local InitPlayerName = nil + + local InitCoalition = nil + local InitCategory = nil + local InitType = nil + local InitUnitCoalition = nil + local InitUnitCategory = nil + local InitUnitType = nil + + local TargetUnit = nil + local TargetUNIT = nil + local TargetUnitName = "" + local TargetGroup = nil + local TargetGroupName = "" + local TargetPlayerName = nil + + local TargetCoalition = nil + local TargetCategory = nil + local TargetType = nil + local TargetUnitCoalition = nil + local TargetUnitCategory = nil + local TargetUnitType = nil + + if Event.IniDCSUnit then + + InitUnit = Event.IniDCSUnit + InitUNIT = Event.IniUnit + InitUnitName = Event.IniDCSUnitName + InitGroup = Event.IniDCSGroup + InitGroupName = Event.IniDCSGroupName + InitPlayerName = Event.IniPlayerName + + InitCoalition = Event.IniCoalition + --TODO: Workaround Client DCS Bug + --InitCategory = InitUnit:getCategory() + --InitCategory = InitUnit:getDesc().category + InitCategory = Event.IniCategory + InitType = Event.IniTypeName + + InitUnitCoalition = _SCORINGCoalition[InitCoalition] + InitUnitCategory = _SCORINGCategory[InitCategory] + InitUnitType = InitType + + self:T( { InitUnitName, InitGroupName, InitPlayerName, InitCoalition, InitCategory, InitType , InitUnitCoalition, InitUnitCategory, InitUnitType } ) + end + + + if Event.TgtDCSUnit then + + TargetUnit = Event.TgtDCSUnit + TargetUNIT = Event.TgtUnit + TargetUnitName = Event.TgtDCSUnitName + TargetGroup = Event.TgtDCSGroup + TargetGroupName = Event.TgtDCSGroupName + TargetPlayerName = Event.TgtPlayerName + + TargetCoalition = Event.TgtCoalition + --TODO: Workaround Client DCS Bug + --TargetCategory = TargetUnit:getCategory() + --TargetCategory = TargetUnit:getDesc().category + TargetCategory = Event.TgtCategory + TargetType = Event.TgtTypeName + + TargetUnitCoalition = _SCORINGCoalition[TargetCoalition] + TargetUnitCategory = _SCORINGCategory[TargetCategory] + TargetUnitType = TargetType + + self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType, TargetUnitCoalition, TargetUnitCategory, TargetUnitType } ) + end + + if InitPlayerName ~= nil then -- It is a player that is hitting something + self:_AddPlayerFromUnit( InitUNIT ) + if self.Players[InitPlayerName] then -- This should normally not happen, but i'll test it anyway. + if TargetPlayerName ~= nil then -- It is a player hitting another player ... + self:_AddPlayerFromUnit( TargetUNIT ) + end + + self:T( "Hitting Something" ) + + -- What is he hitting? + if TargetCategory then + + -- A target got hit, score it. + -- Player contains the score data from self.Players[InitPlayerName] + local Player = self.Players[InitPlayerName] + + -- Ensure there is a hit table per TargetCategory and TargetUnitName. + Player.Hit[TargetCategory] = Player.Hit[TargetCategory] or {} + Player.Hit[TargetCategory][TargetUnitName] = Player.Hit[TargetCategory][TargetUnitName] or {} + + -- PlayerHit contains the score counters and data per unit that was hit. + local PlayerHit = Player.Hit[TargetCategory][TargetUnitName] + + PlayerHit.Score = PlayerHit.Score or 0 + PlayerHit.Penalty = PlayerHit.Penalty or 0 + PlayerHit.ScoreHit = PlayerHit.ScoreHit or 0 + PlayerHit.PenaltyHit = PlayerHit.PenaltyHit or 0 + PlayerHit.TimeStamp = PlayerHit.TimeStamp or 0 + PlayerHit.UNIT = PlayerHit.UNIT or TargetUNIT + PlayerHit.ThreatLevel, PlayerHit.ThreatType = PlayerHit.UNIT:GetThreatLevel() + + -- Only grant hit scores if there was more than one second between the last hit. + if timer.getTime() - PlayerHit.TimeStamp > 1 then + PlayerHit.TimeStamp = timer.getTime() + + if TargetPlayerName ~= nil then -- It is a player hitting another player ... + + -- Ensure there is a Player to Player hit reference table. + Player.HitPlayers[TargetPlayerName] = true + end + + local Score = 0 + + if InitCoalition then -- A coalition object was hit. + if InitCoalition == TargetCoalition then + Player.Penalty = Player.Penalty + 10 + PlayerHit.Penalty = PlayerHit.Penalty + 10 + PlayerHit.PenaltyHit = PlayerHit.PenaltyHit + 1 + + if TargetPlayerName ~= nil then -- It is a player hitting another player ... + MESSAGE + :NewType( self.DisplayMessagePrefix .. "Player '" .. InitPlayerName .. "' hit friendly player '" .. TargetPlayerName .. "' " .. + TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.PenaltyHit .. " times. " .. + "Penalty: -" .. PlayerHit.Penalty .. ". Score Total:" .. Player.Score - Player.Penalty, + MESSAGE.Type.Update + ) + :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) + else + MESSAGE + :NewType( self.DisplayMessagePrefix .. "Player '" .. InitPlayerName .. "' hit friendly target " .. + TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.PenaltyHit .. " times. " .. + "Penalty: -" .. PlayerHit.Penalty .. ". Score Total:" .. Player.Score - Player.Penalty, + MESSAGE.Type.Update + ) + :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) + end + self:ScoreCSV( InitPlayerName, TargetPlayerName, "HIT_PENALTY", 1, -10, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + else + Player.Score = Player.Score + 1 + PlayerHit.Score = PlayerHit.Score + 1 + PlayerHit.ScoreHit = PlayerHit.ScoreHit + 1 + if TargetPlayerName ~= nil then -- It is a player hitting another player ... + MESSAGE + :NewType( self.DisplayMessagePrefix .. "Player '" .. InitPlayerName .. "' hit enemy player '" .. TargetPlayerName .. "' " .. + TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.ScoreHit .. " times. " .. + "Score: " .. PlayerHit.Score .. ". Score Total:" .. Player.Score - Player.Penalty, + MESSAGE.Type.Update + ) + :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) + else + MESSAGE + :NewType( self.DisplayMessagePrefix .. "Player '" .. InitPlayerName .. "' hit enemy target " .. + TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.ScoreHit .. " times. " .. + "Score: " .. PlayerHit.Score .. ". Score Total:" .. Player.Score - Player.Penalty, + MESSAGE.Type.Update + ) + :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) + end + self:ScoreCSV( InitPlayerName, TargetPlayerName, "HIT_SCORE", 1, 1, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + end + else -- A scenery object was hit. + MESSAGE + :NewType( self.DisplayMessagePrefix .. "Player '" .. InitPlayerName .. "' hit scenery object.", + MESSAGE.Type.Update + ) + :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) + self:ScoreCSV( InitPlayerName, "", "HIT_SCORE", 1, 0, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, "", "Scenery", TargetUnitType ) + end + end + end + end + elseif InitPlayerName == nil then -- It is an AI hitting a player??? + + end + + -- It is a weapon initiated by a player, that is hitting something + -- This seems to occur only with scenery and static objects. + if Event.WeaponPlayerName ~= nil then + self:_AddPlayerFromUnit( Event.WeaponUNIT ) + if self.Players[Event.WeaponPlayerName] then -- This should normally not happen, but i'll test it anyway. + if TargetPlayerName ~= nil then -- It is a player hitting another player ... + self:_AddPlayerFromUnit( TargetUNIT ) + end + + self:T( "Hitting Scenery" ) + + -- What is he hitting? + if TargetCategory then + + -- A scenery or static got hit, score it. + -- Player contains the score data from self.Players[WeaponPlayerName] + local Player = self.Players[Event.WeaponPlayerName] + + -- Ensure there is a hit table per TargetCategory and TargetUnitName. + Player.Hit[TargetCategory] = Player.Hit[TargetCategory] or {} + Player.Hit[TargetCategory][TargetUnitName] = Player.Hit[TargetCategory][TargetUnitName] or {} + + -- PlayerHit contains the score counters and data per unit that was hit. + local PlayerHit = Player.Hit[TargetCategory][TargetUnitName] + + PlayerHit.Score = PlayerHit.Score or 0 + PlayerHit.Penalty = PlayerHit.Penalty or 0 + PlayerHit.ScoreHit = PlayerHit.ScoreHit or 0 + PlayerHit.PenaltyHit = PlayerHit.PenaltyHit or 0 + PlayerHit.TimeStamp = PlayerHit.TimeStamp or 0 + PlayerHit.UNIT = PlayerHit.UNIT or TargetUNIT + PlayerHit.ThreatLevel, PlayerHit.ThreatType = PlayerHit.UNIT:GetThreatLevel() + + -- Only grant hit scores if there was more than one second between the last hit. + if timer.getTime() - PlayerHit.TimeStamp > 1 then + PlayerHit.TimeStamp = timer.getTime() + + local Score = 0 + + if InitCoalition then -- A coalition object was hit, probably a static. + if InitCoalition == TargetCoalition then + -- TODO: Penalty according scale + Player.Penalty = Player.Penalty + 10 + PlayerHit.Penalty = PlayerHit.Penalty + 10 + PlayerHit.PenaltyHit = PlayerHit.PenaltyHit + 1 + + MESSAGE + :NewType( self.DisplayMessagePrefix .. "Player '" .. Event.WeaponPlayerName .. "' hit friendly target " .. + TargetUnitCategory .. " ( " .. TargetType .. " ) " .. + "Penalty: -" .. PlayerHit.Penalty .. " = " .. Player.Score - Player.Penalty, + MESSAGE.Type.Update + ) + :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) + :ToCoalitionIf( Event.WeaponCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) + self:ScoreCSV( Event.WeaponPlayerName, TargetPlayerName, "HIT_PENALTY", 1, -10, Event.WeaponName, Event.WeaponCoalition, Event.WeaponCategory, Event.WeaponTypeName, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + else + Player.Score = Player.Score + 1 + PlayerHit.Score = PlayerHit.Score + 1 + PlayerHit.ScoreHit = PlayerHit.ScoreHit + 1 + MESSAGE + :NewType( self.DisplayMessagePrefix .. "Player '" .. Event.WeaponPlayerName .. "' hit enemy target " .. + TargetUnitCategory .. " ( " .. TargetType .. " ) " .. + "Score: +" .. PlayerHit.Score .. " = " .. Player.Score - Player.Penalty, + MESSAGE.Type.Update + ) + :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) + :ToCoalitionIf( Event.WeaponCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) + self:ScoreCSV( Event.WeaponPlayerName, TargetPlayerName, "HIT_SCORE", 1, 1, Event.WeaponName, Event.WeaponCoalition, Event.WeaponCategory, Event.WeaponTypeName, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + end + else -- A scenery object was hit. + MESSAGE + :NewType( self.DisplayMessagePrefix .. "Player '" .. Event.WeaponPlayerName .. "' hit scenery object.", + MESSAGE.Type.Update + ) + :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) + self:ScoreCSV( Event.WeaponPlayerName, "", "HIT_SCORE", 1, 0, Event.WeaponName, Event.WeaponCoalition, Event.WeaponCategory, Event.WeaponTypeName, TargetUnitName, "", "Scenery", TargetUnitType ) + end + end + end + end + end +end + +--- Track DEAD or CRASH events for the scoring. +-- @param #SCORING self +-- @param Core.Event#EVENTDATA Event +function SCORING:_EventOnDeadOrCrash( Event ) + self:F( { Event } ) + + local TargetUnit = nil + local TargetGroup = nil + local TargetUnitName = "" + local TargetGroupName = "" + local TargetPlayerName = "" + local TargetCoalition = nil + local TargetCategory = nil + local TargetType = nil + local TargetUnitCoalition = nil + local TargetUnitCategory = nil + local TargetUnitType = nil + + if Event.IniDCSUnit then + + TargetUnit = Event.IniUnit + TargetUnitName = Event.IniDCSUnitName + TargetGroup = Event.IniDCSGroup + TargetGroupName = Event.IniDCSGroupName + TargetPlayerName = Event.IniPlayerName + + TargetCoalition = Event.IniCoalition + --TargetCategory = TargetUnit:getCategory() + --TargetCategory = TargetUnit:getDesc().category -- Workaround + TargetCategory = Event.IniCategory + TargetType = Event.IniTypeName + + TargetUnitCoalition = _SCORINGCoalition[TargetCoalition] + TargetUnitCategory = _SCORINGCategory[TargetCategory] + TargetUnitType = TargetType + + self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType } ) + end + + -- Player contains the score and reference data for the player. + for PlayerName, Player in pairs( self.Players ) do + if Player then -- This should normally not happen, but i'll test it anyway. + self:T( "Something got destroyed" ) + + -- Some variables + local InitUnitName = Player.UnitName + local InitUnitType = Player.UnitType + local InitCoalition = Player.UnitCoalition + local InitCategory = Player.UnitCategory + local InitUnitCoalition = _SCORINGCoalition[InitCoalition] + local InitUnitCategory = _SCORINGCategory[InitCategory] + + self:T( { InitUnitName, InitUnitType, InitUnitCoalition, InitCoalition, InitUnitCategory, InitCategory } ) + + local Destroyed = false + + -- What is the player destroying? + if Player and Player.Hit and Player.Hit[TargetCategory] and Player.Hit[TargetCategory][TargetUnitName] and Player.Hit[TargetCategory][TargetUnitName].TimeStamp ~= 0 then -- Was there a hit for this unit for this player before registered??? + + local TargetThreatLevel = Player.Hit[TargetCategory][TargetUnitName].ThreatLevel + local TargetThreatType = Player.Hit[TargetCategory][TargetUnitName].ThreatType + + Player.Destroy[TargetCategory] = Player.Destroy[TargetCategory] or {} + Player.Destroy[TargetCategory][TargetType] = Player.Destroy[TargetCategory][TargetType] or {} + + -- PlayerDestroy contains the destroy score data per category and target type of the player. + local TargetDestroy = Player.Destroy[TargetCategory][TargetType] + TargetDestroy.Score = TargetDestroy.Score or 0 + TargetDestroy.ScoreDestroy = TargetDestroy.ScoreDestroy or 0 + TargetDestroy.Penalty = TargetDestroy.Penalty or 0 + TargetDestroy.PenaltyDestroy = TargetDestroy.PenaltyDestroy or 0 + + if TargetCoalition then + if InitCoalition == TargetCoalition then + local ThreatLevelTarget = TargetThreatLevel + local ThreatTypeTarget = TargetThreatType + local ThreatLevelPlayer = Player.ThreatLevel / 10 + 1 + local ThreatPenalty = math.ceil( ( ThreatLevelTarget / ThreatLevelPlayer ) * self.ScaleDestroyPenalty / 10 ) + self:F( { ThreatLevel = ThreatPenalty, ThreatLevelTarget = ThreatLevelTarget, ThreatTypeTarget = ThreatTypeTarget, ThreatLevelPlayer = ThreatLevelPlayer } ) + + Player.Penalty = Player.Penalty + ThreatPenalty + TargetDestroy.Penalty = TargetDestroy.Penalty + ThreatPenalty + TargetDestroy.PenaltyDestroy = TargetDestroy.PenaltyDestroy + 1 + + if Player.HitPlayers[TargetPlayerName] then -- A player destroyed another player + MESSAGE + :NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' destroyed friendly player '" .. TargetPlayerName .. "' " .. + TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. + "Penalty: -" .. TargetDestroy.Penalty .. " = " .. Player.Score - Player.Penalty, + MESSAGE.Type.Information + ) + :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() ) + else + MESSAGE + :NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' destroyed friendly target " .. + TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. + "Penalty: -" .. TargetDestroy.Penalty .. " = " .. Player.Score - Player.Penalty, + MESSAGE.Type.Information + ) + :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() ) + end + + Destroyed = true + self:ScoreCSV( PlayerName, TargetPlayerName, "DESTROY_PENALTY", 1, ThreatPenalty, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + else + + local ThreatLevelTarget = TargetThreatLevel + local ThreatTypeTarget = TargetThreatType + local ThreatLevelPlayer = Player.ThreatLevel / 10 + 1 + local ThreatScore = math.ceil( ( ThreatLevelTarget / ThreatLevelPlayer ) * self.ScaleDestroyScore / 10 ) + + self:F( { ThreatLevel = ThreatScore, ThreatLevelTarget = ThreatLevelTarget, ThreatTypeTarget = ThreatTypeTarget, ThreatLevelPlayer = ThreatLevelPlayer } ) + + Player.Score = Player.Score + ThreatScore + TargetDestroy.Score = TargetDestroy.Score + ThreatScore + TargetDestroy.ScoreDestroy = TargetDestroy.ScoreDestroy + 1 + if Player.HitPlayers[TargetPlayerName] then -- A player destroyed another player + MESSAGE + :NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' destroyed enemy player '" .. TargetPlayerName .. "' " .. + TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. + "Score: +" .. TargetDestroy.Score .. " = " .. Player.Score - Player.Penalty, + MESSAGE.Type.Information + ) + :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() ) + else + MESSAGE + :NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' destroyed enemy " .. + TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. + "Score: +" .. TargetDestroy.Score .. " = " .. Player.Score - Player.Penalty, + MESSAGE.Type.Information + ) + :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() ) + end + Destroyed = true + self:ScoreCSV( PlayerName, TargetPlayerName, "DESTROY_SCORE", 1, ThreatScore, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + + local UnitName = TargetUnit:GetName() + local Score = self.ScoringObjects[UnitName] + if Score then + Player.Score = Player.Score + Score + TargetDestroy.Score = TargetDestroy.Score + Score + MESSAGE + :NewType( self.DisplayMessagePrefix .. "Special target '" .. TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. " destroyed! " .. + "Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! Total: " .. Player.Score - Player.Penalty, + MESSAGE.Type.Information + ) + :ToAllIf( self:IfMessagesScore() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesScore() and self:IfMessagesToCoalition() ) + self:ScoreCSV( PlayerName, TargetPlayerName, "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + Destroyed = true + end + + -- Check if there are Zones where the destruction happened. + for ZoneName, ScoreZoneData in pairs( self.ScoringZones ) do + self:F( { ScoringZone = ScoreZoneData } ) + local ScoreZone = ScoreZoneData.ScoreZone -- Core.Zone#ZONE_BASE + local Score = ScoreZoneData.Score + if ScoreZone:IsVec2InZone( TargetUnit:GetVec2() ) then + Player.Score = Player.Score + Score + TargetDestroy.Score = TargetDestroy.Score + Score + MESSAGE + :NewType( self.DisplayMessagePrefix .. "Target destroyed in zone '" .. ScoreZone:GetName() .. "'." .. + "Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! " .. + "Total: " .. Player.Score - Player.Penalty, + MESSAGE.Type.Information ) + :ToAllIf( self:IfMessagesZone() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesZone() and self:IfMessagesToCoalition() ) + self:ScoreCSV( PlayerName, TargetPlayerName, "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + Destroyed = true + end + end + + end + else + -- Check if there are Zones where the destruction happened. + for ZoneName, ScoreZoneData in pairs( self.ScoringZones ) do + self:F( { ScoringZone = ScoreZoneData } ) + local ScoreZone = ScoreZoneData.ScoreZone -- Core.Zone#ZONE_BASE + local Score = ScoreZoneData.Score + if ScoreZone:IsVec2InZone( TargetUnit:GetVec2() ) then + Player.Score = Player.Score + Score + TargetDestroy.Score = TargetDestroy.Score + Score + MESSAGE + :NewType( self.DisplayMessagePrefix .. "Scenery destroyed in zone '" .. ScoreZone:GetName() .. "'." .. + "Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! " .. + "Total: " .. Player.Score - Player.Penalty, + MESSAGE.Type.Information + ) + :ToAllIf( self:IfMessagesZone() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesZone() and self:IfMessagesToCoalition() ) + Destroyed = true + self:ScoreCSV( PlayerName, "", "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, "", "Scenery", TargetUnitType ) + end + end + end + + -- Delete now the hit cache if the target was destroyed. + -- Otherwise points will be granted every time a target gets killed by the players that hit that target. + -- This is only relevant for player to player destroys. + if Destroyed then + Player.Hit[TargetCategory][TargetUnitName].TimeStamp = 0 + end + end + end + end +end + + +--- Produce detailed report of player hit scores. +-- @param #SCORING self +-- @param #string PlayerName The name of the player. +-- @return #string The report. +function SCORING:ReportDetailedPlayerHits( PlayerName ) + + local ScoreMessage = "" + local PlayerScore = 0 + local PlayerPenalty = 0 + + local PlayerData = self.Players[PlayerName] + if PlayerData then -- This should normally not happen, but i'll test it anyway. + self:T( "Score Player: " .. PlayerName ) + + -- Some variables + local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] + local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] + local InitUnitType = PlayerData.UnitType + local InitUnitName = PlayerData.UnitName + + local ScoreMessageHits = "" + for CategoryID, CategoryName in pairs( _SCORINGCategory ) do + self:T( CategoryName ) + if PlayerData.Hit[CategoryID] then + self:T( "Hit scores exist for player " .. PlayerName ) + local Score = 0 + local ScoreHit = 0 + local Penalty = 0 + local PenaltyHit = 0 + for UnitName, UnitData in pairs( PlayerData.Hit[CategoryID] ) do + Score = Score + UnitData.Score + ScoreHit = ScoreHit + UnitData.ScoreHit + Penalty = Penalty + UnitData.Penalty + PenaltyHit = UnitData.PenaltyHit + end + local ScoreMessageHit = string.format( "%s:%d ", CategoryName, Score - Penalty ) + self:T( ScoreMessageHit ) + ScoreMessageHits = ScoreMessageHits .. ScoreMessageHit + PlayerScore = PlayerScore + Score + PlayerPenalty = PlayerPenalty + Penalty + else + --ScoreMessageHits = ScoreMessageHits .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) + end + end + if ScoreMessageHits ~= "" then + ScoreMessage = "Hits: " .. ScoreMessageHits + end + end + + return ScoreMessage, PlayerScore, PlayerPenalty +end + + +--- Produce detailed report of player destroy scores. +-- @param #SCORING self +-- @param #string PlayerName The name of the player. +-- @return #string The report. +function SCORING:ReportDetailedPlayerDestroys( PlayerName ) + + local ScoreMessage = "" + local PlayerScore = 0 + local PlayerPenalty = 0 + + local PlayerData = self.Players[PlayerName] + if PlayerData then -- This should normally not happen, but i'll test it anyway. + self:T( "Score Player: " .. PlayerName ) + + -- Some variables + local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] + local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] + local InitUnitType = PlayerData.UnitType + local InitUnitName = PlayerData.UnitName + + local ScoreMessageDestroys = "" + for CategoryID, CategoryName in pairs( _SCORINGCategory ) do + if PlayerData.Destroy[CategoryID] then + self:T( "Destroy scores exist for player " .. PlayerName ) + local Score = 0 + local ScoreDestroy = 0 + local Penalty = 0 + local PenaltyDestroy = 0 + + for UnitName, UnitData in pairs( PlayerData.Destroy[CategoryID] ) do + self:F( { UnitData = UnitData } ) + if UnitData ~= {} then + Score = Score + UnitData.Score + ScoreDestroy = ScoreDestroy + UnitData.ScoreDestroy + Penalty = Penalty + UnitData.Penalty + PenaltyDestroy = PenaltyDestroy + UnitData.PenaltyDestroy + end + end + + local ScoreMessageDestroy = string.format( " %s:%d ", CategoryName, Score - Penalty ) + self:T( ScoreMessageDestroy ) + ScoreMessageDestroys = ScoreMessageDestroys .. ScoreMessageDestroy + + PlayerScore = PlayerScore + Score + PlayerPenalty = PlayerPenalty + Penalty + else + --ScoreMessageDestroys = ScoreMessageDestroys .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) + end + end + if ScoreMessageDestroys ~= "" then + ScoreMessage = "Destroys: " .. ScoreMessageDestroys + end + end + + return ScoreMessage, PlayerScore, PlayerPenalty +end + +--- Produce detailed report of player penalty scores because of changing the coalition. +-- @param #SCORING self +-- @param #string PlayerName The name of the player. +-- @return #string The report. +function SCORING:ReportDetailedPlayerCoalitionChanges( PlayerName ) + + local ScoreMessage = "" + local PlayerScore = 0 + local PlayerPenalty = 0 + + local PlayerData = self.Players[PlayerName] + if PlayerData then -- This should normally not happen, but i'll test it anyway. + self:T( "Score Player: " .. PlayerName ) + + -- Some variables + local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] + local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] + local InitUnitType = PlayerData.UnitType + local InitUnitName = PlayerData.UnitName + + local ScoreMessageCoalitionChangePenalties = "" + if PlayerData.PenaltyCoalition ~= 0 then + ScoreMessageCoalitionChangePenalties = ScoreMessageCoalitionChangePenalties .. string.format( " -%d (%d changed)", PlayerData.Penalty, PlayerData.PenaltyCoalition ) + PlayerPenalty = PlayerPenalty + PlayerData.Penalty + end + if ScoreMessageCoalitionChangePenalties ~= "" then + ScoreMessage = ScoreMessage .. "Coalition Penalties: " .. ScoreMessageCoalitionChangePenalties + end + end + + return ScoreMessage, PlayerScore, PlayerPenalty +end + +--- Produce detailed report of player goal scores. +-- @param #SCORING self +-- @param #string PlayerName The name of the player. +-- @return #string The report. +function SCORING:ReportDetailedPlayerGoals( PlayerName ) + + local ScoreMessage = "" + local PlayerScore = 0 + local PlayerPenalty = 0 + + local PlayerData = self.Players[PlayerName] + if PlayerData then -- This should normally not happen, but i'll test it anyway. + self:T( "Score Player: " .. PlayerName ) + + -- Some variables + local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] + local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] + local InitUnitType = PlayerData.UnitType + local InitUnitName = PlayerData.UnitName + + local ScoreMessageGoal = "" + local ScoreGoal = 0 + local ScoreTask = 0 + for GoalName, GoalData in pairs( PlayerData.Goals ) do + ScoreGoal = ScoreGoal + GoalData.Score + ScoreMessageGoal = ScoreMessageGoal .. "'" .. GoalName .. "':" .. GoalData.Score .. "; " + end + PlayerScore = PlayerScore + ScoreGoal + + if ScoreMessageGoal ~= "" then + ScoreMessage = "Goals: " .. ScoreMessageGoal + end + end + + return ScoreMessage, PlayerScore, PlayerPenalty +end + +--- Produce detailed report of player penalty scores because of changing the coalition. +-- @param #SCORING self +-- @param #string PlayerName The name of the player. +-- @return #string The report. +function SCORING:ReportDetailedPlayerMissions( PlayerName ) + + local ScoreMessage = "" + local PlayerScore = 0 + local PlayerPenalty = 0 + + local PlayerData = self.Players[PlayerName] + if PlayerData then -- This should normally not happen, but i'll test it anyway. + self:T( "Score Player: " .. PlayerName ) + + -- Some variables + local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] + local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] + local InitUnitType = PlayerData.UnitType + local InitUnitName = PlayerData.UnitName + + local ScoreMessageMission = "" + local ScoreMission = 0 + local ScoreTask = 0 + for MissionName, MissionData in pairs( PlayerData.Mission ) do + ScoreMission = ScoreMission + MissionData.ScoreMission + ScoreTask = ScoreTask + MissionData.ScoreTask + ScoreMessageMission = ScoreMessageMission .. "'" .. MissionName .. "'; " + end + PlayerScore = PlayerScore + ScoreMission + ScoreTask + + if ScoreMessageMission ~= "" then + ScoreMessage = "Tasks: " .. ScoreTask .. " Mission: " .. ScoreMission .. " ( " .. ScoreMessageMission .. ")" + end + end + + return ScoreMessage, PlayerScore, PlayerPenalty +end + + +--- Report Group Score Summary +-- @param #SCORING self +-- @param Wrapper.Group#GROUP PlayerGroup The player group. +function SCORING:ReportScoreGroupSummary( PlayerGroup ) + + local PlayerMessage = "" + + self:T( "Report Score Group Summary" ) + + local PlayerUnits = PlayerGroup:GetUnits() + for UnitID, PlayerUnit in pairs( PlayerUnits ) do + local PlayerUnit = PlayerUnit -- Wrapper.Unit#UNIT + local PlayerName = PlayerUnit:GetPlayerName() + + if PlayerName then + + local ReportHits, ScoreHits, PenaltyHits = self:ReportDetailedPlayerHits( PlayerName ) + ReportHits = ReportHits ~= "" and "\n- " .. ReportHits or ReportHits + self:F( { ReportHits, ScoreHits, PenaltyHits } ) + + local ReportDestroys, ScoreDestroys, PenaltyDestroys = self:ReportDetailedPlayerDestroys( PlayerName ) + ReportDestroys = ReportDestroys ~= "" and "\n- " .. ReportDestroys or ReportDestroys + self:F( { ReportDestroys, ScoreDestroys, PenaltyDestroys } ) + + local ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges = self:ReportDetailedPlayerCoalitionChanges( PlayerName ) + ReportCoalitionChanges = ReportCoalitionChanges ~= "" and "\n- " .. ReportCoalitionChanges or ReportCoalitionChanges + self:F( { ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges } ) + + local ReportGoals, ScoreGoals, PenaltyGoals = self:ReportDetailedPlayerGoals( PlayerName ) + ReportGoals = ReportGoals ~= "" and "\n- " .. ReportGoals or ReportGoals + self:F( { ReportGoals, ScoreGoals, PenaltyGoals } ) + + local ReportMissions, ScoreMissions, PenaltyMissions = self:ReportDetailedPlayerMissions( PlayerName ) + ReportMissions = ReportMissions ~= "" and "\n- " .. ReportMissions or ReportMissions + self:F( { ReportMissions, ScoreMissions, PenaltyMissions } ) + + local PlayerScore = ScoreHits + ScoreDestroys + ScoreCoalitionChanges + ScoreGoals + ScoreMissions + local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + PenaltyGoals + PenaltyMissions + + PlayerMessage = + string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )", + PlayerName, + PlayerScore - PlayerPenalty, + PlayerScore, + PlayerPenalty + ) + MESSAGE:NewType( PlayerMessage, MESSAGE.Type.Detailed ):ToGroup( PlayerGroup ) + end + end + +end + +--- Report Group Score Detailed +-- @param #SCORING self +-- @param Wrapper.Group#GROUP PlayerGroup The player group. +function SCORING:ReportScoreGroupDetailed( PlayerGroup ) + + local PlayerMessage = "" + + self:T( "Report Score Group Detailed" ) + + local PlayerUnits = PlayerGroup:GetUnits() + for UnitID, PlayerUnit in pairs( PlayerUnits ) do + local PlayerUnit = PlayerUnit -- Wrapper.Unit#UNIT + local PlayerName = PlayerUnit:GetPlayerName() + + if PlayerName then + + local ReportHits, ScoreHits, PenaltyHits = self:ReportDetailedPlayerHits( PlayerName ) + ReportHits = ReportHits ~= "" and "\n- " .. ReportHits or ReportHits + self:F( { ReportHits, ScoreHits, PenaltyHits } ) + + local ReportDestroys, ScoreDestroys, PenaltyDestroys = self:ReportDetailedPlayerDestroys( PlayerName ) + ReportDestroys = ReportDestroys ~= "" and "\n- " .. ReportDestroys or ReportDestroys + self:F( { ReportDestroys, ScoreDestroys, PenaltyDestroys } ) + + local ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges = self:ReportDetailedPlayerCoalitionChanges( PlayerName ) + ReportCoalitionChanges = ReportCoalitionChanges ~= "" and "\n- " .. ReportCoalitionChanges or ReportCoalitionChanges + self:F( { ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges } ) + + local ReportGoals, ScoreGoals, PenaltyGoals = self:ReportDetailedPlayerGoals( PlayerName ) + ReportGoals = ReportGoals ~= "" and "\n- " .. ReportGoals or ReportGoals + self:F( { ReportGoals, ScoreGoals, PenaltyGoals } ) + + local ReportMissions, ScoreMissions, PenaltyMissions = self:ReportDetailedPlayerMissions( PlayerName ) + ReportMissions = ReportMissions ~= "" and "\n- " .. ReportMissions or ReportMissions + self:F( { ReportMissions, ScoreMissions, PenaltyMissions } ) + + local PlayerScore = ScoreHits + ScoreDestroys + ScoreCoalitionChanges + ScoreGoals + ScoreMissions + local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + ScoreGoals + PenaltyMissions + + PlayerMessage = + string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )%s%s%s%s%s", + PlayerName, + PlayerScore - PlayerPenalty, + PlayerScore, + PlayerPenalty, + ReportHits, + ReportDestroys, + ReportCoalitionChanges, + ReportGoals, + ReportMissions + ) + MESSAGE:NewType( PlayerMessage, MESSAGE.Type.Detailed ):ToGroup( PlayerGroup ) + end + end + +end + +--- Report all players score +-- @param #SCORING self +-- @param Wrapper.Group#GROUP PlayerGroup The player group. +function SCORING:ReportScoreAllSummary( PlayerGroup ) + + local PlayerMessage = "" + + self:T( { "Summary Score Report of All Players", Players = self.Players } ) + + for PlayerName, PlayerData in pairs( self.Players ) do + + self:T( { PlayerName = PlayerName, PlayerGroup = PlayerGroup } ) + + if PlayerName then + + local ReportHits, ScoreHits, PenaltyHits = self:ReportDetailedPlayerHits( PlayerName ) + ReportHits = ReportHits ~= "" and "\n- " .. ReportHits or ReportHits + self:F( { ReportHits, ScoreHits, PenaltyHits } ) + + local ReportDestroys, ScoreDestroys, PenaltyDestroys = self:ReportDetailedPlayerDestroys( PlayerName ) + ReportDestroys = ReportDestroys ~= "" and "\n- " .. ReportDestroys or ReportDestroys + self:F( { ReportDestroys, ScoreDestroys, PenaltyDestroys } ) + + local ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges = self:ReportDetailedPlayerCoalitionChanges( PlayerName ) + ReportCoalitionChanges = ReportCoalitionChanges ~= "" and "\n- " .. ReportCoalitionChanges or ReportCoalitionChanges + self:F( { ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges } ) + + local ReportGoals, ScoreGoals, PenaltyGoals = self:ReportDetailedPlayerGoals( PlayerName ) + ReportGoals = ReportGoals ~= "" and "\n- " .. ReportGoals or ReportGoals + self:F( { ReportGoals, ScoreGoals, PenaltyGoals } ) + + local ReportMissions, ScoreMissions, PenaltyMissions = self:ReportDetailedPlayerMissions( PlayerName ) + ReportMissions = ReportMissions ~= "" and "\n- " .. ReportMissions or ReportMissions + self:F( { ReportMissions, ScoreMissions, PenaltyMissions } ) + + local PlayerScore = ScoreHits + ScoreDestroys + ScoreCoalitionChanges + ScoreGoals + ScoreMissions + local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + ScoreGoals + PenaltyMissions + + PlayerMessage = + string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )", + PlayerName, + PlayerScore - PlayerPenalty, + PlayerScore, + PlayerPenalty + ) + MESSAGE:NewType( PlayerMessage, MESSAGE.Type.Overview ):ToGroup( PlayerGroup ) + end + end + +end + + +function SCORING:SecondsToClock(sSeconds) + local nSeconds = sSeconds + if nSeconds == 0 then + --return nil; + return "00:00:00"; + else + nHours = string.format("%02.f", math.floor(nSeconds/3600)); + nMins = string.format("%02.f", math.floor(nSeconds/60 - (nHours*60))); + nSecs = string.format("%02.f", math.floor(nSeconds - nHours*3600 - nMins *60)); + return nHours..":"..nMins..":"..nSecs + end +end + +--- Opens a score CSV file to log the scores. +-- @param #SCORING self +-- @param #string ScoringCSV +-- @return #SCORING self +-- @usage +-- -- Open a new CSV file to log the scores of the game Gori Valley. Let the name of the CSV file begin with "Player Scores". +-- ScoringObject = SCORING:New( "Gori Valley" ) +-- ScoringObject:OpenCSV( "Player Scores" ) +function SCORING:OpenCSV( ScoringCSV ) + self:F( ScoringCSV ) + + if lfs and io and os then + if ScoringCSV then + self.ScoringCSV = ScoringCSV + local fdir = lfs.writedir() .. [[Logs\]] .. self.ScoringCSV .. " " .. os.date( "%Y-%m-%d %H-%M-%S" ) .. ".csv" + + self.CSVFile, self.err = io.open( fdir, "w+" ) + if not self.CSVFile then + error( "Error: Cannot open CSV file in " .. lfs.writedir() ) + end + + self.CSVFile:write( '"GameName","RunTime","Time","PlayerName","TargetPlayerName","ScoreType","PlayerUnitCoaltion","PlayerUnitCategory","PlayerUnitType","PlayerUnitName","TargetUnitCoalition","TargetUnitCategory","TargetUnitType","TargetUnitName","Times","Score"\n' ) + + self.RunTime = os.date("%y-%m-%d_%H-%M-%S") + else + error( "A string containing the CSV file name must be given." ) + end + else + self:F( "The MissionScripting.lua file has not been changed to allow lfs, io and os modules to be used..." ) + end + return self +end + + +--- Registers a score for a player. +-- @param #SCORING self +-- @param #string PlayerName The name of the player. +-- @param #string TargetPlayerName The name of the target player. +-- @param #string ScoreType The type of the score. +-- @param #string ScoreTimes The amount of scores achieved. +-- @param #string ScoreAmount The score given. +-- @param #string PlayerUnitName The unit name of the player. +-- @param #string PlayerUnitCoalition The coalition of the player unit. +-- @param #string PlayerUnitCategory The category of the player unit. +-- @param #string PlayerUnitType The type of the player unit. +-- @param #string TargetUnitName The name of the target unit. +-- @param #string TargetUnitCoalition The coalition of the target unit. +-- @param #string TargetUnitCategory The category of the target unit. +-- @param #string TargetUnitType The type of the target unit. +-- @return #SCORING self +function SCORING:ScoreCSV( PlayerName, TargetPlayerName, ScoreType, ScoreTimes, ScoreAmount, PlayerUnitName, PlayerUnitCoalition, PlayerUnitCategory, PlayerUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + --write statistic information to file + local ScoreTime = self:SecondsToClock( timer.getTime() ) + PlayerName = PlayerName:gsub( '"', '_' ) + + TargetPlayerName = TargetPlayerName or "" + TargetPlayerName = TargetPlayerName:gsub( '"', '_' ) + + if PlayerUnitName and PlayerUnitName ~= '' then + local PlayerUnit = Unit.getByName( PlayerUnitName ) + + if PlayerUnit then + if not PlayerUnitCategory then + --PlayerUnitCategory = SCORINGCategory[PlayerUnit:getCategory()] + PlayerUnitCategory = _SCORINGCategory[PlayerUnit:getDesc().category] + end + + if not PlayerUnitCoalition then + PlayerUnitCoalition = _SCORINGCoalition[PlayerUnit:getCoalition()] + end + + if not PlayerUnitType then + PlayerUnitType = PlayerUnit:getTypeName() + end + else + PlayerUnitName = '' + PlayerUnitCategory = '' + PlayerUnitCoalition = '' + PlayerUnitType = '' + end + else + PlayerUnitName = '' + PlayerUnitCategory = '' + PlayerUnitCoalition = '' + PlayerUnitType = '' + end + + TargetUnitCoalition = TargetUnitCoalition or "" + TargetUnitCategory = TargetUnitCategory or "" + TargetUnitType = TargetUnitType or "" + TargetUnitName = TargetUnitName or "" + + if lfs and io and os then + self.CSVFile:write( + '"' .. self.GameName .. '"' .. ',' .. + '"' .. self.RunTime .. '"' .. ',' .. + '' .. ScoreTime .. '' .. ',' .. + '"' .. PlayerName .. '"' .. ',' .. + '"' .. TargetPlayerName .. '"' .. ',' .. + '"' .. ScoreType .. '"' .. ',' .. + '"' .. PlayerUnitCoalition .. '"' .. ',' .. + '"' .. PlayerUnitCategory .. '"' .. ',' .. + '"' .. PlayerUnitType .. '"' .. ',' .. + '"' .. PlayerUnitName .. '"' .. ',' .. + '"' .. TargetUnitCoalition .. '"' .. ',' .. + '"' .. TargetUnitCategory .. '"' .. ',' .. + '"' .. TargetUnitType .. '"' .. ',' .. + '"' .. TargetUnitName .. '"' .. ',' .. + '' .. ScoreTimes .. '' .. ',' .. + '' .. ScoreAmount + ) + + self.CSVFile:write( "\n" ) + end +end + + +function SCORING:CloseCSV() + if lfs and io and os then + self.CSVFile:close() + end +end + +--- **Functional** -- Keep airbases clean of crashing or colliding airplanes, and kill missiles when being fired at airbases. +-- +-- === +-- +-- ## Features: +-- +-- +-- * Try to keep the airbase clean and operational. +-- * Prevent airplanes from crashing. +-- * Clean up obstructing airplanes from the runway that are standing still for a period of time. +-- * Prevent airplanes firing missiles within the airbase zone. +-- +-- === +-- +-- ## Missions: +-- +-- [CLA - CleanUp Airbase](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/CLA%20-%20CleanUp%20Airbase) +-- +-- === +-- +-- Specific airbases need to be provided that need to be guarded. Each airbase registered, will be guarded within a zone of 8 km around the airbase. +-- Any unit that fires a missile, or shoots within the zone of an airbase, will be monitored by CLEANUP_AIRBASE. +-- Within the 8km zone, units cannot fire any missile, which prevents the airbase runway to receive missile or bomb hits. +-- Any airborne or ground unit that is on the runway below 30 meters (default value) will be automatically removed if it is damaged. +-- +-- This is not a full 100% secure implementation. It is still possible that CLEANUP_AIRBASE cannot prevent (in-time) to keep the airbase clean. +-- The following situations may happen that will still stop the runway of an airbase: +-- +-- * A damaged unit is not removed on time when above the runway, and crashes on the runway. +-- * A bomb or missile is still able to dropped on the runway. +-- * Units collide on the airbase, and could not be removed on time. +-- +-- When a unit is within the airbase zone and needs to be monitored, +-- its status will be checked every 0.25 seconds! This is required to ensure that the airbase is kept clean. +-- But as a result, there is more CPU overload. +-- +-- So as an advise, I suggest you use the CLEANUP_AIRBASE class with care: +-- +-- * Only monitor airbases that really need to be monitored! +-- * Try not to monitor airbases that are likely to be invaded by enemy troops. +-- For these airbases, there is little use to keep them clean, as they will be invaded anyway... +-- +-- By following the above guidelines, you can add airbase cleanup with acceptable CPU overhead. +-- +-- === +-- +-- ### Author: **FlightControl** +-- ### Contributions: +-- +-- === +-- +-- @module Functional.CleanUp +-- @image CleanUp_Airbases.JPG + +--- @type CLEANUP_AIRBASE.__ Methods which are not intended for mission designers, but which are used interally by the moose designer :-) +-- @field #map<#string,Wrapper.Airbase#AIRBASE> Airbases Map of Airbases. +-- @extends Core.Base#BASE + +--- @type CLEANUP_AIRBASE +-- @extends #CLEANUP_AIRBASE.__ + +--- Keeps airbases clean, and tries to guarantee continuous airbase operations, even under combat. +-- +-- # 1. CLEANUP_AIRBASE Constructor +-- +-- Creates the main object which is preventing the airbase to get polluted with debris on the runway, which halts the airbase. +-- +-- -- Clean these Zones. +-- CleanUpAirports = CLEANUP_AIRBASE:New( { AIRBASE.Caucasus.Tbilisi, AIRBASE.Caucasus.Kutaisi } ) +-- +-- -- or +-- CleanUpTbilisi = CLEANUP_AIRBASE:New( AIRBASE.Caucasus.Tbilisi ) +-- CleanUpKutaisi = CLEANUP_AIRBASE:New( AIRBASE.Caucasus.Kutaisi ) +-- +-- # 2. Add or Remove airbases +-- +-- The method @{#CLEANUP_AIRBASE.AddAirbase}() to add an airbase to the cleanup validation process. +-- The method @{#CLEANUP_AIRBASE.RemoveAirbase}() removes an airbase from the cleanup validation process. +-- +-- # 3. Clean missiles and bombs within the airbase zone. +-- +-- When missiles or bombs hit the runway, the airbase operations stop. +-- Use the method @{#CLEANUP_AIRBASE.SetCleanMissiles}() to control the cleaning of missiles, which will prevent airbases to stop. +-- Note that this method will not allow anymore airbases to be attacked, so there is a trade-off here to do. +-- +-- @field #CLEANUP_AIRBASE +CLEANUP_AIRBASE = { + ClassName = "CLEANUP_AIRBASE", + TimeInterval = 0.2, + CleanUpList = {}, +} + +-- @field #CLEANUP_AIRBASE.__ +CLEANUP_AIRBASE.__ = {} + +--- @field #CLEANUP_AIRBASE.__.Airbases +CLEANUP_AIRBASE.__.Airbases = {} + +--- Creates the main object which is handling the cleaning of the debris within the given Zone Names. +-- @param #CLEANUP_AIRBASE self +-- @param #list<#string> AirbaseNames Is a table of airbase names where the debris should be cleaned. Also a single string can be passed with one airbase name. +-- @return #CLEANUP_AIRBASE +-- @usage +-- -- Clean these Zones. +-- CleanUpAirports = CLEANUP_AIRBASE:New( { AIRBASE.Caucasus.Tbilisi, AIRBASE.Caucasus.Kutaisi ) +-- or +-- CleanUpTbilisi = CLEANUP_AIRBASE:New( AIRBASE.Caucasus.Tbilisi ) +-- CleanUpKutaisi = CLEANUP_AIRBASE:New( AIRBASE.Caucasus.Kutaisi ) +function CLEANUP_AIRBASE:New( AirbaseNames ) + + local self = BASE:Inherit( self, BASE:New() ) -- #CLEANUP_AIRBASE + self:F( { AirbaseNames } ) + + if type( AirbaseNames ) == 'table' then + for AirbaseID, AirbaseName in pairs( AirbaseNames ) do + self:AddAirbase( AirbaseName ) + end + else + local AirbaseName = AirbaseNames + self:AddAirbase( AirbaseName ) + end + + self:HandleEvent( EVENTS.Birth, self.__.OnEventBirth ) + + self.__.CleanUpScheduler = SCHEDULER:New( self, self.__.CleanUpSchedule, {}, 1, self.TimeInterval ) + + self:HandleEvent( EVENTS.EngineShutdown , self.__.EventAddForCleanUp ) + self:HandleEvent( EVENTS.EngineStartup, self.__.EventAddForCleanUp ) + self:HandleEvent( EVENTS.Hit, self.__.EventAddForCleanUp ) + self:HandleEvent( EVENTS.PilotDead, self.__.OnEventCrash ) + self:HandleEvent( EVENTS.Dead, self.__.OnEventCrash ) + self:HandleEvent( EVENTS.Crash, self.__.OnEventCrash ) + + for UnitName, Unit in pairs( _DATABASE.UNITS ) do + local Unit = Unit -- Wrapper.Unit#UNIT + if Unit:IsAlive() ~= nil then + if self:IsInAirbase( Unit:GetVec2() ) then + self:F( { UnitName = UnitName } ) + self.CleanUpList[UnitName] = {} + self.CleanUpList[UnitName].CleanUpUnit = Unit + self.CleanUpList[UnitName].CleanUpGroup = Unit:GetGroup() + self.CleanUpList[UnitName].CleanUpGroupName = Unit:GetGroup():GetName() + self.CleanUpList[UnitName].CleanUpUnitName = Unit:GetName() + end + end + end + + return self +end + +--- Adds an airbase to the airbase validation list. +-- @param #CLEANUP_AIRBASE self +-- @param #string AirbaseName +-- @return #CLEANUP_AIRBASE +function CLEANUP_AIRBASE:AddAirbase( AirbaseName ) + self.__.Airbases[AirbaseName] = AIRBASE:FindByName( AirbaseName ) + self:F({"Airbase:", AirbaseName, self.__.Airbases[AirbaseName]:GetDesc()}) + + return self +end + +--- Removes an airbase from the airbase validation list. +-- @param #CLEANUP_AIRBASE self +-- @param #string AirbaseName +-- @return #CLEANUP_AIRBASE +function CLEANUP_AIRBASE:RemoveAirbase( AirbaseName ) + self.__.Airbases[AirbaseName] = nil + return self +end + +--- Enables or disables the cleaning of missiles within the airbase zones. +-- Airbase operations stop when a missile or bomb is dropped at a runway. +-- Note that when this method is used, the airbase operations won't stop if +-- the missile or bomb was cleaned within the airbase zone, which is 8km from the center of the airbase. +-- However, there is a trade-off to make. Attacks on airbases won't be possible anymore if this method is used. +-- Note, one can also use the method @{#CLEANUP_AIRBASE.RemoveAirbase}() to remove the airbase from the control process as a whole, +-- when an enemy unit is near. That is also an option... +-- @param #CLEANUP_AIRBASE self +-- @param #string CleanMissiles (Default=true) If true, missiles fired are immediately destroyed. If false missiles are not controlled. +-- @return #CLEANUP_AIRBASE +function CLEANUP_AIRBASE:SetCleanMissiles( CleanMissiles ) + + if CleanMissiles then + self:HandleEvent( EVENTS.Shot, self.__.OnEventShot ) + else + self:UnHandleEvent( EVENTS.Shot ) + end +end + +function CLEANUP_AIRBASE.__:IsInAirbase( Vec2 ) + + local InAirbase = false + for AirbaseName, Airbase in pairs( self.__.Airbases ) do + local Airbase = Airbase -- Wrapper.Airbase#AIRBASE + if Airbase:GetZone():IsVec2InZone( Vec2 ) then + InAirbase = true + break; + end + end + + return InAirbase +end + + + +--- Destroys a @{Wrapper.Unit} from the simulator, but checks first if it is still existing! +-- @param #CLEANUP_AIRBASE self +-- @param Wrapper.Unit#UNIT CleanUpUnit The object to be destroyed. +function CLEANUP_AIRBASE.__:DestroyUnit( CleanUpUnit ) + self:F( { CleanUpUnit } ) + + if CleanUpUnit then + local CleanUpUnitName = CleanUpUnit:GetName() + local CleanUpGroup = CleanUpUnit:GetGroup() + -- TODO DCS BUG - Client bug in 1.5.3 + if CleanUpGroup:IsAlive() then + local CleanUpGroupUnits = CleanUpGroup:GetUnits() + if #CleanUpGroupUnits == 1 then + local CleanUpGroupName = CleanUpGroup:GetName() + CleanUpGroup:Destroy() + else + CleanUpUnit:Destroy() + end + self.CleanUpList[CleanUpUnitName] = nil + end + end +end + + + +--- Destroys a missile from the simulator, but checks first if it is still existing! +-- @param #CLEANUP_AIRBASE self +-- @param DCS#Weapon MissileObject +function CLEANUP_AIRBASE.__:DestroyMissile( MissileObject ) + self:F( { MissileObject } ) + + if MissileObject and MissileObject:isExist() then + MissileObject:destroy() + self:T( "MissileObject Destroyed") + end +end + +--- @param #CLEANUP_AIRBASE self +-- @param Core.Event#EVENTDATA EventData +function CLEANUP_AIRBASE.__:OnEventBirth( EventData ) + self:F( { EventData } ) + + if EventData and EventData.IniUnit and EventData.IniUnit:IsAlive() ~= nil then + if self:IsInAirbase( EventData.IniUnit:GetVec2() ) then + self.CleanUpList[EventData.IniDCSUnitName] = {} + self.CleanUpList[EventData.IniDCSUnitName].CleanUpUnit = EventData.IniUnit + self.CleanUpList[EventData.IniDCSUnitName].CleanUpGroup = EventData.IniGroup + self.CleanUpList[EventData.IniDCSUnitName].CleanUpGroupName = EventData.IniDCSGroupName + self.CleanUpList[EventData.IniDCSUnitName].CleanUpUnitName = EventData.IniDCSUnitName + end + end + +end + + +--- Detects if a crash event occurs. +-- Crashed units go into a CleanUpList for removal. +-- @param #CLEANUP_AIRBASE self +-- @param Core.Event#EVENTDATA Event +function CLEANUP_AIRBASE.__:OnEventCrash( Event ) + self:F( { Event } ) + + --TODO: DCS BUG - This stuff is not working due to a DCS bug. Burning units cannot be destroyed. + -- self:T("before getGroup") + -- local _grp = Unit.getGroup(event.initiator)-- Identify the group that fired + -- self:T("after getGroup") + -- _grp:destroy() + -- self:T("after deactivateGroup") + -- event.initiator:destroy() + + if Event.IniDCSUnitName and Event.IniCategory == Object.Category.UNIT then + self.CleanUpList[Event.IniDCSUnitName] = {} + self.CleanUpList[Event.IniDCSUnitName].CleanUpUnit = Event.IniUnit + self.CleanUpList[Event.IniDCSUnitName].CleanUpGroup = Event.IniGroup + self.CleanUpList[Event.IniDCSUnitName].CleanUpGroupName = Event.IniDCSGroupName + self.CleanUpList[Event.IniDCSUnitName].CleanUpUnitName = Event.IniDCSUnitName + end + +end + +--- Detects if a unit shoots a missile. +-- If this occurs within one of the airbases, then the weapon used must be destroyed. +-- @param #CLEANUP_AIRBASE self +-- @param Core.Event#EVENTDATA Event +function CLEANUP_AIRBASE.__:OnEventShot( Event ) + self:F( { Event } ) + + -- Test if the missile was fired within one of the CLEANUP_AIRBASE.AirbaseNames. + if self:IsInAirbase( Event.IniUnit:GetVec2() ) then + -- Okay, the missile was fired within the CLEANUP_AIRBASE.AirbaseNames, destroy the fired weapon. + self:DestroyMissile( Event.Weapon ) + end +end + +--- Detects if the Unit has an S_EVENT_HIT within the given AirbaseNames. If this is the case, destroy the unit. +-- @param #CLEANUP_AIRBASE self +-- @param Core.Event#EVENTDATA Event +function CLEANUP_AIRBASE.__:OnEventHit( Event ) + self:F( { Event } ) + + if Event.IniUnit then + if self:IsInAirbase( Event.IniUnit:GetVec2() ) then + self:T( { "Life: ", Event.IniDCSUnitName, ' = ', Event.IniUnit:GetLife(), "/", Event.IniUnit:GetLife0() } ) + if Event.IniUnit:GetLife() < Event.IniUnit:GetLife0() then + self:T( "CleanUp: Destroy: " .. Event.IniDCSUnitName ) + CLEANUP_AIRBASE.__:DestroyUnit( Event.IniUnit ) + end + end + end + + if Event.TgtUnit then + if self:IsInAirbase( Event.TgtUnit:GetVec2() ) then + self:T( { "Life: ", Event.TgtDCSUnitName, ' = ', Event.TgtUnit:GetLife(), "/", Event.TgtUnit:GetLife0() } ) + if Event.TgtUnit:GetLife() < Event.TgtUnit:GetLife0() then + self:T( "CleanUp: Destroy: " .. Event.TgtDCSUnitName ) + CLEANUP_AIRBASE.__:DestroyUnit( Event.TgtUnit ) + end + end + end +end + +--- Add the @{DCS#Unit} to the CleanUpList for CleanUp. +-- @param #CLEANUP_AIRBASE self +-- @param DCS#UNIT CleanUpUnit +-- @oaram #string CleanUpUnitName +function CLEANUP_AIRBASE.__:AddForCleanUp( CleanUpUnit, CleanUpUnitName ) + self:F( { CleanUpUnit, CleanUpUnitName } ) + + self.CleanUpList[CleanUpUnitName] = {} + self.CleanUpList[CleanUpUnitName].CleanUpUnit = CleanUpUnit + self.CleanUpList[CleanUpUnitName].CleanUpUnitName = CleanUpUnitName + + local CleanUpGroup = CleanUpUnit:GetGroup() + + self.CleanUpList[CleanUpUnitName].CleanUpGroup = CleanUpGroup + self.CleanUpList[CleanUpUnitName].CleanUpGroupName = CleanUpGroup:GetName() + self.CleanUpList[CleanUpUnitName].CleanUpTime = timer.getTime() + self.CleanUpList[CleanUpUnitName].CleanUpMoved = false + + self:T( { "CleanUp: Add to CleanUpList: ", CleanUpGroup:GetName(), CleanUpUnitName } ) + +end + +--- Detects if the Unit has an S_EVENT_ENGINE_SHUTDOWN or an S_EVENT_HIT within the given AirbaseNames. If this is the case, add the Group to the CLEANUP_AIRBASE List. +-- @param #CLEANUP_AIRBASE.__ self +-- @param Core.Event#EVENTDATA Event +function CLEANUP_AIRBASE.__:EventAddForCleanUp( Event ) + + self:F({Event}) + + + if Event.IniDCSUnit and Event.IniCategory == Object.Category.UNIT then + if self.CleanUpList[Event.IniDCSUnitName] == nil then + if self:IsInAirbase( Event.IniUnit:GetVec2() ) then + self:AddForCleanUp( Event.IniUnit, Event.IniDCSUnitName ) + end + end + end + + if Event.TgtDCSUnit and Event.TgtCategory == Object.Category.UNIT then + if self.CleanUpList[Event.TgtDCSUnitName] == nil then + if self:IsInAirbase( Event.TgtUnit:GetVec2() ) then + self:AddForCleanUp( Event.TgtUnit, Event.TgtDCSUnitName ) + end + end + end + +end + + +--- At the defined time interval, CleanUp the Groups within the CleanUpList. +-- @param #CLEANUP_AIRBASE self +function CLEANUP_AIRBASE.__:CleanUpSchedule() + + local CleanUpCount = 0 + for CleanUpUnitName, CleanUpListData in pairs( self.CleanUpList ) do + CleanUpCount = CleanUpCount + 1 + + local CleanUpUnit = CleanUpListData.CleanUpUnit -- Wrapper.Unit#UNIT + local CleanUpGroupName = CleanUpListData.CleanUpGroupName + + if CleanUpUnit:IsAlive() ~= nil then + + if self:IsInAirbase( CleanUpUnit:GetVec2() ) then + + if _DATABASE:GetStatusGroup( CleanUpGroupName ) ~= "ReSpawn" then + + local CleanUpCoordinate = CleanUpUnit:GetCoordinate() + + self:T( { "CleanUp Scheduler", CleanUpUnitName } ) + if CleanUpUnit:GetLife() <= CleanUpUnit:GetLife0() * 0.95 then + if CleanUpUnit:IsAboveRunway() then + if CleanUpUnit:InAir() then + + local CleanUpLandHeight = CleanUpCoordinate:GetLandHeight() + local CleanUpUnitHeight = CleanUpCoordinate.y - CleanUpLandHeight + + if CleanUpUnitHeight < 100 then + self:T( { "CleanUp Scheduler", "Destroy " .. CleanUpUnitName .. " because below safe height and damaged." } ) + self:DestroyUnit( CleanUpUnit ) + end + else + self:T( { "CleanUp Scheduler", "Destroy " .. CleanUpUnitName .. " because on runway and damaged." } ) + self:DestroyUnit( CleanUpUnit ) + end + end + end + -- Clean Units which are waiting for a very long time in the CleanUpZone. + if CleanUpUnit and not CleanUpUnit:GetPlayerName() then + local CleanUpUnitVelocity = CleanUpUnit:GetVelocityKMH() + if CleanUpUnitVelocity < 1 then + if CleanUpListData.CleanUpMoved then + if CleanUpListData.CleanUpTime + 180 <= timer.getTime() then + self:T( { "CleanUp Scheduler", "Destroy due to not moving anymore " .. CleanUpUnitName } ) + self:DestroyUnit( CleanUpUnit ) + end + end + else + CleanUpListData.CleanUpTime = timer.getTime() + CleanUpListData.CleanUpMoved = true + end + end + else + -- not anymore in an airbase zone, remove from cleanup list. + self.CleanUpList[CleanUpUnitName] = nil + end + else + -- Do nothing ... + self.CleanUpList[CleanUpUnitName] = nil + end + else + self:T( "CleanUp: Group " .. CleanUpUnitName .. " cannot be found in DCS RTE, removing ..." ) + self.CleanUpList[CleanUpUnitName] = nil + end + end + self:T(CleanUpCount) + + return true +end +--- **Functional** -- Limit the movement of simulaneous moving ground vehicles. +-- +-- === +-- +-- Limit the simultaneous movement of Groups within a running Mission. +-- This module is defined to improve the performance in missions, and to bring additional realism for GROUND vehicles. +-- Performance: If in a DCSRTE there are a lot of moving GROUND units, then in a multi player mission, this WILL create lag if +-- the main DCS execution core of your CPU is fully utilized. So, this class will limit the amount of simultaneous moving GROUND units +-- on defined intervals (currently every minute). +-- @module Functional.Movement +-- @image MOOSE.JPG + +--- @type MOVEMENT +-- @extends Core.Base#BASE + +--- +--@field #MOVEMENT +MOVEMENT = { + ClassName = "MOVEMENT", +} + +--- Creates the main object which is handling the GROUND forces movement. +-- @param table{string,...}|string MovePrefixes is a table of the Prefixes (names) of the GROUND Groups that need to be controlled by the MOVEMENT Object. +-- @param number MoveMaximum is a number that defines the maximum amount of GROUND Units to be moving during one minute. +-- @return MOVEMENT +-- @usage +-- -- Limit the amount of simultaneous moving units on the ground to prevent lag. +-- Movement_US_Platoons = MOVEMENT:New( { 'US Tank Platoon Left', 'US Tank Platoon Middle', 'US Tank Platoon Right', 'US CH-47D Troops' }, 15 ) + +function MOVEMENT:New( MovePrefixes, MoveMaximum ) + local self = BASE:Inherit( self, BASE:New() ) -- #MOVEMENT + self:F( { MovePrefixes, MoveMaximum } ) + + if type( MovePrefixes ) == 'table' then + self.MovePrefixes = MovePrefixes + else + self.MovePrefixes = { MovePrefixes } + end + self.MoveCount = 0 -- The internal counter of the amount of Moveing the has happened since MoveStart. + self.MoveMaximum = MoveMaximum -- Contains the Maximum amount of units that are allowed to move... + self.AliveUnits = 0 -- Contains the counter how many units are currently alive + self.MoveUnits = {} -- Reflects if the Moving for this MovePrefixes is going to be scheduled or not. + + self:HandleEvent( EVENTS.Birth ) + +-- self:AddEvent( world.event.S_EVENT_BIRTH, self.OnBirth ) +-- +-- self:EnableEvents() + + self:ScheduleStart() + + return self +end + +--- Call this function to start the MOVEMENT scheduling. +function MOVEMENT:ScheduleStart() + self:F() + --self.MoveFunction = routines.scheduleFunction( self._Scheduler, { self }, timer.getTime() + 1, 120 ) + self.MoveFunction = SCHEDULER:New( self, self._Scheduler, {}, 1, 120 ) +end + +--- Call this function to stop the MOVEMENT scheduling. +-- @todo need to implement it ... Forgot. +function MOVEMENT:ScheduleStop() + self:F() + +end + +--- Captures the birth events when new Units were spawned. +-- @todo This method should become obsolete. The new @{DATABASE} class will handle the collection administration. +-- @param #MOVEMENT self +-- @param Core.Event#EVENTDATA self +function MOVEMENT:OnEventBirth( EventData ) + self:F( { EventData } ) + + if timer.getTime0() < timer.getAbsTime() then -- dont need to add units spawned in at the start of the mission if mist is loaded in init line + if EventData.IniDCSUnit then + self:T( "Birth object : " .. EventData.IniDCSUnitName ) + if EventData.IniDCSGroup and EventData.IniDCSGroup:isExist() then + for MovePrefixID, MovePrefix in pairs( self.MovePrefixes ) do + if string.find( EventData.IniDCSUnitName, MovePrefix, 1, true ) then + self.AliveUnits = self.AliveUnits + 1 + self.MoveUnits[EventData.IniDCSUnitName] = EventData.IniDCSGroupName + self:T( self.AliveUnits ) + end + end + end + end + + EventData.IniUnit:HandleEvent( EVENTS.DEAD, self.OnDeadOrCrash ) + end + +end + +--- Captures the Dead or Crash events when Units crash or are destroyed. +-- @todo This method should become obsolete. The new @{DATABASE} class will handle the collection administration. +function MOVEMENT:OnDeadOrCrash( Event ) + self:F( { Event } ) + + if Event.IniDCSUnit then + self:T( "Dead object : " .. Event.IniDCSUnitName ) + for MovePrefixID, MovePrefix in pairs( self.MovePrefixes ) do + if string.find( Event.IniDCSUnitName, MovePrefix, 1, true ) then + self.AliveUnits = self.AliveUnits - 1 + self.MoveUnits[Event.IniDCSUnitName] = nil + self:T( self.AliveUnits ) + end + end + end +end + +--- This function is called automatically by the MOVEMENT scheduler. A new function is scheduled when MoveScheduled is true. +function MOVEMENT:_Scheduler() + self:F( { self.MovePrefixes, self.MoveMaximum, self.AliveUnits, self.MovementGroups } ) + + if self.AliveUnits > 0 then + local MoveProbability = ( self.MoveMaximum * 100 ) / self.AliveUnits + self:T( 'Move Probability = ' .. MoveProbability ) + + for MovementUnitName, MovementGroupName in pairs( self.MoveUnits ) do + local MovementGroup = Group.getByName( MovementGroupName ) + if MovementGroup and MovementGroup:isExist() then + local MoveOrStop = math.random( 1, 100 ) + self:T( 'MoveOrStop = ' .. MoveOrStop ) + if MoveOrStop <= MoveProbability then + self:T( 'Group continues moving = ' .. MovementGroupName ) + trigger.action.groupContinueMoving( MovementGroup ) + else + self:T( 'Group stops moving = ' .. MovementGroupName ) + trigger.action.groupStopMoving( MovementGroup ) + end + else + self.MoveUnits[MovementUnitName] = nil + end + end + end + return true +end +--- **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: Aug 2021 +-- +-- === +-- +-- @module Functional.Sead +-- @image SEAD.JPG + +--- +-- @type SEAD +-- @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 + Padding = 10, +} + + --- Missile enumerators + -- @field Harms + SEAD.Harms = { + ["AGM_88"] = "AGM_88", + ["AGM_45"] = "AGM_45", + ["AGM_122"] = "AGM_122", + ["AGM_84"] = "AGM_84", + ["AGM_45"] = "AGM_45", + ["ALARM"] = "ALARM", + ["LD-10"] = "LD-10", + ["X_58"] = "X_58", + ["X_28"] = "X_28", + ["X_25"] = "X_25", + ["X_31"] = "X_31", + ["Kh25"] = "Kh25", + } + + --- Missile enumerators - from DCS ME and Wikipedia + -- @field HarmData + SEAD.HarmData = { + -- km and mach + ["AGM_88"] = { 150, 3}, + ["AGM_45"] = { 12, 2}, + ["AGM_122"] = { 16.5, 2.3}, + ["AGM_84"] = { 280, 0.85}, + ["ALARM"] = { 45, 2}, + ["LD-10"] = { 60, 4}, + ["X_58"] = { 70, 4}, + ["X_28"] = { 80, 2.5}, + ["X_25"] = { 25, 0.76}, + ["X_31"] = {150, 3}, + ["Kh25"] = {25, 0.8}, + } + +--- 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. +-- @param #SEAD self +-- @param #table SEADGroupPrefixes Table of #string entries or single #string, which is a table of Prefixes of the SA Groups in the DCS mission editor on which evasive actions need to be taken. +-- @param #number Padding (Optional) Extra number of seconds to add to radar switch-back-on time +-- @return #SEAD self +-- @usage +-- -- CCCP SEAD Defenses +-- -- Defends the Russian SA installations from SEAD attacks. +-- 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, Padding ) + + 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 + + local padding = Padding or 10 + if padding < 10 then padding = 10 end + self.Padding = padding + + self:HandleEvent( EVENTS.Shot, self.HandleEventShot ) + + self:I("*** SEAD - Started Version 0.3.1") + return self +end + +--- Update the active SEAD Set +-- @param #SEAD self +-- @param #table SEADGroupPrefixes The prefixes to add, note: can also be a single #string +-- @return #SEAD self +function SEAD:UpdateSet( SEADGroupPrefixes ) + + self:T( SEADGroupPrefixes ) + + if type( SEADGroupPrefixes ) == 'table' then + for SEADGroupPrefixID, SEADGroupPrefix in pairs( SEADGroupPrefixes ) do + self.SEADGroupPrefixes[SEADGroupPrefix] = SEADGroupPrefix + end + else + self.SEADGroupPrefixes[SEADGroupPrefixes] = SEADGroupPrefixes + end + + return self +end + +--- Sets the engagement range of the SAMs. Defaults to 75% to make it more deadly. Feature Request #1355 +-- @param #SEAD self +-- @param #number range Set the engagement range in percent, e.g. 50 +-- @return #SEAD self +function SEAD:SetEngagementRange(range) + self:T( { range } ) + range = range or 75 + if range < 0 or range > 100 then + range = 75 + end + self.EngagementRange = range + self:T(string.format("*** SEAD - Engagement range set to %s",range)) + return self +end + +--- Set the padding in seconds, which extends the radar off time calculated by SEAD +-- @param #SEAD self +-- @param #number Padding Extra number of seconds to add for the switch-on +-- @return #SEAD self +function SEAD:SetPadding(Padding) + self:T( { Padding } ) + local padding = Padding or 10 + if padding < 10 then padding = 10 end + self.Padding = padding + return self +end + + --- Check if a known HARM was fired + -- @param #SEAD self + -- @param #string WeaponName + -- @return #boolean Returns true for a match + -- @return #string name Name of hit in table + function SEAD:_CheckHarms(WeaponName) + self:T( { WeaponName } ) + local hit = false + local name = "" + for _,_name in pairs (SEAD.Harms) do + if string.find(WeaponName,_name,1) then + hit = true + name = _name + break + end + end + return hit, name + end + + --- (Internal) Return distance in meters between two coordinates or -1 on error. + -- @param #SEAD self + -- @param Core.Point#COORDINATE _point1 Coordinate one + -- @param Core.Point#COORDINATE _point2 Coordinate two + -- @return #number Distance in meters + function SEAD:_GetDistance(_point1, _point2) + self:T("_GetDistance") + if _point1 and _point2 then + local distance1 = _point1:Get2DDistance(_point2) + local distance2 = _point1:DistanceFromPointVec2(_point2) + --self:T({dist1=distance1, dist2=distance2}) + if distance1 and type(distance1) == "number" then + return distance1 + elseif distance2 and type(distance2) == "number" then + return distance2 + else + self:E("*****Cannot calculate distance!") + self:E({_point1,_point2}) + return -1 + end + else + self:E("******Cannot calculate distance!") + self:E({_point1,_point2}) + return -1 + end + end + +--- Detects if an SAM site was shot with an anti radiation missile. In this case, take evasive actions based on the skill level set within the ME. +-- @see SEAD +-- @param #SEAD +-- @param Core.Event#EVENTDATA EventData +-- @return #SEAD self +function SEAD:HandleEventShot( EventData ) + self:T( { EventData.id } ) + local SEADPlane = EventData.IniUnit -- Wrapper.Unit#UNIT + local SEADPlanePos = SEADPlane:GetCoordinate() -- Core.Point#COORDINATE + 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 }) + + if self:_CheckHarms(SEADWeaponName) then + self:T( '*** SEAD - Weapon Match' ) + local _targetskill = "Random" + local _targetgroupname = "none" + local _target = EventData.Weapon:getTarget() -- Identify target + local _targetUnit = UNIT:Find(_target) -- Wrapper.Unit#UNIT + local _targetgroup = nil -- Wrapper.Group#GROUP + if _targetUnit and _targetUnit:IsAlive() then + _targetgroup = _targetUnit:GetGroup() + _targetgroupname = _targetgroup:GetName() -- group name + local _targetUnitName = _targetUnit:GetName() + _targetUnit:GetSkill() + _targetskill = _targetUnit:GetSkill() + end + -- see if we are shot at + local SEADGroupFound = false + for SEADGroupPrefixID, SEADGroupPrefix in pairs( self.SEADGroupPrefixes ) do + self:T( _targetgroupname, SEADGroupPrefix ) + if string.find( _targetgroupname, SEADGroupPrefix ) then + SEADGroupFound = true + self:T( '*** SEAD - Group Match 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 + local _evade = math.random (1,100) -- random number for chance of evading action + if (_evade > self.TargetSkill[_targetskill].Evade) then + self:T("*** SEAD - Evading") + -- calculate distance of attacker + local _targetpos = _targetgroup:GetCoordinate() + local _distance = self:_GetDistance(SEADPlanePos, _targetpos) + -- weapon speed + local hit, data = self:_CheckHarms(SEADWeaponName) + local wpnspeed = 666 -- ;) + local reach = 10 + if hit then + local wpndata = SEAD.HarmData[data] + reach = wpndata[1] * 1,1 + local mach = wpndata[2] + wpnspeed = math.floor(mach * 340.29) + end + -- time to impact + local _tti = math.floor(_distance / wpnspeed) -- estimated impact time + if _distance > 0 then + _distance = math.floor(_distance / 1000) -- km + else + _distance = 0 + end + + self:T( string.format("*** SEAD - target skill %s, distance %dkm, reach %dkm, tti %dsec", _targetskill, _distance,reach,_tti )) + + if reach >= _distance then + self:T("*** SEAD - Shot in Reach") + + local function SuppressionStart(args) + self:T(string.format("*** SEAD - %s Radar Off & Relocating",args[2])) + local grp = args[1] -- Wrapper.Group#GROUP + grp:OptionAlarmStateGreen() + grp:RelocateGroundRandomInRadius(20,300,false,false,"Diamond") + end + + local function SuppressionStop(args) + self:T(string.format("*** SEAD - %s Radar On",args[2])) + local grp = args[1] -- Wrapper.Group#GROUP + grp:OptionAlarmStateRed() + grp:OptionEngageRange(self.EngagementRange) + self.SuppressedGroups[args[2]] = false + end + + -- randomize switch-on time + local delay = math.random(self.TargetSkill[_targetskill].DelayOn[1], self.TargetSkill[_targetskill].DelayOn[2]) + if delay > _tti then delay = delay / 2 end -- speed up + if _tti > (3*delay) then delay = (_tti / 2) * 0.9 end -- shot from afar + + local SuppressionStartTime = timer.getTime() + delay + local SuppressionEndTime = timer.getTime() + _tti + self.Padding + + if not self.SuppressedGroups[_targetgroupname] then + self:T(string.format("*** SEAD - %s | Parameters TTI %ds | Switch-Off in %ds",_targetgroupname,_tti,delay)) + timer.scheduleFunction(SuppressionStart,{_targetgroup,_targetgroupname},SuppressionStartTime) + timer.scheduleFunction(SuppressionStop,{_targetgroup,_targetgroupname},SuppressionEndTime) + self.SuppressedGroups[_targetgroupname] = true + end + + end + end + end + end + end + return self +end +--- **Functional** -- Taking the lead of AI escorting your flight. +-- +-- === +-- +-- ## Features: +-- +-- * Escort navigation commands. +-- * Escort hold at position commands. +-- * Escorts reporting detected targets. +-- * Escorts scanning targets in advance. +-- * Escorts attacking specific targets. +-- * Request assistance from other groups for attack. +-- * Manage rule of engagement of escorts. +-- * Manage the allowed evasion techniques of escorts. +-- * Make escort to execute a defined mission or path. +-- * Escort tactical situation reporting. +-- +-- === +-- +-- ## Missions: +-- +-- [ESC - Escorting](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/ESC%20-%20Escorting) +-- +-- === +-- +-- Allows you to interact with escorting AI on your flight and take the lead. +-- +-- Each escorting group can be commanded with a whole set of radio commands (radio menu in your flight, and then F10). +-- +-- The radio commands will vary according the category of the group. The richest set of commands are with Helicopters and AirPlanes. +-- Ships and Ground troops will have a more limited set, but they can provide support through the bombing of targets designated by the other escorts. +-- +-- # RADIO MENUs that can be created: +-- +-- Find a summary below of the current available commands: +-- +-- ## Navigation ...: +-- +-- Escort group navigation functions: +-- +-- * **"Join-Up and Follow at x meters":** The escort group fill follow you at about x meters, and they will follow you. +-- * **"Flare":** Provides menu commands to let the escort group shoot a flare in the air in a color. +-- * **"Smoke":** Provides menu commands to let the escort group smoke the air in a color. Note that smoking is only available for ground and naval troops. +-- +-- ## Hold position ...: +-- +-- Escort group navigation functions: +-- +-- * **"At current location":** Stops the escort group and they will hover 30 meters above the ground at the position they stopped. +-- * **"At client location":** Stops the escort group and they will hover 30 meters above the ground at the position they stopped. +-- +-- ## Report targets ...: +-- +-- Report targets will make the escort group to report any target that it identifies within a 8km range. Any detected target can be attacked using the 4. Attack nearby targets function. (see below). +-- +-- * **"Report now":** Will report the current detected targets. +-- * **"Report targets on":** Will make the escort group to report detected targets and will fill the "Attack nearby targets" menu list. +-- * **"Report targets off":** Will stop detecting targets. +-- +-- ## Scan targets ...: +-- +-- Menu items to pop-up the escort group for target scanning. After scanning, the escort group will resume with the mission or defined task. +-- +-- * **"Scan targets 30 seconds":** Scan 30 seconds for targets. +-- * **"Scan targets 60 seconds":** Scan 60 seconds for targets. +-- +-- ## Attack targets ...: +-- +-- This menu item will list all detected targets within a 15km range. Depending on the level of detection (known/unknown) and visuality, the targets type will also be listed. +-- +-- ## Request assistance from ...: +-- +-- This menu item will list all detected targets within a 15km range, as with the menu item **Attack Targets**. +-- This menu item allows to request attack support from other escorts supporting the current client group. +-- eg. the function allows a player to request support from the Ship escort to attack a target identified by the Plane escort with its Tomahawk missiles. +-- eg. the function allows a player to request support from other Planes escorting to bomb the unit with illumination missiles or bombs, so that the main plane escort can attack the area. +-- +-- ## ROE ...: +-- +-- Sets the Rules of Engagement (ROE) of the escort group when in flight. +-- +-- * **"Hold Fire":** The escort group will hold fire. +-- * **"Return Fire":** The escort group will return fire. +-- * **"Open Fire":** The escort group will open fire on designated targets. +-- * **"Weapon Free":** The escort group will engage with any target. +-- +-- ## Evasion ...: +-- +-- Will define the evasion techniques that the escort group will perform during flight or combat. +-- +-- * **"Fight until death":** The escort group will have no reaction to threats. +-- * **"Use flares, chaff and jammers":** The escort group will use passive defense using flares and jammers. No evasive manoeuvres are executed. +-- * **"Evade enemy fire":** The rescort group will evade enemy fire before firing. +-- * **"Go below radar and evade fire":** The escort group will perform evasive vertical manoeuvres. +-- +-- ## Resume Mission ...: +-- +-- Escort groups can have their own mission. This menu item will allow the escort group to resume their Mission from a given waypoint. +-- Note that this is really fantastic, as you now have the dynamic of taking control of the escort groups, and allowing them to resume their path or mission. +-- +-- === +-- +-- ### Authors: **FlightControl** +-- +-- === +-- +-- @module Functional.Escort +-- @image Escorting.JPG + + + +--- @type ESCORT +-- @extends Core.Base#BASE +-- @field Wrapper.Client#CLIENT EscortClient +-- @field Wrapper.Group#GROUP EscortGroup +-- @field #string EscortName +-- @field #ESCORT.MODE EscortMode The mode the escort is in. +-- @field Core.Scheduler#SCHEDULER FollowScheduler The instance of the SCHEDULER class. +-- @field #number FollowDistance The current follow distance. +-- @field #boolean ReportTargets If true, nearby targets are reported. +-- @Field DCS#AI.Option.Air.val.ROE OptionROE Which ROE is set to the EscortGroup. +-- @field DCS#AI.Option.Air.val.REACTION_ON_THREAT OptionReactionOnThreat Which REACTION_ON_THREAT is set to the EscortGroup. +-- @field FunctionalMENU_GROUPDETECTION_BASE Detection + +--- ESCORT class +-- +-- # ESCORT construction methods. +-- +-- Create a new SPAWN object with the @{#ESCORT.New} method: +-- +-- * @{#ESCORT.New}: Creates a new ESCORT object from a @{Wrapper.Group#GROUP} for a @{Wrapper.Client#CLIENT}, with an optional briefing text. +-- +-- @usage +-- -- Declare a new EscortPlanes object as follows: +-- +-- -- First find the GROUP object and the CLIENT object. +-- local EscortClient = CLIENT:FindByName( "Unit Name" ) -- The Unit Name is the name of the unit flagged with the skill Client in the mission editor. +-- local EscortGroup = GROUP:FindByName( "Group Name" ) -- The Group Name is the name of the group that will escort the Escort Client. +-- +-- -- Now use these 2 objects to construct the new EscortPlanes object. +-- EscortPlanes = ESCORT:New( EscortClient, EscortGroup, "Desert", "Welcome to the mission. You are escorted by a plane with code name 'Desert', which can be instructed through the F10 radio menu." ) +-- +-- @field #ESCORT +ESCORT = { + ClassName = "ESCORT", + EscortName = nil, -- The Escort Name + EscortClient = nil, + EscortGroup = nil, + EscortMode = 1, + MODE = { + FOLLOW = 1, + MISSION = 2, + }, + Targets = {}, -- The identified targets + FollowScheduler = nil, + ReportTargets = true, + OptionROE = AI.Option.Air.val.ROE.OPEN_FIRE, + OptionReactionOnThreat = AI.Option.Air.val.REACTION_ON_THREAT.ALLOW_ABORT_MISSION, + SmokeDirectionVector = false, + TaskPoints = {} +} + +--- ESCORT.Mode class +-- @type ESCORT.MODE +-- @field #number FOLLOW +-- @field #number MISSION + +--- MENUPARAM type +-- @type MENUPARAM +-- @field #ESCORT ParamSelf +-- @field #Distance ParamDistance +-- @field #function ParamFunction +-- @field #string ParamMessage + +--- ESCORT class constructor for an AI group +-- @param #ESCORT self +-- @param Wrapper.Client#CLIENT EscortClient The client escorted by the EscortGroup. +-- @param Wrapper.Group#GROUP EscortGroup The group AI escorting the EscortClient. +-- @param #string EscortName Name of the escort. +-- @param #string EscortBriefing A text showing the ESCORT briefing to the player. Note that if no EscortBriefing is provided, the default briefing will be shown. +-- @return #ESCORT self +-- @usage +-- -- Declare a new EscortPlanes object as follows: +-- +-- -- First find the GROUP object and the CLIENT object. +-- local EscortClient = CLIENT:FindByName( "Unit Name" ) -- The Unit Name is the name of the unit flagged with the skill Client in the mission editor. +-- local EscortGroup = GROUP:FindByName( "Group Name" ) -- The Group Name is the name of the group that will escort the Escort Client. +-- +-- -- Now use these 2 objects to construct the new EscortPlanes object. +-- EscortPlanes = ESCORT:New( EscortClient, EscortGroup, "Desert", "Welcome to the mission. You are escorted by a plane with code name 'Desert', which can be instructed through the F10 radio menu." ) +function ESCORT:New( EscortClient, EscortGroup, EscortName, EscortBriefing ) + + local self = BASE:Inherit( self, BASE:New() ) -- #ESCORT + self:F( { EscortClient, EscortGroup, EscortName } ) + + self.EscortClient = EscortClient -- Wrapper.Client#CLIENT + self.EscortGroup = EscortGroup -- Wrapper.Group#GROUP + self.EscortName = EscortName + self.EscortBriefing = EscortBriefing + + self.EscortSetGroup = SET_GROUP:New() + self.EscortSetGroup:AddObject( self.EscortGroup ) + self.EscortSetGroup:Flush() + self.Detection = DETECTION_UNITS:New( self.EscortSetGroup, 15000 ) + + self.EscortGroup.Detection = self.Detection + + -- Set EscortGroup known at EscortClient. + if not self.EscortClient._EscortGroups then + self.EscortClient._EscortGroups = {} + end + + if not self.EscortClient._EscortGroups[EscortGroup:GetName()] then + self.EscortClient._EscortGroups[EscortGroup:GetName()] = {} + self.EscortClient._EscortGroups[EscortGroup:GetName()].EscortGroup = self.EscortGroup + self.EscortClient._EscortGroups[EscortGroup:GetName()].EscortName = self.EscortName + self.EscortClient._EscortGroups[EscortGroup:GetName()].Detection = self.EscortGroup.Detection + end + + self.EscortMenu = MENU_GROUP:New( self.EscortClient:GetGroup(), self.EscortName ) + + self.EscortGroup:WayPointInitialize(1) + + self.EscortGroup:OptionROTVertical() + self.EscortGroup:OptionROEOpenFire() + + if not EscortBriefing then + EscortGroup:MessageToClient( EscortGroup:GetCategoryName() .. " '" .. EscortName .. "' (" .. EscortGroup:GetCallsign() .. ") reporting! " .. + "We're escorting your flight. " .. + "Use the Radio Menu and F10 and use the options under + " .. EscortName .. "\n", + 60, EscortClient + ) + else + EscortGroup:MessageToClient( EscortGroup:GetCategoryName() .. " '" .. EscortName .. "' (" .. EscortGroup:GetCallsign() .. ") " .. EscortBriefing, + 60, EscortClient + ) + end + + self.FollowDistance = 100 + self.CT1 = 0 + self.GT1 = 0 + + self.FollowScheduler, self.FollowSchedule = SCHEDULER:New( self, self._FollowScheduler, {}, 1, .5, .01 ) + self.FollowScheduler:Stop( self.FollowSchedule ) + + self.EscortMode = ESCORT.MODE.MISSION + + + return self +end + +--- Set a Detection method for the EscortClient to be reported upon. +-- Detection methods are based on the derived classes from DETECTION_BASE. +-- @param #ESCORT self +-- @param Function.Detection#DETECTION_BASE Detection +function ESCORT:SetDetection( Detection ) + + self.Detection = Detection + self.EscortGroup.Detection = self.Detection + self.EscortClient._EscortGroups[self.EscortGroup:GetName()].Detection = self.EscortGroup.Detection + + Detection:__Start( 1 ) + +end + +--- This function is for test, it will put on the frequency of the FollowScheduler a red smoke at the direction vector calculated for the escort to fly to. +-- This allows to visualize where the escort is flying to. +-- @param #ESCORT self +-- @param #boolean SmokeDirection If true, then the direction vector will be smoked. +function ESCORT:TestSmokeDirectionVector( SmokeDirection ) + self.SmokeDirectionVector = ( SmokeDirection == true ) and true or false +end + + +--- Defines the default menus +-- @param #ESCORT self +-- @return #ESCORT +function ESCORT:Menus() + self:F() + + self:MenuFollowAt( 100 ) + self:MenuFollowAt( 200 ) + self:MenuFollowAt( 300 ) + self:MenuFollowAt( 400 ) + + self:MenuScanForTargets( 100, 60 ) + + self:MenuHoldAtEscortPosition( 30 ) + self:MenuHoldAtLeaderPosition( 30 ) + + self:MenuFlare() + self:MenuSmoke() + + self:MenuReportTargets( 60 ) + self:MenuAssistedAttack() + self:MenuROE() + self:MenuEvasion() + self:MenuResumeMission() + + + return self +end + + + +--- Defines a menu slot to let the escort Join and Follow you at a certain distance. +-- This menu will appear under **Navigation**. +-- @param #ESCORT self +-- @param DCS#Distance Distance The distance in meters that the escort needs to follow the client. +-- @return #ESCORT +function ESCORT:MenuFollowAt( Distance ) + self:F(Distance) + + if self.EscortGroup:IsAir() then + if not self.EscortMenuReportNavigation then + self.EscortMenuReportNavigation = MENU_GROUP:New( self.EscortClient:GetGroup(), "Navigation", self.EscortMenu ) + end + + if not self.EscortMenuJoinUpAndFollow then + self.EscortMenuJoinUpAndFollow = {} + end + + self.EscortMenuJoinUpAndFollow[#self.EscortMenuJoinUpAndFollow+1] = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Join-Up and Follow at " .. Distance, self.EscortMenuReportNavigation, ESCORT._JoinUpAndFollow, self, Distance ) + + self.EscortMode = ESCORT.MODE.FOLLOW + end + + return self +end + +--- Defines a menu slot to let the escort hold at their current position and stay low with a specified height during a specified time in seconds. +-- This menu will appear under **Hold position**. +-- @param #ESCORT self +-- @param DCS#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. +-- @param DCS#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. +-- @param #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed. +-- @return #ESCORT +-- TODO: Implement Seconds parameter. Challenge is to first develop the "continue from last activity" function. +function ESCORT:MenuHoldAtEscortPosition( Height, Seconds, MenuTextFormat ) + self:F( { Height, Seconds, MenuTextFormat } ) + + if self.EscortGroup:IsAir() then + + if not self.EscortMenuHold then + self.EscortMenuHold = MENU_GROUP:New( self.EscortClient:GetGroup(), "Hold position", self.EscortMenu ) + end + + if not Height then + Height = 30 + end + + if not Seconds then + Seconds = 0 + end + + local MenuText = "" + if not MenuTextFormat then + if Seconds == 0 then + MenuText = string.format( "Hold at %d meter", Height ) + else + MenuText = string.format( "Hold at %d meter for %d seconds", Height, Seconds ) + end + else + if Seconds == 0 then + MenuText = string.format( MenuTextFormat, Height ) + else + MenuText = string.format( MenuTextFormat, Height, Seconds ) + end + end + + if not self.EscortMenuHoldPosition then + self.EscortMenuHoldPosition = {} + end + + self.EscortMenuHoldPosition[#self.EscortMenuHoldPosition+1] = MENU_GROUP_COMMAND + :New( + self.EscortClient:GetGroup(), + MenuText, + self.EscortMenuHold, + ESCORT._HoldPosition, + self, + self.EscortGroup, + Height, + Seconds + ) + end + + return self +end + + +--- Defines a menu slot to let the escort hold at the client position and stay low with a specified height during a specified time in seconds. +-- This menu will appear under **Navigation**. +-- @param #ESCORT self +-- @param DCS#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. +-- @param DCS#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. +-- @param #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain one or two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed. +-- @return #ESCORT +-- TODO: Implement Seconds parameter. Challenge is to first develop the "continue from last activity" function. +function ESCORT:MenuHoldAtLeaderPosition( Height, Seconds, MenuTextFormat ) + self:F( { Height, Seconds, MenuTextFormat } ) + + if self.EscortGroup:IsAir() then + + if not self.EscortMenuHold then + self.EscortMenuHold = MENU_GROUP:New( self.EscortClient:GetGroup(), "Hold position", self.EscortMenu ) + end + + if not Height then + Height = 30 + end + + if not Seconds then + Seconds = 0 + end + + local MenuText = "" + if not MenuTextFormat then + if Seconds == 0 then + MenuText = string.format( "Rejoin and hold at %d meter", Height ) + else + MenuText = string.format( "Rejoin and hold at %d meter for %d seconds", Height, Seconds ) + end + else + if Seconds == 0 then + MenuText = string.format( MenuTextFormat, Height ) + else + MenuText = string.format( MenuTextFormat, Height, Seconds ) + end + end + + if not self.EscortMenuHoldAtLeaderPosition then + self.EscortMenuHoldAtLeaderPosition = {} + end + + self.EscortMenuHoldAtLeaderPosition[#self.EscortMenuHoldAtLeaderPosition+1] = MENU_GROUP_COMMAND + :New( + self.EscortClient:GetGroup(), + MenuText, + self.EscortMenuHold, + ESCORT._HoldPosition, + { ParamSelf = self, + ParamOrbitGroup = self.EscortClient, + ParamHeight = Height, + ParamSeconds = Seconds + } + ) + end + + return self +end + +--- Defines a menu slot to let the escort scan for targets at a certain height for a certain time in seconds. +-- This menu will appear under **Scan targets**. +-- @param #ESCORT self +-- @param DCS#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. +-- @param DCS#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. +-- @param #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain one or two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed. +-- @return #ESCORT +function ESCORT:MenuScanForTargets( Height, Seconds, MenuTextFormat ) + self:F( { Height, Seconds, MenuTextFormat } ) + + if self.EscortGroup:IsAir() then + if not self.EscortMenuScan then + self.EscortMenuScan = MENU_GROUP:New( self.EscortClient:GetGroup(), "Scan for targets", self.EscortMenu ) + end + + if not Height then + Height = 100 + end + + if not Seconds then + Seconds = 30 + end + + local MenuText = "" + if not MenuTextFormat then + if Seconds == 0 then + MenuText = string.format( "At %d meter", Height ) + else + MenuText = string.format( "At %d meter for %d seconds", Height, Seconds ) + end + else + if Seconds == 0 then + MenuText = string.format( MenuTextFormat, Height ) + else + MenuText = string.format( MenuTextFormat, Height, Seconds ) + end + end + + if not self.EscortMenuScanForTargets then + self.EscortMenuScanForTargets = {} + end + + self.EscortMenuScanForTargets[#self.EscortMenuScanForTargets+1] = MENU_GROUP_COMMAND + :New( + self.EscortClient:GetGroup(), + MenuText, + self.EscortMenuScan, + ESCORT._ScanTargets, + self, + 30 + ) + end + + return self +end + + + +--- Defines a menu slot to let the escort disperse a flare in a certain color. +-- This menu will appear under **Navigation**. +-- The flare will be fired from the first unit in the group. +-- @param #ESCORT self +-- @param #string MenuTextFormat Optional parameter that shows the menu option text. If no text is given, the default text will be displayed. +-- @return #ESCORT +function ESCORT:MenuFlare( MenuTextFormat ) + self:F() + + if not self.EscortMenuReportNavigation then + self.EscortMenuReportNavigation = MENU_GROUP:New( self.EscortClient:GetGroup(), "Navigation", self.EscortMenu ) + end + + local MenuText = "" + if not MenuTextFormat then + MenuText = "Flare" + else + MenuText = MenuTextFormat + end + + if not self.EscortMenuFlare then + self.EscortMenuFlare = MENU_GROUP:New( self.EscortClient:GetGroup(), MenuText, self.EscortMenuReportNavigation, ESCORT._Flare, self ) + self.EscortMenuFlareGreen = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Release green flare", self.EscortMenuFlare, ESCORT._Flare, self, FLARECOLOR.Green, "Released a green flare!" ) + self.EscortMenuFlareRed = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Release red flare", self.EscortMenuFlare, ESCORT._Flare, self, FLARECOLOR.Red, "Released a red flare!" ) + self.EscortMenuFlareWhite = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Release white flare", self.EscortMenuFlare, ESCORT._Flare, self, FLARECOLOR.White, "Released a white flare!" ) + self.EscortMenuFlareYellow = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Release yellow flare", self.EscortMenuFlare, ESCORT._Flare, self, FLARECOLOR.Yellow, "Released a yellow flare!" ) + end + + return self +end + +--- Defines a menu slot to let the escort disperse a smoke in a certain color. +-- This menu will appear under **Navigation**. +-- Note that smoke menu options will only be displayed for ships and ground units. Not for air units. +-- The smoke will be fired from the first unit in the group. +-- @param #ESCORT self +-- @param #string MenuTextFormat Optional parameter that shows the menu option text. If no text is given, the default text will be displayed. +-- @return #ESCORT +function ESCORT:MenuSmoke( MenuTextFormat ) + self:F() + + if not self.EscortGroup:IsAir() then + if not self.EscortMenuReportNavigation then + self.EscortMenuReportNavigation = MENU_GROUP:New( self.EscortClient:GetGroup(), "Navigation", self.EscortMenu ) + end + + local MenuText = "" + if not MenuTextFormat then + MenuText = "Smoke" + else + MenuText = MenuTextFormat + end + + if not self.EscortMenuSmoke then + self.EscortMenuSmoke = MENU_GROUP:New( self.EscortClient:GetGroup(), "Smoke", self.EscortMenuReportNavigation, ESCORT._Smoke, self ) + self.EscortMenuSmokeGreen = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Release green smoke", self.EscortMenuSmoke, ESCORT._Smoke, self, SMOKECOLOR.Green, "Releasing green smoke!" ) + self.EscortMenuSmokeRed = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Release red smoke", self.EscortMenuSmoke, ESCORT._Smoke, self, SMOKECOLOR.Red, "Releasing red smoke!" ) + self.EscortMenuSmokeWhite = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Release white smoke", self.EscortMenuSmoke, ESCORT._Smoke, self, SMOKECOLOR.White, "Releasing white smoke!" ) + self.EscortMenuSmokeOrange = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Release orange smoke", self.EscortMenuSmoke, ESCORT._Smoke, self, SMOKECOLOR.Orange, "Releasing orange smoke!" ) + self.EscortMenuSmokeBlue = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Release blue smoke", self.EscortMenuSmoke, ESCORT._Smoke, self, SMOKECOLOR.Blue, "Releasing blue smoke!" ) + end + end + + return self +end + +--- Defines a menu slot to let the escort report their current detected targets with a specified time interval in seconds. +-- This menu will appear under **Report targets**. +-- Note that if a report targets menu is not specified, no targets will be detected by the escort, and the attack and assisted attack menus will not be displayed. +-- @param #ESCORT self +-- @param DCS#Time Seconds Optional parameter that lets the escort report their current detected targets after specified time interval in seconds. The default time is 30 seconds. +-- @return #ESCORT +function ESCORT:MenuReportTargets( Seconds ) + self:F( { Seconds } ) + + if not self.EscortMenuReportNearbyTargets then + self.EscortMenuReportNearbyTargets = MENU_GROUP:New( self.EscortClient:GetGroup(), "Report targets", self.EscortMenu ) + end + + if not Seconds then + Seconds = 30 + end + + -- Report Targets + self.EscortMenuReportNearbyTargetsNow = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Report targets now!", self.EscortMenuReportNearbyTargets, ESCORT._ReportNearbyTargetsNow, self ) + self.EscortMenuReportNearbyTargetsOn = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Report targets on", self.EscortMenuReportNearbyTargets, ESCORT._SwitchReportNearbyTargets, self, true ) + self.EscortMenuReportNearbyTargetsOff = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Report targets off", self.EscortMenuReportNearbyTargets, ESCORT._SwitchReportNearbyTargets, self, false ) + + -- Attack Targets + self.EscortMenuAttackNearbyTargets = MENU_GROUP:New( self.EscortClient:GetGroup(), "Attack targets", self.EscortMenu ) + + + self.ReportTargetsScheduler = SCHEDULER:New( self, self._ReportTargetsScheduler, {}, 1, Seconds ) + + return self +end + +--- Defines a menu slot to let the escort attack its detected targets using assisted attack from another escort joined also with the client. +-- This menu will appear under **Request assistance from**. +-- Note that this method needs to be preceded with the method MenuReportTargets. +-- @param #ESCORT self +-- @return #ESCORT +function ESCORT:MenuAssistedAttack() + self:F() + + -- Request assistance from other escorts. + -- This is very useful to let f.e. an escorting ship attack a target detected by an escorting plane... + self.EscortMenuTargetAssistance = MENU_GROUP:New( self.EscortClient:GetGroup(), "Request assistance from", self.EscortMenu ) + + return self +end + +--- Defines a menu to let the escort set its rules of engagement. +-- All rules of engagement will appear under the menu **ROE**. +-- @param #ESCORT self +-- @return #ESCORT +function ESCORT:MenuROE( MenuTextFormat ) + self:F( MenuTextFormat ) + + if not self.EscortMenuROE then + -- Rules of Engagement + self.EscortMenuROE = MENU_GROUP:New( self.EscortClient:GetGroup(), "ROE", self.EscortMenu ) + if self.EscortGroup:OptionROEHoldFirePossible() then + self.EscortMenuROEHoldFire = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Hold Fire", self.EscortMenuROE, ESCORT._ROE, self, self.EscortGroup:OptionROEHoldFire(), "Holding weapons!" ) + end + if self.EscortGroup:OptionROEReturnFirePossible() then + self.EscortMenuROEReturnFire = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Return Fire", self.EscortMenuROE, ESCORT._ROE, self, self.EscortGroup:OptionROEReturnFire(), "Returning fire!" ) + end + if self.EscortGroup:OptionROEOpenFirePossible() then + self.EscortMenuROEOpenFire = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Open Fire", self.EscortMenuROE, ESCORT._ROE, self, self.EscortGroup:OptionROEOpenFire(), "Opening fire on designated targets!!" ) + end + if self.EscortGroup:OptionROEWeaponFreePossible() then + self.EscortMenuROEWeaponFree = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Weapon Free", self.EscortMenuROE, ESCORT._ROE, self, self.EscortGroup:OptionROEWeaponFree(), "Opening fire on targets of opportunity!" ) + end + end + + return self +end + + +--- Defines a menu to let the escort set its evasion when under threat. +-- All rules of engagement will appear under the menu **Evasion**. +-- @param #ESCORT self +-- @return #ESCORT +function ESCORT:MenuEvasion( MenuTextFormat ) + self:F( MenuTextFormat ) + + if self.EscortGroup:IsAir() then + if not self.EscortMenuEvasion then + -- Reaction to Threats + self.EscortMenuEvasion = MENU_GROUP:New( self.EscortClient:GetGroup(), "Evasion", self.EscortMenu ) + if self.EscortGroup:OptionROTNoReactionPossible() then + self.EscortMenuEvasionNoReaction = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Fight until death", self.EscortMenuEvasion, ESCORT._ROT, self, self.EscortGroup:OptionROTNoReaction(), "Fighting until death!" ) + end + if self.EscortGroup:OptionROTPassiveDefensePossible() then + self.EscortMenuEvasionPassiveDefense = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Use flares, chaff and jammers", self.EscortMenuEvasion, ESCORT._ROT, self, self.EscortGroup:OptionROTPassiveDefense(), "Defending using jammers, chaff and flares!" ) + end + if self.EscortGroup:OptionROTEvadeFirePossible() then + self.EscortMenuEvasionEvadeFire = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Evade enemy fire", self.EscortMenuEvasion, ESCORT._ROT, self, self.EscortGroup:OptionROTEvadeFire(), "Evading on enemy fire!" ) + end + if self.EscortGroup:OptionROTVerticalPossible() then + self.EscortMenuOptionEvasionVertical = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Go below radar and evade fire", self.EscortMenuEvasion, ESCORT._ROT, self, self.EscortGroup:OptionROTVertical(), "Evading on enemy fire with vertical manoeuvres!" ) + end + end + end + + return self +end + +--- Defines a menu to let the escort resume its mission from a waypoint on its route. +-- All rules of engagement will appear under the menu **Resume mission from**. +-- @param #ESCORT self +-- @return #ESCORT +function ESCORT:MenuResumeMission() + self:F() + + if not self.EscortMenuResumeMission then + -- Mission Resume Menu Root + self.EscortMenuResumeMission = MENU_GROUP:New( self.EscortClient:GetGroup(), "Resume mission from", self.EscortMenu ) + end + + return self +end + + +--- @param #MENUPARAM MenuParam +function ESCORT:_HoldPosition( OrbitGroup, OrbitHeight, OrbitSeconds ) + + local EscortGroup = self.EscortGroup + local EscortClient = self.EscortClient + + local OrbitUnit = OrbitGroup:GetUnit(1) -- Wrapper.Unit#UNIT + + self.FollowScheduler:Stop( self.FollowSchedule ) + + local PointFrom = {} + local GroupVec3 = EscortGroup:GetUnit(1):GetVec3() + PointFrom = {} + PointFrom.x = GroupVec3.x + PointFrom.y = GroupVec3.z + PointFrom.speed = 250 + PointFrom.type = AI.Task.WaypointType.TURNING_POINT + PointFrom.alt = GroupVec3.y + PointFrom.alt_type = AI.Task.AltitudeType.BARO + + local OrbitPoint = OrbitUnit:GetVec2() + local PointTo = {} + PointTo.x = OrbitPoint.x + PointTo.y = OrbitPoint.y + PointTo.speed = 250 + PointTo.type = AI.Task.WaypointType.TURNING_POINT + PointTo.alt = OrbitHeight + PointTo.alt_type = AI.Task.AltitudeType.BARO + PointTo.task = EscortGroup:TaskOrbitCircleAtVec2( OrbitPoint, OrbitHeight, 0 ) + + local Points = { PointFrom, PointTo } + + EscortGroup:OptionROEHoldFire() + EscortGroup:OptionROTPassiveDefense() + + EscortGroup:SetTask( EscortGroup:TaskRoute( Points ) ) + EscortGroup:MessageToClient( "Orbiting at location.", 10, EscortClient ) + +end + +--- @param #MENUPARAM MenuParam +function ESCORT:_JoinUpAndFollow( Distance ) + + local EscortGroup = self.EscortGroup + local EscortClient = self.EscortClient + + self.Distance = Distance + + self:JoinUpAndFollow( EscortGroup, EscortClient, self.Distance ) +end + +--- JoinsUp and Follows a CLIENT. +-- @param Functional.Escort#ESCORT self +-- @param Wrapper.Group#GROUP EscortGroup +-- @param Wrapper.Client#CLIENT EscortClient +-- @param DCS#Distance Distance +function ESCORT:JoinUpAndFollow( EscortGroup, EscortClient, Distance ) + self:F( { EscortGroup, EscortClient, Distance } ) + + self.FollowScheduler:Stop( self.FollowSchedule ) + + EscortGroup:OptionROEHoldFire() + EscortGroup:OptionROTPassiveDefense() + + self.EscortMode = ESCORT.MODE.FOLLOW + + self.CT1 = 0 + self.GT1 = 0 + self.FollowScheduler:Start( self.FollowSchedule ) + + EscortGroup:MessageToClient( "Rejoining and Following at " .. Distance .. "!", 30, EscortClient ) +end + +--- @param #MENUPARAM MenuParam +function ESCORT:_Flare( Color, Message ) + + local EscortGroup = self.EscortGroup + local EscortClient = self.EscortClient + + EscortGroup:GetUnit(1):Flare( Color ) + EscortGroup:MessageToClient( Message, 10, EscortClient ) +end + +--- @param #MENUPARAM MenuParam +function ESCORT:_Smoke( Color, Message ) + + local EscortGroup = self.EscortGroup + local EscortClient = self.EscortClient + + EscortGroup:GetUnit(1):Smoke( Color ) + EscortGroup:MessageToClient( Message, 10, EscortClient ) +end + + +--- @param #MENUPARAM MenuParam +function ESCORT:_ReportNearbyTargetsNow() + + local EscortGroup = self.EscortGroup + local EscortClient = self.EscortClient + + self:_ReportTargetsScheduler() + +end + +function ESCORT:_SwitchReportNearbyTargets( ReportTargets ) + + local EscortGroup = self.EscortGroup + local EscortClient = self.EscortClient + + self.ReportTargets = ReportTargets + + if self.ReportTargets then + if not self.ReportTargetsScheduler then + self.ReportTargetsScheduler:Schedule( self, self._ReportTargetsScheduler, {}, 1, 30 ) + end + else + routines.removeFunction( self.ReportTargetsScheduler ) + self.ReportTargetsScheduler = nil + end +end + +--- @param #MENUPARAM MenuParam +function ESCORT:_ScanTargets( ScanDuration ) + + local EscortGroup = self.EscortGroup -- Wrapper.Group#GROUP + local EscortClient = self.EscortClient + + self.FollowScheduler:Stop( self.FollowSchedule ) + + if EscortGroup:IsHelicopter() then + EscortGroup:PushTask( + EscortGroup:TaskControlled( + EscortGroup:TaskOrbitCircle( 200, 20 ), + EscortGroup:TaskCondition( nil, nil, nil, nil, ScanDuration, nil ) + ), 1 ) + elseif EscortGroup:IsAirPlane() then + EscortGroup:PushTask( + EscortGroup:TaskControlled( + EscortGroup:TaskOrbitCircle( 1000, 500 ), + EscortGroup:TaskCondition( nil, nil, nil, nil, ScanDuration, nil ) + ), 1 ) + end + + EscortGroup:MessageToClient( "Scanning targets for " .. ScanDuration .. " seconds.", ScanDuration, EscortClient ) + + if self.EscortMode == ESCORT.MODE.FOLLOW then + self.FollowScheduler:Start( self.FollowSchedule ) + end + +end + +--- @param Wrapper.Group#GROUP EscortGroup +function _Resume( EscortGroup ) + env.info( '_Resume' ) + + local Escort = EscortGroup:GetState( EscortGroup, "Escort" ) + env.info( "EscortMode = " .. Escort.EscortMode ) + if Escort.EscortMode == ESCORT.MODE.FOLLOW then + Escort:JoinUpAndFollow( EscortGroup, Escort.EscortClient, Escort.Distance ) + end + +end + +--- @param #ESCORT self +-- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem +function ESCORT:_AttackTarget( DetectedItem ) + + local EscortGroup = self.EscortGroup -- Wrapper.Group#GROUP + self:F( EscortGroup ) + + local EscortClient = self.EscortClient + + self.FollowScheduler:Stop( self.FollowSchedule ) + + if EscortGroup:IsAir() then + EscortGroup:OptionROEOpenFire() + EscortGroup:OptionROTPassiveDefense() + EscortGroup:SetState( EscortGroup, "Escort", self ) + + local DetectedSet = self.Detection:GetDetectedItemSet( DetectedItem ) + + local Tasks = {} + + DetectedSet:ForEachUnit( + --- @param Wrapper.Unit#UNIT DetectedUnit + function( DetectedUnit, Tasks ) + if DetectedUnit:IsAlive() then + Tasks[#Tasks+1] = EscortGroup:TaskAttackUnit( DetectedUnit ) + end + end, Tasks + ) + + Tasks[#Tasks+1] = EscortGroup:TaskFunction( "_Resume", { "''" } ) + + EscortGroup:SetTask( + EscortGroup:TaskCombo( + Tasks + ), 1 + ) + + else + + local DetectedSet = self.Detection:GetDetectedItemSet( DetectedItem ) + + local Tasks = {} + + DetectedSet:ForEachUnit( + --- @param Wrapper.Unit#UNIT DetectedUnit + function( DetectedUnit, Tasks ) + if DetectedUnit:IsAlive() then + Tasks[#Tasks+1] = EscortGroup:TaskFireAtPoint( DetectedUnit:GetVec2(), 50 ) + end + end, Tasks + ) + + EscortGroup:SetTask( + EscortGroup:TaskCombo( + Tasks + ), 1 + ) + + end + + EscortGroup:MessageToClient( "Engaging Designated Unit!", 10, EscortClient ) + +end + +--- +--- @param #ESCORT self +-- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem +function ESCORT:_AssistTarget( EscortGroupAttack, DetectedItem ) + + local EscortGroup = self.EscortGroup + local EscortClient = self.EscortClient + + self.FollowScheduler:Stop( self.FollowSchedule ) + + if EscortGroupAttack:IsAir() then + EscortGroupAttack:OptionROEOpenFire() + EscortGroupAttack:OptionROTVertical() + + local DetectedSet = self.Detection:GetDetectedItemSet( DetectedItem ) + + local Tasks = {} + + DetectedSet:ForEachUnit( + --- @param Wrapper.Unit#UNIT DetectedUnit + function( DetectedUnit, Tasks ) + if DetectedUnit:IsAlive() then + Tasks[#Tasks+1] = EscortGroupAttack:TaskAttackUnit( DetectedUnit ) + end + end, Tasks + ) + + Tasks[#Tasks+1] = EscortGroupAttack:TaskOrbitCircle( 500, 350 ) + + EscortGroupAttack:SetTask( + EscortGroupAttack:TaskCombo( + Tasks + ), 1 + ) + + else + local DetectedSet = self.Detection:GetDetectedItemSet( DetectedItem ) + + local Tasks = {} + + DetectedSet:ForEachUnit( + --- @param Wrapper.Unit#UNIT DetectedUnit + function( DetectedUnit, Tasks ) + if DetectedUnit:IsAlive() then + Tasks[#Tasks+1] = EscortGroupAttack:TaskFireAtPoint( DetectedUnit:GetVec2(), 50 ) + end + end, Tasks + ) + + EscortGroupAttack:SetTask( + EscortGroupAttack:TaskCombo( + Tasks + ), 1 + ) + + end + + EscortGroupAttack:MessageToClient( "Assisting with the destroying the enemy unit!", 10, EscortClient ) + +end + +--- @param #MENUPARAM MenuParam +function ESCORT:_ROE( EscortROEFunction, EscortROEMessage ) + + local EscortGroup = self.EscortGroup + local EscortClient = self.EscortClient + + pcall( function() EscortROEFunction() end ) + EscortGroup:MessageToClient( EscortROEMessage, 10, EscortClient ) +end + +--- @param #MENUPARAM MenuParam +function ESCORT:_ROT( EscortROTFunction, EscortROTMessage ) + + local EscortGroup = self.EscortGroup + local EscortClient = self.EscortClient + + pcall( function() EscortROTFunction() end ) + EscortGroup:MessageToClient( EscortROTMessage, 10, EscortClient ) +end + +--- @param #MENUPARAM MenuParam +function ESCORT:_ResumeMission( WayPoint ) + + local EscortGroup = self.EscortGroup + local EscortClient = self.EscortClient + + self.FollowScheduler:Stop( self.FollowSchedule ) + + local WayPoints = EscortGroup:GetTaskRoute() + self:T( WayPoint, WayPoints ) + + for WayPointIgnore = 1, WayPoint do + table.remove( WayPoints, 1 ) + end + + SCHEDULER:New( EscortGroup, EscortGroup.SetTask, { EscortGroup:TaskRoute( WayPoints ) }, 1 ) + + EscortGroup:MessageToClient( "Resuming mission from waypoint " .. WayPoint .. ".", 10, EscortClient ) +end + +--- Registers the waypoints +-- @param #ESCORT self +-- @return #table +function ESCORT:RegisterRoute() + self:F() + + local EscortGroup = self.EscortGroup -- Wrapper.Group#GROUP + + local TaskPoints = EscortGroup:GetTaskRoute() + + self:T( TaskPoints ) + + return TaskPoints +end + +--- @param Functional.Escort#ESCORT self +function ESCORT:_FollowScheduler() + self:F( { self.FollowDistance } ) + + self:T( {self.EscortClient.UnitName, self.EscortGroup.GroupName } ) + if self.EscortGroup:IsAlive() and self.EscortClient:IsAlive() then + + local ClientUnit = self.EscortClient:GetClientGroupUnit() + local GroupUnit = self.EscortGroup:GetUnit( 1 ) + local FollowDistance = self.FollowDistance + + self:T( {ClientUnit.UnitName, GroupUnit.UnitName } ) + + if self.CT1 == 0 and self.GT1 == 0 then + self.CV1 = ClientUnit:GetVec3() + self:T( { "self.CV1", self.CV1 } ) + self.CT1 = timer.getTime() + self.GV1 = GroupUnit:GetVec3() + self.GT1 = timer.getTime() + else + local CT1 = self.CT1 + local CT2 = timer.getTime() + local CV1 = self.CV1 + local CV2 = ClientUnit:GetVec3() + self.CT1 = CT2 + self.CV1 = CV2 + + local CD = ( ( CV2.x - CV1.x )^2 + ( CV2.y - CV1.y )^2 + ( CV2.z - CV1.z )^2 ) ^ 0.5 + local CT = CT2 - CT1 + + local CS = ( 3600 / CT ) * ( CD / 1000 ) + + self:T2( { "Client:", CS, CD, CT, CV2, CV1, CT2, CT1 } ) + + local GT1 = self.GT1 + local GT2 = timer.getTime() + local GV1 = self.GV1 + local GV2 = GroupUnit:GetVec3() + self.GT1 = GT2 + self.GV1 = GV2 + + local GD = ( ( GV2.x - GV1.x )^2 + ( GV2.y - GV1.y )^2 + ( GV2.z - GV1.z )^2 ) ^ 0.5 + local GT = GT2 - GT1 + + local GS = ( 3600 / GT ) * ( GD / 1000 ) + + self:T2( { "Group:", GS, GD, GT, GV2, GV1, GT2, GT1 } ) + + -- Calculate the group direction vector + local GV = { x = GV2.x - CV2.x, y = GV2.y - CV2.y, z = GV2.z - CV2.z } + + -- Calculate GH2, GH2 with the same height as CV2. + local GH2 = { x = GV2.x, y = CV2.y, z = GV2.z } + + -- Calculate the angle of GV to the orthonormal plane + local alpha = math.atan2( GV.z, GV.x ) + + -- Now we calculate the intersecting vector between the circle around CV2 with radius FollowDistance and GH2. + -- From the GeoGebra model: CVI = (x(CV2) + FollowDistance cos(alpha), y(GH2) + FollowDistance sin(alpha), z(CV2)) + local CVI = { x = CV2.x + FollowDistance * math.cos(alpha), + y = GH2.y, + z = CV2.z + FollowDistance * math.sin(alpha), + } + + -- Calculate the direction vector DV of the escort group. We use CVI as the base and CV2 as the direction. + local DV = { x = CV2.x - CVI.x, y = CV2.y - CVI.y, z = CV2.z - CVI.z } + + -- We now calculate the unary direction vector DVu, so that we can multiply DVu with the speed, which is expressed in meters / s. + -- We need to calculate this vector to predict the point the escort group needs to fly to according its speed. + -- The distance of the destination point should be far enough not to have the aircraft starting to swipe left to right... + local DVu = { x = DV.x / FollowDistance, y = DV.y / FollowDistance, z = DV.z / FollowDistance } + + -- Now we can calculate the group destination vector GDV. + local GDV = { x = DVu.x * CS * 8 + CVI.x, y = CVI.y, z = DVu.z * CS * 8 + CVI.z } + + if self.SmokeDirectionVector == true then + trigger.action.smoke( GDV, trigger.smokeColor.Red ) + end + + self:T2( { "CV2:", CV2 } ) + self:T2( { "CVI:", CVI } ) + self:T2( { "GDV:", GDV } ) + + -- Measure distance between client and group + local CatchUpDistance = ( ( GDV.x - GV2.x )^2 + ( GDV.y - GV2.y )^2 + ( GDV.z - GV2.z )^2 ) ^ 0.5 + + -- The calculation of the Speed would simulate that the group would take 30 seconds to overcome + -- the requested Distance). + local Time = 10 + local CatchUpSpeed = ( CatchUpDistance - ( CS * 8.4 ) ) / Time + + local Speed = CS + CatchUpSpeed + if Speed < 0 then + Speed = 0 + end + + self:T( { "Client Speed, Escort Speed, Speed, FollowDistance, Time:", CS, GS, Speed, FollowDistance, Time } ) + + -- Now route the escort to the desired point with the desired speed. + self.EscortGroup:RouteToVec3( GDV, Speed / 3.6 ) -- DCS models speed in Mps (Miles per second) + end + + return true + end + + return false +end + + +--- Report Targets Scheduler. +-- @param #ESCORT self +function ESCORT:_ReportTargetsScheduler() + self:F( self.EscortGroup:GetName() ) + + if self.EscortGroup:IsAlive() and self.EscortClient:IsAlive() then + + if true then + + local EscortGroupName = self.EscortGroup:GetName() + + self.EscortMenuAttackNearbyTargets:RemoveSubMenus() + + if self.EscortMenuTargetAssistance then + self.EscortMenuTargetAssistance:RemoveSubMenus() + end + + local DetectedItems = self.Detection:GetDetectedItems() + self:F( DetectedItems ) + + local DetectedTargets = false + + local DetectedMsgs = {} + + for ClientEscortGroupName, EscortGroupData in pairs( self.EscortClient._EscortGroups ) do + + local ClientEscortTargets = EscortGroupData.Detection + --local EscortUnit = EscortGroupData:GetUnit( 1 ) + + for DetectedItemIndex, DetectedItem in pairs( DetectedItems ) do + self:F( { DetectedItemIndex, DetectedItem } ) + -- Remove the sub menus of the Attack menu of the Escort for the EscortGroup. + + local DetectedItemReportSummary = self.Detection:DetectedItemReportSummary( DetectedItem, EscortGroupData.EscortGroup, _DATABASE:GetPlayerSettings( self.EscortClient:GetPlayerName() ) ) + + if ClientEscortGroupName == EscortGroupName then + + local DetectedMsg = DetectedItemReportSummary:Text("\n") + DetectedMsgs[#DetectedMsgs+1] = DetectedMsg + + self:T( DetectedMsg ) + + MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), + DetectedMsg, + self.EscortMenuAttackNearbyTargets, + ESCORT._AttackTarget, + self, + DetectedItem + ) + else + if self.EscortMenuTargetAssistance then + + local DetectedMsg = DetectedItemReportSummary:Text("\n") + self:T( DetectedMsg ) + + local MenuTargetAssistance = MENU_GROUP:New( self.EscortClient:GetGroup(), EscortGroupData.EscortName, self.EscortMenuTargetAssistance ) + MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), + DetectedMsg, + MenuTargetAssistance, + ESCORT._AssistTarget, + self, + EscortGroupData.EscortGroup, + DetectedItem + ) + end + end + + DetectedTargets = true + + end + end + self:F( DetectedMsgs ) + if DetectedTargets then + self.EscortGroup:MessageToClient( "Reporting detected targets:\n" .. table.concat( DetectedMsgs, "\n" ), 20, self.EscortClient ) + else + self.EscortGroup:MessageToClient( "No targets detected.", 10, self.EscortClient ) + end + + return true + else +-- local EscortGroupName = self.EscortGroup:GetName() +-- local EscortTargets = self.EscortGroup:GetDetectedTargets() +-- +-- local ClientEscortTargets = self.EscortClient._EscortGroups[EscortGroupName].Targets +-- +-- local EscortTargetMessages = "" +-- for EscortTargetID, EscortTarget in pairs( EscortTargets ) do +-- local EscortObject = EscortTarget.object +-- self:T( EscortObject ) +-- if EscortObject and EscortObject:isExist() and EscortObject.id_ < 50000000 then +-- +-- local EscortTargetUnit = UNIT:Find( EscortObject ) +-- local EscortTargetUnitName = EscortTargetUnit:GetName() +-- +-- +-- +-- -- local EscortTargetIsDetected, +-- -- EscortTargetIsVisible, +-- -- EscortTargetLastTime, +-- -- EscortTargetKnowType, +-- -- EscortTargetKnowDistance, +-- -- EscortTargetLastPos, +-- -- EscortTargetLastVelocity +-- -- = self.EscortGroup:IsTargetDetected( EscortObject ) +-- -- +-- -- self:T( { EscortTargetIsDetected, +-- -- EscortTargetIsVisible, +-- -- EscortTargetLastTime, +-- -- EscortTargetKnowType, +-- -- EscortTargetKnowDistance, +-- -- EscortTargetLastPos, +-- -- EscortTargetLastVelocity } ) +-- +-- +-- local EscortTargetUnitVec3 = EscortTargetUnit:GetVec3() +-- local EscortVec3 = self.EscortGroup:GetVec3() +-- local Distance = ( ( EscortTargetUnitVec3.x - EscortVec3.x )^2 + +-- ( EscortTargetUnitVec3.y - EscortVec3.y )^2 + +-- ( EscortTargetUnitVec3.z - EscortVec3.z )^2 +-- ) ^ 0.5 / 1000 +-- +-- self:T( { self.EscortGroup:GetName(), EscortTargetUnit:GetName(), Distance, EscortTarget } ) +-- +-- if Distance <= 15 then +-- +-- if not ClientEscortTargets[EscortTargetUnitName] then +-- ClientEscortTargets[EscortTargetUnitName] = {} +-- end +-- ClientEscortTargets[EscortTargetUnitName].AttackUnit = EscortTargetUnit +-- ClientEscortTargets[EscortTargetUnitName].visible = EscortTarget.visible +-- ClientEscortTargets[EscortTargetUnitName].type = EscortTarget.type +-- ClientEscortTargets[EscortTargetUnitName].distance = EscortTarget.distance +-- else +-- if ClientEscortTargets[EscortTargetUnitName] then +-- ClientEscortTargets[EscortTargetUnitName] = nil +-- end +-- end +-- end +-- end +-- +-- self:T( { "Sorting Targets Table:", ClientEscortTargets } ) +-- table.sort( ClientEscortTargets, function( a, b ) return a.Distance < b.Distance end ) +-- self:T( { "Sorted Targets Table:", ClientEscortTargets } ) +-- +-- -- Remove the sub menus of the Attack menu of the Escort for the EscortGroup. +-- self.EscortMenuAttackNearbyTargets:RemoveSubMenus() +-- +-- if self.EscortMenuTargetAssistance then +-- self.EscortMenuTargetAssistance:RemoveSubMenus() +-- end +-- +-- --for MenuIndex = 1, #self.EscortMenuAttackTargets do +-- -- self:T( { "Remove Menu:", self.EscortMenuAttackTargets[MenuIndex] } ) +-- -- self.EscortMenuAttackTargets[MenuIndex] = self.EscortMenuAttackTargets[MenuIndex]:Remove() +-- --end +-- +-- +-- if ClientEscortTargets then +-- for ClientEscortTargetUnitName, ClientEscortTargetData in pairs( ClientEscortTargets ) do +-- +-- for ClientEscortGroupName, EscortGroupData in pairs( self.EscortClient._EscortGroups ) do +-- +-- if ClientEscortTargetData and ClientEscortTargetData.AttackUnit:IsAlive() then +-- +-- local EscortTargetMessage = "" +-- local EscortTargetCategoryName = ClientEscortTargetData.AttackUnit:GetCategoryName() +-- local EscortTargetCategoryType = ClientEscortTargetData.AttackUnit:GetTypeName() +-- if ClientEscortTargetData.type then +-- EscortTargetMessage = EscortTargetMessage .. EscortTargetCategoryName .. " (" .. EscortTargetCategoryType .. ") at " +-- else +-- EscortTargetMessage = EscortTargetMessage .. "Unknown target at " +-- end +-- +-- local EscortTargetUnitVec3 = ClientEscortTargetData.AttackUnit:GetVec3() +-- local EscortVec3 = self.EscortGroup:GetVec3() +-- local Distance = ( ( EscortTargetUnitVec3.x - EscortVec3.x )^2 + +-- ( EscortTargetUnitVec3.y - EscortVec3.y )^2 + +-- ( EscortTargetUnitVec3.z - EscortVec3.z )^2 +-- ) ^ 0.5 / 1000 +-- +-- self:T( { self.EscortGroup:GetName(), ClientEscortTargetData.AttackUnit:GetName(), Distance, ClientEscortTargetData.AttackUnit } ) +-- if ClientEscortTargetData.visible == false then +-- EscortTargetMessage = EscortTargetMessage .. string.format( "%.2f", Distance ) .. " estimated km" +-- else +-- EscortTargetMessage = EscortTargetMessage .. string.format( "%.2f", Distance ) .. " km" +-- end +-- +-- if ClientEscortTargetData.visible then +-- EscortTargetMessage = EscortTargetMessage .. ", visual" +-- end +-- +-- if ClientEscortGroupName == EscortGroupName then +-- +-- MENU_GROUP_COMMAND:New( self.EscortClient, +-- EscortTargetMessage, +-- self.EscortMenuAttackNearbyTargets, +-- ESCORT._AttackTarget, +-- { ParamSelf = self, +-- ParamUnit = ClientEscortTargetData.AttackUnit +-- } +-- ) +-- EscortTargetMessages = EscortTargetMessages .. "\n - " .. EscortTargetMessage +-- else +-- if self.EscortMenuTargetAssistance then +-- local MenuTargetAssistance = MENU_GROUP:New( self.EscortClient, EscortGroupData.EscortName, self.EscortMenuTargetAssistance ) +-- MENU_GROUP_COMMAND:New( self.EscortClient, +-- EscortTargetMessage, +-- MenuTargetAssistance, +-- ESCORT._AssistTarget, +-- self, +-- EscortGroupData.EscortGroup, +-- ClientEscortTargetData.AttackUnit +-- ) +-- end +-- end +-- else +-- ClientEscortTargetData = nil +-- end +-- end +-- end +-- +-- if EscortTargetMessages ~= "" and self.ReportTargets == true then +-- self.EscortGroup:MessageToClient( "Detected targets within 15 km range:" .. EscortTargetMessages:gsub("\n$",""), 20, self.EscortClient ) +-- else +-- self.EscortGroup:MessageToClient( "No targets detected!", 20, self.EscortClient ) +-- end +-- end +-- +-- if self.EscortMenuResumeMission then +-- self.EscortMenuResumeMission:RemoveSubMenus() +-- +-- -- if self.EscortMenuResumeWayPoints then +-- -- for MenuIndex = 1, #self.EscortMenuResumeWayPoints do +-- -- self:T( { "Remove Menu:", self.EscortMenuResumeWayPoints[MenuIndex] } ) +-- -- self.EscortMenuResumeWayPoints[MenuIndex] = self.EscortMenuResumeWayPoints[MenuIndex]:Remove() +-- -- end +-- -- end +-- +-- local TaskPoints = self:RegisterRoute() +-- for WayPointID, WayPoint in pairs( TaskPoints ) do +-- local EscortVec3 = self.EscortGroup:GetVec3() +-- local Distance = ( ( WayPoint.x - EscortVec3.x )^2 + +-- ( WayPoint.y - EscortVec3.z )^2 +-- ) ^ 0.5 / 1000 +-- MENU_GROUP_COMMAND:New( self.EscortClient, "Waypoint " .. WayPointID .. " at " .. string.format( "%.2f", Distance ).. "km", self.EscortMenuResumeMission, ESCORT._ResumeMission, { ParamSelf = self, ParamWayPoint = WayPointID } ) +-- end +-- end +-- +-- return true + end + end + + return false +end +--- **Functional** -- Train missile defence and deflection. +-- +-- === +-- +-- ## Features: +-- +-- * Track the missiles fired at you and other players, providing bearing and range information of the missiles towards the airplanes. +-- * Provide alerts of missile launches, including detailed information of the units launching, including bearing, range +-- * Provide alerts when a missile would have killed your aircraft. +-- * Provide alerts when the missile self destructs. +-- * Enable / Disable and Configure the Missile Trainer using the various menu options. +-- +-- === +-- +-- ## Missions: +-- +-- [MIT - Missile Trainer](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/MIT%20-%20Missile%20Trainer) +-- +-- === +-- +-- Uses the MOOSE messaging system to be alerted of any missiles fired, and when a missile would hit your aircraft, +-- the class will destroy the missile within a certain range, to avoid damage to your aircraft. +-- +-- When running a mission where the missile trainer is used, the following radio menu structure ( 'Radio Menu' -> 'Other (F10)' -> 'MissileTrainer' ) options are available for the players: +-- +-- * **Messages**: Menu to configure all messages. +-- * **Messages On**: Show all messages. +-- * **Messages Off**: Disable all messages. +-- * **Tracking**: Menu to configure missile tracking messages. +-- * **To All**: Shows missile tracking messages to all players. +-- * **To Target**: Shows missile tracking messages only to the player where the missile is targetted at. +-- * **Tracking On**: Show missile tracking messages. +-- * **Tracking Off**: Disable missile tracking messages. +-- * **Frequency Increase**: Increases the missile tracking message frequency with one second. +-- * **Frequency Decrease**: Decreases the missile tracking message frequency with one second. +-- * **Alerts**: Menu to configure alert messages. +-- * **To All**: Shows alert messages to all players. +-- * **To Target**: Shows alert messages only to the player where the missile is (was) targetted at. +-- * **Hits On**: Show missile hit alert messages. +-- * **Hits Off**: Disable missile hit alert messages. +-- * **Launches On**: Show missile launch messages. +-- * **Launches Off**: Disable missile launch messages. +-- * **Details**: Menu to configure message details. +-- * **Range On**: Shows range information when a missile is fired to a target. +-- * **Range Off**: Disable range information when a missile is fired to a target. +-- * **Bearing On**: Shows bearing information when a missile is fired to a target. +-- * **Bearing Off**: Disable bearing information when a missile is fired to a target. +-- * **Distance**: Menu to configure the distance when a missile needs to be destroyed when near to a player, during tracking. This will improve/influence hit calculation accuracy, but has the risk of damaging the aircraft when the missile reaches the aircraft before the distance is measured. +-- * **50 meter**: Destroys the missile when the distance to the aircraft is below or equal to 50 meter. +-- * **100 meter**: Destroys the missile when the distance to the aircraft is below or equal to 100 meter. +-- * **150 meter**: Destroys the missile when the distance to the aircraft is below or equal to 150 meter. +-- * **200 meter**: Destroys the missile when the distance to the aircraft is below or equal to 200 meter. +-- +-- === +-- +-- ### Authors: **FlightControl** +-- +-- ### Contributions: +-- +-- * **Stuka (Danny)**: Who you can search on the Eagle Dynamics Forums. Working together with Danny has resulted in the MISSILETRAINER class. +-- Danny has shared his ideas and together we made a design. +-- Together with the **476 virtual team**, we tested the MISSILETRAINER class, and got much positive feedback! +-- * **132nd Squadron**: Testing and optimizing the logic. +-- +-- === +-- +-- @module Functional.MissileTrainer +-- @image Missile_Trainer.JPG + + +--- @type MISSILETRAINER +-- @field Core.Set#SET_CLIENT DBClients +-- @extends Core.Base#BASE + + +--- +-- +-- # Constructor: +-- +-- Create a new MISSILETRAINER object with the @{#MISSILETRAINER.New} method: +-- +-- * @{#MISSILETRAINER.New}: Creates a new MISSILETRAINER object taking the maximum distance to your aircraft to evaluate when a missile needs to be destroyed. +-- +-- MISSILETRAINER will collect each unit declared in the mission with a skill level "Client" and "Player", and will monitor the missiles shot at those. +-- +-- # Initialization: +-- +-- A MISSILETRAINER object will behave differently based on the usage of initialization methods: +-- +-- * @{#MISSILETRAINER.InitMessagesOnOff}: Sets by default the display of any message to be ON or OFF. +-- * @{#MISSILETRAINER.InitTrackingToAll}: Sets by default the missile tracking report for all players or only for those missiles targetted to you. +-- * @{#MISSILETRAINER.InitTrackingOnOff}: Sets by default the display of missile tracking report to be ON or OFF. +-- * @{#MISSILETRAINER.InitTrackingFrequency}: Increases, decreases the missile tracking message display frequency with the provided time interval in seconds. +-- * @{#MISSILETRAINER.InitAlertsToAll}: Sets by default the display of alerts to be shown to all players or only to you. +-- * @{#MISSILETRAINER.InitAlertsHitsOnOff}: Sets by default the display of hit alerts ON or OFF. +-- * @{#MISSILETRAINER.InitAlertsLaunchesOnOff}: Sets by default the display of launch alerts ON or OFF. +-- * @{#MISSILETRAINER.InitRangeOnOff}: Sets by default the display of range information of missiles ON of OFF. +-- * @{#MISSILETRAINER.InitBearingOnOff}: Sets by default the display of bearing information of missiles ON of OFF. +-- * @{#MISSILETRAINER.InitMenusOnOff}: Allows to configure the options through the radio menu. +-- +-- @field #MISSILETRAINER +MISSILETRAINER = { + ClassName = "MISSILETRAINER", + TrackingMissiles = {}, +} + +function MISSILETRAINER._Alive( Client, self ) + + if self.Briefing then + Client:Message( self.Briefing, 15, "Trainer" ) + end + + if self.MenusOnOff == true then + Client:Message( "Use the 'Radio Menu' -> 'Other (F10)' -> 'Missile Trainer' menu options to change the Missile Trainer settings (for all players).", 15, "Trainer" ) + + Client.MainMenu = MENU_GROUP:New( Client:GetGroup(), "Missile Trainer", nil ) -- Menu#MENU_GROUP + + Client.MenuMessages = MENU_GROUP:New( Client:GetGroup(), "Messages", Client.MainMenu ) + Client.MenuOn = MENU_GROUP_COMMAND:New( Client:GetGroup(), "Messages On", Client.MenuMessages, self._MenuMessages, { MenuSelf = self, MessagesOnOff = true } ) + Client.MenuOff = MENU_GROUP_COMMAND:New( Client:GetGroup(), "Messages Off", Client.MenuMessages, self._MenuMessages, { MenuSelf = self, MessagesOnOff = false } ) + + Client.MenuTracking = MENU_GROUP:New( Client:GetGroup(), "Tracking", Client.MainMenu ) + Client.MenuTrackingToAll = MENU_GROUP_COMMAND:New( Client:GetGroup(), "To All", Client.MenuTracking, self._MenuMessages, { MenuSelf = self, TrackingToAll = true } ) + Client.MenuTrackingToTarget = MENU_GROUP_COMMAND:New( Client:GetGroup(), "To Target", Client.MenuTracking, self._MenuMessages, { MenuSelf = self, TrackingToAll = false } ) + Client.MenuTrackOn = MENU_GROUP_COMMAND:New( Client:GetGroup(), "Tracking On", Client.MenuTracking, self._MenuMessages, { MenuSelf = self, TrackingOnOff = true } ) + Client.MenuTrackOff = MENU_GROUP_COMMAND:New( Client:GetGroup(), "Tracking Off", Client.MenuTracking, self._MenuMessages, { MenuSelf = self, TrackingOnOff = false } ) + Client.MenuTrackIncrease = MENU_GROUP_COMMAND:New( Client:GetGroup(), "Frequency Increase", Client.MenuTracking, self._MenuMessages, { MenuSelf = self, TrackingFrequency = -1 } ) + Client.MenuTrackDecrease = MENU_GROUP_COMMAND:New( Client:GetGroup(), "Frequency Decrease", Client.MenuTracking, self._MenuMessages, { MenuSelf = self, TrackingFrequency = 1 } ) + + Client.MenuAlerts = MENU_GROUP:New( Client:GetGroup(), "Alerts", Client.MainMenu ) + Client.MenuAlertsToAll = MENU_GROUP_COMMAND:New( Client:GetGroup(), "To All", Client.MenuAlerts, self._MenuMessages, { MenuSelf = self, AlertsToAll = true } ) + Client.MenuAlertsToTarget = MENU_GROUP_COMMAND:New( Client:GetGroup(), "To Target", Client.MenuAlerts, self._MenuMessages, { MenuSelf = self, AlertsToAll = false } ) + Client.MenuHitsOn = MENU_GROUP_COMMAND:New( Client:GetGroup(), "Hits On", Client.MenuAlerts, self._MenuMessages, { MenuSelf = self, AlertsHitsOnOff = true } ) + Client.MenuHitsOff = MENU_GROUP_COMMAND:New( Client:GetGroup(), "Hits Off", Client.MenuAlerts, self._MenuMessages, { MenuSelf = self, AlertsHitsOnOff = false } ) + Client.MenuLaunchesOn = MENU_GROUP_COMMAND:New( Client:GetGroup(), "Launches On", Client.MenuAlerts, self._MenuMessages, { MenuSelf = self, AlertsLaunchesOnOff = true } ) + Client.MenuLaunchesOff = MENU_GROUP_COMMAND:New( Client:GetGroup(), "Launches Off", Client.MenuAlerts, self._MenuMessages, { MenuSelf = self, AlertsLaunchesOnOff = false } ) + + Client.MenuDetails = MENU_GROUP:New( Client:GetGroup(), "Details", Client.MainMenu ) + Client.MenuDetailsDistanceOn = MENU_GROUP_COMMAND:New( Client:GetGroup(), "Range On", Client.MenuDetails, self._MenuMessages, { MenuSelf = self, DetailsRangeOnOff = true } ) + Client.MenuDetailsDistanceOff = MENU_GROUP_COMMAND:New( Client:GetGroup(), "Range Off", Client.MenuDetails, self._MenuMessages, { MenuSelf = self, DetailsRangeOnOff = false } ) + Client.MenuDetailsBearingOn = MENU_GROUP_COMMAND:New( Client:GetGroup(), "Bearing On", Client.MenuDetails, self._MenuMessages, { MenuSelf = self, DetailsBearingOnOff = true } ) + Client.MenuDetailsBearingOff = MENU_GROUP_COMMAND:New( Client:GetGroup(), "Bearing Off", Client.MenuDetails, self._MenuMessages, { MenuSelf = self, DetailsBearingOnOff = false } ) + + Client.MenuDistance = MENU_GROUP:New( Client:GetGroup(), "Set distance to plane", Client.MainMenu ) + Client.MenuDistance50 = MENU_GROUP_COMMAND:New( Client:GetGroup(), "50 meter", Client.MenuDistance, self._MenuMessages, { MenuSelf = self, Distance = 50 / 1000 } ) + Client.MenuDistance100 = MENU_GROUP_COMMAND:New( Client:GetGroup(), "100 meter", Client.MenuDistance, self._MenuMessages, { MenuSelf = self, Distance = 100 / 1000 } ) + Client.MenuDistance150 = MENU_GROUP_COMMAND:New( Client:GetGroup(), "150 meter", Client.MenuDistance, self._MenuMessages, { MenuSelf = self, Distance = 150 / 1000 } ) + Client.MenuDistance200 = MENU_GROUP_COMMAND:New( Client:GetGroup(), "200 meter", Client.MenuDistance, self._MenuMessages, { MenuSelf = self, Distance = 200 / 1000 } ) + else + if Client.MainMenu then + Client.MainMenu:Remove() + end + end + + local ClientID = Client:GetID() + self:T( ClientID ) + if not self.TrackingMissiles[ClientID] then + self.TrackingMissiles[ClientID] = {} + end + self.TrackingMissiles[ClientID].Client = Client + if not self.TrackingMissiles[ClientID].MissileData then + self.TrackingMissiles[ClientID].MissileData = {} + end +end + +--- Creates the main object which is handling missile tracking. +-- When a missile is fired a SCHEDULER is set off that follows the missile. When near a certain a client player, the missile will be destroyed. +-- @param #MISSILETRAINER self +-- @param #number Distance The distance in meters when a tracked missile needs to be destroyed when close to a player. +-- @param #string Briefing (Optional) Will show a text to the players when starting their mission. Can be used for briefing purposes. +-- @return #MISSILETRAINER +function MISSILETRAINER:New( Distance, Briefing ) + local self = BASE:Inherit( self, BASE:New() ) + self:F( Distance ) + + if Briefing then + self.Briefing = Briefing + end + + self.Schedulers = {} + self.SchedulerID = 0 + + self.MessageInterval = 2 + self.MessageLastTime = timer.getTime() + + self.Distance = Distance / 1000 + + self:HandleEvent( EVENTS.Shot ) + + self.DBClients = SET_CLIENT:New():FilterStart() + + +-- for ClientID, Client in pairs( self.DBClients.Database ) do +-- self:F( "ForEach:" .. Client.UnitName ) +-- Client:Alive( self._Alive, self ) +-- end +-- + self.DBClients:ForEachClient( + function( Client ) + self:F( "ForEach:" .. Client.UnitName ) + Client:Alive( self._Alive, self ) + end + ) + + + +-- self.DB:ForEachClient( +-- --- @param Wrapper.Client#CLIENT Client +-- function( Client ) +-- +-- ... actions ... +-- +-- end +-- ) + + self.MessagesOnOff = true + + self.TrackingToAll = false + self.TrackingOnOff = true + self.TrackingFrequency = 3 + + self.AlertsToAll = true + self.AlertsHitsOnOff = true + self.AlertsLaunchesOnOff = true + + self.DetailsRangeOnOff = true + self.DetailsBearingOnOff = true + + self.MenusOnOff = true + + self.TrackingMissiles = {} + + self.TrackingScheduler = SCHEDULER:New( self, self._TrackMissiles, {}, 0.5, 0.05, 0 ) + + return self +end + +-- Initialization methods. + + + +--- Sets by default the display of any message to be ON or OFF. +-- @param #MISSILETRAINER self +-- @param #boolean MessagesOnOff true or false +-- @return #MISSILETRAINER self +function MISSILETRAINER:InitMessagesOnOff( MessagesOnOff ) + self:F( MessagesOnOff ) + + self.MessagesOnOff = MessagesOnOff + if self.MessagesOnOff == true then + MESSAGE:New( "Messages ON", 15, "Menu" ):ToAll() + else + MESSAGE:New( "Messages OFF", 15, "Menu" ):ToAll() + end + + return self +end + +--- Sets by default the missile tracking report for all players or only for those missiles targetted to you. +-- @param #MISSILETRAINER self +-- @param #boolean TrackingToAll true or false +-- @return #MISSILETRAINER self +function MISSILETRAINER:InitTrackingToAll( TrackingToAll ) + self:F( TrackingToAll ) + + self.TrackingToAll = TrackingToAll + if self.TrackingToAll == true then + MESSAGE:New( "Missile tracking to all players ON", 15, "Menu" ):ToAll() + else + MESSAGE:New( "Missile tracking to all players OFF", 15, "Menu" ):ToAll() + end + + return self +end + +--- Sets by default the display of missile tracking report to be ON or OFF. +-- @param #MISSILETRAINER self +-- @param #boolean TrackingOnOff true or false +-- @return #MISSILETRAINER self +function MISSILETRAINER:InitTrackingOnOff( TrackingOnOff ) + self:F( TrackingOnOff ) + + self.TrackingOnOff = TrackingOnOff + if self.TrackingOnOff == true then + MESSAGE:New( "Missile tracking ON", 15, "Menu" ):ToAll() + else + MESSAGE:New( "Missile tracking OFF", 15, "Menu" ):ToAll() + end + + return self +end + +--- Increases, decreases the missile tracking message display frequency with the provided time interval in seconds. +-- The default frequency is a 3 second interval, so the Tracking Frequency parameter specifies the increase or decrease from the default 3 seconds or the last frequency update. +-- @param #MISSILETRAINER self +-- @param #number TrackingFrequency Provide a negative or positive value in seconds to incraese or decrease the display frequency. +-- @return #MISSILETRAINER self +function MISSILETRAINER:InitTrackingFrequency( TrackingFrequency ) + self:F( TrackingFrequency ) + + self.TrackingFrequency = self.TrackingFrequency + TrackingFrequency + if self.TrackingFrequency < 0.5 then + self.TrackingFrequency = 0.5 + end + if self.TrackingFrequency then + MESSAGE:New( "Missile tracking frequency is " .. self.TrackingFrequency .. " seconds.", 15, "Menu" ):ToAll() + end + + return self +end + +--- Sets by default the display of alerts to be shown to all players or only to you. +-- @param #MISSILETRAINER self +-- @param #boolean AlertsToAll true or false +-- @return #MISSILETRAINER self +function MISSILETRAINER:InitAlertsToAll( AlertsToAll ) + self:F( AlertsToAll ) + + self.AlertsToAll = AlertsToAll + if self.AlertsToAll == true then + MESSAGE:New( "Alerts to all players ON", 15, "Menu" ):ToAll() + else + MESSAGE:New( "Alerts to all players OFF", 15, "Menu" ):ToAll() + end + + return self +end + +--- Sets by default the display of hit alerts ON or OFF. +-- @param #MISSILETRAINER self +-- @param #boolean AlertsHitsOnOff true or false +-- @return #MISSILETRAINER self +function MISSILETRAINER:InitAlertsHitsOnOff( AlertsHitsOnOff ) + self:F( AlertsHitsOnOff ) + + self.AlertsHitsOnOff = AlertsHitsOnOff + if self.AlertsHitsOnOff == true then + MESSAGE:New( "Alerts Hits ON", 15, "Menu" ):ToAll() + else + MESSAGE:New( "Alerts Hits OFF", 15, "Menu" ):ToAll() + end + + return self +end + +--- Sets by default the display of launch alerts ON or OFF. +-- @param #MISSILETRAINER self +-- @param #boolean AlertsLaunchesOnOff true or false +-- @return #MISSILETRAINER self +function MISSILETRAINER:InitAlertsLaunchesOnOff( AlertsLaunchesOnOff ) + self:F( AlertsLaunchesOnOff ) + + self.AlertsLaunchesOnOff = AlertsLaunchesOnOff + if self.AlertsLaunchesOnOff == true then + MESSAGE:New( "Alerts Launches ON", 15, "Menu" ):ToAll() + else + MESSAGE:New( "Alerts Launches OFF", 15, "Menu" ):ToAll() + end + + return self +end + +--- Sets by default the display of range information of missiles ON of OFF. +-- @param #MISSILETRAINER self +-- @param #boolean DetailsRangeOnOff true or false +-- @return #MISSILETRAINER self +function MISSILETRAINER:InitRangeOnOff( DetailsRangeOnOff ) + self:F( DetailsRangeOnOff ) + + self.DetailsRangeOnOff = DetailsRangeOnOff + if self.DetailsRangeOnOff == true then + MESSAGE:New( "Range display ON", 15, "Menu" ):ToAll() + else + MESSAGE:New( "Range display OFF", 15, "Menu" ):ToAll() + end + + return self +end + +--- Sets by default the display of bearing information of missiles ON of OFF. +-- @param #MISSILETRAINER self +-- @param #boolean DetailsBearingOnOff true or false +-- @return #MISSILETRAINER self +function MISSILETRAINER:InitBearingOnOff( DetailsBearingOnOff ) + self:F( DetailsBearingOnOff ) + + self.DetailsBearingOnOff = DetailsBearingOnOff + if self.DetailsBearingOnOff == true then + MESSAGE:New( "Bearing display OFF", 15, "Menu" ):ToAll() + else + MESSAGE:New( "Bearing display OFF", 15, "Menu" ):ToAll() + end + + return self +end + +--- Enables / Disables the menus. +-- @param #MISSILETRAINER self +-- @param #boolean MenusOnOff true or false +-- @return #MISSILETRAINER self +function MISSILETRAINER:InitMenusOnOff( MenusOnOff ) + self:F( MenusOnOff ) + + self.MenusOnOff = MenusOnOff + if self.MenusOnOff == true then + MESSAGE:New( "Menus are ENABLED (only when a player rejoins a slot)", 15, "Menu" ):ToAll() + else + MESSAGE:New( "Menus are DISABLED", 15, "Menu" ):ToAll() + end + + return self +end + + +-- Menu functions + +function MISSILETRAINER._MenuMessages( MenuParameters ) + + local self = MenuParameters.MenuSelf + + if MenuParameters.MessagesOnOff ~= nil then + self:InitMessagesOnOff( MenuParameters.MessagesOnOff ) + end + + if MenuParameters.TrackingToAll ~= nil then + self:InitTrackingToAll( MenuParameters.TrackingToAll ) + end + + if MenuParameters.TrackingOnOff ~= nil then + self:InitTrackingOnOff( MenuParameters.TrackingOnOff ) + end + + if MenuParameters.TrackingFrequency ~= nil then + self:InitTrackingFrequency( MenuParameters.TrackingFrequency ) + end + + if MenuParameters.AlertsToAll ~= nil then + self:InitAlertsToAll( MenuParameters.AlertsToAll ) + end + + if MenuParameters.AlertsHitsOnOff ~= nil then + self:InitAlertsHitsOnOff( MenuParameters.AlertsHitsOnOff ) + end + + if MenuParameters.AlertsLaunchesOnOff ~= nil then + self:InitAlertsLaunchesOnOff( MenuParameters.AlertsLaunchesOnOff ) + end + + if MenuParameters.DetailsRangeOnOff ~= nil then + self:InitRangeOnOff( MenuParameters.DetailsRangeOnOff ) + end + + if MenuParameters.DetailsBearingOnOff ~= nil then + self:InitBearingOnOff( MenuParameters.DetailsBearingOnOff ) + end + + if MenuParameters.Distance ~= nil then + self.Distance = MenuParameters.Distance + MESSAGE:New( "Hit detection distance set to " .. ( self.Distance * 1000 ) .. " meters", 15, "Menu" ):ToAll() + end + +end + +--- Detects if an SA site was shot with an anti radiation missile. In this case, take evasive actions based on the skill level set within the ME. +-- @param #MISSILETRAINER self +-- @param Core.Event#EVENTDATA EventData +function MISSILETRAINER:OnEventShot( EVentData ) + self:F( { EVentData } ) + + local TrainerSourceDCSUnit = EVentData.IniDCSUnit + local TrainerSourceDCSUnitName = EVentData.IniDCSUnitName + local TrainerWeapon = EVentData.Weapon -- Identify the weapon fired + local TrainerWeaponName = EVentData.WeaponName -- return weapon type + + self:T( "Missile Launched = " .. TrainerWeaponName ) + + local TrainerTargetDCSUnit = TrainerWeapon:getTarget() -- Identify target + if TrainerTargetDCSUnit then + local TrainerTargetDCSUnitName = Unit.getName( TrainerTargetDCSUnit ) + local TrainerTargetSkill = _DATABASE.Templates.Units[TrainerTargetDCSUnitName].Template.skill + + self:T(TrainerTargetDCSUnitName ) + + local Client = self.DBClients:FindClient( TrainerTargetDCSUnitName ) + if Client then + + local TrainerSourceUnit = UNIT:Find( TrainerSourceDCSUnit ) + local TrainerTargetUnit = UNIT:Find( TrainerTargetDCSUnit ) + + if self.MessagesOnOff == true and self.AlertsLaunchesOnOff == true then + + local Message = MESSAGE:New( + string.format( "%s launched a %s", + TrainerSourceUnit:GetTypeName(), + TrainerWeaponName + ) .. self:_AddRange( Client, TrainerWeapon ) .. self:_AddBearing( Client, TrainerWeapon ), 5, "Launch Alert" ) + + if self.AlertsToAll then + Message:ToAll() + else + Message:ToClient( Client ) + end + end + + local ClientID = Client:GetID() + self:T( ClientID ) + local MissileData = {} + MissileData.TrainerSourceUnit = TrainerSourceUnit + MissileData.TrainerWeapon = TrainerWeapon + MissileData.TrainerTargetUnit = TrainerTargetUnit + MissileData.TrainerWeaponTypeName = TrainerWeapon:getTypeName() + MissileData.TrainerWeaponLaunched = true + table.insert( self.TrackingMissiles[ClientID].MissileData, MissileData ) + --self:T( self.TrackingMissiles ) + end + else + -- TODO: some weapons don't know the target unit... Need to develop a workaround for this. + if ( TrainerWeapon:getTypeName() == "9M311" ) then + SCHEDULER:New( TrainerWeapon, TrainerWeapon.destroy, {}, 1 ) + else + end + end +end + +function MISSILETRAINER:_AddRange( Client, TrainerWeapon ) + + local RangeText = "" + + if self.DetailsRangeOnOff then + + local PositionMissile = TrainerWeapon:getPoint() + local TargetVec3 = Client:GetVec3() + + local Range = ( ( PositionMissile.x - TargetVec3.x )^2 + + ( PositionMissile.y - TargetVec3.y )^2 + + ( PositionMissile.z - TargetVec3.z )^2 + ) ^ 0.5 / 1000 + + RangeText = string.format( ", at %4.2fkm", Range ) + end + + return RangeText +end + +function MISSILETRAINER:_AddBearing( Client, TrainerWeapon ) + + local BearingText = "" + + if self.DetailsBearingOnOff then + + local PositionMissile = TrainerWeapon:getPoint() + local TargetVec3 = Client:GetVec3() + + self:T2( { TargetVec3, PositionMissile }) + + local DirectionVector = { x = PositionMissile.x - TargetVec3.x, y = PositionMissile.y - TargetVec3.y, z = PositionMissile.z - TargetVec3.z } + local DirectionRadians = math.atan2( DirectionVector.z, DirectionVector.x ) + --DirectionRadians = DirectionRadians + routines.getNorthCorrection( PositionTarget ) + if DirectionRadians < 0 then + DirectionRadians = DirectionRadians + 2 * math.pi + end + local DirectionDegrees = DirectionRadians * 180 / math.pi + + BearingText = string.format( ", %d degrees", DirectionDegrees ) + end + + return BearingText +end + + +function MISSILETRAINER:_TrackMissiles() + self:F2() + + + local ShowMessages = false + if self.MessagesOnOff and self.MessageLastTime + self.TrackingFrequency <= timer.getTime() then + self.MessageLastTime = timer.getTime() + ShowMessages = true + end + + -- ALERTS PART + + -- Loop for all Player Clients to check the alerts and deletion of missiles. + for ClientDataID, ClientData in pairs( self.TrackingMissiles ) do + + local Client = ClientData.Client + + if Client and Client:IsAlive() then + + for MissileDataID, MissileData in pairs( ClientData.MissileData ) do + self:T3( MissileDataID ) + + local TrainerSourceUnit = MissileData.TrainerSourceUnit + local TrainerWeapon = MissileData.TrainerWeapon + local TrainerTargetUnit = MissileData.TrainerTargetUnit + local TrainerWeaponTypeName = MissileData.TrainerWeaponTypeName + local TrainerWeaponLaunched = MissileData.TrainerWeaponLaunched + + if Client and Client:IsAlive() and TrainerSourceUnit and TrainerSourceUnit:IsAlive() and TrainerWeapon and TrainerWeapon:isExist() and TrainerTargetUnit and TrainerTargetUnit:IsAlive() then + local PositionMissile = TrainerWeapon:getPosition().p + local TargetVec3 = Client:GetVec3() + + local Distance = ( ( PositionMissile.x - TargetVec3.x )^2 + + ( PositionMissile.y - TargetVec3.y )^2 + + ( PositionMissile.z - TargetVec3.z )^2 + ) ^ 0.5 / 1000 + + if Distance <= self.Distance then + -- Hit alert + TrainerWeapon:destroy() + if self.MessagesOnOff == true and self.AlertsHitsOnOff == true then + + self:T( "killed" ) + + local Message = MESSAGE:New( + string.format( "%s launched by %s killed %s", + TrainerWeapon:getTypeName(), + TrainerSourceUnit:GetTypeName(), + TrainerTargetUnit:GetPlayerName() + ), 15, "Hit Alert" ) + + if self.AlertsToAll == true then + Message:ToAll() + else + Message:ToClient( Client ) + end + + MissileData = nil + table.remove( ClientData.MissileData, MissileDataID ) + self:T(ClientData.MissileData) + end + end + else + if not ( TrainerWeapon and TrainerWeapon:isExist() ) then + if self.MessagesOnOff == true and self.AlertsLaunchesOnOff == true then + -- Weapon does not exist anymore. Delete from Table + local Message = MESSAGE:New( + string.format( "%s launched by %s self destructed!", + TrainerWeaponTypeName, + TrainerSourceUnit:GetTypeName() + ), 5, "Tracking" ) + + if self.AlertsToAll == true then + Message:ToAll() + else + Message:ToClient( Client ) + end + end + MissileData = nil + table.remove( ClientData.MissileData, MissileDataID ) + self:T( ClientData.MissileData ) + end + end + end + else + self.TrackingMissiles[ClientDataID] = nil + end + end + + if ShowMessages == true and self.MessagesOnOff == true and self.TrackingOnOff == true then -- Only do this when tracking information needs to be displayed. + + -- TRACKING PART + + -- For the current client, the missile range and bearing details are displayed To the Player Client. + -- For the other clients, the missile range and bearing details are displayed To the other Player Clients. + -- To achieve this, a cross loop is done for each Player Client <-> Other Player Client missile information. + + -- Main Player Client loop + for ClientDataID, ClientData in pairs( self.TrackingMissiles ) do + + local Client = ClientData.Client + --self:T2( { Client:GetName() } ) + + + ClientData.MessageToClient = "" + ClientData.MessageToAll = "" + + -- Other Players Client loop + for TrackingDataID, TrackingData in pairs( self.TrackingMissiles ) do + + for MissileDataID, MissileData in pairs( TrackingData.MissileData ) do + --self:T3( MissileDataID ) + + local TrainerSourceUnit = MissileData.TrainerSourceUnit + local TrainerWeapon = MissileData.TrainerWeapon + local TrainerTargetUnit = MissileData.TrainerTargetUnit + local TrainerWeaponTypeName = MissileData.TrainerWeaponTypeName + local TrainerWeaponLaunched = MissileData.TrainerWeaponLaunched + + if Client and Client:IsAlive() and TrainerSourceUnit and TrainerSourceUnit:IsAlive() and TrainerWeapon and TrainerWeapon:isExist() and TrainerTargetUnit and TrainerTargetUnit:IsAlive() then + + if ShowMessages == true then + local TrackingTo + TrackingTo = string.format( " -> %s", + TrainerWeaponTypeName + ) + + if ClientDataID == TrackingDataID then + if ClientData.MessageToClient == "" then + ClientData.MessageToClient = "Missiles to You:\n" + end + ClientData.MessageToClient = ClientData.MessageToClient .. TrackingTo .. self:_AddRange( ClientData.Client, TrainerWeapon ) .. self:_AddBearing( ClientData.Client, TrainerWeapon ) .. "\n" + else + if self.TrackingToAll == true then + if ClientData.MessageToAll == "" then + ClientData.MessageToAll = "Missiles to other Players:\n" + end + ClientData.MessageToAll = ClientData.MessageToAll .. TrackingTo .. self:_AddRange( ClientData.Client, TrainerWeapon ) .. self:_AddBearing( ClientData.Client, TrainerWeapon ) .. " ( " .. TrainerTargetUnit:GetPlayerName() .. " )\n" + end + end + end + end + end + end + + -- Once the Player Client and the Other Player Client tracking messages are prepared, show them. + if ClientData.MessageToClient ~= "" or ClientData.MessageToAll ~= "" then + local Message = MESSAGE:New( ClientData.MessageToClient .. ClientData.MessageToAll, 1, "Tracking" ):ToClient( Client ) + end + end + end + + return true +end +--- **Functional** -- Monitor airbase traffic and regulate speed while taxiing. +-- +-- === +-- +-- ## Features: +-- +-- * Monitor speed of the airplanes of players during taxi. +-- * Communicate ATC ground operations. +-- * Kick speeding players during taxi. +-- +-- === +-- +-- ## Missions: +-- +-- [ABP - Airbase Police](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/ABP%20-%20Airbase%20Police) +-- +-- === +-- +-- ### Contributions: Dutch Baron - Concept & Testing +-- ### Author: FlightControl - Framework Design & Programming +-- +-- === +-- +-- @module Functional.ATC_Ground +-- @image Air_Traffic_Control_Ground_Operations.JPG + +--- @type ATC_GROUND +-- @field Core.Set#SET_CLIENT SetClient +-- @extends Core.Base#BASE + +--- Base class for ATC\_GROUND implementations. +-- @field #ATC_GROUND +ATC_GROUND = { + ClassName = "ATC_GROUND", + SetClient = nil, + Airbases = nil, + AirbaseNames = nil, + --KickSpeed = nil, -- The maximum speed in meters per second for all airbases until a player gets kicked. This is overridden at each derived class. +} + +--- @type ATC_GROUND.AirbaseNames +-- @list <#string> + + +--- Creates a new ATC\_GROUND object. +-- @param #ATC_GROUND self +-- @param Airbases A table of Airbase Names. +-- @return #ATC_GROUND self +function ATC_GROUND:New( Airbases, AirbaseList ) + + -- Inherits from BASE + local self = BASE:Inherit( self, BASE:New() ) -- #ATC_GROUND + self:E( { self.ClassName, Airbases } ) + + self.Airbases = Airbases + self.AirbaseList = AirbaseList + + self.SetClient = SET_CLIENT:New():FilterCategories( "plane" ):FilterStart() + + + for AirbaseID, Airbase in pairs( self.Airbases ) do + -- Specified ZoneBoundary is used if setted or Airbase radius by default + if Airbase.ZoneBoundary then + Airbase.ZoneBoundary = ZONE_POLYGON_BASE:New( "Boundary " .. AirbaseID, Airbase.ZoneBoundary ) + else + Airbase.ZoneBoundary = _DATABASE:FindAirbase( AirbaseID ):GetZone() + end + + Airbase.ZoneRunways = {} + for PointsRunwayID, PointsRunway in pairs( Airbase.PointsRunways ) do + Airbase.ZoneRunways[PointsRunwayID] = ZONE_POLYGON_BASE:New( "Runway " .. PointsRunwayID, PointsRunway ) + end + Airbase.Monitor = self.AirbaseList and false or true -- When AirbaseList is not given, monitor every Airbase, otherwise don't monitor any (yet). + end + + -- Now activate the monitoring for the airbases that need to be monitored. + for AirbaseID, AirbaseName in pairs( self.AirbaseList or {} ) do + self.Airbases[AirbaseName].Monitor = true + end + +-- -- Template +-- local TemplateBoundary = GROUP:FindByName( "Template Boundary" ) +-- self.Airbases.Template.ZoneBoundary = ZONE_POLYGON:New( "Template Boundary", TemplateBoundary ):SmokeZone(SMOKECOLOR.White):Flush() +-- +-- local TemplateRunway1 = GROUP:FindByName( "Template Runway 1" ) +-- self.Airbases.Template.ZoneRunways[1] = ZONE_POLYGON:New( "Template Runway 1", TemplateRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + + self.SetClient:ForEachClient( + --- @param Wrapper.Client#CLIENT Client + function( Client ) + Client:SetState( self, "Speeding", false ) + Client:SetState( self, "Warnings", 0) + Client:SetState( self, "IsOffRunway", false ) + Client:SetState( self, "OffRunwayWarnings", 0 ) + Client:SetState( self, "Taxi", false ) + end + ) + + -- This is simple slot blocker is used on the server. + SSB = USERFLAG:New( "SSB" ) + SSB:Set( 100 ) + + return self +end + + +--- Smoke the airbases runways. +-- @param #ATC_GROUND self +-- @param Utilities.Utils#SMOKECOLOR SmokeColor The color of the smoke around the runways. +-- @return #ATC_GROUND self +function ATC_GROUND:SmokeRunways( SmokeColor ) + + for AirbaseID, Airbase in pairs( self.Airbases ) do + for PointsRunwayID, PointsRunway in pairs( Airbase.PointsRunways ) do + Airbase.ZoneRunways[PointsRunwayID]:SmokeZone( SmokeColor ) + end + end +end + + +--- Set the maximum speed in meters per second (Mps) until the player gets kicked. +-- An airbase can be specified to set the kick speed for. +-- @param #ATC_GROUND self +-- @param #number KickSpeed The speed in Mps. +-- @param Wrapper.Airbase#AIRBASE Airbase (optional) The airbase to set the kick speed for. +-- @return #ATC_GROUND self +-- @usage +-- +-- -- Declare Atc_Ground using one of those, depending on the map. +-- +-- Atc_Ground = ATC_GROUND_CAUCAUS:New() +-- Atc_Ground = ATC_GROUND_NEVADA:New() +-- Atc_Ground = ATC_GROUND_NORMANDY:New() +-- Atc_Ground = ATC_GROUND_PERSIANGULF:New() +-- +-- -- Then use one of these methods... +-- +-- Atc_Ground:SetKickSpeed( UTILS.KmphToMps( 80 ) ) -- Kick the players at 80 kilometers per hour +-- +-- Atc_Ground:SetKickSpeed( UTILS.MiphToMps( 100 ) ) -- Kick the players at 100 miles per hour +-- +-- Atc_Ground:SetKickSpeed( 24 ) -- Kick the players at 24 meters per second ( 24 * 3.6 = 86.4 kilometers per hour ) +-- +function ATC_GROUND:SetKickSpeed( KickSpeed, Airbase ) + + if not Airbase then + self.KickSpeed = KickSpeed + else + self.Airbases[Airbase].KickSpeed = KickSpeed + end + + return self +end + +--- Set the maximum speed in Kmph until the player gets kicked. +-- @param #ATC_GROUND self +-- @param #number KickSpeed Set the speed in Kmph. +-- @param Wrapper.Airbase#AIRBASE Airbase (optional) The airbase to set the kick speed for. +-- @return #ATC_GROUND self +-- +-- Atc_Ground:SetKickSpeedKmph( 80 ) -- Kick the players at 80 kilometers per hour +-- +function ATC_GROUND:SetKickSpeedKmph( KickSpeed, Airbase ) + + self:SetKickSpeed( UTILS.KmphToMps( KickSpeed ), Airbase ) + + return self +end + +--- Set the maximum speed in Miph until the player gets kicked. +-- @param #ATC_GROUND self +-- @param #number KickSpeedMiph Set the speed in Mph. +-- @param Wrapper.Airbase#AIRBASE Airbase (optional) The airbase to set the kick speed for. +-- @return #ATC_GROUND self +-- +-- Atc_Ground:SetKickSpeedMiph( 100 ) -- Kick the players at 100 miles per hour +-- +function ATC_GROUND:SetKickSpeedMiph( KickSpeedMiph, Airbase ) + + self:SetKickSpeed( UTILS.MiphToMps( KickSpeedMiph ), Airbase ) + + return self +end + + +--- Set the maximum kick speed in meters per second (Mps) until the player gets kicked. +-- There are no warnings given if this speed is reached, and is to prevent players to take off from the airbase! +-- An airbase can be specified to set the maximum kick speed for. +-- @param #ATC_GROUND self +-- @param #number MaximumKickSpeed The speed in Mps. +-- @param Wrapper.Airbase#AIRBASE Airbase (optional) The airbase to set the kick speed for. +-- @return #ATC_GROUND self +-- @usage +-- +-- -- Declare Atc_Ground using one of those, depending on the map. +-- +-- Atc_Ground = ATC_GROUND_CAUCAUS:New() +-- Atc_Ground = ATC_GROUND_NEVADA:New() +-- Atc_Ground = ATC_GROUND_NORMANDY:New() +-- Atc_Ground = ATC_GROUND_PERSIANGULF:New() +-- +-- -- Then use one of these methods... +-- +-- Atc_Ground:SetMaximumKickSpeed( UTILS.KmphToMps( 80 ) ) -- Kick the players at 80 kilometers per hour +-- +-- Atc_Ground:SetMaximumKickSpeed( UTILS.MiphToMps( 100 ) ) -- Kick the players at 100 miles per hour +-- +-- Atc_Ground:SetMaximumKickSpeed( 24 ) -- Kick the players at 24 meters per second ( 24 * 3.6 = 86.4 kilometers per hour ) +-- +function ATC_GROUND:SetMaximumKickSpeed( MaximumKickSpeed, Airbase ) + + if not Airbase then + self.MaximumKickSpeed = MaximumKickSpeed + else + self.Airbases[Airbase].MaximumKickSpeed = MaximumKickSpeed + end + + return self +end + +--- Set the maximum kick speed in kilometers per hour (Kmph) until the player gets kicked. +-- There are no warnings given if this speed is reached, and is to prevent players to take off from the airbase! +-- An airbase can be specified to set the maximum kick speed for. +-- @param #ATC_GROUND self +-- @param #number MaximumKickSpeed Set the speed in Kmph. +-- @param Wrapper.Airbase#AIRBASE Airbase (optional) The airbase to set the kick speed for. +-- @return #ATC_GROUND self +-- +-- Atc_Ground:SetMaximumKickSpeedKmph( 150 ) -- Kick the players at 150 kilometers per hour +-- +function ATC_GROUND:SetMaximumKickSpeedKmph( MaximumKickSpeed, Airbase ) + + self:SetMaximumKickSpeed( UTILS.KmphToMps( MaximumKickSpeed ), Airbase ) + + return self +end + +--- Set the maximum kick speed in miles per hour (Miph) until the player gets kicked. +-- There are no warnings given if this speed is reached, and is to prevent players to take off from the airbase! +-- An airbase can be specified to set the maximum kick speed for. +-- @param #ATC_GROUND self +-- @param #number MaximumKickSpeedMiph Set the speed in Mph. +-- @param Wrapper.Airbase#AIRBASE Airbase (optional) The airbase to set the kick speed for. +-- @return #ATC_GROUND self +-- +-- Atc_Ground:SetMaximumKickSpeedMiph( 100 ) -- Kick the players at 100 miles per hour +-- +function ATC_GROUND:SetMaximumKickSpeedMiph( MaximumKickSpeedMiph, Airbase ) + + self:SetMaximumKickSpeed( UTILS.MiphToMps( MaximumKickSpeedMiph ), Airbase ) + + return self +end + + +--- @param #ATC_GROUND self +function ATC_GROUND:_AirbaseMonitor() + + self.SetClient:ForEachClient( + --- @param Wrapper.Client#CLIENT Client + function( Client ) + + if Client:IsAlive() then + + local IsOnGround = Client:InAir() == false + + for AirbaseID, AirbaseMeta in pairs( self.Airbases ) do + self:E( AirbaseID, AirbaseMeta.KickSpeed ) + + if AirbaseMeta.Monitor == true and Client:IsInZone( AirbaseMeta.ZoneBoundary ) then + + local NotInRunwayZone = true + for ZoneRunwayID, ZoneRunway in pairs( AirbaseMeta.ZoneRunways ) do + NotInRunwayZone = ( Client:IsNotInZone( ZoneRunway ) == true ) and NotInRunwayZone or false + end + + if NotInRunwayZone then + + if IsOnGround then + local Taxi = Client:GetState( self, "Taxi" ) + self:E( Taxi ) + if Taxi == false then + local Velocity = VELOCITY:New( AirbaseMeta.KickSpeed or self.KickSpeed ) + Client:Message( "Welcome to " .. AirbaseID .. ". The maximum taxiing speed is " .. + Velocity:ToString() , 20, "ATC" ) + Client:SetState( self, "Taxi", true ) + end + + -- TODO: GetVelocityKMH function usage + local Velocity = VELOCITY_POSITIONABLE:New( Client ) + --MESSAGE:New( "Velocity = " .. Velocity:ToString(), 1 ):ToAll() + local IsAboveRunway = Client:IsAboveRunway() + self:T( IsAboveRunway, IsOnGround ) + + if IsOnGround then + local Speeding = false + if AirbaseMeta.MaximumKickSpeed then + if Velocity:Get() > AirbaseMeta.MaximumKickSpeed then + Speeding = true + end + else + if Velocity:Get() > self.MaximumKickSpeed then + Speeding = true + end + end + if Speeding == true then + MESSAGE:New( "Penalty! Player " .. Client:GetPlayerName() .. + " has been kicked, due to a severe airbase traffic rule violation ...", 10, "ATC" ):ToAll() + Client:Destroy() + Client:SetState( self, "Speeding", false ) + Client:SetState( self, "Warnings", 0 ) + end + end + + + if IsOnGround then + + local Speeding = false + if AirbaseMeta.KickSpeed then -- If there is a speed defined for the airbase, use that only. + if Velocity:Get() > AirbaseMeta.KickSpeed then + Speeding = true + end + else + if Velocity:Get() > self.KickSpeed then + Speeding = true + end + end + if Speeding == true then + local IsSpeeding = Client:GetState( self, "Speeding" ) + + if IsSpeeding == true then + local SpeedingWarnings = Client:GetState( self, "Warnings" ) + self:T( SpeedingWarnings ) + + if SpeedingWarnings <= 3 then + Client:Message( "Warning " .. SpeedingWarnings .. "/3! Airbase traffic rule violation! Slow down now! Your speed is " .. + Velocity:ToString(), 5, "ATC" ) + Client:SetState( self, "Warnings", SpeedingWarnings + 1 ) + else + MESSAGE:New( "Penalty! Player " .. Client:GetPlayerName() .. " has been kicked, due to a severe airbase traffic rule violation ...", 10, "ATC" ):ToAll() + --- @param Wrapper.Client#CLIENT Client + Client:Destroy() + Client:SetState( self, "Speeding", false ) + Client:SetState( self, "Warnings", 0 ) + end + + else + Client:Message( "Attention! You are speeding on the taxiway, slow down! Your speed is " .. + Velocity:ToString(), 5, "ATC" ) + Client:SetState( self, "Speeding", true ) + Client:SetState( self, "Warnings", 1 ) + end + + else + Client:SetState( self, "Speeding", false ) + Client:SetState( self, "Warnings", 0 ) + end + end + + if IsOnGround and not IsAboveRunway then + + local IsOffRunway = Client:GetState( self, "IsOffRunway" ) + + if IsOffRunway == true then + local OffRunwayWarnings = Client:GetState( self, "OffRunwayWarnings" ) + self:T( OffRunwayWarnings ) + + if OffRunwayWarnings <= 3 then + Client:Message( "Warning " .. OffRunwayWarnings .. "/3! Airbase traffic rule violation! Get back on the taxi immediately!", 5, "ATC" ) + Client:SetState( self, "OffRunwayWarnings", OffRunwayWarnings + 1 ) + else + MESSAGE:New( "Penalty! Player " .. Client:GetPlayerName() .. " has been kicked, due to a severe airbase traffic rule violation ...", 10, "ATC" ):ToAll() + --- @param Wrapper.Client#CLIENT Client + Client:Destroy() + Client:SetState( self, "IsOffRunway", false ) + Client:SetState( self, "OffRunwayWarnings", 0 ) + end + else + Client:Message( "Attention! You are off the taxiway. Get back on the taxiway immediately!", 5, "ATC" ) + Client:SetState( self, "IsOffRunway", true ) + Client:SetState( self, "OffRunwayWarnings", 1 ) + end + + else + Client:SetState( self, "IsOffRunway", false ) + Client:SetState( self, "OffRunwayWarnings", 0 ) + end + end + else + Client:SetState( self, "Speeding", false ) + Client:SetState( self, "Warnings", 0 ) + Client:SetState( self, "IsOffRunway", false ) + Client:SetState( self, "OffRunwayWarnings", 0 ) + local Taxi = Client:GetState( self, "Taxi" ) + if Taxi == true then + Client:Message( "You have progressed to the runway ... Await take-off clearance ...", 20, "ATC" ) + Client:SetState( self, "Taxi", false ) + end + end + end + end + else + Client:SetState( self, "Taxi", false ) + end + end + ) + + return true +end + + +--- @type ATC_GROUND_CAUCASUS +-- @extends #ATC_GROUND + +--- # ATC\_GROUND\_CAUCASUS, extends @{#ATC_GROUND} +-- +-- The ATC\_GROUND\_CAUCASUS class monitors the speed of the airplanes at the airbase during taxi. +-- The pilots may not drive faster than the maximum speed for the airbase, or they will be despawned. +-- +-- --- +-- +-- ![Banner Image](..\Presentations\ATC_GROUND\Dia1.JPG) +-- +-- --- +-- +-- The default maximum speed for the airbases at Caucasus is **50 km/h**. Warnings are given if this speed limit is trespassed. +-- Players will be immediately kicked when driving faster than **150 km/h** on the taxi way. +-- +-- +-- The pilot will receive 3 times a warning during speeding. After the 3rd warning, if the pilot is still driving +-- faster than the maximum allowed speed, the pilot will be kicked. +-- +-- Different airbases have different maximum speeds, according safety regulations. +-- +-- # Airbases monitored +-- +-- The following airbases are monitored at the Caucasus region. +-- Use the @{Wrapper.Airbase#AIRBASE.Caucasus} enumeration to select the airbases to be monitored. +-- +-- * `AIRBASE.Caucasus.Anapa_Vityazevo` +-- * `AIRBASE.Caucasus.Batumi` +-- * `AIRBASE.Caucasus.Beslan` +-- * `AIRBASE.Caucasus.Gelendzhik` +-- * `AIRBASE.Caucasus.Gudauta` +-- * `AIRBASE.Caucasus.Kobuleti` +-- * `AIRBASE.Caucasus.Krasnodar_Center` +-- * `AIRBASE.Caucasus.Krasnodar_Pashkovsky` +-- * `AIRBASE.Caucasus.Krymsk` +-- * `AIRBASE.Caucasus.Kutaisi` +-- * `AIRBASE.Caucasus.Maykop_Khanskaya` +-- * `AIRBASE.Caucasus.Mineralnye_Vody` +-- * `AIRBASE.Caucasus.Mozdok` +-- * `AIRBASE.Caucasus.Nalchik` +-- * `AIRBASE.Caucasus.Novorossiysk` +-- * `AIRBASE.Caucasus.Senaki_Kolkhi` +-- * `AIRBASE.Caucasus.Sochi_Adler` +-- * `AIRBASE.Caucasus.Soganlug` +-- * `AIRBASE.Caucasus.Sukhumi_Babushara` +-- * `AIRBASE.Caucasus.Tbilisi_Lochini` +-- * `AIRBASE.Caucasus.Vaziani` +-- +-- +-- # Installation +-- +-- ## In Single Player Missions +-- +-- ATC\_GROUND is fully functional in single player. +-- +-- ## In Multi Player Missions +-- +-- ATC\_GROUND is functional in multi player, however ... +-- +-- Due to a bug in DCS since release 1.5, the despawning of clients are not anymore working in multi player. +-- To **work around this problem**, a much better solution has been made, using the **slot blocker** script designed +-- by Ciribob. +-- +-- With the help of __Ciribob__, this script has been extended to also kick client players while in flight. +-- ATC\_GROUND is communicating with this modified script to kick players! +-- +-- Install the file **SimpleSlotBlockGameGUI.lua** on the server, following the installation instructions described by Ciribob. +-- +-- [Simple Slot Blocker from Ciribob & FlightControl](https://github.com/ciribob/DCS-SimpleSlotBlock) +-- +-- # Script it! +-- +-- ## 1. ATC\_GROUND\_CAUCASUS Constructor +-- +-- Creates a new ATC_GROUND_CAUCASUS object that will monitor pilots taxiing behaviour. +-- +-- -- This creates a new ATC_GROUND_CAUCASUS object. +-- +-- -- Monitor all the airbases. +-- ATC_Ground = ATC_GROUND_CAUCASUS:New() +-- +-- -- Monitor specific airbases only. +-- +-- ATC_Ground = ATC_GROUND_CAUCASUS:New( +-- { AIRBASE.Caucasus.Gelendzhik, +-- AIRBASE.Caucasus.Krymsk +-- } +-- ) +-- +-- ## 2. Set various options +-- +-- There are various methods that you can use to tweak the behaviour of the ATC\_GROUND classes. +-- +-- ### 2.1 Speed limit at an airbase. +-- +-- * @{#ATC_GROUND.SetKickSpeed}(): Set the speed limit allowed at an airbase in meters per second. +-- * @{#ATC_GROUND.SetKickSpeedKmph}(): Set the speed limit allowed at an airbase in kilometers per hour. +-- * @{#ATC_GROUND.SetKickSpeedMiph}(): Set the speed limit allowed at an airbase in miles per hour. +-- +-- ### 2.2 Prevent Takeoff at an airbase. Players will be kicked immediately. +-- +-- * @{#ATC_GROUND.SetMaximumKickSpeed}(): Set the maximum speed allowed at an airbase in meters per second. +-- * @{#ATC_GROUND.SetMaximumKickSpeedKmph}(): Set the maximum speed allowed at an airbase in kilometers per hour. +-- * @{#ATC_GROUND.SetMaximumKickSpeedMiph}(): Set the maximum speed allowed at an airbase in miles per hour. +-- +-- +-- @field #ATC_GROUND_CAUCASUS +ATC_GROUND_CAUCASUS = { + ClassName = "ATC_GROUND_CAUCASUS", + Airbases = { + [AIRBASE.Caucasus.Anapa_Vityazevo] = { + PointsRunways = { + [1] = { + [1]={["y"]=242140.57142858,["x"]=-6478.8571428583,}, + [2]={["y"]=242188.57142858,["x"]=-6522.0000000011,}, + [3]={["y"]=244124.2857143,["x"]=-4344.0000000011,}, + [4]={["y"]=244068.2857143,["x"]=-4296.5714285726,}, + [5]={["y"]=242140.57142858,["x"]=-6480.0000000011,} + }, + }, + }, + [AIRBASE.Caucasus.Batumi] = { + PointsRunways = { + [1] = { + [1]={["y"]=616442.28571429,["x"]=-355090.28571429,}, + [2]={["y"]=618450.57142857,["x"]=-356522,}, + [3]={["y"]=618407.71428571,["x"]=-356584.85714286,}, + [4]={["y"]=618361.99999999,["x"]=-356554.85714286,}, + [5]={["y"]=618324.85714285,["x"]=-356599.14285715,}, + [6]={["y"]=618250.57142856,["x"]=-356543.42857143,}, + [7]={["y"]=618257.7142857,["x"]=-356496.28571429,}, + [8]={["y"]=618237.7142857,["x"]=-356459.14285715,}, + [9]={["y"]=616555.71428571,["x"]=-355258.85714286,}, + [10]={["y"]=616486.28571428,["x"]=-355280.57142858,}, + [11]={["y"]=616410.57142856,["x"]=-355227.71428572,}, + [12]={["y"]=616441.99999999,["x"]=-355179.14285715,}, + [13]={["y"]=616401.99999999,["x"]=-355147.71428572,}, + [14]={["y"]=616441.42857142,["x"]=-355092.57142858,}, + }, + }, + }, + [AIRBASE.Caucasus.Beslan] = { + PointsRunways = { + [1] = { + [1]={["y"]=842104.57142857,["x"]=-148460.57142857,}, + [2]={["y"]=845225.71428572,["x"]=-148656,}, + [3]={["y"]=845220.57142858,["x"]=-148750,}, + [4]={["y"]=842098.85714286,["x"]=-148556.28571429,}, + [5]={["y"]=842104,["x"]=-148460.28571429,}, + }, + }, + }, + [AIRBASE.Caucasus.Gelendzhik] = { + PointsRunways = { + [1] = { + [1]={["y"]=297834.00000001,["x"]=-51107.428571429,}, + [2]={["y"]=297786.57142858,["x"]=-51068.857142858,}, + [3]={["y"]=298946.57142858,["x"]=-49686.000000001,}, + [4]={["y"]=298993.14285715,["x"]=-49725.714285715,}, + [5]={["y"]=297835.14285715,["x"]=-51107.714285715,}, + }, + }, + }, + [AIRBASE.Caucasus.Gudauta] = { + PointsRunways = { + [1] = { + [1]={["y"]=517096.57142857,["x"]=-197804.57142857,}, + [2]={["y"]=515880.85714285,["x"]=-195590.28571429,}, + [3]={["y"]=515812.28571428,["x"]=-195628.85714286,}, + [4]={["y"]=517036.57142857,["x"]=-197834.57142857,}, + [5]={["y"]=517097.99999999,["x"]=-197807.42857143,}, + }, + }, + }, + [AIRBASE.Caucasus.Kobuleti] = { + PointsRunways = { + [1] = { + [1]={["y"]=634509.71428571,["x"]=-318339.42857144,}, + [2]={["y"]=636767.42857143,["x"]=-317516.57142858,}, + [3]={["y"]=636790,["x"]=-317575.71428572,}, + [4]={["y"]=634531.42857143,["x"]=-318398.00000001,}, + [5]={["y"]=634510.28571429,["x"]=-318339.71428572,}, + }, + }, + }, + [AIRBASE.Caucasus.Krasnodar_Center] = { + PointsRunways = { + [1] = { + [1]={["y"]=369205.42857144,["x"]=11789.142857142,}, + [2]={["y"]=369209.71428572,["x"]=11714.857142856,}, + [3]={["y"]=366699.71428572,["x"]=11581.714285713,}, + [4]={["y"]=366698.28571429,["x"]=11659.142857142,}, + [5]={["y"]=369208.85714286,["x"]=11788.57142857,}, + }, + }, + }, + [AIRBASE.Caucasus.Krasnodar_Pashkovsky] = { + PointsRunways = { + [1] = { + [1]={["y"]=385891.14285715,["x"]=8416.5714285703,}, + [2]={["y"]=385842.28571429,["x"]=8467.9999999989,}, + [3]={["y"]=384180.85714286,["x"]=6917.1428571417,}, + [4]={["y"]=384228.57142858,["x"]=6867.7142857132,}, + [5]={["y"]=385891.14285715,["x"]=8416.5714285703,}, + }, + [2] = { + [1]={["y"]=386714.85714286,["x"]=6674.857142856,}, + [2]={["y"]=386757.71428572,["x"]=6627.7142857132,}, + [3]={["y"]=389028.57142858,["x"]=8741.4285714275,}, + [4]={["y"]=388981.71428572,["x"]=8790.5714285703,}, + [5]={["y"]=386714.57142858,["x"]=6674.5714285703,}, + }, + }, + }, + [AIRBASE.Caucasus.Krymsk] = { + PointsRunways = { + [1] = { + [1]={["y"]=293522.00000001,["x"]=-7567.4285714297,}, + [2]={["y"]=293578.57142858,["x"]=-7616.0000000011,}, + [3]={["y"]=295246.00000001,["x"]=-5591.142857144,}, + [4]={["y"]=295187.71428573,["x"]=-5546.0000000011,}, + [5]={["y"]=293523.14285715,["x"]=-7568.2857142868,}, + }, + }, + }, + [AIRBASE.Caucasus.Kutaisi] = { + PointsRunways = { + [1] = { + [1]={["y"]=682638,["x"]=-285202.28571429,}, + [2]={["y"]=685050.28571429,["x"]=-284507.42857144,}, + [3]={["y"]=685068.85714286,["x"]=-284578.85714286,}, + [4]={["y"]=682657.42857143,["x"]=-285264.28571429,}, + [5]={["y"]=682638.28571429,["x"]=-285202.85714286,}, + }, + }, + }, + [AIRBASE.Caucasus.Maykop_Khanskaya] = { + PointsRunways = { + [1] = { + [1]={["y"]=457005.42857143,["x"]=-27668.000000001,}, + [2]={["y"]=459028.85714286,["x"]=-25168.857142858,}, + [3]={["y"]=459082.57142857,["x"]=-25216.857142858,}, + [4]={["y"]=457060,["x"]=-27714.285714287,}, + [5]={["y"]=457004.57142857,["x"]=-27669.714285715,}, + }, + }, + }, + [AIRBASE.Caucasus.Mineralnye_Vody] = { + PointsRunways = { + [1] = { + [1]={["y"]=703904,["x"]=-50352.571428573,}, + [2]={["y"]=707596.28571429,["x"]=-52094.571428573,}, + [3]={["y"]=707560.57142858,["x"]=-52161.714285716,}, + [4]={["y"]=703871.71428572,["x"]=-50420.571428573,}, + [5]={["y"]=703902,["x"]=-50352.000000002,}, + }, + }, + }, + [AIRBASE.Caucasus.Mozdok] = { + PointsRunways = { + [1] = { + [1]={["y"]=832201.14285715,["x"]=-83699.428571431,}, + [2]={["y"]=832212.57142857,["x"]=-83780.571428574,}, + [3]={["y"]=835730.28571429,["x"]=-83335.714285717,}, + [4]={["y"]=835718.85714286,["x"]=-83246.571428574,}, + [5]={["y"]=832200.57142857,["x"]=-83700.000000002,}, + }, + }, + }, + [AIRBASE.Caucasus.Nalchik] = { + PointsRunways = { + [1] = { + [1]={["y"]=759454.28571429,["x"]=-125551.42857143,}, + [2]={["y"]=759492.85714286,["x"]=-125610.85714286,}, + [3]={["y"]=761406.28571429,["x"]=-124304.28571429,}, + [4]={["y"]=761361.14285714,["x"]=-124239.71428572,}, + [5]={["y"]=759456,["x"]=-125552.57142857,}, + }, + }, + }, + [AIRBASE.Caucasus.Novorossiysk] = { + PointsRunways = { + [1] = { + [1]={["y"]=278673.14285716,["x"]=-41615.142857144,}, + [2]={["y"]=278625.42857144,["x"]=-41570.571428572,}, + [3]={["y"]=279835.42857144,["x"]=-40226.000000001,}, + [4]={["y"]=279882.2857143,["x"]=-40270.000000001,}, + [5]={["y"]=278672.00000001,["x"]=-41614.857142858,}, + }, + }, + }, + [AIRBASE.Caucasus.Senaki_Kolkhi] = { + PointsRunways = { + [1] = { + [1]={["y"]=646060.85714285,["x"]=-281736,}, + [2]={["y"]=646056.57142857,["x"]=-281631.71428571,}, + [3]={["y"]=648442.28571428,["x"]=-281840.28571428,}, + [4]={["y"]=648432.28571428,["x"]=-281918.85714286,}, + [5]={["y"]=646063.71428571,["x"]=-281738.85714286,}, + }, + }, + }, + [AIRBASE.Caucasus.Sochi_Adler] = { + PointsRunways = { + [1] = { + [1]={["y"]=460831.42857143,["x"]=-165180,}, + [2]={["y"]=460878.57142857,["x"]=-165257.14285714,}, + [3]={["y"]=463663.71428571,["x"]=-163793.14285714,}, + [4]={["y"]=463612.28571428,["x"]=-163697.42857143,}, + [5]={["y"]=460831.42857143,["x"]=-165177.14285714,}, + }, + [2] = { + [1]={["y"]=460831.42857143,["x"]=-165180,}, + [2]={["y"]=460878.57142857,["x"]=-165257.14285714,}, + [3]={["y"]=463663.71428571,["x"]=-163793.14285714,}, + [4]={["y"]=463612.28571428,["x"]=-163697.42857143,}, + [5]={["y"]=460831.42857143,["x"]=-165177.14285714,}, + }, + }, + }, + [AIRBASE.Caucasus.Soganlug] = { + PointsRunways = { + [1] = { + [1]={["y"]=894525.71428571,["x"]=-316964,}, + [2]={["y"]=896363.14285714,["x"]=-318634.28571428,}, + [3]={["y"]=896299.14285714,["x"]=-318702.85714286,}, + [4]={["y"]=894464,["x"]=-317031.71428571,}, + [5]={["y"]=894524.57142857,["x"]=-316963.71428571,}, + }, + }, + }, + [AIRBASE.Caucasus.Sukhumi_Babushara] = { + PointsRunways = { + [1] = { + [1]={["y"]=562684,["x"]=-219779.71428571,}, + [2]={["y"]=562717.71428571,["x"]=-219718,}, + [3]={["y"]=566046.85714286,["x"]=-221376.57142857,}, + [4]={["y"]=566012.28571428,["x"]=-221446.57142857,}, + [5]={["y"]=562684.57142857,["x"]=-219782.57142857,}, + }, + }, + }, + [AIRBASE.Caucasus.Tbilisi_Lochini] = { + PointsRunways = { + [1] = { + [1]={["y"]=895261.14285715,["x"]=-314652.28571428,}, + [2]={["y"]=897654.57142857,["x"]=-316523.14285714,}, + [3]={["y"]=897711.71428571,["x"]=-316450.28571429,}, + [4]={["y"]=895327.42857143,["x"]=-314568.85714286,}, + [5]={["y"]=895261.71428572,["x"]=-314656,}, + }, + [2] = { + [1]={["y"]=895605.71428572,["x"]=-314724.57142857,}, + [2]={["y"]=897639.71428572,["x"]=-316148,}, + [3]={["y"]=897683.42857143,["x"]=-316087.14285714,}, + [4]={["y"]=895650,["x"]=-314660,}, + [5]={["y"]=895606,["x"]=-314724.85714286,} + }, + }, + }, + [AIRBASE.Caucasus.Vaziani] = { + PointsRunways = { + [1] = { + [1]={["y"]=902239.14285714,["x"]=-318190.85714286,}, + [2]={["y"]=904014.28571428,["x"]=-319994.57142857,}, + [3]={["y"]=904064.85714285,["x"]=-319945.14285715,}, + [4]={["y"]=902294.57142857,["x"]=-318146,}, + [5]={["y"]=902247.71428571,["x"]=-318190.85714286,}, + }, + }, + }, + }, +} + +--- Creates a new ATC_GROUND_CAUCASUS object. +-- @param #ATC_GROUND_CAUCASUS self +-- @param AirbaseNames A list {} of airbase names (Use AIRBASE.Caucasus enumerator). +-- @return #ATC_GROUND_CAUCASUS self +function ATC_GROUND_CAUCASUS:New( AirbaseNames ) + + -- Inherits from BASE + local self = BASE:Inherit( self, ATC_GROUND:New( self.Airbases, AirbaseNames ) ) + + self:SetKickSpeedKmph( 50 ) + self:SetMaximumKickSpeedKmph( 150 ) + + -- -- AnapaVityazevo + -- local AnapaVityazevoBoundary = GROUP:FindByName( "AnapaVityazevo Boundary" ) + -- self.Airbases.AnapaVityazevo.ZoneBoundary = ZONE_POLYGON:New( "AnapaVityazevo Boundary", AnapaVityazevoBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local AnapaVityazevoRunway1 = GROUP:FindByName( "AnapaVityazevo Runway 1" ) + -- self.Airbases.AnapaVityazevo.ZoneRunways[1] = ZONE_POLYGON:New( "AnapaVityazevo Runway 1", AnapaVityazevoRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- Batumi + -- local BatumiBoundary = GROUP:FindByName( "Batumi Boundary" ) + -- self.Airbases.Batumi.ZoneBoundary = ZONE_POLYGON:New( "Batumi Boundary", BatumiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local BatumiRunway1 = GROUP:FindByName( "Batumi Runway 1" ) + -- self.Airbases.Batumi.ZoneRunways[1] = ZONE_POLYGON:New( "Batumi Runway 1", BatumiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- Beslan + -- local BeslanBoundary = GROUP:FindByName( "Beslan Boundary" ) + -- self.Airbases.Beslan.ZoneBoundary = ZONE_POLYGON:New( "Beslan Boundary", BeslanBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local BeslanRunway1 = GROUP:FindByName( "Beslan Runway 1" ) + -- self.Airbases.Beslan.ZoneRunways[1] = ZONE_POLYGON:New( "Beslan Runway 1", BeslanRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- Gelendzhik + -- local GelendzhikBoundary = GROUP:FindByName( "Gelendzhik Boundary" ) + -- self.Airbases.Gelendzhik.ZoneBoundary = ZONE_POLYGON:New( "Gelendzhik Boundary", GelendzhikBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local GelendzhikRunway1 = GROUP:FindByName( "Gelendzhik Runway 1" ) + -- self.Airbases.Gelendzhik.ZoneRunways[1] = ZONE_POLYGON:New( "Gelendzhik Runway 1", GelendzhikRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- Gudauta + -- local GudautaBoundary = GROUP:FindByName( "Gudauta Boundary" ) + -- self.Airbases.Gudauta.ZoneBoundary = ZONE_POLYGON:New( "Gudauta Boundary", GudautaBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local GudautaRunway1 = GROUP:FindByName( "Gudauta Runway 1" ) + -- self.Airbases.Gudauta.ZoneRunways[1] = ZONE_POLYGON:New( "Gudauta Runway 1", GudautaRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- Kobuleti + -- local KobuletiBoundary = GROUP:FindByName( "Kobuleti Boundary" ) + -- self.Airbases.Kobuleti.ZoneBoundary = ZONE_POLYGON:New( "Kobuleti Boundary", KobuletiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local KobuletiRunway1 = GROUP:FindByName( "Kobuleti Runway 1" ) + -- self.Airbases.Kobuleti.ZoneRunways[1] = ZONE_POLYGON:New( "Kobuleti Runway 1", KobuletiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- KrasnodarCenter + -- local KrasnodarCenterBoundary = GROUP:FindByName( "KrasnodarCenter Boundary" ) + -- self.Airbases.KrasnodarCenter.ZoneBoundary = ZONE_POLYGON:New( "KrasnodarCenter Boundary", KrasnodarCenterBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local KrasnodarCenterRunway1 = GROUP:FindByName( "KrasnodarCenter Runway 1" ) + -- self.Airbases.KrasnodarCenter.ZoneRunways[1] = ZONE_POLYGON:New( "KrasnodarCenter Runway 1", KrasnodarCenterRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- KrasnodarPashkovsky + -- local KrasnodarPashkovskyBoundary = GROUP:FindByName( "KrasnodarPashkovsky Boundary" ) + -- self.Airbases.KrasnodarPashkovsky.ZoneBoundary = ZONE_POLYGON:New( "KrasnodarPashkovsky Boundary", KrasnodarPashkovskyBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local KrasnodarPashkovskyRunway1 = GROUP:FindByName( "KrasnodarPashkovsky Runway 1" ) + -- self.Airbases.KrasnodarPashkovsky.ZoneRunways[1] = ZONE_POLYGON:New( "KrasnodarPashkovsky Runway 1", KrasnodarPashkovskyRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- local KrasnodarPashkovskyRunway2 = GROUP:FindByName( "KrasnodarPashkovsky Runway 2" ) + -- self.Airbases.KrasnodarPashkovsky.ZoneRunways[2] = ZONE_POLYGON:New( "KrasnodarPashkovsky Runway 2", KrasnodarPashkovskyRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- Krymsk + -- local KrymskBoundary = GROUP:FindByName( "Krymsk Boundary" ) + -- self.Airbases.Krymsk.ZoneBoundary = ZONE_POLYGON:New( "Krymsk Boundary", KrymskBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local KrymskRunway1 = GROUP:FindByName( "Krymsk Runway 1" ) + -- self.Airbases.Krymsk.ZoneRunways[1] = ZONE_POLYGON:New( "Krymsk Runway 1", KrymskRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- Kutaisi + -- local KutaisiBoundary = GROUP:FindByName( "Kutaisi Boundary" ) + -- self.Airbases.Kutaisi.ZoneBoundary = ZONE_POLYGON:New( "Kutaisi Boundary", KutaisiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local KutaisiRunway1 = GROUP:FindByName( "Kutaisi Runway 1" ) + -- self.Airbases.Kutaisi.ZoneRunways[1] = ZONE_POLYGON:New( "Kutaisi Runway 1", KutaisiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- MaykopKhanskaya + -- local MaykopKhanskayaBoundary = GROUP:FindByName( "MaykopKhanskaya Boundary" ) + -- self.Airbases.MaykopKhanskaya.ZoneBoundary = ZONE_POLYGON:New( "MaykopKhanskaya Boundary", MaykopKhanskayaBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local MaykopKhanskayaRunway1 = GROUP:FindByName( "MaykopKhanskaya Runway 1" ) + -- self.Airbases.MaykopKhanskaya.ZoneRunways[1] = ZONE_POLYGON:New( "MaykopKhanskaya Runway 1", MaykopKhanskayaRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- MineralnyeVody + -- local MineralnyeVodyBoundary = GROUP:FindByName( "MineralnyeVody Boundary" ) + -- self.Airbases.MineralnyeVody.ZoneBoundary = ZONE_POLYGON:New( "MineralnyeVody Boundary", MineralnyeVodyBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local MineralnyeVodyRunway1 = GROUP:FindByName( "MineralnyeVody Runway 1" ) + -- self.Airbases.MineralnyeVody.ZoneRunways[1] = ZONE_POLYGON:New( "MineralnyeVody Runway 1", MineralnyeVodyRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- Mozdok + -- local MozdokBoundary = GROUP:FindByName( "Mozdok Boundary" ) + -- self.Airbases.Mozdok.ZoneBoundary = ZONE_POLYGON:New( "Mozdok Boundary", MozdokBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local MozdokRunway1 = GROUP:FindByName( "Mozdok Runway 1" ) + -- self.Airbases.Mozdok.ZoneRunways[1] = ZONE_POLYGON:New( "Mozdok Runway 1", MozdokRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- Nalchik + -- local NalchikBoundary = GROUP:FindByName( "Nalchik Boundary" ) + -- self.Airbases.Nalchik.ZoneBoundary = ZONE_POLYGON:New( "Nalchik Boundary", NalchikBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local NalchikRunway1 = GROUP:FindByName( "Nalchik Runway 1" ) + -- self.Airbases.Nalchik.ZoneRunways[1] = ZONE_POLYGON:New( "Nalchik Runway 1", NalchikRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- Novorossiysk + -- local NovorossiyskBoundary = GROUP:FindByName( "Novorossiysk Boundary" ) + -- self.Airbases.Novorossiysk.ZoneBoundary = ZONE_POLYGON:New( "Novorossiysk Boundary", NovorossiyskBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local NovorossiyskRunway1 = GROUP:FindByName( "Novorossiysk Runway 1" ) + -- self.Airbases.Novorossiysk.ZoneRunways[1] = ZONE_POLYGON:New( "Novorossiysk Runway 1", NovorossiyskRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- SenakiKolkhi + -- local SenakiKolkhiBoundary = GROUP:FindByName( "SenakiKolkhi Boundary" ) + -- self.Airbases.SenakiKolkhi.ZoneBoundary = ZONE_POLYGON:New( "SenakiKolkhi Boundary", SenakiKolkhiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local SenakiKolkhiRunway1 = GROUP:FindByName( "SenakiKolkhi Runway 1" ) + -- self.Airbases.SenakiKolkhi.ZoneRunways[1] = ZONE_POLYGON:New( "SenakiKolkhi Runway 1", SenakiKolkhiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- SochiAdler + -- local SochiAdlerBoundary = GROUP:FindByName( "SochiAdler Boundary" ) + -- self.Airbases.SochiAdler.ZoneBoundary = ZONE_POLYGON:New( "SochiAdler Boundary", SochiAdlerBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local SochiAdlerRunway1 = GROUP:FindByName( "SochiAdler Runway 1" ) + -- self.Airbases.SochiAdler.ZoneRunways[1] = ZONE_POLYGON:New( "SochiAdler Runway 1", SochiAdlerRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- local SochiAdlerRunway2 = GROUP:FindByName( "SochiAdler Runway 2" ) + -- self.Airbases.SochiAdler.ZoneRunways[2] = ZONE_POLYGON:New( "SochiAdler Runway 2", SochiAdlerRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- Soganlug + -- local SoganlugBoundary = GROUP:FindByName( "Soganlug Boundary" ) + -- self.Airbases.Soganlug.ZoneBoundary = ZONE_POLYGON:New( "Soganlug Boundary", SoganlugBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local SoganlugRunway1 = GROUP:FindByName( "Soganlug Runway 1" ) + -- self.Airbases.Soganlug.ZoneRunways[1] = ZONE_POLYGON:New( "Soganlug Runway 1", SoganlugRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- SukhumiBabushara + -- local SukhumiBabusharaBoundary = GROUP:FindByName( "SukhumiBabushara Boundary" ) + -- self.Airbases.SukhumiBabushara.ZoneBoundary = ZONE_POLYGON:New( "SukhumiBabushara Boundary", SukhumiBabusharaBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local SukhumiBabusharaRunway1 = GROUP:FindByName( "SukhumiBabushara Runway 1" ) + -- self.Airbases.SukhumiBabushara.ZoneRunways[1] = ZONE_POLYGON:New( "SukhumiBabushara Runway 1", SukhumiBabusharaRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- TbilisiLochini + -- local TbilisiLochiniBoundary = GROUP:FindByName( "TbilisiLochini Boundary" ) + -- self.Airbases.TbilisiLochini.ZoneBoundary = ZONE_POLYGON:New( "TbilisiLochini Boundary", TbilisiLochiniBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local TbilisiLochiniRunway1 = GROUP:FindByName( "TbilisiLochini Runway 1" ) + -- self.Airbases.TbilisiLochini.ZoneRunways[1] = ZONE_POLYGON:New( "TbilisiLochini Runway 1", TbilisiLochiniRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- local TbilisiLochiniRunway2 = GROUP:FindByName( "TbilisiLochini Runway 2" ) + -- self.Airbases.TbilisiLochini.ZoneRunways[2] = ZONE_POLYGON:New( "TbilisiLochini Runway 2", TbilisiLochiniRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- Vaziani + -- local VazianiBoundary = GROUP:FindByName( "Vaziani Boundary" ) + -- self.Airbases.Vaziani.ZoneBoundary = ZONE_POLYGON:New( "Vaziani Boundary", VazianiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local VazianiRunway1 = GROUP:FindByName( "Vaziani Runway 1" ) + -- self.Airbases.Vaziani.ZoneRunways[1] = ZONE_POLYGON:New( "Vaziani Runway 1", VazianiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + + + -- Template + -- local TemplateBoundary = GROUP:FindByName( "Template Boundary" ) + -- self.Airbases.Template.ZoneBoundary = ZONE_POLYGON:New( "Template Boundary", TemplateBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local TemplateRunway1 = GROUP:FindByName( "Template Runway 1" ) + -- self.Airbases.Template.ZoneRunways[1] = ZONE_POLYGON:New( "Template Runway 1", TemplateRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + + return self +end + + +--- Start SCHEDULER for ATC_GROUND_CAUCASUS object. +-- @param #ATC_GROUND_CAUCASUS self +-- @param RepeatScanSeconds Time in second for defining occurency of alerts. +-- @return nothing +function ATC_GROUND_CAUCASUS:Start( RepeatScanSeconds ) + RepeatScanSeconds = RepeatScanSeconds or 0.05 + self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, RepeatScanSeconds ) +end + + + +--- @type ATC_GROUND_NEVADA +-- @extends #ATC_GROUND + + +--- # ATC\_GROUND\_NEVADA, extends @{#ATC_GROUND} +-- +-- The ATC\_GROUND\_NEVADA class monitors the speed of the airplanes at the airbase during taxi. +-- The pilots may not drive faster than the maximum speed for the airbase, or they will be despawned. +-- +-- --- +-- +-- ![Banner Image](..\Presentations\ATC_GROUND\Dia1.JPG) +-- +-- --- +-- +-- The default maximum speed for the airbases at Nevada is **50 km/h**. Warnings are given if this speed limit is trespassed. +-- Players will be immediately kicked when driving faster than **150 km/h** on the taxi way. +-- +-- The ATC\_GROUND\_NEVADA class monitors the speed of the airplanes at the airbase during taxi. +-- The pilots may not drive faster than the maximum speed for the airbase, or they will be despawned. +-- +-- The pilot will receive 3 times a warning during speeding. After the 3rd warning, if the pilot is still driving +-- faster than the maximum allowed speed, the pilot will be kicked. +-- +-- Different airbases have different maximum speeds, according safety regulations. +-- +-- # Airbases monitored +-- +-- The following airbases are monitored at the Nevada region. +-- Use the @{Wrapper.Airbase#AIRBASE.Nevada} enumeration to select the airbases to be monitored. +-- +-- * `AIRBASE.Nevada.Beatty_Airport` +-- * `AIRBASE.Nevada.Boulder_City_Airport` +-- * `AIRBASE.Nevada.Creech_AFB` +-- * `AIRBASE.Nevada.Echo_Bay` +-- * `AIRBASE.Nevada.Groom_Lake_AFB` +-- * `AIRBASE.Nevada.Henderson_Executive_Airport` +-- * `AIRBASE.Nevada.Jean_Airport` +-- * `AIRBASE.Nevada.Laughlin_Airport` +-- * `AIRBASE.Nevada.Lincoln_County` +-- * `AIRBASE.Nevada.McCarran_International_Airport` +-- * `AIRBASE.Nevada.Mesquite` +-- * `AIRBASE.Nevada.Mina_Airport_3Q0` +-- * `AIRBASE.Nevada.Nellis_AFB` +-- * `AIRBASE.Nevada.North_Las_Vegas` +-- * `AIRBASE.Nevada.Pahute_Mesa_Airstrip` +-- * `AIRBASE.Nevada.Tonopah_Airport` +-- * `AIRBASE.Nevada.Tonopah_Test_Range_Airfield` +-- +-- # Installation +-- +-- ## In Single Player Missions +-- +-- ATC\_GROUND is fully functional in single player. +-- +-- ## In Multi Player Missions +-- +-- ATC\_GROUND is functional in multi player, however ... +-- +-- Due to a bug in DCS since release 1.5, the despawning of clients are not anymore working in multi player. +-- To **work around this problem**, a much better solution has been made, using the **slot blocker** script designed +-- by Ciribob. +-- +-- With the help of __Ciribob__, this script has been extended to also kick client players while in flight. +-- ATC\_GROUND is communicating with this modified script to kick players! +-- +-- Install the file **SimpleSlotBlockGameGUI.lua** on the server, following the installation instructions described by Ciribob. +-- +-- [Simple Slot Blocker from Ciribob & FlightControl](https://github.com/ciribob/DCS-SimpleSlotBlock) +-- +-- # Script it! +-- +-- ## 1. ATC_GROUND_NEVADA Constructor +-- +-- Creates a new ATC_GROUND_NEVADA object that will monitor pilots taxiing behaviour. +-- +-- -- This creates a new ATC_GROUND_NEVADA object. +-- +-- -- Monitor all the airbases. +-- ATC_Ground = ATC_GROUND_NEVADA:New() +-- +-- +-- -- Monitor specific airbases. +-- ATC_Ground = ATC_GROUND_NEVADA:New( +-- { AIRBASE.Nevada.Laughlin_Airport, +-- AIRBASE.Nevada.Lincoln_County, +-- AIRBASE.Nevada.North_Las_Vegas, +-- AIRBASE.Nevada.McCarran_International_Airport +-- } +-- ) +-- +-- ## 2. Set various options +-- +-- There are various methods that you can use to tweak the behaviour of the ATC\_GROUND classes. +-- +-- ### 2.1 Speed limit at an airbase. +-- +-- * @{#ATC_GROUND.SetKickSpeed}(): Set the speed limit allowed at an airbase in meters per second. +-- * @{#ATC_GROUND.SetKickSpeedKmph}(): Set the speed limit allowed at an airbase in kilometers per hour. +-- * @{#ATC_GROUND.SetKickSpeedMiph}(): Set the speed limit allowed at an airbase in miles per hour. +-- +-- ### 2.2 Prevent Takeoff at an airbase. Players will be kicked immediately. +-- +-- * @{#ATC_GROUND.SetMaximumKickSpeed}(): Set the maximum speed allowed at an airbase in meters per second. +-- * @{#ATC_GROUND.SetMaximumKickSpeedKmph}(): Set the maximum speed allowed at an airbase in kilometers per hour. +-- * @{#ATC_GROUND.SetMaximumKickSpeedMiph}(): Set the maximum speed allowed at an airbase in miles per hour. +-- +-- +-- @field #ATC_GROUND_NEVADA +ATC_GROUND_NEVADA = { + ClassName = "ATC_GROUND_NEVADA", + Airbases = { + + [AIRBASE.Nevada.Beatty_Airport] = { + PointsRunways = { + [1] = { + [1]={["y"]=-174950.05857143,["x"]=-329679.65,}, + [2]={["y"]=-174946.53828571,["x"]=-331394.03885715,}, + [3]={["y"]=-174967.10971429,["x"]=-331394.32457143,}, + [4]={["y"]=-174971.01828571,["x"]=-329682.59171429,}, + }, + }, + }, + [AIRBASE.Nevada.Boulder_City_Airport] = { + PointsRunways = { + [1] = { + [1] = {["y"]=-1317.841714286,["x"]=-429014.92857142,}, + [2] = {["y"]=-951.26228571458,["x"]=-430310.21142856,}, + [3] = {["y"]=-978.11942857172,["x"]=-430317.06857142,}, + [4] = {["y"]=-1347.5088571432,["x"]=-429023.98485713,}, + }, + [2] = { + [1] = {["y"]=-1879.955714286,["x"]=-429783.83742856,}, + [2] = {["y"]=-256.25257142886,["x"]=-430023.63542856,}, + [3] = {["y"]=-260.25257142886,["x"]=-430048.77828571,}, + [4] = {["y"]=-1883.955714286,["x"]=-429807.83742856,}, + }, + }, + }, + [AIRBASE.Nevada.Creech_AFB] = { + PointsRunways = { + [1] = { + [1] = {["y"]=-74234.729142857,["x"]=-360501.80857143,}, + [2] = {["y"]=-77606.122285714,["x"]=-360417.86542857,}, + [3] = {["y"]=-77608.578,["x"]=-360486.13428571,}, + [4] = {["y"]=-74237.930571428,["x"]=-360586.25628571,}, + }, + [2] = { + [1] = {["y"]=-75807.571428572,["x"]=-359073.42857142,}, + [2] = {["y"]=-74770.142857144,["x"]=-360581.71428571,}, + [3] = {["y"]=-74641.285714287,["x"]=-360585.42857142,}, + [4] = {["y"]=-75734.142857144,["x"]=-359023.14285714,}, + }, + }, + }, + [AIRBASE.Nevada.Echo_Bay] = { + PointsRunways = { + [1] = { + [1] = {["y"]=33182.919428572,["x"]=-388698.21657142,}, + [2] = {["y"]=34202.543142857,["x"]=-388469.55485714,}, + [3] = {["y"]=34207.686,["x"]=-388488.69771428,}, + [4] = {["y"]=33185.422285715,["x"]=-388717.82228571,}, + }, + }, + }, + [AIRBASE.Nevada.Groom_Lake_AFB] = { + PointsRunways = { + [1] = { + [1] = {["y"]=-85971.465428571,["x"]=-290567.77,}, + [2] = {["y"]=-87691.155428571,["x"]=-286637.75428571,}, + [3] = {["y"]=-87756.714285715,["x"]=-286663.99999999,}, + [4] = {["y"]=-86035.940285714,["x"]=-290598.81314286,}, + }, + [2] = { + [1] = {["y"]=-86741.547142857,["x"]=-290353.31971428,}, + [2] = {["y"]=-89672.714285714,["x"]=-283546.57142855,}, + [3] = {["y"]=-89772.142857143,["x"]=-283587.71428569,}, + [4] = {["y"]=-86799.623714285,["x"]=-290374.16771428,}, + }, + }, + }, + [AIRBASE.Nevada.Henderson_Executive_Airport] = { + PointsRunways = { + [1] = { + [1] = {["y"]=-25837.500571429,["x"]=-426404.25257142,}, + [2] = {["y"]=-25843.509428571,["x"]=-428752.67942856,}, + [3] = {["y"]=-25902.343714286,["x"]=-428749.96399999,}, + [4] = {["y"]=-25934.667142857,["x"]=-426411.45657142,}, + }, + [2] = { + [1] = {["y"]=-25650.296285714,["x"]=-426510.17971428,}, + [2] = {["y"]=-25632.443428571,["x"]=-428297.11428571,}, + [3] = {["y"]=-25686.690285714,["x"]=-428299.37457142,}, + [4] = {["y"]=-25708.296285714,["x"]=-426515.15114285,}, + }, + }, + }, + [AIRBASE.Nevada.Jean_Airport] = { + PointsRunways = { + [1] = { + [1] = {["y"]=-42549.187142857,["x"]=-449663.23257143,}, + [2] = {["y"]=-43367.466285714,["x"]=-451044.77657143,}, + [3] = {["y"]=-43395.180571429,["x"]=-451028.20514286,}, + [4] = {["y"]=-42579.893142857,["x"]=-449648.18371428,}, + }, + [2] = { + [1] = {["y"]=-42588.359428572,["x"]=-449900.14342857,}, + [2] = {["y"]=-43349.698285714,["x"]=-451185.46857143,}, + [3] = {["y"]=-43369.624571429,["x"]=-451173.49342857,}, + [4] = {["y"]=-42609.216571429,["x"]=-449891.28628571,}, + }, + }, + }, + [AIRBASE.Nevada.Laughlin_Airport] = { + PointsRunways = { + [1] = { + [1] = {["y"]=28231.600857143,["x"]=-515555.94114286,}, + [2] = {["y"]=28453.728285714,["x"]=-518170.78885714,}, + [3] = {["y"]=28370.788285714,["x"]=-518176.25742857,}, + [4] = {["y"]=28138.022857143,["x"]=-515573.07514286,}, + }, + [2] = { + [1] = {["y"]=28231.600857143,["x"]=-515555.94114286,}, + [2] = {["y"]=28453.728285714,["x"]=-518170.78885714,}, + [3] = {["y"]=28370.788285714,["x"]=-518176.25742857,}, + [4] = {["y"]=28138.022857143,["x"]=-515573.07514286,}, + }, + }, + }, + [AIRBASE.Nevada.Lincoln_County] = { + PointsRunways = { + [1] = { + [1]={["y"]=33222.34171429,["x"]=-223959.40171429,}, + [2]={["y"]=33200.040000004,["x"]=-225369.36828572,}, + [3]={["y"]=33177.634571428,["x"]=-225369.21485715,}, + [4]={["y"]=33201.198857147,["x"]=-223960.54457143,}, + }, + }, + }, + [AIRBASE.Nevada.McCarran_International_Airport] = { + PointsRunways = { + [1] = { + [1] = {["y"]=-29406.035714286,["x"]=-416102.48199999,}, + [2] = {["y"]=-24680.714285715,["x"]=-416003.14285713,}, + [3] = {["y"]=-24681.857142858,["x"]=-415926.57142856,}, + [4] = {["y"]=-29408.42857143,["x"]=-416016.57142856,}, + }, + [2] = { + [1] = {["y"]=-28567.221714286,["x"]=-416378.61799999,}, + [2] = {["y"]=-25109.912285714,["x"]=-416309.92914285,}, + [3] = {["y"]=-25112.508,["x"]=-416240.78714285,}, + [4] = {["y"]=-28576.247428571,["x"]=-416308.49514285,}, + }, + [3] = { + [1] = {["y"]=-29255.953142857,["x"]=-416307.10657142,}, + [2] = {["y"]=-28005.571428572,["x"]=-413449.7142857,}, + [3] = {["y"]=-28068.714285715,["x"]=-413422.85714284,}, + [4] = {["y"]=-29331.000000001,["x"]=-416275.7142857,}, + }, + [4] = { + [1] = {["y"]=-28994.901714286,["x"]=-416423.0522857,}, + [2] = {["y"]=-27697.571428572,["x"]=-413464.57142856,}, + [3] = {["y"]=-27767.857142858,["x"]=-413434.28571427,}, + [4] = {["y"]=-29073.000000001,["x"]=-416386.85714284,}, + }, + }, + }, + [AIRBASE.Nevada.Mesquite] = { + PointsRunways = { + [1] = { + [1] = {["y"]=68188.340285714,["x"]=-330302.54742857,}, + [2] = {["y"]=68911.303428571,["x"]=-328920.76571429,}, + [3] = {["y"]=68936.927142857,["x"]=-328933.888,}, + [4] = {["y"]=68212.460285714,["x"]=-330317.19171429,}, + }, + }, + }, + [AIRBASE.Nevada.Mina_Airport_3Q0] = { + PointsRunways = { + [1] = { + [1] = {["y"]=-290054.57371429,["x"]=-160930.02228572,}, + [2] = {["y"]=-289469.77457143,["x"]=-162048.73571429,}, + [3] = {["y"]=-289520.06028572,["x"]=-162074.73571429,}, + [4] = {["y"]=-290104.69085714,["x"]=-160956.19457143,}, + }, + }, + }, + [AIRBASE.Nevada.Nellis_AFB] = { + PointsRunways = { + [1] = { + [1] = {["y"]=-18614.218571428,["x"]=-399437.91085714,}, + [2] = {["y"]=-16217.857142857,["x"]=-396596.85714286,}, + [3] = {["y"]=-16300.142857143,["x"]=-396530,}, + [4] = {["y"]=-18692.543428571,["x"]=-399381.31114286,}, + }, + [2] = { + [1] = {["y"]=-18388.948857143,["x"]=-399630.51828571,}, + [2] = {["y"]=-16011,["x"]=-396806.85714286,}, + [3] = {["y"]=-16074.714285714,["x"]=-396751.71428572,}, + [4] = {["y"]=-18451.571428572,["x"]=-399580.85714285,}, + }, + }, + }, + [AIRBASE.Nevada.Pahute_Mesa_Airstrip] = { + PointsRunways = { + [1] = { + [1] = {["y"]=-132690.40942857,["x"]=-302733.53085714,}, + [2] = {["y"]=-133112.43228571,["x"]=-304499.70742857,}, + [3] = {["y"]=-133179.91685714,["x"]=-304485.544,}, + [4] = {["y"]=-132759.988,["x"]=-302723.326,}, + }, + }, + }, + [AIRBASE.Nevada.Tonopah_Test_Range_Airfield] = { + PointsRunways = { + [1] = { + [1] = {["y"]=-175389.162,["x"]=-224778.07685715,}, + [2] = {["y"]=-173942.15485714,["x"]=-228210.27571429,}, + [3] = {["y"]=-174001.77085714,["x"]=-228233.60371429,}, + [4] = {["y"]=-175452.38685714,["x"]=-224806.84200001,}, + }, + }, + }, + [AIRBASE.Nevada.Tonopah_Airport] = { + PointsRunways = { + [1] = { + [1] = {["y"]=-202128.25228571,["x"]=-196701.34314286,}, + [2] = {["y"]=-201562.40828571,["x"]=-198814.99714286,}, + [3] = {["y"]=-201591.44828571,["x"]=-198820.93714286,}, + [4] = {["y"]=-202156.06828571,["x"]=-196707.68714286,}, + }, + [2] = { + [1] = {["y"]=-202084.57171428,["x"]=-196722.02228572,}, + [2] = {["y"]=-200592.75485714,["x"]=-197768.05571429,}, + [3] = {["y"]=-200605.37285714,["x"]=-197783.49228572,}, + [4] = {["y"]=-202097.14314285,["x"]=-196739.16514286,}, + }, + }, + }, + [AIRBASE.Nevada.North_Las_Vegas] = { + PointsRunways = { + [1] = { + [1] = {["y"]=-32599.017714286,["x"]=-400913.26485714,}, + [2] = {["y"]=-30881.068857143,["x"]=-400837.94628571,}, + [3] = {["y"]=-30879.354571428,["x"]=-400873.08914285,}, + [4] = {["y"]=-32595.966285714,["x"]=-400947.13571428,}, + }, + [2] = { + [1] = {["y"]=-32499.448571428,["x"]=-400690.99514285,}, + [2] = {["y"]=-31247.514857143,["x"]=-401868.95571428,}, + [3] = {["y"]=-31271.802857143,["x"]=-401894.97857142,}, + [4] = {["y"]=-32520.02,["x"]=-400716.99514285,}, + }, + [3] = { + [1] = {["y"]=-31865.254857143,["x"]=-400999.74057143,}, + [2] = {["y"]=-30893.604,["x"]=-401908.85742857,}, + [3] = {["y"]=-30915.578857143,["x"]=-401936.03685714,}, + [4] = {["y"]=-31884.969142858,["x"]=-401020.59771429,}, + }, + }, + }, + }, +} + +--- Creates a new ATC_GROUND_NEVADA object. +-- @param #ATC_GROUND_NEVADA self +-- @param AirbaseNames A list {} of airbase names (Use AIRBASE.Nevada enumerator). +-- @return #ATC_GROUND_NEVADA self +function ATC_GROUND_NEVADA:New( AirbaseNames ) + + -- Inherits from BASE + local self = BASE:Inherit( self, ATC_GROUND:New( self.Airbases, AirbaseNames ) ) + + self:SetKickSpeedKmph( 50 ) + self:SetMaximumKickSpeedKmph( 150 ) + + -- These lines here are for the demonstration mission. + -- They create in the dcs.log the coordinates of the runway polygons, that are then + -- taken by the moose designer from the dcs.log and reworked to define the + -- Airbases structure, which is part of the class. + -- When new airbases are added or airbases are changed on the map, + -- the MOOSE designer willde-comment this section and apply the changes in the demo + -- mission, and do a re-run to create a new dcs.log, and then add the changed coordinates + -- in the Airbases structure. + -- So, this needs to stay commented normally once a map has been finished. + + --[[ + + -- Beatty + do + local VillagePrefix = "Beatty" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + -- Boulder + do + local VillagePrefix = "Boulder" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) + local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + -- Creech + do + local VillagePrefix = "Creech" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) + local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + -- Echo + do + local VillagePrefix = "Echo" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + -- Groom Lake + do + local VillagePrefix = "GroomLake" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) + local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + -- Henderson + do + local VillagePrefix = "Henderson" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) + local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + -- Jean + do + local VillagePrefix = "Jean" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) + local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + -- Laughlin + do + local VillagePrefix = "Laughlin" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + -- Lincoln + do + local VillagePrefix = "Lincoln" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + -- McCarran + do + local VillagePrefix = "McCarran" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) + local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway3 = GROUP:FindByName( VillagePrefix .. " 3" ) + local Zone3 = ZONE_POLYGON:New( VillagePrefix .. " 3", Runway3 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway4 = GROUP:FindByName( VillagePrefix .. " 4" ) + local Zone4 = ZONE_POLYGON:New( VillagePrefix .. " 4", Runway4 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + -- Mesquite + do + local VillagePrefix = "Mesquite" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + -- Mina + do + local VillagePrefix = "Mina" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + -- Nellis + do + local VillagePrefix = "Nellis" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) + local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + -- Pahute + do + local VillagePrefix = "Pahute" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + -- TonopahTR + do + local VillagePrefix = "TonopahTR" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + -- Tonopah + do + local VillagePrefix = "Tonopah" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) + local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + -- Vegas + do + local VillagePrefix = "Vegas" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) + local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway3 = GROUP:FindByName( VillagePrefix .. " 3" ) + local Zone3 = ZONE_POLYGON:New( VillagePrefix .. " 3", Runway3 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + --]] + + return self +end + +--- Start SCHEDULER for ATC_GROUND_NEVADA object. +-- @param #ATC_GROUND_NEVADA self +-- @param RepeatScanSeconds Time in second for defining occurency of alerts. +-- @return nothing +function ATC_GROUND_NEVADA:Start( RepeatScanSeconds ) + RepeatScanSeconds = RepeatScanSeconds or 0.05 + self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, RepeatScanSeconds ) +end + + +--- @type ATC_GROUND_NORMANDY +-- @extends #ATC_GROUND + + +--- # ATC\_GROUND\_NORMANDY, extends @{#ATC_GROUND} +-- +-- The ATC\_GROUND\_NORMANDY class monitors the speed of the airplanes at the airbase during taxi. +-- The pilots may not drive faster than the maximum speed for the airbase, or they will be despawned. +-- +-- --- +-- +-- ![Banner Image](..\Presentations\ATC_GROUND\Dia1.JPG) +-- +-- --- +-- +-- The default maximum speed for the airbases at Normandy is **40 km/h**. Warnings are given if this speed limit is trespassed. +-- Players will be immediately kicked when driving faster than **100 km/h** on the taxi way. +-- +-- The ATC\_GROUND\_NORMANDY class monitors the speed of the airplanes at the airbase during taxi. +-- The pilots may not drive faster than the maximum speed for the airbase, or they will be despawned. +-- +-- The pilot will receive 3 times a warning during speeding. After the 3rd warning, if the pilot is still driving +-- faster than the maximum allowed speed, the pilot will be kicked. +-- +-- Different airbases have different maximum speeds, according safety regulations. +-- +-- # Airbases monitored +-- +-- The following airbases are monitored at the Normandy region. +-- Use the @{Wrapper.Airbase#AIRBASE.Normandy} enumeration to select the airbases to be monitored. +-- +-- * `AIRBASE.Normandy.Azeville` +-- * `AIRBASE.Normandy.Bazenville` +-- * `AIRBASE.Normandy.Beny_sur_Mer` +-- * `AIRBASE.Normandy.Beuzeville` +-- * `AIRBASE.Normandy.Biniville` +-- * `AIRBASE.Normandy.Brucheville` +-- * `AIRBASE.Normandy.Cardonville` +-- * `AIRBASE.Normandy.Carpiquet` +-- * `AIRBASE.Normandy.Chailey` +-- * `AIRBASE.Normandy.Chippelle` +-- * `AIRBASE.Normandy.Cretteville` +-- * `AIRBASE.Normandy.Cricqueville_en_Bessin` +-- * `AIRBASE.Normandy.Deux_Jumeaux` +-- * `AIRBASE.Normandy.Evreux` +-- * `AIRBASE.Normandy.Ford` +-- * `AIRBASE.Normandy.Funtington` +-- * `AIRBASE.Normandy.Lantheuil` +-- * `AIRBASE.Normandy.Le_Molay` +-- * `AIRBASE.Normandy.Lessay` +-- * `AIRBASE.Normandy.Lignerolles` +-- * `AIRBASE.Normandy.Longues_sur_Mer` +-- * `AIRBASE.Normandy.Maupertus` +-- * `AIRBASE.Normandy.Meautis` +-- * `AIRBASE.Normandy.Needs_Oar_Point` +-- * `AIRBASE.Normandy.Picauville` +-- * `AIRBASE.Normandy.Rucqueville` +-- * `AIRBASE.Normandy.Saint_Pierre_du_Mont` +-- * `AIRBASE.Normandy.Sainte_Croix_sur_Mer` +-- * `AIRBASE.Normandy.Sainte_Laurent_sur_Mer` +-- * `AIRBASE.Normandy.Sommervieu` +-- * `AIRBASE.Normandy.Tangmere` +-- * `AIRBASE.Normandy.Argentan` +-- * `AIRBASE.Normandy.Goulet` +-- * `AIRBASE.Normandy.Essay` +-- * `AIRBASE.Normandy.Hauterive` +-- * `AIRBASE.Normandy.Barville` +-- * `AIRBASE.Normandy.Conches` +-- * `AIRBASE.Normandy.Vrigny` +-- +-- # Installation +-- +-- ## In Single Player Missions +-- +-- ATC\_GROUND is fully functional in single player. +-- +-- ## In Multi Player Missions +-- +-- ATC\_GROUND is functional in multi player, however ... +-- +-- Due to a bug in DCS since release 1.5, the despawning of clients are not anymore working in multi player. +-- To **work around this problem**, a much better solution has been made, using the **slot blocker** script designed +-- by Ciribob. +-- +-- With the help of __Ciribob__, this script has been extended to also kick client players while in flight. +-- ATC\_GROUND is communicating with this modified script to kick players! +-- +-- Install the file **SimpleSlotBlockGameGUI.lua** on the server, following the installation instructions described by Ciribob. +-- +-- [Simple Slot Blocker from Ciribob & FlightControl](https://github.com/ciribob/DCS-SimpleSlotBlock) +-- +-- # Script it! +-- +-- ## 1. ATC_GROUND_NORMANDY Constructor +-- +-- Creates a new ATC_GROUND_NORMANDY object that will monitor pilots taxiing behaviour. +-- +-- -- This creates a new ATC_GROUND_NORMANDY object. +-- +-- -- Monitor for these clients the airbases. +-- AirbasePoliceCaucasus = ATC_GROUND_NORMANDY:New() +-- +-- ATC_Ground = ATC_GROUND_NORMANDY:New( +-- { AIRBASE.Normandy.Chippelle, +-- AIRBASE.Normandy.Beuzeville +-- } +-- ) +-- +-- +-- ## 2. Set various options +-- +-- There are various methods that you can use to tweak the behaviour of the ATC\_GROUND classes. +-- +-- ### 2.1 Speed limit at an airbase. +-- +-- * @{#ATC_GROUND.SetKickSpeed}(): Set the speed limit allowed at an airbase in meters per second. +-- * @{#ATC_GROUND.SetKickSpeedKmph}(): Set the speed limit allowed at an airbase in kilometers per hour. +-- * @{#ATC_GROUND.SetKickSpeedMiph}(): Set the speed limit allowed at an airbase in miles per hour. +-- +-- ### 2.2 Prevent Takeoff at an airbase. Players will be kicked immediately. +-- +-- * @{#ATC_GROUND.SetMaximumKickSpeed}(): Set the maximum speed allowed at an airbase in meters per second. +-- * @{#ATC_GROUND.SetMaximumKickSpeedKmph}(): Set the maximum speed allowed at an airbase in kilometers per hour. +-- * @{#ATC_GROUND.SetMaximumKickSpeedMiph}(): Set the maximum speed allowed at an airbase in miles per hour. +-- +-- @field #ATC_GROUND_NORMANDY +ATC_GROUND_NORMANDY = { + ClassName = "ATC_GROUND_NORMANDY", + Airbases = { + [AIRBASE.Normandy.Azeville] = { + PointsRunways = { + [1] = { + [1]={["y"]=-74194.387714285,["x"]=-2691.1399999998,}, + [2]={["y"]=-73160.282571428,["x"]=-2310.0274285712,}, + [3]={["y"]=-73141.711142857,["x"]=-2357.7417142855,}, + [4]={["y"]=-74176.959142857,["x"]=-2741.997142857,}, + }, + }, + }, + [AIRBASE.Normandy.Bazenville] = { + PointsRunways = { + [1] = { + [1]={["y"]=-19246.209999999,["x"]=-21246.748,}, + [2]={["y"]=-17883.70142857,["x"]=-20219.009714285,}, + [3]={["y"]=-17855.415714285,["x"]=-20256.438285714,}, + [4]={["y"]=-19217.791999999,["x"]=-21283.597714285,}, + }, + }, + }, + [AIRBASE.Normandy.Beny_sur_Mer] = { + PointsRunways = { + [1] = { + [1]={["y"]=-8592.7442857133,["x"]=-20386.15542857,}, + [2]={["y"]=-8404.4931428561,["x"]=-21744.113142856,}, + [3]={["y"]=-8267.9917142847,["x"]=-21724.97742857,}, + [4]={["y"]=-8451.0482857133,["x"]=-20368.87542857,}, + }, + }, + }, + [AIRBASE.Normandy.Beuzeville] = { + PointsRunways = { + [1] = { + [1]={["y"]=-71552.573428571,["x"]=-8744.3688571427,}, + [2]={["y"]=-72577.765714285,["x"]=-9638.5682857141,}, + [3]={["y"]=-72609.304285714,["x"]=-9601.2954285712,}, + [4]={["y"]=-71585.849428571,["x"]=-8709.9648571426,}, + }, + }, + }, + [AIRBASE.Normandy.Biniville] = { + PointsRunways = { + [1] = { + [1]={["y"]=-84757.320285714,["x"]=-7377.1354285713,}, + [2]={["y"]=-84271.482,["x"]=-7956.4859999999,}, + [3]={["y"]=-84299.482,["x"]=-7981.6288571427,}, + [4]={["y"]=-84784.969714286,["x"]=-7402.0588571427,}, + }, + }, + }, + [AIRBASE.Normandy.Brucheville] = { + PointsRunways = { + [1] = { + [1]={["y"]=-65546.792857142,["x"]=-14615.640857143,}, + [2]={["y"]=-66914.692,["x"]=-15232.713714285,}, + [3]={["y"]=-66896.527714285,["x"]=-15271.948571428,}, + [4]={["y"]=-65528.393714285,["x"]=-14657.995714286,}, + }, + }, + }, + [AIRBASE.Normandy.Cardonville] = { + PointsRunways = { + [1] = { + [1]={["y"]=-54280.445428571,["x"]=-15843.749142857,}, + [2]={["y"]=-53646.998571428,["x"]=-17143.012285714,}, + [3]={["y"]=-53683.93,["x"]=-17161.317428571,}, + [4]={["y"]=-54323.354571428,["x"]=-15855.004,}, + }, + }, + }, + [AIRBASE.Normandy.Carpiquet] = { + PointsRunways = { + [1] = { + [1]={["y"]=-10751.325714285,["x"]=-34229.494,}, + [2]={["y"]=-9283.5279999993,["x"]=-35192.352857142,}, + [3]={["y"]=-9325.2005714274,["x"]=-35260.967714285,}, + [4]={["y"]=-10794.90942857,["x"]=-34287.041428571,}, + }, + }, + }, + [AIRBASE.Normandy.Chailey] = { + PointsRunways = { + [1] = { + [1]={["y"]=12895.585714292,["x"]=164683.05657144,}, + [2]={["y"]=11410.727142863,["x"]=163606.54485715,}, + [3]={["y"]=11363.012857149,["x"]=163671.97342858,}, + [4]={["y"]=12797.537142863,["x"]=164711.01857144,}, + [5]={["y"]=12862.902857149,["x"]=164726.99685715,}, + }, + [2] = { + [1]={["y"]=11805.316000006,["x"]=164502.90971429,}, + [2]={["y"]=11997.280857149,["x"]=163032.65542858,}, + [3]={["y"]=11918.640857149,["x"]=163023.04657144,}, + [4]={["y"]=11726.973428578,["x"]=164489.94257143,}, + }, + }, + }, + [AIRBASE.Normandy.Chippelle] = { + PointsRunways = { + [1] = { + [1]={["y"]=-48540.313999999,["x"]=-28884.795999999,}, + [2]={["y"]=-47251.820285713,["x"]=-28140.128571427,}, + [3]={["y"]=-47274.551714285,["x"]=-28103.758285713,}, + [4]={["y"]=-48555.657714285,["x"]=-28839.90142857,}, + }, + }, + }, + [AIRBASE.Normandy.Cretteville] = { + PointsRunways = { + [1] = { + [1]={["y"]=-78351.723142857,["x"]=-18177.725428571,}, + [2]={["y"]=-77220.322285714,["x"]=-19125.687714286,}, + [3]={["y"]=-77247.899428571,["x"]=-19158.49,}, + [4]={["y"]=-78380.008857143,["x"]=-18208.011142857,}, + }, + }, + }, + [AIRBASE.Normandy.Cricqueville_en_Bessin] = { + PointsRunways = { + [1] = { + [1]={["y"]=-50875.034571428,["x"]=-14322.404571428,}, + [2]={["y"]=-50681.148571428,["x"]=-15825.258,}, + [3]={["y"]=-50717.434285713,["x"]=-15829.829428571,}, + [4]={["y"]=-50910.569428571,["x"]=-14327.562857142,}, + }, + }, + }, + [AIRBASE.Normandy.Deux_Jumeaux] = { + PointsRunways = { + [1] = { + [1]={["y"]=-49575.410857142,["x"]=-16575.161142857,}, + [2]={["y"]=-48149.077999999,["x"]=-16952.193428571,}, + [3]={["y"]=-48159.935142856,["x"]=-16996.764857142,}, + [4]={["y"]=-49584.839428571,["x"]=-16617.732571428,}, + }, + }, + }, + [AIRBASE.Normandy.Evreux] = { + PointsRunways = { + [1] = { + [1]={["y"]=112906.84828572,["x"]=-45585.824857142,}, + [2]={["y"]=112050.38228572,["x"]=-46811.871999999,}, + [3]={["y"]=111980.05371429,["x"]=-46762.173142856,}, + [4]={["y"]=112833.54542857,["x"]=-45540.010571428,}, + }, + [2] = { + [1]={["y"]=112046.02085714,["x"]=-45091.056571428,}, + [2]={["y"]=112488.668,["x"]=-46623.617999999,}, + [3]={["y"]=112405.66914286,["x"]=-46647.419142856,}, + [4]={["y"]=111966.03657143,["x"]=-45112.604285713,}, + }, + }, + }, + [AIRBASE.Normandy.Ford_AF] = { + PointsRunways = { + [1] = { + [1]={["y"]=-26506.13971428,["x"]=147514.39971429,}, + [2]={["y"]=-25012.977428565,["x"]=147566.14485715,}, + [3]={["y"]=-25009.851428565,["x"]=147482.63600001,}, + [4]={["y"]=-26503.693999994,["x"]=147427.33228572,}, + }, + [2] = { + [1]={["y"]=-25169.701999994,["x"]=148421.09257143,}, + [2]={["y"]=-26092.421999994,["x"]=147190.89628572,}, + [3]={["y"]=-26158.136285708,["x"]=147240.89628572,}, + [4]={["y"]=-25252.357999994,["x"]=148448.64457143,}, + }, + }, + }, + [AIRBASE.Normandy.Funtington] = { + PointsRunways = { + [1] = { + [1]={["y"]=-44698.388571423,["x"]=152952.17257143,}, + [2]={["y"]=-46452.993142851,["x"]=152388.77885714,}, + [3]={["y"]=-46476.361142851,["x"]=152470.05885714,}, + [4]={["y"]=-44787.256571423,["x"]=153009.52,}, + [5]={["y"]=-44715.581428566,["x"]=153002.08714286,}, + }, + [2] = { + [1]={["y"]=-45792.665999994,["x"]=153123.894,}, + [2]={["y"]=-46068.084857137,["x"]=151665.98342857,}, + [3]={["y"]=-46148.632285708,["x"]=151681.58685714,}, + [4]={["y"]=-45871.25971428,["x"]=153136.82714286,}, + }, + }, + }, + [AIRBASE.Normandy.Lantheuil] = { + PointsRunways = { + [1] = { + [1]={["y"]=-17158.84542857,["x"]=-24602.999428571,}, + [2]={["y"]=-15978.59342857,["x"]=-23922.978571428,}, + [3]={["y"]=-15932.021999999,["x"]=-24004.121428571,}, + [4]={["y"]=-17090.734857142,["x"]=-24673.248,}, + }, + }, + }, + [AIRBASE.Normandy.Lessay] = { + PointsRunways = { + [1] = { + [1]={["y"]=-87667.304571429,["x"]=-33220.165714286,}, + [2]={["y"]=-86146.607714286,["x"]=-34248.483142857,}, + [3]={["y"]=-86191.538285714,["x"]=-34316.991142857,}, + [4]={["y"]=-87712.212,["x"]=-33291.774857143,}, + }, + [2] = { + [1]={["y"]=-87125.123142857,["x"]=-34183.682571429,}, + [2]={["y"]=-85803.278285715,["x"]=-33498.428857143,}, + [3]={["y"]=-85768.408285715,["x"]=-33570.13,}, + [4]={["y"]=-87087.688571429,["x"]=-34258.272285715,}, + }, + }, + }, + [AIRBASE.Normandy.Lignerolles] = { + PointsRunways = { + [1] = { + [1]={["y"]=-35279.611714285,["x"]=-35232.026857142,}, + [2]={["y"]=-33804.948857142,["x"]=-35770.713999999,}, + [3]={["y"]=-33789.876285713,["x"]=-35726.655714284,}, + [4]={["y"]=-35263.548285713,["x"]=-35192.75542857,}, + }, + }, + }, + [AIRBASE.Normandy.Longues_sur_Mer] = { + PointsRunways = { + [1] = { + [1]={["y"]=-29444.070285713,["x"]=-16334.105428571,}, + [2]={["y"]=-28265.52942857,["x"]=-17011.557999999,}, + [3]={["y"]=-28344.74742857,["x"]=-17143.587999999,}, + [4]={["y"]=-29529.616285713,["x"]=-16477.766571428,}, + }, + }, + }, + [AIRBASE.Normandy.Maupertus] = { + PointsRunways = { + [1] = { + [1]={["y"]=-85605.340857143,["x"]=16175.267714286,}, + [2]={["y"]=-84132.567142857,["x"]=15895.905714286,}, + [3]={["y"]=-84139.995142857,["x"]=15847.623714286,}, + [4]={["y"]=-85613.626571429,["x"]=16132.410571429,}, + }, + }, + }, + [AIRBASE.Normandy.Meautis] = { + PointsRunways = { + [1] = { + [1]={["y"]=-72642.527714286,["x"]=-24593.622285714,}, + [2]={["y"]=-71298.672571429,["x"]=-24352.651142857,}, + [3]={["y"]=-71290.101142857,["x"]=-24398.365428571,}, + [4]={["y"]=-72631.715714286,["x"]=-24639.966857143,}, + }, + }, + }, + [AIRBASE.Normandy.Le_Molay] = { + PointsRunways = { + [1] = { + [1]={["y"]=-41876.526857142,["x"]=-26701.052285713,}, + [2]={["y"]=-40979.545714285,["x"]=-25675.045999999,}, + [3]={["y"]=-41017.687428571,["x"]=-25644.272571427,}, + [4]={["y"]=-41913.638285713,["x"]=-26665.137999999,}, + }, + }, + }, + [AIRBASE.Normandy.Needs_Oar_Point] = { + PointsRunways = { + [1] = { + [1]={["y"]=-83882.441142851,["x"]=141429.83314286,}, + [2]={["y"]=-85138.159428566,["x"]=140187.52828572,}, + [3]={["y"]=-85208.323428566,["x"]=140161.04371429,}, + [4]={["y"]=-85245.751999994,["x"]=140201.61514286,}, + [5]={["y"]=-83939.966571423,["x"]=141485.22085714,}, + }, + [2] = { + [1]={["y"]=-84528.76571428,["x"]=141988.01428572,}, + [2]={["y"]=-84116.98971428,["x"]=140565.78685714,}, + [3]={["y"]=-84199.35771428,["x"]=140541.14685714,}, + [4]={["y"]=-84605.051428566,["x"]=141966.01428572,}, + }, + }, + }, + [AIRBASE.Normandy.Picauville] = { + PointsRunways = { + [1] = { + [1]={["y"]=-80808.838571429,["x"]=-11834.554571428,}, + [2]={["y"]=-79531.574285714,["x"]=-12311.274,}, + [3]={["y"]=-79549.355428571,["x"]=-12356.928285714,}, + [4]={["y"]=-80827.815142857,["x"]=-11901.835142857,}, + }, + }, + }, + [AIRBASE.Normandy.Rucqueville] = { + PointsRunways = { + [1] = { + [1]={["y"]=-20023.988857141,["x"]=-26569.565428571,}, + [2]={["y"]=-18688.92542857,["x"]=-26571.086571428,}, + [3]={["y"]=-18688.012571427,["x"]=-26611.252285713,}, + [4]={["y"]=-20022.218857141,["x"]=-26608.505428571,}, + }, + }, + }, + [AIRBASE.Normandy.Saint_Pierre_du_Mont] = { + PointsRunways = { + [1] = { + [1]={["y"]=-48015.384571428,["x"]=-11886.631714285,}, + [2]={["y"]=-46540.412285713,["x"]=-11945.226571428,}, + [3]={["y"]=-46541.349999999,["x"]=-11991.174571428,}, + [4]={["y"]=-48016.837142856,["x"]=-11929.371142857,}, + }, + }, + }, + [AIRBASE.Normandy.Sainte_Croix_sur_Mer] = { + PointsRunways = { + [1] = { + [1]={["y"]=-15877.817999999,["x"]=-18812.579999999,}, + [2]={["y"]=-14464.377142856,["x"]=-18807.46,}, + [3]={["y"]=-14463.879714285,["x"]=-18759.706857142,}, + [4]={["y"]=-15878.229142856,["x"]=-18764.071428571,}, + }, + }, + }, + [AIRBASE.Normandy.Sainte_Laurent_sur_Mer] = { + PointsRunways = { + [1] = { + [1]={["y"]=-41676.834857142,["x"]=-14475.109428571,}, + [2]={["y"]=-40566.11142857,["x"]=-14817.319999999,}, + [3]={["y"]=-40579.543999999,["x"]=-14860.059999999,}, + [4]={["y"]=-41687.120571427,["x"]=-14509.680857142,}, + }, + }, + }, + [AIRBASE.Normandy.Sommervieu] = { + PointsRunways = { + [1] = { + [1]={["y"]=-26821.913714284,["x"]=-21390.466571427,}, + [2]={["y"]=-25465.308857142,["x"]=-21296.859999999,}, + [3]={["y"]=-25462.451714284,["x"]=-21343.717142856,}, + [4]={["y"]=-26818.002285713,["x"]=-21440.532857142,}, + }, + }, + }, + [AIRBASE.Normandy.Tangmere] = { + PointsRunways = { + [1] = { + [1]={["y"]=-34684.581142851,["x"]=150459.61657143,}, + [2]={["y"]=-33250.625428566,["x"]=149954.17,}, + [3]={["y"]=-33275.724285708,["x"]=149874.69028572,}, + [4]={["y"]=-34709.020571423,["x"]=150377.93742857,}, + }, + [2] = { + [1]={["y"]=-33103.438857137,["x"]=150812.72542857,}, + [2]={["y"]=-34410.246285708,["x"]=150009.73142857,}, + [3]={["y"]=-34453.535142851,["x"]=150082.02685714,}, + [4]={["y"]=-33176.545999994,["x"]=150870.22542857,}, + }, + }, + }, + [AIRBASE.Normandy.Argentan] = { + PointsRunways = { + [1] = { + [1]={["y"]=22322.280338032,["x"]=-78607.309765269,}, + [2]={["y"]=23032.778713963,["x"]=-78967.17709893,}, + [3]={["y"]=23015.27074041,["x"]=-79008.02903722,}, + [4]={["y"]=22299.944963827,["x"]=-78650.366148928,}, + }, + }, + }, + [AIRBASE.Normandy.Goulet] = { + PointsRunways = { + [1] = { + [1]={["y"]=24901.788373185,["x"]=-89139.367511763,}, + [2]={["y"]=25459.965967043,["x"]=-89709.67940114,}, + [3]={["y"]=25422.459962713,["x"]=-89741.669816598,}, + [4]={["y"]=24857.663662208,["x"]=-89173.56416277,}, + }, + }, + }, + [AIRBASE.Normandy.Essay] = { + PointsRunways = { + [1] = { + [1]={["y"]=44610.072022849,["x"]=-105469.21149064,}, + [2]={["y"]=45417.939023956,["x"]=-105536.08535277,}, + [3]={["y"]=45412.558368383,["x"]=-105585.27991801,}, + [4]={["y"]=44602.38537203,["x"]=-105516.10006064,}, + }, + }, + }, + [AIRBASE.Normandy.Hauterive] = { + PointsRunways = { + [1] = { + [1]={["y"]=40617.185360953,["x"]=-107657.10147517,}, + [2]={["y"]=41114.628372034,["x"]=-108298.77015609,}, + [3]={["y"]=41080.006684855,["x"]=-108319.06562788,}, + [4]={["y"]=40584.558402807,["x"]=-107692.29370481,}, + }, + }, + }, + [AIRBASE.Normandy.Vrigny] = { + PointsRunways = { + [1] = { + [1]={["y"]=24892.131051827,["x"]=-89131.628297486,}, + [2]={["y"]=25469.738000575,["x"]=-89709.235246234,}, + [3]={["y"]=25418.869206793,["x"]=-89738.771965204,}, + [4]={["y"]=24859.312475193,["x"]=-89171.010589446,}, + }, + }, + }, + [AIRBASE.Normandy.Barville] = { + PointsRunways = { + [1] = { + [1]={["y"]=49027.850333166,["x"]=-109217.05049066,}, + [2]={["y"]=49755.022185805,["x"]=-110346.63783457,}, + [3]={["y"]=49682.657996586,["x"]=-110401.35222154,}, + [4]={["y"]=48921.951519675,["x"]=-109285.88471943,}, + }, + [2] = { + [1]={["y"]=48429.522036941,["x"]=-109818.90874734,}, + [2]={["y"]=49746.197284681,["x"]=-109954.81222465,}, + [3]={["y"]=49735.607403332,["x"]=-110032.47135455,}, + [4]={["y"]=48420.697135816,["x"]=-109900.09783768,}, + }, + }, + }, + [AIRBASE.Normandy.Conches] = { + PointsRunways = { + [1] = { + [1]={["y"]=95099.187473266,["x"]=-56389.619005858,}, + [2]={["y"]=95181.545025963,["x"]=-56465.440244849,}, + [3]={["y"]=94071.678958666,["x"]=-57627.596821795,}, + [4]={["y"]=94005.008558864,["x"]=-57558.31189651,}, + }, + }, + }, + }, +} + + +--- Creates a new ATC_GROUND_NORMANDY object. +-- @param #ATC_GROUND_NORMANDY self +-- @param AirbaseNames A list {} of airbase names (Use AIRBASE.Normandy enumerator). +-- @return #ATC_GROUND_NORMANDY self +function ATC_GROUND_NORMANDY:New( AirbaseNames ) + + -- Inherits from BASE + local self = BASE:Inherit( self, ATC_GROUND:New( self.Airbases, AirbaseNames ) ) -- #ATC_GROUND_NORMANDY + + self:SetKickSpeedKmph( 40 ) + self:SetMaximumKickSpeedKmph( 100 ) + + -- These lines here are for the demonstration mission. + -- They create in the dcs.log the coordinates of the runway polygons, that are then + -- taken by the moose designer from the dcs.log and reworked to define the + -- Airbases structure, which is part of the class. + -- When new airbases are added or airbases are changed on the map, + -- the MOOSE designer willde-comment this section and apply the changes in the demo + -- mission, and do a re-run to create a new dcs.log, and then add the changed coordinates + -- in the Airbases structure. + -- So, this needs to stay commented normally once a map has been finished. + + --[[ + + -- Azeville + do + local VillagePrefix = "Azeville" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Bazenville + do + local VillagePrefix = "Bazenville" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Beny + do + local VillagePrefix = "Beny" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Beuzeville + do + local VillagePrefix = "Beuzeville" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Biniville + do + local VillagePrefix = "Biniville" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Brucheville + do + local VillagePrefix = "Brucheville" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Cardonville + do + local VillagePrefix = "Cardonville" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Carpiquet + do + local VillagePrefix = "Carpiquet" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Chailey + do + local VillagePrefix = "Chailey" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) + local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Chippelle + do + local VillagePrefix = "Chippelle" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Cretteville + do + local VillagePrefix = "Cretteville" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Cricqueville + do + local VillagePrefix = "Cricqueville" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Deux + do + local VillagePrefix = "Deux" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Evreux + do + local VillagePrefix = "Evreux" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) + local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Ford + do + local VillagePrefix = "Ford" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) + local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Funtington + do + local VillagePrefix = "Funtington" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) + local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Lantheuil + do + local VillagePrefix = "Lantheuil" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Lessay + do + local VillagePrefix = "Lessay" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) + local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Lignerolles + do + local VillagePrefix = "Lignerolles" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Longues + do + local VillagePrefix = "Longues" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Maupertus + do + local VillagePrefix = "Maupertus" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Meautis + do + local VillagePrefix = "Meautis" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Molay + do + local VillagePrefix = "Molay" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Oar + do + local VillagePrefix = "Oar" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) + local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Picauville + do + local VillagePrefix = "Picauville" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Rucqueville + do + local VillagePrefix = "Rucqueville" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- SaintPierre + do + local VillagePrefix = "SaintPierre" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- SainteCroix + do + local VillagePrefix = "SainteCroix" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + --SainteLaurent + do + local VillagePrefix = "SainteLaurent" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Sommervieu + do + local VillagePrefix = "Sommervieu" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Tangmere + do + local VillagePrefix = "Tangmere" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) + local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + --]] + + return self +end + + +--- Start SCHEDULER for ATC_GROUND_NORMANDY object. +-- @param #ATC_GROUND_NORMANDY self +-- @param RepeatScanSeconds Time in second for defining occurency of alerts. +-- @return nothing +function ATC_GROUND_NORMANDY:Start( RepeatScanSeconds ) + RepeatScanSeconds = RepeatScanSeconds or 0.05 + self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, RepeatScanSeconds ) +end + +--- @type ATC_GROUND_PERSIANGULF +-- @extends #ATC_GROUND + + +--- # ATC\_GROUND\_PERSIANGULF, extends @{#ATC_GROUND} +-- +-- The ATC\_GROUND\_PERSIANGULF class monitors the speed of the airplanes at the airbase during taxi. +-- The pilots may not drive faster than the maximum speed for the airbase, or they will be despawned. +-- +-- --- +-- +-- ![Banner Image](..\Presentations\ATC_GROUND\Dia1.JPG) +-- +-- --- +-- +-- The default maximum speed for the airbases at Persian Gulf is **50 km/h**. Warnings are given if this speed limit is trespassed. +-- Players will be immediately kicked when driving faster than **150 km/h** on the taxi way. +-- +-- The ATC\_GROUND\_PERSIANGULF class monitors the speed of the airplanes at the airbase during taxi. +-- The pilots may not drive faster than the maximum speed for the airbase, or they will be despawned. +-- +-- The pilot will receive 3 times a warning during speeding. After the 3rd warning, if the pilot is still driving +-- faster than the maximum allowed speed, the pilot will be kicked. +-- +-- Different airbases have different maximum speeds, according safety regulations. +-- +-- # Airbases monitored +-- +-- The following airbases are monitored at the PersianGulf region. +-- Use the @{Wrapper.Airbase#AIRBASE.PersianGulf} enumeration to select the airbases to be monitored. +-- +-- * `AIRBASE.PersianGulf.Abu_Musa_Island_Airport` +-- * `AIRBASE.PersianGulf.Al_Dhafra_AB` +-- * `AIRBASE.PersianGulf.Al_Maktoum_Intl` +-- * `AIRBASE.PersianGulf.Al_Minhad_AB` +-- * `AIRBASE.PersianGulf.Bandar_Abbas_Intl` +-- * `AIRBASE.PersianGulf.Bandar_Lengeh` +-- * `AIRBASE.PersianGulf.Dubai_Intl` +-- * `AIRBASE.PersianGulf.Fujairah_Intl` +-- * `AIRBASE.PersianGulf.Havadarya` +-- * `AIRBASE.PersianGulf.Kerman_Airport` +-- * `AIRBASE.PersianGulf.Khasab` +-- * `AIRBASE.PersianGulf.Lar_Airbase` +-- * `AIRBASE.PersianGulf.Qeshm_Island` +-- * `AIRBASE.PersianGulf.Sharjah_Intl` +-- * `AIRBASE.PersianGulf.Shiraz_International_Airport` +-- * `AIRBASE.PersianGulf.Sir_Abu_Nuayr` +-- * `AIRBASE.PersianGulf.Sirri_Island` +-- * `AIRBASE.PersianGulf.Tunb_Island_AFB` +-- * `AIRBASE.PersianGulf.Tunb_Kochak` +-- * `AIRBASE.PersianGulf.Sas_Al_Nakheel_Airport` +-- * `AIRBASE.PersianGulf.Bandar_e_Jask_airfield` +-- * `AIRBASE.PersianGulf.Abu_Dhabi_International_Airport` +-- * `AIRBASE.PersianGulf.Al_Bateen_Airport` +-- * `AIRBASE.PersianGulf.Kish_International_Airport` +-- * `AIRBASE.PersianGulf.Al_Ain_International_Airport` +-- * `AIRBASE.PersianGulf.Lavan_Island_Airport` +-- * `AIRBASE.PersianGulf.Jiroft_Airport` +-- +-- # Installation +-- +-- ## In Single Player Missions +-- +-- ATC\_GROUND is fully functional in single player. +-- +-- ## In Multi Player Missions +-- +-- ATC\_GROUND is functional in multi player, however ... +-- +-- Due to a bug in DCS since release 1.5, the despawning of clients are not anymore working in multi player. +-- To **work around this problem**, a much better solution has been made, using the **slot blocker** script designed +-- by Ciribob. +-- +-- With the help of __Ciribob__, this script has been extended to also kick client players while in flight. +-- ATC\_GROUND is communicating with this modified script to kick players! +-- +-- Install the file **SimpleSlotBlockGameGUI.lua** on the server, following the installation instructions described by Ciribob. +-- +-- [Simple Slot Blocker from Ciribob & FlightControl](https://github.com/ciribob/DCS-SimpleSlotBlock) +-- +-- # Script it! +-- +-- ## 1. ATC_GROUND_PERSIANGULF Constructor +-- +-- Creates a new ATC_GROUND_PERSIANGULF object that will monitor pilots taxiing behaviour. +-- +-- -- This creates a new ATC_GROUND_PERSIANGULF object. +-- +-- -- Monitor for these clients the airbases. +-- AirbasePoliceCaucasus = ATC_GROUND_PERSIANGULF:New() +-- +-- ATC_Ground = ATC_GROUND_PERSIANGULF:New( +-- { AIRBASE.PersianGulf.Kerman_Airport, +-- AIRBASE.PersianGulf.Al_Minhad_AB +-- } +-- ) +-- +-- +-- ## 2. Set various options +-- +-- There are various methods that you can use to tweak the behaviour of the ATC\_GROUND classes. +-- +-- ### 2.1 Speed limit at an airbase. +-- +-- * @{#ATC_GROUND.SetKickSpeed}(): Set the speed limit allowed at an airbase in meters per second. +-- * @{#ATC_GROUND.SetKickSpeedKmph}(): Set the speed limit allowed at an airbase in kilometers per hour. +-- * @{#ATC_GROUND.SetKickSpeedMiph}(): Set the speed limit allowed at an airbase in miles per hour. +-- +-- ### 2.2 Prevent Takeoff at an airbase. Players will be kicked immediately. +-- +-- * @{#ATC_GROUND.SetMaximumKickSpeed}(): Set the maximum speed allowed at an airbase in meters per second. +-- * @{#ATC_GROUND.SetMaximumKickSpeedKmph}(): Set the maximum speed allowed at an airbase in kilometers per hour. +-- * @{#ATC_GROUND.SetMaximumKickSpeedMiph}(): Set the maximum speed allowed at an airbase in miles per hour. +-- +-- @field #ATC_GROUND_PERSIANGULF +ATC_GROUND_PERSIANGULF = { + ClassName = "ATC_GROUND_PERSIANGULF", + Airbases = { + [AIRBASE.PersianGulf.Abu_Musa_Island_Airport] = { + PointsRunways = { + [1] = { + [1]={["y"]=-122813.71002344,["x"]=-31689.936027827,}, + [2]={["y"]=-122827.82488722,["x"]=-31590.105445836,}, + [3]={["y"]=-122769.5689949,["x"]=-31583.176330891,}, + [4]={["y"]=-122726.96776968,["x"]=-31614.998932862,}, + [5]={["y"]=-121293.92414543,["x"]=-31467.947715689,}, + [6]={["y"]=-121296.4904843,["x"]=-31432.018971528,}, + [7]={["y"]=-121236.18152088,["x"]=-31424.576588809,}, + [8]={["y"]=-121190.50068902,["x"]=-31458.452261875,}, + [9]={["y"]=-119839.83654246,["x"]=-31319.356695194,}, + [10]={["y"]=-119824.69514313,["x"]=-31423.293419374,}, + [11]={["y"]=-119886.80054375,["x"]=-31430.22253432,}, + [12]={["y"]=-119932.22474173,["x"]=-31395.320325706,}, + [13]={["y"]=-122813.9472789,["x"]=-31689.81193251,}, + }, + }, + }, + [AIRBASE.PersianGulf.Al_Dhafra_AB] = { + PointsRunways = { + [1] = { + [1]={["y"]=-174672.06004916,["x"]=-209880.97145616,}, + [2]={["y"]=-174705.15693282,["x"]=-209923.15131918,}, + [3]={["y"]=-171819.05380065,["x"]=-212172.84298281,}, + [4]={["y"]=-171785.09826475,["x"]=-212129.87417284,}, + [5]={["y"]=-174671.96413454,["x"]=-209880.52453983,}, + }, + [2] = { + [1]={["y"]=-174351.95872272,["x"]=-211813.88516693,}, + [2]={["y"]=-174381.29169939,["x"]=-211851.81242636,}, + [3]={["y"]=-171493.65648904,["x"]=-214102.92235002,}, + [4]={["y"]=-171464.99693831,["x"]=-214062.78788361,}, + [5]={["y"]=-174351.8628081,["x"]=-211813.4382506,}, + }, + }, + }, + [AIRBASE.PersianGulf.Al_Maktoum_Intl] = { + PointsRunways = { + [1] = { + [1]={["y"]=-111879.49046471,["x"]=-138953.80105841,}, + [2]={["y"]=-111917.23447224,["x"]=-139018.2804046,}, + [3]={["y"]=-108092.98121312,["x"]=-141406.67838426,}, + [4]={["y"]=-108052.34416748,["x"]=-141341.82058294,}, + [5]={["y"]=-111879.5412879,["x"]=-138952.87693763,}, + }, + }, + }, + [AIRBASE.PersianGulf.Al_Minhad_AB] = { + PointsRunways = { + [1] = { + [1]={["y"]=-91070.628933035,["x"]=-125989.64095162,}, + [2]={["y"]=-91072.346560159,["x"]=-126040.59722299,}, + [3]={["y"]=-87098.282779771,["x"]=-126039.41747017,}, + [4]={["y"]=-87099.632735396,["x"]=-125991.26905291,}, + [5]={["y"]=-91071.031270042,["x"]=-125987.44617225,}, + }, + }, + }, + [AIRBASE.PersianGulf.Bandar_Abbas_Intl] = { + PointsRunways = { + [1] = { + [1]={["y"]=12988.484058788,["x"]=113979.99250505,}, + [2]={["y"]=13037.8836239,["x"]=113952.60241152,}, + [3]={["y"]=14877.313199902,["x"]=117414.37833333,}, + [4]={["y"]=14828.777486364,["x"]=117439.06043783,}, + [5]={["y"]=12988.939584604,["x"]=113979.52494386,}, + }, + [2] = { + [1]={["y"]=13203.406014284,["x"]=113848.44907555,}, + [2]={["y"]=13258.268500181,["x"]=113818.47303925,}, + [3]={["y"]=15315.015323566,["x"]=117694.27156647,}, + [4]={["y"]=15264.815746383,["x"]=117725.22168173,}, + [5]={["y"]=13203.861540099,["x"]=113847.98151436,}, + }, + }, + }, + [AIRBASE.PersianGulf.Bandar_Lengeh] = { + PointsRunways = { + [1] = { + [1]={["y"]=-142373.15541415,["x"]=41364.94047809,}, + [2]={["y"]=-142363.30071107,["x"]=41298.112282592,}, + [3]={["y"]=-142217.57151662,["x"]=41320.35666061,}, + [4]={["y"]=-142213.00856728,["x"]=41291.838227254,}, + [5]={["y"]=-142131.44584788,["x"]=41301.534494595,}, + [6]={["y"]=-142132.58658522,["x"]=41323.778872613,}, + [7]={["y"]=-142123.17550221,["x"]=41336.041798956,}, + [8]={["y"]=-139580.45381288,["x"]=41711.022304533,}, + [9]={["y"]=-139590.04241918,["x"]=41778.350996659,}, + [10]={["y"]=-139732.41237808,["x"]=41757.089304408,}, + [11]={["y"]=-139736.7897853,["x"]=41785.646675372,}, + [12]={["y"]=-139816.41690726,["x"]=41775.641173137,}, + [13]={["y"]=-139816.00001133,["x"]=41754.58792885,}, + [14]={["y"]=-139824.1294819,["x"]=41743.748634761,}, + [15]={["y"]=-142373.20183966,["x"]=41365.161507021,}, + }, + }, + }, + [AIRBASE.PersianGulf.Dubai_Intl] = { + PointsRunways = { + [1] = { + [1]={["y"]=-89693.511670714,["x"]=-100490.47082052,}, + [2]={["y"]=-89731.488328846,["x"]=-100555.50584758,}, + [3]={["y"]=-85706.437275049,["x"]=-103076.68123933,}, + [4]={["y"]=-85669.519216262,["x"]=-103010.44994755,}, + [5]={["y"]=-89693.036962487,["x"]=-100489.9961123,}, + }, + [2] = { + [1]={["y"]=-90797.505501889,["x"]=-99344.082465487,}, + [2]={["y"]=-90835.482160021,["x"]=-99409.11749254,}, + [3]={["y"]=-87210.216900398,["x"]=-101681.72494832,}, + [4]={["y"]=-87171.474397253,["x"]=-101619.20256393,}, + [5]={["y"]=-90797.030793662,["x"]=-99343.607757261,}, + }, + }, + }, + [AIRBASE.PersianGulf.Fujairah_Intl] = { + PointsRunways = { + [1] = { + [1]={["y"]=5808.8716147284,["x"]=-116602.15633995,}, + [2]={["y"]=5781.9885293892,["x"]=-116666.67574476,}, + [3]={["y"]=9435.1910907931,["x"]=-118192.91910235,}, + [4]={["y"]=9459.878635843,["x"]=-118134.40047704,}, + [5]={["y"]=5808.4078522575,["x"]=-116603.31550719,}, + }, + }, + }, + [AIRBASE.PersianGulf.Havadarya] = { + PointsRunways = { + [1] = { + [1]={["y"]=-7565.4887830428,["x"]=109074.13162774,}, + [2]={["y"]=-7557.8281079193,["x"]=109030.65729641,}, + [3]={["y"]=-4987.3556518085,["x"]=109524.49147773,}, + [4]={["y"]=-4996.215358578,["x"]=109566.57508489,}, + [5]={["y"]=-7565.4936338604,["x"]=109074.32262205,}, + }, + }, + }, + [AIRBASE.PersianGulf.Kerman_Airport] = { + PointsRunways = { + [1] = { + [1]={["y"]=70375.468628778,["x"]=456046.12685302,}, + [2]={["y"]=70297.050081575,["x"]=456015.1578105,}, + [3]={["y"]=71814.291673715,["x"]=452165.51037702,}, + [4]={["y"]=71902.918622452,["x"]=452188.46411914,}, + [5]={["y"]=70860.465673482,["x"]=454829.89695989,}, + [6]={["y"]=70862.525255971,["x"]=454892.77675983,}, + [7]={["y"]=70816.157465062,["x"]=454922.77944807,}, + [8]={["y"]=70462.749176371,["x"]=455833.38051827,}, + [9]={["y"]=70483.400377364,["x"]=455901.17880077,}, + [10]={["y"]=70453.787334431,["x"]=455974.8217628,}, + [11]={["y"]=70405.860962315,["x"]=455961.57382254,}, + [12]={["y"]=70374.689338175,["x"]=456046.51649833,}, + }, + }, + }, + [AIRBASE.PersianGulf.Khasab] = { + PointsRunways = { + [1] = { + [1]={["y"]=-534.81827307392,["x"]=-1495.070060483,}, + [2]={["y"]=-434.82912685139,["x"]=-1519.8421462589,}, + [3]={["y"]=-405.55302547993,["x"]=-1413.0969766429,}, + [4]={["y"]=-424.92029254105,["x"]=-1352.0675653224,}, + [5]={["y"]=216.05735069389,["x"]=1206.9187095195,}, + [6]={["y"]=116.42961315781,["x"]=1229.9576238247,}, + [7]={["y"]=88.253643635887,["x"]=1123.7918160128,}, + [8]={["y"]=101.1741158476,["x"]=1042.6886109249,}, + [9]={["y"]=-535.31436058928,["x"]=-1494.8762081291,}, + }, + }, + }, + [AIRBASE.PersianGulf.Lar_Airbase] = { + PointsRunways = { + [1] = { + [1]={["y"]=-183987.5454359,["x"]=169021.72039309,}, + [2]={["y"]=-183988.41292374,["x"]=168955.27082471,}, + [3]={["y"]=-180847.92031188,["x"]=168930.46175795,}, + [4]={["y"]=-180806.58653731,["x"]=168888.39641215,}, + [5]={["y"]=-180740.37934087,["x"]=168886.56748407,}, + [6]={["y"]=-180735.62412787,["x"]=168932.65647164,}, + [7]={["y"]=-180685.14571291,["x"]=168934.11961411,}, + [8]={["y"]=-180682.5852136,["x"]=169001.78995301,}, + [9]={["y"]=-183987.48111493,["x"]=169021.35002828,}, + }, + }, + }, + [AIRBASE.PersianGulf.Qeshm_Island] = { + PointsRunways = { + [1] = { + [1]={["y"]=-35140.372717152,["x"]=63373.658918509,}, + [2]={["y"]=-35098.556715749,["x"]=63320.377239302,}, + [3]={["y"]=-34991.318905699,["x"]=63408.730403557,}, + [4]={["y"]=-34984.574389344,["x"]=63401.311435566,}, + [5]={["y"]=-34991.993357335,["x"]=63313.632722947,}, + [6]={["y"]=-34956.921872287,["x"]=63265.746656824,}, + [7]={["y"]=-34917.129225791,["x"]=63261.699947011,}, + [8]={["y"]=-34832.822771349,["x"]=63337.23853019,}, + [9]={["y"]=-34915.105870884,["x"]=63436.382920614,}, + [10]={["y"]=-34906.337999622,["x"]=63478.198922017,}, + [11]={["y"]=-32728.533668488,["x"]=65307.986209216,}, + [12]={["y"]=-32676.600892552,["x"]=65299.218337954,}, + [13]={["y"]=-32623.99366498,["x"]=65334.964274638,}, + [14]={["y"]=-32626.691471522,["x"]=65388.92040548,}, + [15]={["y"]=-31822.745121968,["x"]=66067.418750826,}, + [16]={["y"]=-31777.556862387,["x"]=66068.767654097,}, + [17]={["y"]=-31691.227053039,["x"]=65974.344425122,}, + [18]={["y"]=-31606.246146962,["x"]=66042.464040311,}, + [19]={["y"]=-31602.199437148,["x"]=66084.280041714,}, + [20]={["y"]=-31632.549760747,["x"]=66124.747139846,}, + [21]={["y"]=-31727.647441358,["x"]=66134.189462744,}, + [22]={["y"]=-31734.391957713,["x"]=66141.608430735,}, + [23]={["y"]=-31632.549760747,["x"]=66225.914885176,}, + [24]={["y"]=-31673.691310515,["x"]=66277.173209477,}, + [25]={["y"]=-35140.880825624,["x"]=63373.905965825,}, + }, + }, + }, + [AIRBASE.PersianGulf.Sharjah_Intl] = { + PointsRunways = { + [1] = { + [1]={["y"]=-71668.808658476,["x"]=-93980.156242153,}, + [2]={["y"]=-75307.847363315,["x"]=-91617.097584505,}, + [3]={["y"]=-75280.458023829,["x"]=-91574.709321014,}, + [4]={["y"]=-72249.697184234,["x"]=-93529.134331507,}, + [5]={["y"]=-72179.919581256,["x"]=-93526.199759419,}, + [6]={["y"]=-72138.183444896,["x"]=-93597.933743788,}, + [7]={["y"]=-71638.654062835,["x"]=-93927.584008321,}, + [8]={["y"]=-71668.325847279,["x"]=-93979.428115206,}, + }, + [2] = { + [1]={["y"]=-71553.225408723,["x"]=-93775.312323319,}, + [2]={["y"]=-75168.13829548,["x"]=-91426.51571111,}, + [3]={["y"]=-75125.388157445,["x"]=-91363.754870166,}, + [4]={["y"]=-71510.511081666,["x"]=-93703.252275385,}, + [5]={["y"]=-71552.247218027,["x"]=-93775.638386885,}, + }, + }, + }, + [AIRBASE.PersianGulf.Shiraz_International_Airport] = { + PointsRunways = { + [1] = { + [1]={["y"]=-353995.75579778,["x"]=382327.42294273,}, + [2]={["y"]=-354029.77009807,["x"]=382265.46199492,}, + [3]={["y"]=-349407.98049238,["x"]=379941.14030526,}, + [4]={["y"]=-349376.87025024,["x"]=380004.69408564,}, + [5]={["y"]=-353995.71101815,["x"]=382327.59771695,}, + }, + [2] = { + [1]={["y"]=-354056.29510012,["x"]=381845.97598829,}, + [2]={["y"]=-354091.48797289,["x"]=381783.6025623,}, + [3]={["y"]=-349650.64038107,["x"]=379550.92898242,}, + [4]={["y"]=-349624.41889127,["x"]=379614.92719482,}, + [5]={["y"]=-354056.25032049,["x"]=381846.15076251,}, + }, + }, + }, + [AIRBASE.PersianGulf.Sir_Abu_Nuayr] = { + PointsRunways = { + [1] = { + [1]={["y"]=-203367.3128691,["x"]=-103017.22553918,}, + [2]={["y"]=-203373.59664477,["x"]=-103054.92819323,}, + [3]={["y"]=-202578.27577922,["x"]=-103188.26018333,}, + [4]={["y"]=-202571.37254488,["x"]=-103151.01482599,}, + [5]={["y"]=-203367.65259839,["x"]=-103016.48202662,}, + [6]={["y"]=-203291.39594004,["x"]=-102985.49774228,}, + }, + }, + }, + [AIRBASE.PersianGulf.Sirri_Island] = { + PointsRunways = { + [1] = { + [1]={["y"]=-169713.12842428,["x"]=-27766.658020853,}, + [2]={["y"]=-169682.02009414,["x"]=-27726.583172021,}, + [3]={["y"]=-169727.21866794,["x"]=-27691.632048154,}, + [4]={["y"]=-169694.28043602,["x"]=-27650.276268081,}, + [5]={["y"]=-169763.08474269,["x"]=-27598.490047901,}, + [6]={["y"]=-169825.30140298,["x"]=-27607.090586235,}, + [7]={["y"]=-171614.98889813,["x"]=-26246.247907014,}, + [8]={["y"]=-171620.85326172,["x"]=-26187.105176343,}, + [9]={["y"]=-171686.10990337,["x"]=-26138.56820961,}, + [10]={["y"]=-171716.55468456,["x"]=-26178.745338885,}, + [11]={["y"]=-171764.9668776,["x"]=-26142.810515186,}, + [12]={["y"]=-171796.29599657,["x"]=-26183.416460911,}, + [13]={["y"]=-169713.5628285,["x"]=-27766.883787223,}, + }, + }, + }, + [AIRBASE.PersianGulf.Tunb_Island_AFB] = { + PointsRunways = { + [1] = { + [1]={["y"]=-92923.634698863,["x"]=9547.6862547173,}, + [2]={["y"]=-92963.030803298,["x"]=9565.7274614215,}, + [3]={["y"]=-92934.128053782,["x"]=9619.2987996964,}, + [4]={["y"]=-92970.946842975,["x"]=9640.1014155901,}, + [5]={["y"]=-92949.591945243,["x"]=9682.8112110532,}, + [6]={["y"]=-92899.518391942,["x"]=9699.7478540817,}, + [7]={["y"]=-91969.13471408,["x"]=11464.627292768,}, + [8]={["y"]=-91983.666755417,["x"]=11515.293058512,}, + [9]={["y"]=-91960.101282978,["x"]=11557.710908902,}, + [10]={["y"]=-91921.021874517,["x"]=11539.251288825,}, + [11]={["y"]=-91893.725202275,["x"]=11589.720675632,}, + [12]={["y"]=-91859.751646175,["x"]=11571.850192366,}, + [13]={["y"]=-92922.149728329,["x"]=9547.2937058617,}, + }, + }, + }, + [AIRBASE.PersianGulf.Tunb_Kochak] = { + PointsRunways = { + [1] = { + [1]={["y"]=-109925.50271188,["x"]=8974.5666013181,}, + [2]={["y"]=-109905.7382908,["x"]=8937.53274444,}, + [3]={["y"]=-109009.93726324,["x"]=9072.2234968343,}, + [4]={["y"]=-109040.82867587,["x"]=9104.9871291834,}, + [5]={["y"]=-109925.26515172,["x"]=8974.091480998,}, + }, + }, + }, + [AIRBASE.PersianGulf.Sas_Al_Nakheel_Airport] = { + PointsRunways = { + [1] = { + [1]={["y"]=-176230.75865538,["x"]=-188732.01369812,}, + [2]={["y"]=-176274.78045186,["x"]=-188744.8049371,}, + [3]={["y"]=-175692.03171595,["x"]=-190564.17145168,}, + [4]={["y"]=-175649.7486572,["x"]=-190550.58435053,}, + [5]={["y"]=-176230.66274076,["x"]=-188731.5667818,}, + }, + }, + }, + [AIRBASE.PersianGulf.Bandar_e_Jask_airfield] = { + PointsRunways = { + [1] = { + [1]={["y"]=155156.73167657,["x"]=-57837.031277333,}, + [2]={["y"]=155130.38996239,["x"]=-57790.475605714,}, + [3]={["y"]=157137.17872571,["x"]=-56710.411783359,}, + [4]={["y"]=157148.46631801,["x"]=-56688.071756941,}, + [5]={["y"]=157220.07198163,["x"]=-56649.035500253,}, + [6]={["y"]=157227.83220133,["x"]=-56662.204357931,}, + [7]={["y"]=157359.6383572,["x"]=-56590.481115222,}, + [8]={["y"]=157383.03659539,["x"]=-56633.044744502,}, + [9]={["y"]=155156.7940421,["x"]=-57837.149989814,}, + }, + }, + }, + [AIRBASE.PersianGulf.Abu_Dhabi_International_Airport] = { + PointsRunways = { + [1] = { + [1]={["y"]=-163964.56943899,["x"]=-189427.63621921,}, + [2]={["y"]=-164005.96838287,["x"]=-189478.90226888,}, + [3]={["y"]=-160798.22080495,["x"]=-192054.59531727,}, + [4]={["y"]=-160755.05282258,["x"]=-192002.58569997,}, + [5]={["y"]=-163964.47352437,["x"]=-189427.18930288,}, + }, + [2] = { + [1]={["y"]=-163615.44952024,["x"]=-187144.00786922,}, + [2]={["y"]=-163656.84846411,["x"]=-187195.27391888,}, + [3]={["y"]=-160452.71811093,["x"]=-189764.86593382,}, + [4]={["y"]=-160411.94568221,["x"]=-189715.47961171,}, + [5]={["y"]=-163615.35360562,["x"]=-187143.56095289,}, + }, + }, + }, + [AIRBASE.PersianGulf.Al_Bateen_Airport] = { + PointsRunways = { + [1] = { + [1]={["y"]=-183207.51774197,["x"]=-189871.8319832,}, + [2]={["y"]=-183240.61462564,["x"]=-189914.01184622,}, + [3]={["y"]=-180748.88998479,["x"]=-191943.30402837,}, + [4]={["y"]=-180711.83076051,["x"]=-191896.52435182,}, + [5]={["y"]=-183207.42182735,["x"]=-189871.38506688,}, + }, + }, + }, + [AIRBASE.PersianGulf.Kish_International_Airport] = { + PointsRunways = { + [1] = { + [1]={["y"]=-227330.79164594,["x"]=42691.91536494,}, + [2]={["y"]=-227321.58531968,["x"]=42758.113234714,}, + [3]={["y"]=-223235.73004619,["x"]=42313.579195302,}, + [4]={["y"]=-223240.99080406,["x"]=42247.819722016,}, + [5]={["y"]=-227330.67774245,["x"]=42691.785682556,}, + }, + [2] = { + [1]={["y"]=-227283.77911886,["x"]=42987.748941936,}, + [2]={["y"]=-227274.5727926,["x"]=43053.946811711,}, + [3]={["y"]=-222907.94761294,["x"]=42580.826755904,}, + [4]={["y"]=-222915.76510871,["x"]=42514.58376547,}, + [5]={["y"]=-227283.66521537,["x"]=42987.619259553,}, + }, + }, + }, + [AIRBASE.PersianGulf.Al_Ain_International_Airport] = { + PointsRunways = { + [1] = { + [1]={["y"]=-65165.315648901,["x"]=-209042.45716363,}, + [2]={["y"]=-65112.933878375,["x"]=-209048.84518442,}, + [3]={["y"]=-65672.013626755,["x"]=-213019.66479976,}, + [4]={["y"]=-65722.555424932,["x"]=-213013.91596964,}, + [5]={["y"]=-65165.400582791,["x"]=-209042.15059908,}, + }, + }, + }, + [AIRBASE.PersianGulf.Lavan_Island_Airport] = { + PointsRunways = { + [1] = { + [1]={["y"]=-288099.83301495,["x"]=76353.443273049,}, + [2]={["y"]=-288119.51457685,["x"]=76302.756224611,}, + [3]={["y"]=-288070.96603401,["x"]=76283.898526152,}, + [4]={["y"]=-288085.61084238,["x"]=76247.386812114,}, + [5]={["y"]=-288032.04695421,["x"]=76224.316223573,}, + [6]={["y"]=-287991.12173627,["x"]=76245.38067398,}, + [7]={["y"]=-287489.96435675,["x"]=76037.610404141,}, + [8]={["y"]=-287497.65444594,["x"]=76017.686082159,}, + [9]={["y"]=-287453.61120787,["x"]=75998.111309685,}, + [10]={["y"]=-287419.70490555,["x"]=76007.199596905,}, + [11]={["y"]=-285642.24565503,["x"]=75279.787069797,}, + [12]={["y"]=-285625.46727862,["x"]=75239.239326815,}, + [13]={["y"]=-285570.23845628,["x"]=75217.217707782,}, + [14]={["y"]=-285555.20782742,["x"]=75252.172658628,}, + [15]={["y"]=-285505.92134673,["x"]=75231.199688121,}, + [16]={["y"]=-285484.28380792,["x"]=75284.258832895,}, + [17]={["y"]=-288099.97979219,["x"]=76354.32393647,}, + }, + }, + }, + [AIRBASE.PersianGulf.Jiroft_Airport] = { + PointsRunways = { + [1] = { + [1]={["y"]=140376.87310595,["x"]=283748.07558774,}, + [2]={["y"]=140299.43760975,["x"]=283655.81201779,}, + [3]={["y"]=143008.43807723,["x"]=281517.41347718,}, + [4]={["y"]=143052.6952428,["x"]=281573.25195709,}, + [5]={["y"]=142946.60213095,["x"]=281656.5960586,}, + [6]={["y"]=142975.14179847,["x"]=281687.20381796,}, + [7]={["y"]=142932.12548801,["x"]=281724.01585287,}, + [8]={["y"]=142870.49635092,["x"]=281719.05243244,}, + [9]={["y"]=140437.35783025,["x"]=283640.84253664,}, + [10]={["y"]=140433.27045062,["x"]=283705.80267729,}, + [11]={["y"]=140376.77702493,["x"]=283747.8442964,}, + }, + }, + }, + }, +} + + +--- Creates a new ATC_GROUND_PERSIANGULF object. +-- @param #ATC_GROUND_PERSIANGULF self +-- @param AirbaseNames A list {} of airbase names (Use AIRBASE.PersianGulf enumerator). +-- @return #ATC_GROUND_PERSIANGULF self +function ATC_GROUND_PERSIANGULF:New( AirbaseNames ) + + -- Inherits from BASE + local self = BASE:Inherit( self, ATC_GROUND:New( self.Airbases, AirbaseNames ) ) -- #ATC_GROUND_PERSIANGULF + + self:SetKickSpeedKmph( 50 ) + self:SetMaximumKickSpeedKmph( 150 ) + + -- These lines here are for the demonstration mission. + -- They create in the dcs.log the coordinates of the runway polygons, that are then + -- taken by the moose designer from the dcs.log and reworked to define the + -- Airbases structure, which is part of the class. + -- When new airbases are added or airbases are changed on the map, + -- the MOOSE designer willde-comment this section and apply the changes in the demo + -- mission, and do a re-run to create a new dcs.log, and then add the changed coordinates + -- in the Airbases structure. + -- So, this needs to stay commented normally once a map has been finished. + + + --[[ + + -- Abu_Musa_Island_Airport + do + local VillagePrefix = "Abu_Musa_Island_Airport" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Al_Dhafra_AB + do + local VillagePrefix = "Al_Dhafra_AB" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) + local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Al_Maktoum_Intl + do + local VillagePrefix = "Al_Maktoum_Intl" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Al_Minhad_AB + do + local VillagePrefix = "Al_Minhad_AB" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Bandar_Abbas_Intl + do + local VillagePrefix = "Bandar_Abbas_Intl" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) + local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Bandar_Lengeh + do + local VillagePrefix = "Bandar_Lengeh" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Dubai_Intl + do + local VillagePrefix = "Dubai_Intl" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) + local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Fujairah_Intl + do + local VillagePrefix = "Fujairah_Intl" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Havadarya + do + local VillagePrefix = "Havadarya" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Kerman_Airport + do + local VillagePrefix = "Kerman_Airport" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Khasab + do + local VillagePrefix = "Khasab" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Lar_Airbase + do + local VillagePrefix = "Lar_Airbase" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Qeshm_Island + do + local VillagePrefix = "Qeshm_Island" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Sharjah_Intl + do + local VillagePrefix = "Sharjah_Intl" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) + local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Shiraz_International_Airport + do + local VillagePrefix = "Shiraz_International_Airport" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) + local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Sir_Abu_Nuayr + do + local VillagePrefix = "Sir_Abu_Nuayr" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Sirri_Island + do + local VillagePrefix = "Sirri_Island" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Tunb_Island_AFB + do + local VillagePrefix = "Tunb_Island_AFB" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Tunb_Kochak + do + local VillagePrefix = "Tunb_Kochak" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Sas_Al_Nakheel_Airport + do + local VillagePrefix = "Sas_Al_Nakheel_Airport" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Bandar_e_Jask_airfield + do + local VillagePrefix = "Bandar_e_Jask_airfield" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Abu_Dhabi_International_Airport + do + local VillagePrefix = "Abu_Dhabi_International_Airport" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) + local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Al_Bateen_Airport + do + local VillagePrefix = "Al_Bateen_Airport" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Kish_International_Airport + do + local VillagePrefix = "Kish_International_Airport" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) + local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Al_Ain_International_Airport + do + local VillagePrefix = "Al_Ain_International_Airport" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Lavan_Island_Airport + do + local VillagePrefix = "Lavan_Island_Airport" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Jiroft_Airport + do + local VillagePrefix = "Jiroft_Airport" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Bandar_Abbas_Intl + do + local VillagePrefix = "Bandar_Abbas_Intl" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) + local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + --]] + + return self +end + +--- Start SCHEDULER for ATC_GROUND_PERSIANGULF object. +-- @param #ATC_GROUND_PERSIANGULF self +-- @param RepeatScanSeconds Time in second for defining occurency of alerts. +-- @return nothing +function ATC_GROUND_PERSIANGULF:Start( RepeatScanSeconds ) + RepeatScanSeconds = RepeatScanSeconds or 0.05 + self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, RepeatScanSeconds ) +end + + + --- @type ATC_GROUND_MARIANAISLANDS +-- @extends #ATC_GROUND + + + +--- # ATC\_GROUND\_MARIANA, extends @{#ATC_GROUND} +-- +-- The ATC\_GROUND\_MARIANA class monitors the speed of the airplanes at the airbase during taxi. +-- The pilots may not drive faster than the maximum speed for the airbase, or they will be despawned. +-- +-- --- +-- +-- ![Banner Image](..\Presentations\ATC_GROUND\Dia1.JPG) +-- +-- --- +-- +-- The default maximum speed for the airbases at Persian Gulf is **50 km/h**. Warnings are given if this speed limit is trespassed. +-- Players will be immediately kicked when driving faster than **150 km/h** on the taxi way. +-- +-- The ATC\_GROUND\_MARIANA class monitors the speed of the airplanes at the airbase during taxi. +-- The pilots may not drive faster than the maximum speed for the airbase, or they will be despawned. +-- +-- The pilot will receive 3 times a warning during speeding. After the 3rd warning, if the pilot is still driving +-- faster than the maximum allowed speed, the pilot will be kicked. +-- +-- Different airbases have different maximum speeds, according safety regulations. +-- +-- # Airbases monitored +-- +-- The following airbases are monitored at the Mariana Island region. +-- Use the @{Wrapper.Airbase#AIRBASE.MarianaIslands} enumeration to select the airbases to be monitored. +-- +-- * AIRBASE.MarianaIslands.Rota_Intl +-- * AIRBASE.MarianaIslands.Andersen_AFB +-- * AIRBASE.MarianaIslands.Antonio_B_Won_Pat_Intl +-- * AIRBASE.MarianaIslands.Saipan_Intl +-- * AIRBASE.MarianaIslands.Tinian_Intl +-- * AIRBASE.MarianaIslands.Olf_Orote +-- +-- # Installation +-- +-- ## In Single Player Missions +-- +-- ATC\_GROUND is fully functional in single player. +-- +-- ## In Multi Player Missions +-- +-- ATC\_GROUND is functional in multi player, however ... +-- +-- Due to a bug in DCS since release 1.5, the despawning of clients are not anymore working in multi player. +-- To **work around this problem**, a much better solution has been made, using the **slot blocker** script designed +-- by Ciribob. +-- +-- With the help of __Ciribob__, this script has been extended to also kick client players while in flight. +-- ATC\_GROUND is communicating with this modified script to kick players! +-- +-- Install the file **SimpleSlotBlockGameGUI.lua** on the server, following the installation instructions described by Ciribob. +-- +-- [Simple Slot Blocker from Ciribob & FlightControl](https://github.com/ciribob/DCS-SimpleSlotBlock) +-- +-- # Script it! +-- +-- ## 1. ATC_GROUND_MARIANAISLANDS Constructor +-- +-- Creates a new ATC_GROUND_MARIANAISLANDS object that will monitor pilots taxiing behaviour. +-- +-- -- This creates a new ATC_GROUND_MARIANAISLANDS object. +-- +-- -- Monitor for these clients the airbases. +-- AirbasePoliceCaucasus = ATC_GROUND_MARIANAISLANDS:New() +-- +-- ATC_Ground = ATC_GROUND_MARIANAISLANDS:New( +-- { AIRBASE.MarianaIslands.Andersen_AFB, +-- AIRBASE.MarianaIslands.Saipan_Intl +-- } +-- ) +-- +-- +-- ## 2. Set various options +-- +-- There are various methods that you can use to tweak the behaviour of the ATC\_GROUND classes. +-- +-- ### 2.1 Speed limit at an airbase. +-- +-- * @{#ATC_GROUND.SetKickSpeed}(): Set the speed limit allowed at an airbase in meters per second. +-- * @{#ATC_GROUND.SetKickSpeedKmph}(): Set the speed limit allowed at an airbase in kilometers per hour. +-- * @{#ATC_GROUND.SetKickSpeedMiph}(): Set the speed limit allowed at an airbase in miles per hour. +-- +-- ### 2.2 Prevent Takeoff at an airbase. Players will be kicked immediately. +-- +-- * @{#ATC_GROUND.SetMaximumKickSpeed}(): Set the maximum speed allowed at an airbase in meters per second. +-- * @{#ATC_GROUND.SetMaximumKickSpeedKmph}(): Set the maximum speed allowed at an airbase in kilometers per hour. +-- * @{#ATC_GROUND.SetMaximumKickSpeedMiph}(): Set the maximum speed allowed at an airbase in miles per hour. +-- +---- @field #ATC_GROUND_MARIANAISLANDS +ATC_GROUND_MARIANAISLANDS = { + ClassName = "ATC_GROUND_MARIANAISLANDS", + Airbases = { + + [AIRBASE.MarianaIslands.Andersen_AFB] = { + ZoneBoundary = { + [1]={["y"]=16534.138036037,["x"]=11357.42159178,}, + [2]={["y"]=16193.406442738,["x"]=12080.012957533,}, + [3]={["y"]=13846.966851869,["x"]=12017.348398727,}, + [4]={["y"]=13085.815989171,["x"]=11686.317876875,}, + [5]={["y"]=13157.991797443,["x"]=11307.826209991,}, + [6]={["y"]=12055.725179065,["x"]=10795.955695916,}, + [7]={["y"]=12762.455491112,["x"]=8890.9830441032,}, + [8]={["y"]=15955.829493693,["x"]=10333.527220132,}, + [9]={["y"]=16537.500532414,["x"]=11302.009499603,}, + }, + PointsRunways = { + [1]={ + [1]={["y"]=12586.683049611,["x"]=10224.374497932,}, + [2]={["y"]=16191.720475696,["x"]=11791.299100017,}, + [3]={["y"]=16126.93956642,["x"]=11938.855615591,}, + [4]={["y"]=12520.758127164,["x"]=10385.177131701,}, + [5]={["y"]=12584.654720512,["x"]=10227.416991581,}, + }, + [2]={ + [1]={["y"]=12663.030391743,["x"]=9661.9623015306,}, + [2]={["y"]=16478.347303358,["x"]=11328.665745976,}, + [3]={["y"]=16405.4731048,["x"]=11479.11570429,}, + [4]={["y"]=12597.277684174,["x"]=9817.9733769647,}, + [5]={["y"]=12661.894752524,["x"]=9674.4462086962,}, + }, + }, + }, + [AIRBASE.MarianaIslands.Antonio_B_Won_Pat_Intl] = { + ZoneBoundary = { + [1]={["y"]=2288.5182403943,["x"]=1469.0170841716,}, + [2]={["y"]=1126.2025877996,["x"]=1174.37135631,}, + [3]={["y"]=-2015.6461924287,["x"]=-484.62000718931,}, + [4]={["y"]=-2102.1292389114,["x"]=-988.03393750566,}, + [5]={["y"]=476.03853524366,["x"]=-1220.1783269883,}, + [6]={["y"]=2059.2220058047,["x"]=78.889693514402,}, + [7]={["y"]=1898.1396965104,["x"]=705.67531284795,}, + [8]={["y"]=2760.1768681934,["x"]=1026.0681119777,}, + [9]={["y"]=2317.2278959994,["x"]=1460.8143254273,}, + }, + PointsRunways = { + [1]={ + [1]={["y"]=-1872.6620108821,["x"]=-924.3572605835,}, + [2]={["y"]=1763.4754603305,["x"]=735.35988877983,}, + [3]={["y"]=1700.6941677961,["x"]=866.32615476157,}, + [4]={["y"]=-1934.0078007732,["x"]=-779.8149298453,}, + [5]={["y"]=-1875.0113982627,["x"]=-914.95971106094,}, + }, + [2]={ + [1]={["y"]=-1512.9403660377,["x"]=-1005.5903386188,}, + [2]={["y"]=1577.9055714735,["x"]=413.22750176368,}, + [3]={["y"]=1523.1182807849,["x"]=543.89726442232,}, + [4]={["y"]=-1572.5102998047,["x"]=-867.04004322806,}, + [5]={["y"]=-1514.2790162347,["x"]=-1003.5823633233,}, + }, + }, + }, + [AIRBASE.MarianaIslands.Rota_Intl] = { + ZoneBoundary = { + [1]={["y"]=47237.615412849,["x"]=76048.890408862,}, + [2]={["y"]=49938.030053628,["x"]=75921.721582932,}, + [3]={["y"]=49931.24873272,["x"]=75735.184004851,}, + [4]={["y"]=49295.999227075,["x"]=75754.716414519,}, + [5]={["y"]=49286.963307515,["x"]=75510.037806569,}, + [6]={["y"]=48774.280745707,["x"]=75513.331990155,}, + [7]={["y"]=48785.021396773,["x"]=75795.691662161,}, + [8]={["y"]=47232.749278491,["x"]=75839.239059146,}, + [9]={["y"]=47236.687866223,["x"]=76042.706764692,}, + }, + PointsRunways = { + [1]={ + [1]={["y"]=49741.295228062,["x"]=75901.50955922,}, + [2]={["y"]=49739.033213305,["x"]=75768.333440425,}, + [3]={["y"]=47448.460520408,["x"]=75857.400271466,}, + [4]={["y"]=47452.270177742,["x"]=75999.965448133,}, + [5]={["y"]=49738.502011054,["x"]=75905.338915708,}, + }, + }, + }, + [AIRBASE.MarianaIslands.Saipan_Intl] = { + ZoneBoundary = { + [1]={["y"]=100489.08491445,["x"]=179799.05158855,}, + [2]={["y"]=100869.73415313,["x"]=179948.98719903,}, + [3]={["y"]=101364.78967515,["x"]=180831.98517043,}, + [4]={["y"]=101563.85713359,["x"]=180885.21496237,}, + [5]={["y"]=101733.92591034,["x"]=180457.73296886,}, + [6]={["y"]=103340.30228775,["x"]=180990.08362622,}, + [7]={["y"]=103459.55080438,["x"]=180453.77747027,}, + [8]={["y"]=100406.63048095,["x"]=179266.60983762,}, + [9]={["y"]=100225.55027532,["x"]=179423.9380961,}, + [10]={["y"]=100477.48558937,["x"]=179791.9827288,}, + }, + PointsRunways = { + [1]={ + [1]={["y"]=103170.38882002,["x"]=180654.56630524,}, + [2]={["y"]=103235.37868835,["x"]=180497.25368418,}, + [3]={["y"]=100564.72969504,["x"]=179435.41443498,}, + [4]={["y"]=100509.30718722,["x"]=179584.65394733,}, + [5]={["y"]=103163.53918905,["x"]=180651.82645285,}, + }, + [2]={ + [1]={["y"]=103048.83223261,["x"]=180819.94107128,}, + [2]={["y"]=103087.60579257,["x"]=180720.06315265,}, + [3]={["y"]=101037.52694966,["x"]=179899.50061624,}, + [4]={["y"]=100994.61708907,["x"]=180009.33151758,}, + [5]={["y"]=103043.26643227,["x"]=180820.40488798,}, + }, + }, + }, + [AIRBASE.MarianaIslands.Tinian_Intl] = { + ZoneBoundary = { + [1]={["y"]=88393.477575413,["x"]=166704.16076438,}, + [2]={["y"]=91581.732441809,["x"]=167402.54409276,}, + [3]={["y"]=91533.451647402,["x"]=166826.23670062,}, + [4]={["y"]=90827.604136952,["x"]=166699.75590414,}, + [5]={["y"]=90894.853975623,["x"]=166375.37836304,}, + [6]={["y"]=89995.027922869,["x"]=166224.92495935,}, + [7]={["y"]=88937.62899352,["x"]=166244.48573911,}, + [8]={["y"]=88408.916178231,["x"]=166480.39896864,}, + [9]={["y"]=88387.745481732,["x"]=166685.82715656,}, + }, + PointsRunways = { + [1]={ + [1]={["y"]=91329.480937912,["x"]=167204.44064529,}, + [2]={["y"]=91363.95475433,["x"]=167038.15603429,}, + [3]={["y"]=88585.849307337,["x"]=166520.3807647,}, + [4]={["y"]=88554.422227212,["x"]=166686.49505251,}, + [5]={["y"]=91318.8152578,["x"]=167203.31794212,}, + }, + }, + }, + + }, +} + +--- Creates a new ATC_GROUND_MARIANAISLANDS object. +-- @param #ATC_GROUND_MARIANAISLANDS self +-- @param AirbaseNames A list {} of airbase names (Use AIRBASE.MarianaIslands enumerator). +-- @return #ATC_GROUND_MARIANAISLANDS self +function ATC_GROUND_MARIANAISLANDS:New( AirbaseNames ) + + -- Inherits from BASE + local self = BASE:Inherit( self, ATC_GROUND:New( self.Airbases, AirbaseNames ) ) + + self:SetKickSpeedKmph( 50 ) + self:SetMaximumKickSpeedKmph( 150 ) + +-- -- Andersen +-- local AndersenBoundary = GROUP:FindByName( "Andersen Boundary" ) +-- self.Airbases[AIRBASE.MarianaIslands.Andersen_AFB].ZoneBoundary = ZONE_POLYGON:New( "Andersen Boundary", AndersenBoundary ):SmokeZone(SMOKECOLOR.White):Flush() +-- +-- local AndersenRunway1 = GROUP:FindByName( "Andersen Runway 1" ) +-- self.Airbases[AIRBASE.MarianaIslands.Andersen_AFB].ZoneRunways[1] = ZONE_POLYGON:New( "Andersen Runway 1", AndersenRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() +-- +-- local AndersenRunway2 = GROUP:FindByName( "Andersen Runway 2" ) +-- self.Airbases[AIRBASE.MarianaIslands.Andersen_AFB].ZoneRunways[2] = ZONE_POLYGON:New( "Andersen Runway 2", AndersenRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() +-- +-- +-- -- Antonio_B_Won_Pat_International_Airport +-- local AntonioBoundary = GROUP:FindByName( "Antonio Boundary" ) +-- self.Airbases[AIRBASE.MarianaIslands.Antonio_B_Won_Pat_Intl].ZoneBoundary = ZONE_POLYGON:New( "Antonio Boundary", AntonioBoundary ):SmokeZone(SMOKECOLOR.White):Flush() +-- +-- local AntonioRunway1 = GROUP:FindByName( "Antonio Runway 1" ) +-- self.Airbases[AIRBASE.MarianaIslands.Antonio_B_Won_Pat_Intl].ZoneRunways[1] = ZONE_POLYGON:New( "Antonio Runway 1", AntonioRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() +-- +-- local AntonioRunway2 = GROUP:FindByName( "Antonio Runway 2" ) +-- self.Airbases[AIRBASE.MarianaIslands.Antonio_B_Won_Pat_Intl].ZoneRunways[2] = ZONE_POLYGON:New( "Antonio Runway 2", AntonioRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() +-- +-- +-- -- Rota_International_Airport +-- local RotaBoundary = GROUP:FindByName( "Rota Boundary" ) +-- self.Airbases[AIRBASE.MarianaIslands.Rota_Intl].ZoneBoundary = ZONE_POLYGON:New( "Rota Boundary", RotaBoundary ):SmokeZone(SMOKECOLOR.White):Flush() +-- +-- local RotaRunway1 = GROUP:FindByName( "Rota Runway 1" ) +-- self.Airbases[AIRBASE.MarianaIslands.Rota_Intl].ZoneRunways[1] = ZONE_POLYGON:New( "Rota Runway 1", RotaRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() +-- +-- +-- -- Saipan_International_Airport +-- local SaipanBoundary = GROUP:FindByName( "Saipan Boundary" ) +-- self.Airbases[AIRBASE.MarianaIslands.Saipan_Intl].ZoneBoundary = ZONE_POLYGON:New( "Saipan Boundary", SaipanBoundary ):SmokeZone(SMOKECOLOR.White):Flush() +-- +-- local SaipanRunway1 = GROUP:FindByName( "Saipan Runway 1" ) +-- self.Airbases[AIRBASE.MarianaIslands.Saipan_Intl].ZoneRunways[1] = ZONE_POLYGON:New( "Saipan Runway 1", SaipanRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() +-- +-- local SaipanRunway2 = GROUP:FindByName( "Saipan Runway 2" ) +-- self.Airbases[AIRBASE.MarianaIslands.Saipan_Intl].ZoneRunways[2] = ZONE_POLYGON:New( "Saipan Runway 2", SaipanRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() +-- +-- +-- -- Tinian_International_Airport +-- local TinianBoundary = GROUP:FindByName( "Tinian Boundary" ) +-- self.Airbases[AIRBASE.MarianaIslands.Tinian_Intl].ZoneBoundary = ZONE_POLYGON:New( "Tinian Boundary", TinianBoundary ):SmokeZone(SMOKECOLOR.White):Flush() +-- +-- local TinianRunway1 = GROUP:FindByName( "Tinian Runway 1" ) +-- self.Airbases[AIRBASE.MarianaIslands.Tinian_Intl].ZoneRunways[1] = ZONE_POLYGON:New( "Tinian Runway 1", TinianRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + + return self +end + + +--- Start SCHEDULER for ATC_GROUND_MARIANAISLANDS object. +-- @param #ATC_GROUND_MARIANAISLANDS self +-- @param RepeatScanSeconds Time in second for defining occurency of alerts. +-- @return nothing +function ATC_GROUND_MARIANAISLANDS:Start( RepeatScanSeconds ) + RepeatScanSeconds = RepeatScanSeconds or 0.05 + self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, RepeatScanSeconds ) +end +--- **Functional** -- Models the detection of enemy units by FACs or RECCEs and group them according various methods. +-- +-- === +-- +-- ## Features: +-- +-- * Detection of targets by recce units. +-- * Group detected targets per unit, type or area (zone). +-- * Keep persistency of detected targets, if when detection is lost. +-- * Provide an indication of detected targets. +-- * Report detected targets. +-- * Refresh detection upon specified time intervals. +-- +-- === +-- +-- ## Missions: +-- +-- [DET - Detection](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/DET%20-%20Detection) +-- +-- === +-- +-- Facilitate the detection of enemy units within the battle zone executed by FACs (Forward Air Controllers) or RECCEs (Reconnassance Units). +-- It uses the in-built detection capabilities of DCS World, but adds new functionalities. +-- +-- === +-- +-- ### Contributions: +-- +-- * Mechanist : Early concept of DETECTION_AREAS. +-- +-- ### Authors: +-- +-- * FlightControl : Analysis, Design, Programming, Testing +-- +-- === +-- +-- @module Functional.Detection +-- @image Detection.JPG + + +do -- DETECTION_BASE + + --- @type DETECTION_BASE + -- @field Core.Set#SET_GROUP DetectionSetGroup The @{Set} of GROUPs in the Forward Air Controller role. + -- @field DCS#Distance DetectionRange The range till which targets are accepted to be detected. + -- @field #DETECTION_BASE.DetectedObjects DetectedObjects The list of detected objects. + -- @field #table DetectedObjectsIdentified Map of the DetectedObjects identified. + -- @field #number DetectionRun + -- @extends Core.Fsm#FSM + + --- Defines the core functions to administer detected objects. + -- The DETECTION_BASE class will detect objects within the battle zone for a list of @{Wrapper.Group}s detecting targets following (a) detection method(s). + -- + -- ## DETECTION_BASE constructor + -- + -- Construct a new DETECTION_BASE instance using the @{#DETECTION_BASE.New}() method. + -- + -- ## Initialization + -- + -- By default, detection will return detected objects with all the detection sensors available. + -- However, you can ask how the objects were found with specific detection methods. + -- If you use one of the below methods, the detection will work with the detection method specified. + -- You can specify to apply multiple detection methods. + -- + -- Use the following functions to report the objects it detected using the methods Visual, Optical, Radar, IRST, RWR, DLINK: + -- + -- * @{#DETECTION_BASE.InitDetectVisual}(): Detected using Visual. + -- * @{#DETECTION_BASE.InitDetectOptical}(): Detected using Optical. + -- * @{#DETECTION_BASE.InitDetectRadar}(): Detected using Radar. + -- * @{#DETECTION_BASE.InitDetectIRST}(): Detected using IRST. + -- * @{#DETECTION_BASE.InitDetectRWR}(): Detected using RWR. + -- * @{#DETECTION_BASE.InitDetectDLINK}(): Detected using DLINK. + -- + -- ## **Filter** detected units based on **category of the unit** + -- + -- Filter the detected units based on Unit.Category using the method @{#DETECTION_BASE.FilterCategories}(). + -- The different values of Unit.Category can be: + -- + -- * Unit.Category.AIRPLANE + -- * Unit.Category.GROUND_UNIT + -- * Unit.Category.HELICOPTER + -- * Unit.Category.SHIP + -- * Unit.Category.STRUCTURE + -- + -- Multiple Unit.Category entries can be given as a table and then these will be evaluated as an OR expression. + -- + -- Example to filter a single category (Unit.Category.AIRPLANE). + -- + -- DetectionObject:FilterCategories( Unit.Category.AIRPLANE ) + -- + -- Example to filter multiple categories (Unit.Category.AIRPLANE, Unit.Category.HELICOPTER). Note the {}. + -- + -- DetectionObject:FilterCategories( { Unit.Category.AIRPLANE, Unit.Category.HELICOPTER } ) + -- + -- + -- ## **DETECTION_ derived classes** group the detected units into a **DetectedItems[]** list + -- + -- DETECTION_BASE derived classes build a list called DetectedItems[], which is essentially a first later + -- of grouping of detected units. Each DetectedItem within the DetectedItems[] list contains + -- a SET_UNIT object that contains the detected units that belong to that group. + -- + -- Derived classes will apply different methods to group the detected units. + -- Examples are per area, per quadrant, per distance, per type. + -- See further the derived DETECTION classes on which grouping methods are currently supported. + -- + -- Various methods exist how to retrieve the grouped items from a DETECTION_BASE derived class: + -- + -- * The method @{Functional.Detection#DETECTION_BASE.GetDetectedItems}() retrieves the DetectedItems[] list. + -- * A DetectedItem from the DetectedItems[] list can be retrieved using the method @{Functional.Detection#DETECTION_BASE.GetDetectedItem}( DetectedItemIndex ). + -- Note that this method returns a DetectedItem element from the list, that contains a Set variable and further information + -- about the DetectedItem that is set by the DETECTION_BASE derived classes, used to group the DetectedItem. + -- * A DetectedSet from the DetectedItems[] list can be retrieved using the method @{Functional.Detection#DETECTION_BASE.GetDetectedSet}( DetectedItemIndex ). + -- This method retrieves the Set from a DetectedItem element from the DetectedItem list (DetectedItems[ DetectedItemIndex ].Set ). + -- + -- ## **Visual filters** to fine-tune the probability of the detected objects + -- + -- By default, DCS World will return any object that is in LOS and within "visual reach", or detectable through one of the electronic detection means. + -- That being said, the DCS World detection algorithm can sometimes be unrealistic. + -- Especially for a visual detection, DCS World is able to report within 1 second a detailed detection of a group of 20 units (including types of the units) that are 10 kilometers away, using only visual capabilities. + -- Additionally, trees and other obstacles are not accounted during the DCS World detection. + -- + -- Therefore, an additional (optional) filtering has been built into the DETECTION_BASE class, that can be set for visual detected units. + -- For electronic detection, this filtering is not applied, only for visually detected targets. + -- + -- The following additional filtering can be applied for visual filtering: + -- + -- * A probability factor per kilometer distance. + -- * A probability factor based on the alpha angle between the detected object and the unit detecting. + -- A detection from a higher altitude allows for better detection than when on the ground. + -- * Define a probability factor for "cloudy zones", which are zones where forests or villages are located. In these zones, detection will be much more difficult. + -- The mission designer needs to define these cloudy zones within the mission, and needs to register these zones in the DETECTION_ objects additing a probability factor per zone. + -- + -- I advise however, that, when you first use the DETECTION derived classes, that you don't use these filters. + -- Only when you experience unrealistic behaviour in your missions, these filters could be applied. + -- + -- + -- ### Distance visual detection probability + -- + -- Upon a **visual** detection, the further away a detected object is, the less likely it is to be detected properly. + -- Also, the speed of accurate detection plays a role. + -- + -- A distance probability factor between 0 and 1 can be given, that will model a linear extrapolated probability over 10 km distance. + -- + -- For example, if a probability factor of 0.6 (60%) is given, the extrapolated probabilities over 15 kilometers would like like: + -- 1 km: 96%, 2 km: 92%, 3 km: 88%, 4 km: 84%, 5 km: 80%, 6 km: 76%, 7 km: 72%, 8 km: 68%, 9 km: 64%, 10 km: 60%, 11 km: 56%, 12 km: 52%, 13 km: 48%, 14 km: 44%, 15 km: 40%. + -- + -- Note that based on this probability factor, not only the detection but also the **type** of the unit will be applied! + -- + -- Use the method @{Functional.Detection#DETECTION_BASE.SetDistanceProbability}() to set the probability factor upon a 10 km distance. + -- + -- ### Alpha Angle visual detection probability + -- + -- Upon a **visual** detection, the higher the unit is during the detecting process, the more likely the detected unit is to be detected properly. + -- A detection at a 90% alpha angle is the most optimal, a detection at 10% is less and a detection at 0% is less likely to be correct. + -- + -- A probability factor between 0 and 1 can be given, that will model a progressive extrapolated probability if the target would be detected at a 0° angle. + -- + -- For example, if a alpha angle probability factor of 0.7 is given, the extrapolated probabilities of the different angles would look like: + -- 0°: 70%, 10°: 75,21%, 20°: 80,26%, 30°: 85%, 40°: 89,28%, 50°: 92,98%, 60°: 95,98%, 70°: 98,19%, 80°: 99,54%, 90°: 100% + -- + -- Use the method @{Functional.Detection#DETECTION_BASE.SetAlphaAngleProbability}() to set the probability factor if 0°. + -- + -- ### Cloudy Zones detection probability + -- + -- Upon a **visual** detection, the more a detected unit is within a cloudy zone, the less likely the detected unit is to be detected successfully. + -- The Cloudy Zones work with the ZONE_BASE derived classes. The mission designer can define within the mission + -- zones that reflect cloudy areas where detected units may not be so easily visually detected. + -- + -- Use the method @{Functional.Detection#DETECTION_BASE.SetZoneProbability}() to set for a defined number of zones, the probability factors. + -- + -- Note however, that the more zones are defined to be "cloudy" within a detection, the more performance it will take + -- from the DETECTION_BASE to calculate the presence of the detected unit within each zone. + -- Expecially for ZONE_POLYGON, try to limit the amount of nodes of the polygon! + -- + -- Typically, this kind of filter would be applied for very specific areas were a detection needs to be very realisting for + -- AI not to detect so easily targets within a forrest or village rich area. + -- + -- ## Accept / Reject detected units + -- + -- DETECTION_BASE can accept or reject successful detections based on the location of the detected object, + -- if it is located in range or located inside or outside of specific zones. + -- + -- ### Detection acceptance of within range limit + -- + -- A range can be set that will limit a successful detection for a unit. + -- Use the method @{Functional.Detection#DETECTION_BASE.SetAcceptRange}() to apply a range in meters till where detected units will be accepted. + -- + -- local SetGroup = SET_GROUP:New():FilterPrefixes( "FAC" ):FilterStart() -- Build a SetGroup of Forward Air Controllers. + -- + -- -- Build a detect object. + -- local Detection = DETECTION_UNITS:New( SetGroup ) + -- + -- -- This will accept detected units if the range is below 5000 meters. + -- Detection:SetAcceptRange( 5000 ) + -- + -- -- Start the Detection. + -- Detection:Start() + -- + -- + -- ### Detection acceptance if within zone(s). + -- + -- Specific ZONE_BASE object(s) can be given as a parameter, which will only accept a detection if the unit is within the specified ZONE_BASE object(s). + -- Use the method @{Functional.Detection#DETECTION_BASE.SetAcceptZones}() will accept detected units if they are within the specified zones. + -- + -- local SetGroup = SET_GROUP:New():FilterPrefixes( "FAC" ):FilterStart() -- Build a SetGroup of Forward Air Controllers. + -- + -- -- Search fo the zones where units are to be accepted. + -- local ZoneAccept1 = ZONE:New( "AcceptZone1" ) + -- local ZoneAccept2 = ZONE:New( "AcceptZone2" ) + -- + -- -- Build a detect object. + -- local Detection = DETECTION_UNITS:New( SetGroup ) + -- + -- -- This will accept detected units by Detection when the unit is within ZoneAccept1 OR ZoneAccept2. + -- Detection:SetAcceptZones( { ZoneAccept1, ZoneAccept2 } ) + -- + -- -- Start the Detection. + -- Detection:Start() + -- + -- ### Detection rejectance if within zone(s). + -- + -- Specific ZONE_BASE object(s) can be given as a parameter, which will reject detection if the unit is within the specified ZONE_BASE object(s). + -- Use the method @{Functional.Detection#DETECTION_BASE.SetRejectZones}() will reject detected units if they are within the specified zones. + -- An example of how to use the method is shown below. + -- + -- local SetGroup = SET_GROUP:New():FilterPrefixes( "FAC" ):FilterStart() -- Build a SetGroup of Forward Air Controllers. + -- + -- -- Search fo the zones where units are to be rejected. + -- local ZoneReject1 = ZONE:New( "RejectZone1" ) + -- local ZoneReject2 = ZONE:New( "RejectZone2" ) + -- + -- -- Build a detect object. + -- local Detection = DETECTION_UNITS:New( SetGroup ) + -- + -- -- This will reject detected units by Detection when the unit is within ZoneReject1 OR ZoneReject2. + -- Detection:SetRejectZones( { ZoneReject1, ZoneReject2 } ) + -- + -- -- Start the Detection. + -- Detection:Start() + -- + -- ## Detection of Friendlies Nearby + -- + -- Use the method @{Functional.Detection#DETECTION_BASE.SetFriendliesRange}() to set the range what will indicate when friendlies are nearby + -- a DetectedItem. The default range is 6000 meters. For air detections, it is advisory to use about 30.000 meters. + -- + -- ## DETECTION_BASE is a Finite State Machine + -- + -- Various Events and State Transitions can be tailored using DETECTION_BASE. + -- + -- ### DETECTION_BASE States + -- + -- * **Detecting**: The detection is running. + -- * **Stopped**: The detection is stopped. + -- + -- ### DETECTION_BASE Events + -- + -- * **Start**: Start the detection process. + -- * **Detect**: Detect new units. + -- * **Detected**: New units have been detected. + -- * **Stop**: Stop the detection process. + -- + -- @field #DETECTION_BASE DETECTION_BASE + -- + DETECTION_BASE = { + ClassName = "DETECTION_BASE", + DetectionSetGroup = nil, + DetectionRange = nil, + DetectedObjects = {}, + DetectionRun = 0, + DetectedObjectsIdentified = {}, + DetectedItems = {}, + DetectedItemsByIndex = {}, + } + + --- @type DETECTION_BASE.DetectedObjects + -- @list <#DETECTION_BASE.DetectedObject> + + --- @type DETECTION_BASE.DetectedObject + -- @field #string Name + -- @field #boolean IsVisible + -- @field #boolean KnowType + -- @field #boolean KnowDistance + -- @field #string Type + -- @field #number Distance + -- @field #boolean Identified + -- @field #number LastTime + -- @field #boolean LastPos + -- @field #number LastVelocity + + + --- @type DETECTION_BASE.DetectedItems + -- @list <#DETECTION_BASE.DetectedItem> + + --- Detected item data structrue. + -- @type DETECTION_BASE.DetectedItem + -- @field #boolean IsDetected Indicates if the DetectedItem has been detected or not. + -- @field Core.Set#SET_UNIT Set The Set of Units in the detected area. + -- @field Core.Zone#ZONE_UNIT Zone The Zone of the detected area. + -- @field #boolean Changed Documents if the detected area has changed. + -- @field #table Changes A list of the changes reported on the detected area. (It is up to the user of the detected area to consume those changes). + -- @field #number ID The identifier of the detected area. + -- @field #boolean FriendliesNearBy Indicates if there are friendlies within the detected area. + -- @field Wrapper.Unit#UNIT NearestFAC The nearest FAC near the Area. + -- @field Core.Point#COORDINATE Coordinate The last known coordinate of the DetectedItem. + -- @field Core.Point#COORDINATE InterceptCoord Intercept coordiante. + -- @field #number DistanceRecce Distance in meters of the Recce. + -- @field #number Index Detected item key. Could also be a string. + -- @field #string ItemID ItemPrefix .. "." .. self.DetectedItemMax. + -- @field #boolean Locked Lock detected item. + -- @field #table PlayersNearBy Table of nearby players. + -- @field #table FriendliesDistance Table of distances to friendly units. + -- @field #string TypeName Type name of the detected unit. + -- @field #string CategoryName Catetory name of the detected unit. + -- @field #string Name Name of the detected object. + -- @field #boolean IsVisible If true, detected object is visible. + -- @field #number LastTime Last time the detected item was seen. + -- @field DCS#Vec3 LastPos Last known position of the detected item. + -- @field DCS#Vec3 LastVelocity Last recorded 3D velocity vector of the detected item. + -- @field #boolean KnowType Type of detected item is known. + -- @field #boolean KnowDistance Distance to the detected item is known. + -- @field #number Distance Distance to the detected item. + + --- DETECTION constructor. + -- @param #DETECTION_BASE self + -- @param Core.Set#SET_GROUP DetectionSet The @{Set} of @{Group}s that is used to detect the units. + -- @return #DETECTION_BASE self + function DETECTION_BASE:New( DetectionSet ) + + -- Inherits from BASE + local self = BASE:Inherit( self, FSM:New() ) -- #DETECTION_BASE + + self.DetectedItemCount = 0 + self.DetectedItemMax = 0 + self.DetectedItems = {} + + self.DetectionSet = DetectionSet + + self.RefreshTimeInterval = 30 + + self:InitDetectVisual( nil ) + self:InitDetectOptical( nil ) + self:InitDetectRadar( nil ) + self:InitDetectRWR( nil ) + self:InitDetectIRST( nil ) + self:InitDetectDLINK( nil ) + + self:FilterCategories( { + Unit.Category.AIRPLANE, + Unit.Category.GROUND_UNIT, + Unit.Category.HELICOPTER, + Unit.Category.SHIP, + Unit.Category.STRUCTURE + } ) + + self:SetFriendliesRange( 6000 ) + + -- Create FSM transitions. + + self:SetStartState( "Stopped" ) + + self:AddTransition( "Stopped", "Start", "Detecting") + + --- OnLeave Transition Handler for State Stopped. + -- @function [parent=#DETECTION_BASE] OnLeaveStopped + -- @param #DETECTION_BASE self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnEnter Transition Handler for State Stopped. + -- @function [parent=#DETECTION_BASE] OnEnterStopped + -- @param #DETECTION_BASE self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- OnBefore Transition Handler for Event Start. + -- @function [parent=#DETECTION_BASE] OnBeforeStart + -- @param #DETECTION_BASE self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Start. + -- @function [parent=#DETECTION_BASE] OnAfterStart + -- @param #DETECTION_BASE self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Start. + -- @function [parent=#DETECTION_BASE] Start + -- @param #DETECTION_BASE self + + --- Asynchronous Event Trigger for Event Start. + -- @function [parent=#DETECTION_BASE] __Start + -- @param #DETECTION_BASE self + -- @param #number Delay The delay in seconds. + + --- OnLeave Transition Handler for State Detecting. + -- @function [parent=#DETECTION_BASE] OnLeaveDetecting + -- @param #DETECTION_BASE self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnEnter Transition Handler for State Detecting. + -- @function [parent=#DETECTION_BASE] OnEnterDetecting + -- @param #DETECTION_BASE self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + self:AddTransition( "Detecting", "Detect", "Detecting" ) + self:AddTransition( "Detecting", "Detection", "Detecting" ) + + --- OnBefore Transition Handler for Event Detect. + -- @function [parent=#DETECTION_BASE] OnBeforeDetect + -- @param #DETECTION_BASE self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Detect. + -- @function [parent=#DETECTION_BASE] OnAfterDetect + -- @param #DETECTION_BASE self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Detect. + -- @function [parent=#DETECTION_BASE] Detect + -- @param #DETECTION_BASE self + + --- Asynchronous Event Trigger for Event Detect. + -- @function [parent=#DETECTION_BASE] __Detect + -- @param #DETECTION_BASE self + -- @param #number Delay The delay in seconds. + + + self:AddTransition( "Detecting", "Detected", "Detecting" ) + + --- OnBefore Transition Handler for Event Detected. + -- @function [parent=#DETECTION_BASE] OnBeforeDetected + -- @param #DETECTION_BASE self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Detected. + -- @function [parent=#DETECTION_BASE] OnAfterDetected + -- @param #DETECTION_BASE self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @param #table Units Table of detected units. + + --- Synchronous Event Trigger for Event Detected. + -- @function [parent=#DETECTION_BASE] Detected + -- @param #DETECTION_BASE self + -- @param #table Units Table of detected units. + + --- Asynchronous Event Trigger for Event Detected. + -- @function [parent=#DETECTION_BASE] __Detected + -- @param #DETECTION_BASE self + -- @param #number Delay The delay in seconds. + -- @param #table Units Table of detected units. + + self:AddTransition( "Detecting", "DetectedItem", "Detecting" ) + + --- OnAfter Transition Handler for Event DetectedItem. + -- @function [parent=#DETECTION_BASE] OnAfterDetectedItem + -- @param #DETECTION_BASE self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @param #table DetectedItem The DetectedItem. + + self:AddTransition( "*", "Stop", "Stopped" ) + + --- OnBefore Transition Handler for Event Stop. + -- @function [parent=#DETECTION_BASE] OnBeforeStop + -- @param #DETECTION_BASE self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Stop. + -- @function [parent=#DETECTION_BASE] OnAfterStop + -- @param #DETECTION_BASE self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Stop. + -- @function [parent=#DETECTION_BASE] Stop + -- @param #DETECTION_BASE self + + --- Asynchronous Event Trigger for Event Stop. + -- @function [parent=#DETECTION_BASE] __Stop + -- @param #DETECTION_BASE self + -- @param #number Delay The delay in seconds. + + --- OnLeave Transition Handler for State Stopped. + -- @function [parent=#DETECTION_BASE] OnLeaveStopped + -- @param #DETECTION_BASE self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnEnter Transition Handler for State Stopped. + -- @function [parent=#DETECTION_BASE] OnEnterStopped + -- @param #DETECTION_BASE self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + return self + end + + do -- State Transition Handling + + --- @param #DETECTION_BASE self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + function DETECTION_BASE:onafterStart(From,Event,To) + self:__Detect( 1 ) + end + + --- @param #DETECTION_BASE self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + function DETECTION_BASE:onafterDetect(From,Event,To) + + local DetectDelay = 0.1 + self.DetectionCount = 0 + self.DetectionRun = 0 + self:UnIdentifyAllDetectedObjects() -- Resets the DetectedObjectsIdentified table + + local DetectionTimeStamp = timer.getTime() + + -- Reset detection cache for the next detection run. + for DetectionObjectName, DetectedObjectData in pairs( self.DetectedObjects ) do + + self.DetectedObjects[DetectionObjectName].IsDetected = false + self.DetectedObjects[DetectionObjectName].IsVisible = false + self.DetectedObjects[DetectionObjectName].KnowDistance = nil + self.DetectedObjects[DetectionObjectName].LastTime = nil + self.DetectedObjects[DetectionObjectName].LastPos = nil + self.DetectedObjects[DetectionObjectName].LastVelocity = nil + self.DetectedObjects[DetectionObjectName].Distance = 10000000 + + end + + -- Count alive(!) groups only. Solves issue #1173 https://github.com/FlightControl-Master/MOOSE/issues/1173 + self.DetectionCount = self:CountAliveRecce() + + local DetectionInterval = self.DetectionCount / ( self.RefreshTimeInterval - 1 ) + + self:ForEachAliveRecce( + function( DetectionGroup ) + self:__Detection( DetectDelay, DetectionGroup, DetectionTimeStamp ) -- Process each detection asynchronously. + DetectDelay = DetectDelay + DetectionInterval + end + ) + + self:__Detect( -self.RefreshTimeInterval ) + + end + + --- @param #DETECTION_BASE self + -- @param #number The amount of alive recce. + function DETECTION_BASE:CountAliveRecce() + + return self.DetectionSet:CountAlive() + + end + + --- @param #DETECTION_BASE self + function DETECTION_BASE:ForEachAliveRecce( IteratorFunction, ... ) + self:F2( arg ) + + self.DetectionSet:ForEachGroupAlive( IteratorFunction, arg ) + + return self + end + + + --- @param #DETECTION_BASE self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @param Wrapper.Group#GROUP DetectionGroup The Group detecting. + -- @param #number DetectionTimeStamp Time stamp of detection event. + function DETECTION_BASE:onafterDetection( From, Event, To, Detection, DetectionTimeStamp ) + + --self:F( { DetectedObjects = self.DetectedObjects } ) + + self.DetectionRun = self.DetectionRun + 1 + + local HasDetectedObjects = false + + if Detection and Detection:IsAlive() then + + --self:T( { "DetectionGroup is Alive", DetectionGroup:GetName() } ) + + local DetectionGroupName = Detection:GetName() + local DetectionUnit = Detection:GetUnit(1) + + local DetectedUnits = {} + + local DetectedTargets = Detection:GetDetectedTargets( + self.DetectVisual, + self.DetectOptical, + self.DetectRadar, + self.DetectIRST, + self.DetectRWR, + self.DetectDLINK + ) + + self:F( { DetectedTargets = DetectedTargets } ) + + for DetectionObjectID, Detection in pairs( DetectedTargets ) do + local DetectedObject = Detection.object -- DCS#Object + + if DetectedObject and DetectedObject:isExist() and DetectedObject.id_ < 50000000 then -- and ( DetectedObject:getCategory() == Object.Category.UNIT or DetectedObject:getCategory() == Object.Category.STATIC ) then + local DetectedObjectName = DetectedObject:getName() + if not self.DetectedObjects[DetectedObjectName] then + self.DetectedObjects[DetectedObjectName] = self.DetectedObjects[DetectedObjectName] or {} + self.DetectedObjects[DetectedObjectName].Name = DetectedObjectName + self.DetectedObjects[DetectedObjectName].Object = DetectedObject + end + end + end + + for DetectionObjectName, DetectedObjectData in pairs( self.DetectedObjects ) do + + local DetectedObject = DetectedObjectData.Object + + if DetectedObject:isExist() then + + local TargetIsDetected, TargetIsVisible, TargetLastTime, TargetKnowType, TargetKnowDistance, TargetLastPos, TargetLastVelocity = DetectionUnit:IsTargetDetected( + DetectedObject, + self.DetectVisual, + self.DetectOptical, + self.DetectRadar, + self.DetectIRST, + self.DetectRWR, + self.DetectDLINK + ) + + --self:T2( { TargetIsDetected = TargetIsDetected, TargetIsVisible = TargetIsVisible, TargetLastTime = TargetLastTime, TargetKnowType = TargetKnowType, TargetKnowDistance = TargetKnowDistance, TargetLastPos = TargetLastPos, TargetLastVelocity = TargetLastVelocity } ) + + -- Only process if the target is visible. Detection also returns invisible units. + --if Detection.visible == true then + + local DetectionAccepted = true + + local DetectedObjectName = DetectedObject:getName() + local DetectedObjectType = DetectedObject:getTypeName() + + local DetectedObjectVec3 = DetectedObject:getPoint() + local DetectedObjectVec2 = { x = DetectedObjectVec3.x, y = DetectedObjectVec3.z } + local DetectionGroupVec3 = Detection:GetVec3() + local DetectionGroupVec2 = { x = DetectionGroupVec3.x, y = DetectionGroupVec3.z } + + local Distance = ( ( DetectedObjectVec3.x - DetectionGroupVec3.x )^2 + + ( DetectedObjectVec3.y - DetectionGroupVec3.y )^2 + + ( DetectedObjectVec3.z - DetectionGroupVec3.z )^2 + ) ^ 0.5 / 1000 + + local DetectedUnitCategory = DetectedObject:getDesc().category + + --self:F( { "Detected Target:", DetectionGroupName, DetectedObjectName, DetectedObjectType, Distance, DetectedUnitCategory } ) + + -- Calculate Acceptance + + DetectionAccepted = self._.FilterCategories[DetectedUnitCategory] ~= nil and DetectionAccepted or false + + -- if Distance > 15000 then + -- if DetectedUnitCategory == Unit.Category.GROUND_UNIT or DetectedUnitCategory == Unit.Category.SHIP then + -- if DetectedObject:hasSensors( Unit.SensorType.RADAR, Unit.RadarType.AS ) == false then + -- DetectionAccepted = false + -- end + -- end + -- end + + if self.AcceptRange and Distance * 1000 > self.AcceptRange then + DetectionAccepted = false + end + + if self.AcceptZones then + local AnyZoneDetection = false + for AcceptZoneID, AcceptZone in pairs( self.AcceptZones ) do + local AcceptZone = AcceptZone -- Core.Zone#ZONE_BASE + if AcceptZone:IsVec2InZone( DetectedObjectVec2 ) then + AnyZoneDetection = true + end + end + if not AnyZoneDetection then + DetectionAccepted = false + end + end + + if self.RejectZones then + for RejectZoneID, RejectZone in pairs( self.RejectZones ) do + local RejectZone = RejectZone -- Core.Zone#ZONE_BASE + if RejectZone:IsPointVec2InZone( DetectedObjectVec2 ) == true then + DetectionAccepted = false + end + end + end + + -- Calculate additional probabilities + + if not self.DetectedObjects[DetectedObjectName] and TargetIsVisible and self.DistanceProbability then + local DistanceFactor = Distance / 4 + local DistanceProbabilityReversed = ( 1 - self.DistanceProbability ) * DistanceFactor + local DistanceProbability = 1 - DistanceProbabilityReversed + DistanceProbability = DistanceProbability * 30 / 300 + local Probability = math.random() -- Selects a number between 0 and 1 + --self:T( { Probability, DistanceProbability } ) + if Probability > DistanceProbability then + DetectionAccepted = false + end + end + + if not self.DetectedObjects[DetectedObjectName] and TargetIsVisible and self.AlphaAngleProbability then + local NormalVec2 = { x = DetectedObjectVec2.x - DetectionGroupVec2.x, y = DetectedObjectVec2.y - DetectionGroupVec2.y } + local AlphaAngle = math.atan2( NormalVec2.y, NormalVec2.x ) + local Sinus = math.sin( AlphaAngle ) + local AlphaAngleProbabilityReversed = ( 1 - self.AlphaAngleProbability ) * ( 1 - Sinus ) + local AlphaAngleProbability = 1 - AlphaAngleProbabilityReversed + + AlphaAngleProbability = AlphaAngleProbability * 30 / 300 + + local Probability = math.random() -- Selects a number between 0 and 1 + --self:T( { Probability, AlphaAngleProbability } ) + if Probability > AlphaAngleProbability then + DetectionAccepted = false + end + + end + + if not self.DetectedObjects[DetectedObjectName] and TargetIsVisible and self.ZoneProbability then + + for ZoneDataID, ZoneData in pairs( self.ZoneProbability ) do + self:F({ZoneData}) + local ZoneObject = ZoneData[1] -- Core.Zone#ZONE_BASE + local ZoneProbability = ZoneData[2] -- #number + ZoneProbability = ZoneProbability * 30 / 300 + + if ZoneObject:IsPointVec2InZone( DetectedObjectVec2 ) == true then + local Probability = math.random() -- Selects a number between 0 and 1 + --self:T( { Probability, ZoneProbability } ) + if Probability > ZoneProbability then + DetectionAccepted = false + break + end + end + end + end + + if DetectionAccepted then + + HasDetectedObjects = true + + self.DetectedObjects[DetectedObjectName] = self.DetectedObjects[DetectedObjectName] or {} + self.DetectedObjects[DetectedObjectName].Name = DetectedObjectName + + if TargetIsDetected and TargetIsDetected == true then + self.DetectedObjects[DetectedObjectName].IsDetected = TargetIsDetected + end + + if TargetIsDetected and TargetIsVisible and TargetIsVisible == true then + self.DetectedObjects[DetectedObjectName].IsVisible = TargetIsDetected and TargetIsVisible + end + + if TargetIsDetected and not self.DetectedObjects[DetectedObjectName].KnowType then + self.DetectedObjects[DetectedObjectName].KnowType = TargetIsDetected and TargetKnowType + end + self.DetectedObjects[DetectedObjectName].KnowDistance = TargetKnowDistance -- Detection.distance -- TargetKnowDistance + self.DetectedObjects[DetectedObjectName].LastTime = ( TargetIsDetected and TargetIsVisible == false ) and TargetLastTime + self.DetectedObjects[DetectedObjectName].LastPos = ( TargetIsDetected and TargetIsVisible == false ) and TargetLastPos + self.DetectedObjects[DetectedObjectName].LastVelocity = ( TargetIsDetected and TargetIsVisible == false ) and TargetLastVelocity + + if not self.DetectedObjects[DetectedObjectName].Distance or ( Distance and self.DetectedObjects[DetectedObjectName].Distance > Distance ) then + self.DetectedObjects[DetectedObjectName].Distance = Distance + end + + self.DetectedObjects[DetectedObjectName].DetectionTimeStamp = DetectionTimeStamp + + self:F( { DetectedObject = self.DetectedObjects[DetectedObjectName] } ) + + local DetectedUnit = UNIT:FindByName( DetectedObjectName ) + + DetectedUnits[DetectedObjectName] = DetectedUnit + else + -- if beyond the DetectionRange then nullify... + self:F( { DetectedObject = "No more detection for " .. DetectedObjectName } ) + if self.DetectedObjects[DetectedObjectName] then + self.DetectedObjects[DetectedObjectName] = nil + end + end + + --self:T2( self.DetectedObjects ) + else + -- The previously detected object does not exist anymore, delete from the cache. + self:F( "Removing from DetectedObjects: " .. DetectionObjectName ) + self.DetectedObjects[DetectionObjectName] = nil + end + end + + if HasDetectedObjects then + self:__Detected( 0.1, DetectedUnits ) + end + + end + + if self.DetectionCount > 0 and self.DetectionRun == self.DetectionCount then + + -- First check if all DetectedObjects were detected. + -- This is important. When there are DetectedObjects in the list, but were not detected, + -- And these remain undetected for more than 60 seconds, then these DetectedObjects will be flagged as not Detected. + -- IsDetected = false! + -- This is used in A2A_TASK_DISPATCHER to initiate fighter sweeping! The TASK_A2A_INTERCEPT tasks will be replaced with TASK_A2A_SWEEP tasks. + for DetectedObjectName, DetectedObject in pairs( self.DetectedObjects ) do + if self.DetectedObjects[DetectedObjectName].IsDetected == true and self.DetectedObjects[DetectedObjectName].DetectionTimeStamp + 300 <= DetectionTimeStamp then + self.DetectedObjects[DetectedObjectName].IsDetected = false + end + end + + self:CreateDetectionItems() -- Polymorphic call to Create/Update the DetectionItems list for the DETECTION_ class grouping method. + + for DetectedItemID, DetectedItem in pairs( self.DetectedItems ) do + + self:UpdateDetectedItemDetection( DetectedItem ) + + self:CleanDetectionItem( DetectedItem, DetectedItemID ) -- Any DetectionItem that has a Set with zero elements in it, must be removed from the DetectionItems list. + + if DetectedItem then + self:__DetectedItem( 0.1, DetectedItem ) + end + + end + end + + + end + + + end + + do -- DetectionItems Creation + + --- Clean the DetectedItem table. + -- @param #DETECTION_BASE self + -- @return #DETECTION_BASE + function DETECTION_BASE:CleanDetectionItem( DetectedItem, DetectedItemID ) + + -- We clean all DetectedItems. + -- if there are any remaining DetectedItems with no Set Objects then the Item in the DetectedItems must be deleted. + + local DetectedSet = DetectedItem.Set + + if DetectedSet:Count() == 0 then + self:RemoveDetectedItem( DetectedItemID ) + end + + return self + end + + --- Forget a Unit from a DetectionItem + -- @param #DETECTION_BASE self + -- @param #string UnitName The UnitName that needs to be forgotten from the DetectionItem Sets. + -- @return #DETECTION_BASE + function DETECTION_BASE:ForgetDetectedUnit( UnitName ) + + local DetectedItems = self:GetDetectedItems() + + for DetectedItemIndex, DetectedItem in pairs( DetectedItems ) do + local DetectedSet = self:GetDetectedItemSet( DetectedItem ) + if DetectedSet then + DetectedSet:RemoveUnitsByName( UnitName ) + end + end + + return self + end + + --- Make a DetectionSet table. This function will be overridden in the derived clsses. + -- @param #DETECTION_BASE self + -- @return #DETECTION_BASE + function DETECTION_BASE:CreateDetectionItems() + + self:F( "Error, in DETECTION_BASE class..." ) + return self + end + + + end + + do -- Initialization methods + + --- Detect Visual. + -- @param #DETECTION_BASE self + -- @param #boolean DetectVisual + -- @return #DETECTION_BASE self + function DETECTION_BASE:InitDetectVisual( DetectVisual ) + + self.DetectVisual = DetectVisual + + return self + end + + --- Detect Optical. + -- @param #DETECTION_BASE self + -- @param #boolean DetectOptical + -- @return #DETECTION_BASE self + function DETECTION_BASE:InitDetectOptical( DetectOptical ) + self:F2() + + self.DetectOptical = DetectOptical + + return self + end + + --- Detect Radar. + -- @param #DETECTION_BASE self + -- @param #boolean DetectRadar + -- @return #DETECTION_BASE self + function DETECTION_BASE:InitDetectRadar( DetectRadar ) + self:F2() + + self.DetectRadar = DetectRadar + + return self + end + + --- Detect IRST. + -- @param #DETECTION_BASE self + -- @param #boolean DetectIRST + -- @return #DETECTION_BASE self + function DETECTION_BASE:InitDetectIRST( DetectIRST ) + self:F2() + + self.DetectIRST = DetectIRST + + return self + end + + --- Detect RWR. + -- @param #DETECTION_BASE self + -- @param #boolean DetectRWR + -- @return #DETECTION_BASE self + function DETECTION_BASE:InitDetectRWR( DetectRWR ) + self:F2() + + self.DetectRWR = DetectRWR + + return self + end + + --- Detect DLINK. + -- @param #DETECTION_BASE self + -- @param #boolean DetectDLINK + -- @return #DETECTION_BASE self + function DETECTION_BASE:InitDetectDLINK( DetectDLINK ) + self:F2() + + self.DetectDLINK = DetectDLINK + + return self + end + + end + + do -- Filter methods + + --- Filter the detected units based on Unit.Category + -- The different values of Unit.Category can be: + -- + -- * Unit.Category.AIRPLANE + -- * Unit.Category.GROUND_UNIT + -- * Unit.Category.HELICOPTER + -- * Unit.Category.SHIP + -- * Unit.Category.STRUCTURE + -- + -- Multiple Unit.Category entries can be given as a table and then these will be evaluated as an OR expression. + -- + -- Example to filter a single category (Unit.Category.AIRPLANE). + -- + -- DetectionObject:FilterCategories( Unit.Category.AIRPLANE ) + -- + -- Example to filter multiple categories (Unit.Category.AIRPLANE, Unit.Category.HELICOPTER). Note the {}. + -- + -- DetectionObject:FilterCategories( { Unit.Category.AIRPLANE, Unit.Category.HELICOPTER } ) + -- + -- @param #DETECTION_BASE self + -- @param #list FilterCategories The Categories entries + -- @return #DETECTION_BASE self + function DETECTION_BASE:FilterCategories( FilterCategories ) + self:F2() + + self._.FilterCategories = {} + if type( FilterCategories ) == "table" then + for CategoryID, Category in pairs( FilterCategories ) do + self._.FilterCategories[Category] = Category + end + else + self._.FilterCategories[FilterCategories] = FilterCategories + end + return self + + end + + end + + do + + --- Set the detection interval time in seconds. + -- @param #DETECTION_BASE self + -- @param #number RefreshTimeInterval Interval in seconds. + -- @return #DETECTION_BASE self + function DETECTION_BASE:SetRefreshTimeInterval( RefreshTimeInterval ) + self:F2() + + self.RefreshTimeInterval = RefreshTimeInterval + + return self + end + + end + + do -- Friendlies Radius + + --- Set the radius in meters to validate if friendlies are nearby. + -- @param #DETECTION_BASE self + -- @param #number FriendliesRange Radius to use when checking if Friendlies are nearby. + -- @return #DETECTION_BASE self + function DETECTION_BASE:SetFriendliesRange( FriendliesRange ) --R2.2 Friendlies range + self:F2() + + self.FriendliesRange = FriendliesRange + + return self + end + + end + + do -- Intercept Point + + --- Set the parameters to calculate to optimal intercept point. + -- @param #DETECTION_BASE self + -- @param #boolean Intercept Intercept is true if an intercept point is calculated. Intercept is false if it is disabled. The default Intercept is false. + -- @param #number InterceptDelay If Intercept is true, then InterceptDelay is the average time it takes to get airplanes airborne. + -- @return #DETECTION_BASE self + function DETECTION_BASE:SetIntercept( Intercept, InterceptDelay ) + self:F2() + + self.Intercept = Intercept + self.InterceptDelay = InterceptDelay + + return self + end + + end + + do -- Accept / Reject detected units + + --- Accept detections if within a range in meters. + -- @param #DETECTION_BASE self + -- @param #number AcceptRange Accept a detection if the unit is within the AcceptRange in meters. + -- @return #DETECTION_BASE self + function DETECTION_BASE:SetAcceptRange( AcceptRange ) + self:F2() + + self.AcceptRange = AcceptRange + + return self + end + + --- Accept detections if within the specified zone(s). + -- @param #DETECTION_BASE self + -- @param Core.Zone#ZONE_BASE AcceptZones Can be a list or ZONE_BASE objects, or a single ZONE_BASE object. + -- @return #DETECTION_BASE self + function DETECTION_BASE:SetAcceptZones( AcceptZones ) + self:F2() + + if type( AcceptZones ) == "table" then + if AcceptZones.ClassName and AcceptZones:IsInstanceOf( ZONE_BASE ) then + self.AcceptZones = { AcceptZones } + else + self.AcceptZones = AcceptZones + end + else + self:F( { "AcceptZones must be a list of ZONE_BASE derived objects or one ZONE_BASE derived object", AcceptZones } ) + error() + end + + return self + end + + --- Reject detections if within the specified zone(s). + -- @param #DETECTION_BASE self + -- @param Core.Zone#ZONE_BASE RejectZones Can be a list or ZONE_BASE objects, or a single ZONE_BASE object. + -- @return #DETECTION_BASE self + function DETECTION_BASE:SetRejectZones( RejectZones ) + self:F2() + + if type( RejectZones ) == "table" then + if RejectZones.ClassName and RejectZones:IsInstanceOf( ZONE_BASE ) then + self.RejectZones = { RejectZones } + else + self.RejectZones = RejectZones + end + else + self:F( { "RejectZones must be a list of ZONE_BASE derived objects or one ZONE_BASE derived object", RejectZones } ) + error() + end + + return self + end + + end + + do -- Probability methods + + --- Upon a **visual** detection, the further away a detected object is, the less likely it is to be detected properly. + -- Also, the speed of accurate detection plays a role. + -- A distance probability factor between 0 and 1 can be given, that will model a linear extrapolated probability over 10 km distance. + -- For example, if a probability factor of 0.6 (60%) is given, the extrapolated probabilities over 15 kilometers would like like: + -- 1 km: 96%, 2 km: 92%, 3 km: 88%, 4 km: 84%, 5 km: 80%, 6 km: 76%, 7 km: 72%, 8 km: 68%, 9 km: 64%, 10 km: 60%, 11 km: 56%, 12 km: 52%, 13 km: 48%, 14 km: 44%, 15 km: 40%. + -- @param #DETECTION_BASE self + -- @param DistanceProbability The probability factor. + -- @return #DETECTION_BASE self + function DETECTION_BASE:SetDistanceProbability( DistanceProbability ) + self:F2() + + self.DistanceProbability = DistanceProbability + + return self + end + + + --- Upon a **visual** detection, the higher the unit is during the detecting process, the more likely the detected unit is to be detected properly. + -- A detection at a 90% alpha angle is the most optimal, a detection at 10% is less and a detection at 0% is less likely to be correct. + -- + -- A probability factor between 0 and 1 can be given, that will model a progressive extrapolated probability if the target would be detected at a 0° angle. + -- + -- For example, if a alpha angle probability factor of 0.7 is given, the extrapolated probabilities of the different angles would look like: + -- 0°: 70%, 10°: 75,21%, 20°: 80,26%, 30°: 85%, 40°: 89,28%, 50°: 92,98%, 60°: 95,98%, 70°: 98,19%, 80°: 99,54%, 90°: 100% + -- @param #DETECTION_BASE self + -- @param AlphaAngleProbability The probability factor. + -- @return #DETECTION_BASE self + function DETECTION_BASE:SetAlphaAngleProbability( AlphaAngleProbability ) + self:F2() + + self.AlphaAngleProbability = AlphaAngleProbability + + return self + end + + --- Upon a **visual** detection, the more a detected unit is within a cloudy zone, the less likely the detected unit is to be detected successfully. + -- The Cloudy Zones work with the ZONE_BASE derived classes. The mission designer can define within the mission + -- zones that reflect cloudy areas where detected units may not be so easily visually detected. + -- @param #DETECTION_BASE self + -- @param ZoneArray Aray of a The ZONE_BASE object and a ZoneProbability pair.. + -- @return #DETECTION_BASE self + function DETECTION_BASE:SetZoneProbability( ZoneArray ) + self:F2() + + self.ZoneProbability = ZoneArray + + return self + end + + + end + + do -- Change processing + + --- Accepts changes from the detected item. + -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedItem DetectedItem + -- @return #DETECTION_BASE self + function DETECTION_BASE:AcceptChanges( DetectedItem ) + + DetectedItem.Changed = false + DetectedItem.Changes = {} + + return self + end + + --- Add a change to the detected zone. + -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedItem DetectedItem + -- @param #string ChangeCode + -- @return #DETECTION_BASE self + function DETECTION_BASE:AddChangeItem( DetectedItem, ChangeCode, ItemUnitType ) + + DetectedItem.Changed = true + local ID = DetectedItem.ID + + DetectedItem.Changes = DetectedItem.Changes or {} + DetectedItem.Changes[ChangeCode] = DetectedItem.Changes[ChangeCode] or {} + DetectedItem.Changes[ChangeCode].ID = ID + DetectedItem.Changes[ChangeCode].ItemUnitType = ItemUnitType + + self:F( { "Change on Detected Item:", DetectedItemID = DetectedItem.ID, ChangeCode = ChangeCode, ItemUnitType = ItemUnitType } ) + + return self + end + + + --- Add a change to the detected zone. + -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedItem DetectedItem + -- @param #string ChangeCode + -- @param #string ChangeUnitType + -- @return #DETECTION_BASE self + function DETECTION_BASE:AddChangeUnit( DetectedItem, ChangeCode, ChangeUnitType ) + + DetectedItem.Changed = true + local ID = DetectedItem.ID + + DetectedItem.Changes = DetectedItem.Changes or {} + DetectedItem.Changes[ChangeCode] = DetectedItem.Changes[ChangeCode] or {} + DetectedItem.Changes[ChangeCode][ChangeUnitType] = DetectedItem.Changes[ChangeCode][ChangeUnitType] or 0 + DetectedItem.Changes[ChangeCode][ChangeUnitType] = DetectedItem.Changes[ChangeCode][ChangeUnitType] + 1 + DetectedItem.Changes[ChangeCode].ID = ID + + self:F( { "Change on Detected Unit:", DetectedItemID = DetectedItem.ID, ChangeCode = ChangeCode, ChangeUnitType = ChangeUnitType } ) + + return self + end + + end + + do -- Friendly calculations + + --- This will allow during friendly search any recce or detection unit to be also considered as a friendly. + -- By default, recce aren't considered friendly, because that would mean that a recce would be also an attacking friendly, + -- and this is wrong. + -- However, in a CAP situation, when the CAP is part of an EWR network, the CAP is also an attacker. + -- This, this method allows to register for a detection the CAP unit name prefixes to be considered CAP. + -- @param #DETECTION_BASE self + -- @param #string FriendlyPrefixes A string or a list of prefixes. + -- @return #DETECTION_BASE + function DETECTION_BASE:SetFriendlyPrefixes( FriendlyPrefixes ) + + self.FriendlyPrefixes = self.FriendlyPrefixes or {} + if type( FriendlyPrefixes ) ~= "table" then + FriendlyPrefixes = { FriendlyPrefixes } + end + for PrefixID, Prefix in pairs( FriendlyPrefixes ) do + self:F( { FriendlyPrefix = Prefix } ) + self.FriendlyPrefixes[Prefix] = Prefix + end + return self + end + + --- This will allow during friendly search only units of the specified list of categories. + -- @param #DETECTION_BASE self + -- @param #string FriendlyCategories A list of unit categories. + -- @return #DETECTION_BASE + -- @usage + -- -- Only allow Ships and Vehicles to be part of the friendly team. + -- Detection:SetFriendlyCategories( { Unit.Category.SHIP, Unit.Category.GROUND_UNIT } ) + + --- Returns if there are friendlies nearby the FAC units ... + -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedItem DetectedItem + -- @param DCS#Unit.Category Category The category of the unit. + -- @return #boolean true if there are friendlies nearby + function DETECTION_BASE:IsFriendliesNearBy( DetectedItem, Category ) +-- self:F( { "FriendliesNearBy Test", DetectedItem.FriendliesNearBy } ) + return ( DetectedItem.FriendliesNearBy and DetectedItem.FriendliesNearBy[Category] ~= nil ) or false + end + + --- Returns friendly units nearby the FAC units ... + -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedItem DetectedItem + -- @param DCS#Unit.Category Category The category of the unit. + -- @return #map<#string,Wrapper.Unit#UNIT> The map of Friendly UNITs. + function DETECTION_BASE:GetFriendliesNearBy( DetectedItem, Category ) + + return DetectedItem.FriendliesNearBy and DetectedItem.FriendliesNearBy[Category] + end + + --- Returns if there are friendlies nearby the intercept ... + -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedItem DetectedItem + -- @return #boolean trhe if there are friendlies near the intercept. + function DETECTION_BASE:IsFriendliesNearIntercept( DetectedItem ) + + return DetectedItem.FriendliesNearIntercept ~= nil or false + end + + --- Returns friendly units nearby the intercept point ... + -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedItem DetectedItem The detected item. + -- @return #map<#string,Wrapper.Unit#UNIT> The map of Friendly UNITs. + function DETECTION_BASE:GetFriendliesNearIntercept( DetectedItem ) + + return DetectedItem.FriendliesNearIntercept + end + + --- Returns the distance used to identify friendlies near the deteted item ... + -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedItem DetectedItem The detected item. + -- @return #table A table of distances to friendlies. + function DETECTION_BASE:GetFriendliesDistance( DetectedItem ) + + return DetectedItem.FriendliesDistance + end + + --- Returns if there are friendlies nearby the FAC units ... + -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedItem DetectedItem + -- @return #boolean trhe if there are friendlies nearby + function DETECTION_BASE:IsPlayersNearBy( DetectedItem ) + + return DetectedItem.PlayersNearBy ~= nil + end + + --- Returns friendly units nearby the FAC units ... + -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedItem DetectedItem The detected item. + -- @return #map<#string,Wrapper.Unit#UNIT> The map of Friendly UNITs. + function DETECTION_BASE:GetPlayersNearBy( DetectedItem ) + + return DetectedItem.PlayersNearBy + end + + --- Background worker function to determine if there are friendlies nearby ... + -- @param #DETECTION_BASE self + -- @param #table TargetData + function DETECTION_BASE:ReportFriendliesNearBy( TargetData ) + --self:F( { "Search Friendlies", DetectedItem = TargetData.DetectedItem } ) + + local DetectedItem = TargetData.DetectedItem --#DETECTION_BASE.DetectedItem + local DetectedSet = TargetData.DetectedItem.Set + local DetectedUnit = DetectedSet:GetFirst() -- Wrapper.Unit#UNIT + + DetectedItem.FriendliesNearBy = nil + + -- We need to ensure that the DetectedUnit is alive! + if DetectedUnit and DetectedUnit:IsAlive() then + + local DetectedUnitCoord = DetectedUnit:GetCoordinate() + local InterceptCoord = TargetData.InterceptCoord or DetectedUnitCoord + + local SphereSearch = { + id = world.VolumeType.SPHERE, + params = { + point = InterceptCoord:GetVec3(), + radius = self.FriendliesRange, + } + + } + + --- @param DCS#Unit FoundDCSUnit + -- @param Wrapper.Group#GROUP ReportGroup + -- @param Core.Set#SET_GROUP ReportSetGroup + local FindNearByFriendlies = function( FoundDCSUnit, ReportGroupData ) + + local DetectedItem = ReportGroupData.DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem + local DetectedSet = ReportGroupData.DetectedItem.Set + local DetectedUnit = DetectedSet:GetFirst() -- Wrapper.Unit#UNIT + local DetectedUnitCoord = DetectedUnit:GetCoordinate() + local InterceptCoord = ReportGroupData.InterceptCoord or DetectedUnitCoord + local ReportSetGroup = ReportGroupData.ReportSetGroup + + local EnemyCoalition = DetectedUnit:GetCoalition() + + local FoundUnitCoalition = FoundDCSUnit:getCoalition() + local FoundUnitCategory = FoundDCSUnit:getDesc().category + local FoundUnitName = FoundDCSUnit:getName() + local FoundUnitGroupName = FoundDCSUnit:getGroup():getName() + local EnemyUnitName = DetectedUnit:GetName() + + local FoundUnitInReportSetGroup = ReportSetGroup:FindGroup( FoundUnitGroupName ) ~= nil + --self:T( { "Friendlies search:", FoundUnitName, FoundUnitCoalition, EnemyUnitName, EnemyCoalition, FoundUnitInReportSetGroup } ) + + if FoundUnitInReportSetGroup == true then + -- If the recce was part of the friendlies found, then check if the recce is part of the allowed friendly unit prefixes. + for PrefixID, Prefix in pairs( self.FriendlyPrefixes or {} ) do + --self:F( { "Friendly Prefix:", Prefix = Prefix } ) + -- In case a match is found (so a recce unit name is part of the friendly prefixes), then report that recce to be part of the friendlies. + -- This is important if CAP planes (so planes using their own radar) to be scanning for targets as part of the EWR network. + -- But CAP planes are also attackers, so they need to be considered friendlies too! + -- I chose to use prefixes because it is the fastest way to check. + if string.find( FoundUnitName, Prefix:gsub ("-", "%%-"), 1 ) then + FoundUnitInReportSetGroup = false + break + end + end + end + + --self:F( { "Friendlies near Target:", FoundUnitName, FoundUnitCoalition, EnemyUnitName, EnemyCoalition, FoundUnitInReportSetGroup } ) + + if FoundUnitCoalition ~= EnemyCoalition and FoundUnitInReportSetGroup == false then + local FriendlyUnit = UNIT:Find( FoundDCSUnit ) + local FriendlyUnitName = FriendlyUnit:GetName() + local FriendlyUnitCategory = FriendlyUnit:GetDesc().category + + -- Friendlies are sorted per unit category. + DetectedItem.FriendliesNearBy = DetectedItem.FriendliesNearBy or {} + DetectedItem.FriendliesNearBy[FoundUnitCategory] = DetectedItem.FriendliesNearBy[FoundUnitCategory] or {} + DetectedItem.FriendliesNearBy[FoundUnitCategory][FriendlyUnitName] = FriendlyUnit + + local Distance = DetectedUnitCoord:Get2DDistance( FriendlyUnit:GetCoordinate() ) + DetectedItem.FriendliesDistance = DetectedItem.FriendliesDistance or {} + DetectedItem.FriendliesDistance[Distance] = FriendlyUnit + --self:F( { "Friendlies Found:", FriendlyUnitName = FriendlyUnitName, Distance = Distance, FriendlyUnitCategory = FriendlyUnitCategory, FriendliesCategory = self.FriendliesCategory } ) + return true + end + + return true + end + + world.searchObjects( Object.Category.UNIT, SphereSearch, FindNearByFriendlies, TargetData ) + + DetectedItem.PlayersNearBy = nil + + _DATABASE:ForEachPlayer( + --- @param Wrapper.Unit#UNIT PlayerUnit + function( PlayerUnitName ) + local PlayerUnit = UNIT:FindByName( PlayerUnitName ) + + -- Fix for issue https://github.com/FlightControl-Master/MOOSE/issues/1225 + if PlayerUnit and PlayerUnit:IsAlive() then + local coord=PlayerUnit:GetCoordinate() + + if coord and coord:IsInRadius( DetectedUnitCoord, self.FriendliesRange ) then + + local PlayerUnitCategory = PlayerUnit:GetDesc().category + + if ( not self.FriendliesCategory ) or ( self.FriendliesCategory and ( self.FriendliesCategory == PlayerUnitCategory ) ) then + + local PlayerUnitName = PlayerUnit:GetName() + + DetectedItem.PlayersNearBy = DetectedItem.PlayersNearBy or {} + DetectedItem.PlayersNearBy[PlayerUnitName] = PlayerUnit + + -- Friendlies are sorted per unit category. + DetectedItem.FriendliesNearBy = DetectedItem.FriendliesNearBy or {} + DetectedItem.FriendliesNearBy[PlayerUnitCategory] = DetectedItem.FriendliesNearBy[PlayerUnitCategory] or {} + DetectedItem.FriendliesNearBy[PlayerUnitCategory][PlayerUnitName] = PlayerUnit + + local Distance = DetectedUnitCoord:Get2DDistance( PlayerUnit:GetCoordinate() ) + DetectedItem.FriendliesDistance = DetectedItem.FriendliesDistance or {} + DetectedItem.FriendliesDistance[Distance] = PlayerUnit + + end + end + end + end + ) + end + + self:F( { Friendlies = DetectedItem.FriendliesNearBy, Players = DetectedItem.PlayersNearBy } ) + + end + + end + + --- Determines if a detected object has already been identified during detection processing. + -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedObject DetectedObject + -- @return #boolean true if already identified. + function DETECTION_BASE:IsDetectedObjectIdentified( DetectedObject ) + + local DetectedObjectName = DetectedObject.Name + if DetectedObjectName then + local DetectedObjectIdentified = self.DetectedObjectsIdentified[DetectedObjectName] == true + return DetectedObjectIdentified + else + return nil + end + end + + --- Identifies a detected object during detection processing. + -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedObject DetectedObject + function DETECTION_BASE:IdentifyDetectedObject( DetectedObject ) + --self:F( { "Identified:", DetectedObject.Name } ) + + local DetectedObjectName = DetectedObject.Name + self.DetectedObjectsIdentified[DetectedObjectName] = true + end + + --- UnIdentify a detected object during detection processing. + -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedObject DetectedObject + function DETECTION_BASE:UnIdentifyDetectedObject( DetectedObject ) + + local DetectedObjectName = DetectedObject.Name + self.DetectedObjectsIdentified[DetectedObjectName] = false + end + + --- UnIdentify all detected objects during detection processing. + -- @param #DETECTION_BASE self + function DETECTION_BASE:UnIdentifyAllDetectedObjects() + + self.DetectedObjectsIdentified = {} -- Table will be garbage collected. + end + + --- Gets a detected object with a given name. + -- @param #DETECTION_BASE self + -- @param #string ObjectName + -- @return #DETECTION_BASE.DetectedObject + function DETECTION_BASE:GetDetectedObject( ObjectName ) + self:F2( { ObjectName = ObjectName } ) + + if ObjectName then + local DetectedObject = self.DetectedObjects[ObjectName] + + if DetectedObject then + --self:F( { DetectedObjects = self.DetectedObjects } ) + -- Only return detected objects that are alive! + local DetectedUnit = UNIT:FindByName( ObjectName ) + if DetectedUnit and DetectedUnit:IsAlive() then + if self:IsDetectedObjectIdentified( DetectedObject ) == false then + --self:F( { DetectedObject = DetectedObject } ) + return DetectedObject + end + end + end + end + + return nil + end + + + --- Gets a detected unit type name, taking into account the detection results. + -- @param #DETECTION_BASE self + -- @param Wrapper.Unit#UNIT DetectedUnit + -- @return #string The type name + function DETECTION_BASE:GetDetectedUnitTypeName( DetectedUnit ) + --self:F2( ObjectName ) + + if DetectedUnit and DetectedUnit:IsAlive() then + local DetectedUnitName = DetectedUnit:GetName() + local DetectedObject = self.DetectedObjects[DetectedUnitName] + + if DetectedObject then + if DetectedObject.KnowType then + return DetectedUnit:GetTypeName() + else + return "Unknown" + end + else + return "Unknown" + end + else + return "Dead:" .. DetectedUnit:GetName() + end + + return "Undetected:" .. DetectedUnit:GetName() + end + + + --- Adds a new DetectedItem to the DetectedItems list. + -- The DetectedItem is a table and contains a SET_UNIT in the field Set. + -- @param #DETECTION_BASE self + -- @param #string ItemPrefix Prefix of detected item. + -- @param #number DetectedItemKey The key of the DetectedItem. Default self.DetectedItemMax. Could also be a string in principle. + -- @param Core.Set#SET_UNIT Set (optional) The Set of Units to be added. + -- @return #DETECTION_BASE.DetectedItem + function DETECTION_BASE:AddDetectedItem( ItemPrefix, DetectedItemKey, Set ) + + local DetectedItem = {} --#DETECTION_BASE.DetectedItem + self.DetectedItemCount = self.DetectedItemCount + 1 + self.DetectedItemMax = self.DetectedItemMax + 1 + + + DetectedItemKey = DetectedItemKey or self.DetectedItemMax + self.DetectedItems[DetectedItemKey] = DetectedItem + self.DetectedItemsByIndex[DetectedItemKey] = DetectedItem + DetectedItem.Index = DetectedItemKey + + DetectedItem.Set = Set or SET_UNIT:New():FilterDeads():FilterCrashes() + DetectedItem.ItemID = ItemPrefix .. "." .. self.DetectedItemMax + DetectedItem.ID = self.DetectedItemMax + DetectedItem.Removed = false + + if self.Locking then + self:LockDetectedItem( DetectedItem ) + end + + return DetectedItem + end + + --- Adds a new DetectedItem to the DetectedItems list. + -- The DetectedItem is a table and contains a SET_UNIT in the field Set. + -- @param #DETECTION_BASE self + -- @param DetectedItemKey The key of the DetectedItem. + -- @param Core.Set#SET_UNIT Set (optional) The Set of Units to be added. + -- @param Core.Zone#ZONE_UNIT Zone (optional) The Zone to be added where the Units are located. + -- @return #DETECTION_BASE.DetectedItem + function DETECTION_BASE:AddDetectedItemZone( ItemPrefix, DetectedItemKey, Set, Zone ) + + self:F( { ItemPrefix, DetectedItemKey, Set, Zone } ) + + local DetectedItem = self:AddDetectedItem( ItemPrefix, DetectedItemKey, Set ) + + DetectedItem.Zone = Zone + + return DetectedItem + end + + --- Removes an existing DetectedItem from the DetectedItems list. + -- The DetectedItem is a table and contains a SET_UNIT in the field Set. + -- @param #DETECTION_BASE self + -- @param DetectedItemKey The key in the DetectedItems list where the item needs to be removed. + function DETECTION_BASE:RemoveDetectedItem( DetectedItemKey ) + + local DetectedItem = self.DetectedItems[DetectedItemKey] + + if DetectedItem then + self.DetectedItemCount = self.DetectedItemCount - 1 + local DetectedItemIndex = DetectedItem.Index + self.DetectedItemsByIndex[DetectedItemIndex] = nil + self.DetectedItems[DetectedItemKey] = nil + end + end + + + --- Get the DetectedItems by Key. + -- This will return the DetectedItems collection, indexed by the Key, which can be any object that acts as the key of the detection. + -- @param #DETECTION_BASE self + -- @return #DETECTION_BASE.DetectedItems + function DETECTION_BASE:GetDetectedItems() + + return self.DetectedItems + end + + --- Get the DetectedItems by Index. + -- This will return the DetectedItems collection, indexed by an internal numerical Index. + -- @param #DETECTION_BASE self + -- @return #DETECTION_BASE.DetectedItems + function DETECTION_BASE:GetDetectedItemsByIndex() + + return self.DetectedItemsByIndex + end + + --- Get the amount of SETs with detected objects. + -- @param #DETECTION_BASE self + -- @return #number The amount of detected items. Note that the amount of detected items can differ with the reality, because detections are not real-time but doen in intervals! + function DETECTION_BASE:GetDetectedItemsCount() + + local DetectedCount = self.DetectedItemCount + return DetectedCount + end + + --- Get a detected item using a given Key. + -- @param #DETECTION_BASE self + -- @param Key + -- @return #DETECTION_BASE.DetectedItem + function DETECTION_BASE:GetDetectedItemByKey( Key ) + + self:F( { DetectedItems = self.DetectedItems } ) + + local DetectedItem = self.DetectedItems[Key] + if DetectedItem then + return DetectedItem + end + + return nil + end + + --- Get a detected item using a given numeric index. + -- @param #DETECTION_BASE self + -- @param #number Index + -- @return #DETECTION_BASE.DetectedItem + function DETECTION_BASE:GetDetectedItemByIndex( Index ) + + self:F( { self.DetectedItemsByIndex } ) + + local DetectedItem = self.DetectedItemsByIndex[Index] + if DetectedItem then + return DetectedItem + end + + return nil + end + + --- Get a detected ItemID using a given numeric index. + -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem. + -- @return #string DetectedItemID + function DETECTION_BASE:GetDetectedItemID( DetectedItem ) --R2.1 + + return DetectedItem and DetectedItem.ItemID or "" + end + + --- Get a detected ID using a given numeric index. + -- @param #DETECTION_BASE self + -- @param #number Index + -- @return #string DetectedItemID + function DETECTION_BASE:GetDetectedID( Index ) --R2.1 + + local DetectedItem = self.DetectedItemsByIndex[Index] + if DetectedItem then + return DetectedItem.ID + end + + return "" + end + + --- Get the @{Core.Set#SET_UNIT} of a detecttion area using a given numeric index. + -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedItem DetectedItem + -- @return Core.Set#SET_UNIT DetectedSet + function DETECTION_BASE:GetDetectedItemSet( DetectedItem ) + + local DetectedSetUnit = DetectedItem and DetectedItem.Set + if DetectedSetUnit then + return DetectedSetUnit + end + + return nil + end + + --- Set IsDetected flag for the DetectedItem, which can have more units. + -- @param #DETECTION_BASE self + -- @return #DETECTION_BASE.DetectedItem DetectedItem + -- @return #boolean true if at least one UNIT is detected from the DetectedSet, false if no UNIT was detected from the DetectedSet. + function DETECTION_BASE:UpdateDetectedItemDetection( DetectedItem ) + + local IsDetected = false + + for UnitName, UnitData in pairs( DetectedItem.Set:GetSet() ) do + local DetectedObject = self.DetectedObjects[UnitName] + self:F({UnitName = UnitName, IsDetected = DetectedObject.IsDetected}) + if DetectedObject.IsDetected then + IsDetected = true + break + end + end + + self:F( { IsDetected = DetectedItem.IsDetected } ) + + DetectedItem.IsDetected = IsDetected + + return IsDetected + end + + --- Checks if there is at least one UNIT detected in the Set of the the DetectedItem. + -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedItem DetectedItem + -- @return #boolean true if at least one UNIT is detected from the DetectedSet, false if no UNIT was detected from the DetectedSet. + function DETECTION_BASE:IsDetectedItemDetected( DetectedItem ) + + return DetectedItem.IsDetected + end + + + do -- Zones + + --- Get the @{Core.Zone#ZONE_UNIT} of a detection area using a given numeric index. + -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem. + -- @return Core.Zone#ZONE_UNIT DetectedZone + function DETECTION_BASE:GetDetectedItemZone( DetectedItem ) + + local DetectedZone = DetectedItem and DetectedItem.Zone + if DetectedZone then + return DetectedZone + end + + local Detected + + return nil + end + + end + + --- Lock the detected items when created and lock all existing detected items. + -- @param #DETECTION_BASE self + -- @return #DETECTION_BASE + function DETECTION_BASE:LockDetectedItems() + + for DetectedItemID, DetectedItem in pairs( self.DetectedItems ) do + self:LockDetectedItem( DetectedItem ) + end + self.Locking = true + + return self + end + + + --- Unlock the detected items when created and unlock all existing detected items. + -- @param #DETECTION_BASE self + -- @return #DETECTION_BASE + function DETECTION_BASE:UnlockDetectedItems() + + for DetectedItemID, DetectedItem in pairs( self.DetectedItems ) do + self:UnlockDetectedItem( DetectedItem ) + end + self.Locking = nil + + return self + end + + --- Validate if the detected item is locked. + -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem. + -- @return #boolean + function DETECTION_BASE:IsDetectedItemLocked( DetectedItem ) + + return self.Locking and DetectedItem.Locked == true + + end + + + --- Lock a detected item. + -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem. + -- @return #DETECTION_BASE + function DETECTION_BASE:LockDetectedItem( DetectedItem ) + + DetectedItem.Locked = true + + return self + end + + --- Unlock a detected item. + -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem. + -- @return #DETECTION_BASE + function DETECTION_BASE:UnlockDetectedItem( DetectedItem ) + + DetectedItem.Locked = nil + + return self + end + + + + + --- Set the detected item coordinate. + -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem to set the coordinate at. + -- @param Core.Point#COORDINATE Coordinate The coordinate to set the last know detected position at. + -- @param Wrapper.Unit#UNIT DetectedItemUnit The unit to set the heading and altitude from. + -- @return #DETECTION_BASE + function DETECTION_BASE:SetDetectedItemCoordinate( DetectedItem, Coordinate, DetectedItemUnit ) + self:F( { Coordinate = Coordinate } ) + + if DetectedItem then + if DetectedItemUnit then + DetectedItem.Coordinate = Coordinate + DetectedItem.Coordinate:SetHeading( DetectedItemUnit:GetHeading() ) + DetectedItem.Coordinate.y = DetectedItemUnit:GetAltitude() + DetectedItem.Coordinate:SetVelocity( DetectedItemUnit:GetVelocityMPS() ) + end + end + end + + + --- Get the detected item coordinate. + -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem to set the coordinate at. + -- @return Core.Point#COORDINATE + function DETECTION_BASE:GetDetectedItemCoordinate( DetectedItem ) + self:F( { DetectedItem = DetectedItem } ) + + if DetectedItem then + return DetectedItem.Coordinate + end + + return nil + end + + --- Get a list of the detected item coordinates. + -- @param #DETECTION_BASE self + -- @return #table A table of Core.Point#COORDINATE + function DETECTION_BASE:GetDetectedItemCoordinates() + + local Coordinates = {} + + for DetectedItemID, DetectedItem in pairs( self:GetDetectedItems() ) do + Coordinates[DetectedItem] = self:GetDetectedItemCoordinate( DetectedItem ) + end + + return Coordinates + end + + --- Set the detected item threatlevel. + -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedItem The DetectedItem to calculate the threatlevel for. + -- @return #DETECTION_BASE + function DETECTION_BASE:SetDetectedItemThreatLevel( DetectedItem ) + + local DetectedSet = DetectedItem.Set + + if DetectedItem then + DetectedItem.ThreatLevel, DetectedItem.ThreatText = DetectedSet:CalculateThreatLevelA2G() + end + end + + + + --- Get the detected item coordinate. + -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem. + -- @return #number ThreatLevel + function DETECTION_BASE:GetDetectedItemThreatLevel( DetectedItem ) + self:F( { DetectedItem = DetectedItem } ) + + if DetectedItem then + self:F( { ThreatLevel = DetectedItem.ThreatLevel, ThreatText = DetectedItem.ThreatText } ) + return DetectedItem.ThreatLevel or 0, DetectedItem.ThreatText or "" + end + + return nil, "" + end + + + --- Report summary of a detected item using a given numeric index. + -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem. + -- @param Wrapper.Group#GROUP AttackGroup The group to generate the report for. + -- @param Core.Settings#SETTINGS Settings Message formatting settings to use. + -- @return Core.Report#REPORT + function DETECTION_BASE:DetectedItemReportSummary( DetectedItem, AttackGroup, Settings ) + self:F() + return nil + end + + --- Report detailed of a detectedion result. + -- @param #DETECTION_BASE self + -- @param Wrapper.Group#GROUP AttackGroup The group to generate the report for. + -- @return #string + function DETECTION_BASE:DetectedReportDetailed( AttackGroup ) + self:F() + return nil + end + + --- Get the Detection Set. + -- @param #DETECTION_BASE self + -- @return #DETECTION_BASE self + function DETECTION_BASE:GetDetectionSet() + + local DetectionSet = self.DetectionSet + return DetectionSet + end + + --- Find the nearest Recce of the DetectedItem. + -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedItem DetectedItem + -- @return Wrapper.Unit#UNIT The nearest FAC unit + function DETECTION_BASE:NearestRecce( DetectedItem ) + + local NearestRecce = nil + local DistanceRecce = 1000000000 -- Units are not further than 1000000 km away from an area :-) + + for RecceGroupName, RecceGroup in pairs( self.DetectionSet:GetSet() ) do + if RecceGroup and RecceGroup:IsAlive() then + for RecceUnit, RecceUnit in pairs( RecceGroup:GetUnits() ) do + if RecceUnit:IsActive() then + local RecceUnitCoord = RecceUnit:GetCoordinate() + local Distance = RecceUnitCoord:Get2DDistance( self:GetDetectedItemCoordinate( DetectedItem ) ) + if Distance < DistanceRecce then + DistanceRecce = Distance + NearestRecce = RecceUnit + end + end + end + end + end + + DetectedItem.NearestFAC = NearestRecce + DetectedItem.DistanceRecce = DistanceRecce + + end + + + + --- Schedule the DETECTION construction. + -- @param #DETECTION_BASE self + -- @param #number DelayTime The delay in seconds to wait the reporting. + -- @param #number RepeatInterval The repeat interval in seconds for the reporting to happen repeatedly. + -- @return #DETECTION_BASE self + function DETECTION_BASE:Schedule( DelayTime, RepeatInterval ) + self:F2() + + self.ScheduleDelayTime = DelayTime + self.ScheduleRepeatInterval = RepeatInterval + + self.DetectionScheduler = SCHEDULER:New( self, self._DetectionScheduler, { self, "Detection" }, DelayTime, RepeatInterval ) + return self + end + +end + +do -- DETECTION_UNITS + + --- @type DETECTION_UNITS + -- @field DCS#Distance DetectionRange The range till which targets are detected. + -- @extends Functional.Detection#DETECTION_BASE + + --- Will detect units within the battle zone. + -- + -- It will build a DetectedItems list filled with DetectedItems. Each DetectedItem will contain a field Set, which contains a @{Core.Set#SET_UNIT} containing ONE @{UNIT} object reference. + -- Beware that when the amount of units detected is large, the DetectedItems list will be large also. + -- + -- @field #DETECTION_UNITS + DETECTION_UNITS = { + ClassName = "DETECTION_UNITS", + DetectionRange = nil, + } + + --- DETECTION_UNITS constructor. + -- @param Functional.Detection#DETECTION_UNITS self + -- @param Core.Set#SET_GROUP DetectionSetGroup The @{Set} of GROUPs in the Forward Air Controller role. + -- @return Functional.Detection#DETECTION_UNITS self + function DETECTION_UNITS:New( DetectionSetGroup ) + + -- Inherits from DETECTION_BASE + local self = BASE:Inherit( self, DETECTION_BASE:New( DetectionSetGroup ) ) -- #DETECTION_UNITS + + self._SmokeDetectedUnits = false + self._FlareDetectedUnits = false + self._SmokeDetectedZones = false + self._FlareDetectedZones = false + self._BoundDetectedZones = false + + return self + end + + --- Make text documenting the changes of the detected zone. + -- @param #DETECTION_UNITS self + -- @param #DETECTION_BASE.DetectedItem DetectedItem + -- @return #string The Changes text + function DETECTION_UNITS:GetChangeText( DetectedItem ) + self:F( DetectedItem ) + + local MT = {} + + for ChangeCode, ChangeData in pairs( DetectedItem.Changes ) do + + if ChangeCode == "AU" then + local MTUT = {} + for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do + if ChangeUnitType ~= "ID" then + MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType + end + end + MT[#MT+1] = " New target(s) detected: " .. table.concat( MTUT, ", " ) .. "." + end + + if ChangeCode == "RU" then + local MTUT = {} + for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do + if ChangeUnitType ~= "ID" then + MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType + end + end + MT[#MT+1] = " Invisible or destroyed target(s): " .. table.concat( MTUT, ", " ) .. "." + end + + end + + return table.concat( MT, "\n" ) + + end + + + --- Create the DetectedItems list from the DetectedObjects table. + -- For each DetectedItem, a one field array is created containing the Unit detected. + -- @param #DETECTION_UNITS self + -- @return #DETECTION_UNITS self + function DETECTION_UNITS:CreateDetectionItems() + -- Loop the current detected items, and check if each object still exists and is detected. + + for DetectedItemKey, _DetectedItem in pairs( self.DetectedItems ) do + local DetectedItem=_DetectedItem --#DETECTION_BASE.DetectedItem + + local DetectedItemSet = DetectedItem.Set -- Core.Set#SET_UNIT + + for DetectedUnitName, DetectedUnitData in pairs( DetectedItemSet:GetSet() ) do + local DetectedUnit = DetectedUnitData -- Wrapper.Unit#UNIT + + local DetectedObject = nil + --self:F( DetectedUnit ) + if DetectedUnit:IsAlive() then + --self:F(DetectedUnit:GetName()) + DetectedObject = self:GetDetectedObject( DetectedUnit:GetName() ) + end + if DetectedObject then + + -- Yes, the DetectedUnit is still detected or exists. Flag as identified. + self:IdentifyDetectedObject( DetectedObject ) + + self:F( { "**DETECTED**", IsVisible = DetectedObject.IsVisible } ) + -- Update the detection with the new data provided. + DetectedItem.TypeName = DetectedUnit:GetTypeName() + DetectedItem.CategoryName = DetectedUnit:GetCategoryName() + DetectedItem.Name = DetectedObject.Name + DetectedItem.IsVisible = DetectedObject.IsVisible + DetectedItem.LastTime = DetectedObject.LastTime + DetectedItem.LastPos = DetectedObject.LastPos + DetectedItem.LastVelocity = DetectedObject.LastVelocity + DetectedItem.KnowType = DetectedObject.KnowType + DetectedItem.KnowDistance = DetectedObject.KnowDistance + DetectedItem.Distance = DetectedObject.Distance + else + -- There was no DetectedObject, remove DetectedUnit from the Set. + self:AddChangeUnit( DetectedItem, "RU", DetectedUnitName ) + DetectedItemSet:Remove( DetectedUnitName ) + end + end + if DetectedItemSet:Count() == 0 then + -- Now the Set is empty, meaning that a detected item has no units anymore. + -- Delete the DetectedItem from the detections + self:RemoveDetectedItem( DetectedItemKey ) + end + end + + + -- Now we need to loop through the unidentified detected units and add these... These are all new items. + for DetectedUnitName, DetectedObjectData in pairs( self.DetectedObjects ) do + + local DetectedObject = self:GetDetectedObject( DetectedUnitName ) + if DetectedObject then + self:T( { "Detected Unit #", DetectedUnitName } ) + + local DetectedUnit = UNIT:FindByName( DetectedUnitName ) -- Wrapper.Unit#UNIT + + if DetectedUnit then + local DetectedTypeName = DetectedUnit:GetTypeName() + local DetectedItem = self:GetDetectedItemByKey( DetectedUnitName ) + if not DetectedItem then + self:T( "Added new DetectedItem" ) + DetectedItem = self:AddDetectedItem( "UNIT", DetectedUnitName ) + DetectedItem.TypeName = DetectedUnit:GetTypeName() + DetectedItem.Name = DetectedObject.Name + DetectedItem.IsVisible = DetectedObject.IsVisible + DetectedItem.LastTime = DetectedObject.LastTime + DetectedItem.LastPos = DetectedObject.LastPos + DetectedItem.LastVelocity = DetectedObject.LastVelocity + DetectedItem.KnowType = DetectedObject.KnowType + DetectedItem.KnowDistance = DetectedObject.KnowDistance + DetectedItem.Distance = DetectedObject.Distance + end + + DetectedItem.Set:AddUnit( DetectedUnit ) + self:AddChangeUnit( DetectedItem, "AU", DetectedTypeName ) + end + end + end + + for DetectedItemID, DetectedItemData in pairs( self.DetectedItems ) do + + local DetectedItem = DetectedItemData -- #DETECTION_BASE.DetectedItem + local DetectedSet = DetectedItem.Set + + -- Set the last known coordinate. + local DetectedFirstUnit = DetectedSet:GetFirst() + local DetectedFirstUnitCoord = DetectedFirstUnit:GetCoordinate() + self:SetDetectedItemCoordinate( DetectedItem, DetectedFirstUnitCoord, DetectedFirstUnit ) + + self:ReportFriendliesNearBy( { DetectedItem = DetectedItem, ReportSetGroup = self.DetectionSet } ) -- Fill the Friendlies table + self:SetDetectedItemThreatLevel( DetectedItem ) + self:NearestRecce( DetectedItem ) + + end + + end + + + --- Report summary of a DetectedItem using a given numeric index. + -- @param #DETECTION_UNITS self + -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem. + -- @param Wrapper.Group#GROUP AttackGroup The group to generate the report for. + -- @param Core.Settings#SETTINGS Settings Message formatting settings to use. + -- @return Core.Report#REPORT The report of the detection items. + function DETECTION_UNITS:DetectedItemReportSummary( DetectedItem, AttackGroup, Settings ) + self:F( { DetectedItem = DetectedItem } ) + + local DetectedItemID = self:GetDetectedItemID( DetectedItem ) + + if DetectedItem then + local ReportSummary = "" + local UnitDistanceText = "" + local UnitCategoryText = "" + + if DetectedItem.KnowType then + local UnitCategoryName = DetectedItem.CategoryName + if UnitCategoryName then + UnitCategoryText = UnitCategoryName + end + if DetectedItem.TypeName then + UnitCategoryText = UnitCategoryText .. " (" .. DetectedItem.TypeName .. ")" + end + else + UnitCategoryText = "Unknown" + end + + if DetectedItem.KnowDistance then + if DetectedItem.IsVisible then + UnitDistanceText = " at " .. string.format( "%.2f", DetectedItem.Distance ) .. " km" + end + else + if DetectedItem.IsVisible then + UnitDistanceText = " at +/- " .. string.format( "%.0f", DetectedItem.Distance ) .. " km" + end + end + + --TODO: solve Index reference + local DetectedItemCoordinate = self:GetDetectedItemCoordinate( DetectedItem ) + local DetectedItemCoordText = DetectedItemCoordinate:ToString( AttackGroup, Settings ) + + local ThreatLevelA2G = self:GetDetectedItemThreatLevel( DetectedItem ) + + local Report = REPORT:New() + Report:Add(DetectedItemID .. ", " .. DetectedItemCoordText) + Report:Add( string.format( "Threat: [%s]", string.rep( "■", ThreatLevelA2G ), string.rep( "□", 10-ThreatLevelA2G ) ) ) + Report:Add( string.format("Type: %s%s", UnitCategoryText, UnitDistanceText ) ) + Report:Add( string.format("Visible: %s", DetectedItem.IsVisible and "yes" or "no" ) ) + Report:Add( string.format("Detected: %s", DetectedItem.IsDetected and "yes" or "no" ) ) + Report:Add( string.format("Distance: %s", DetectedItem.KnowDistance and "yes" or "no" ) ) + return Report + end + return nil + end + + + --- Report detailed of a detection result. + -- @param #DETECTION_UNITS self + -- @param Wrapper.Group#GROUP AttackGroup The group to generate the report for. + -- @return #string + function DETECTION_UNITS:DetectedReportDetailed( AttackGroup ) + self:F() + + local Report = REPORT:New() + for DetectedItemIndex, DetectedItem in pairs( self.DetectedItems ) do + local DetectedItem = DetectedItem -- #DETECTION_BASE.DetectedItem + local ReportSummary = self:DetectedItemReportSummary( DetectedItem, AttackGroup ) + Report:SetTitle( "Detected units:" ) + Report:Add( ReportSummary:Text() ) + end + + local ReportText = Report:Text() + + return ReportText + end + +end + +do -- DETECTION_TYPES + + --- @type DETECTION_TYPES + -- @extends Functional.Detection#DETECTION_BASE + + --- Will detect units within the battle zone. + -- It will build a DetectedItems[] list filled with DetectedItems, grouped by the type of units detected. + -- Each DetectedItem will contain a field Set, which contains a @{Core.Set#SET_UNIT} containing ONE @{UNIT} object reference. + -- Beware that when the amount of different types detected is large, the DetectedItems[] list will be large also. + -- + -- @field #DETECTION_TYPES + DETECTION_TYPES = { + ClassName = "DETECTION_TYPES", + DetectionRange = nil, + } + + --- DETECTION_TYPES constructor. + -- @param Functional.Detection#DETECTION_TYPES self + -- @param Core.Set#SET_GROUP DetectionSetGroup The @{Set} of GROUPs in the Recce role. + -- @return Functional.Detection#DETECTION_TYPES self + function DETECTION_TYPES:New( DetectionSetGroup ) + + -- Inherits from DETECTION_BASE + local self = BASE:Inherit( self, DETECTION_BASE:New( DetectionSetGroup ) ) -- #DETECTION_TYPES + + self._SmokeDetectedUnits = false + self._FlareDetectedUnits = false + self._SmokeDetectedZones = false + self._FlareDetectedZones = false + self._BoundDetectedZones = false + + return self + end + + --- Make text documenting the changes of the detected zone. + -- @param #DETECTION_TYPES self + -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem + -- @return #string The Changes text + function DETECTION_TYPES:GetChangeText( DetectedItem ) + self:F( DetectedItem ) + + local MT = {} + + for ChangeCode, ChangeData in pairs( DetectedItem.Changes ) do + + if ChangeCode == "AU" then + local MTUT = {} + for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do + if ChangeUnitType ~= "ID" then + MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType + end + end + MT[#MT+1] = " New target(s) detected: " .. table.concat( MTUT, ", " ) .. "." + end + + if ChangeCode == "RU" then + local MTUT = {} + for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do + if ChangeUnitType ~= "ID" then + MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType + end + end + MT[#MT+1] = " Invisible or destroyed target(s): " .. table.concat( MTUT, ", " ) .. "." + end + + end + + return table.concat( MT, "\n" ) + + end + + + --- Create the DetectedItems list from the DetectedObjects table. + -- For each DetectedItem, a one field array is created containing the Unit detected. + -- @param #DETECTION_TYPES self + -- @return #DETECTION_TYPES self + function DETECTION_TYPES:CreateDetectionItems() + + -- Loop the current detected items, and check if each object still exists and is detected. + + for DetectedItemKey, DetectedItem in pairs( self.DetectedItems ) do + + local DetectedItemSet = DetectedItem.Set -- Core.Set#SET_UNIT + local DetectedTypeName = DetectedItem.TypeName + + for DetectedUnitName, DetectedUnitData in pairs( DetectedItemSet:GetSet() ) do + local DetectedUnit = DetectedUnitData -- Wrapper.Unit#UNIT + + local DetectedObject = nil + if DetectedUnit:IsAlive() then + --self:F(DetectedUnit:GetName()) + DetectedObject = self:GetDetectedObject( DetectedUnit:GetName() ) + end + if DetectedObject then + + -- Yes, the DetectedUnit is still detected or exists. Flag as identified. + self:IdentifyDetectedObject( DetectedObject ) + else + -- There was no DetectedObject, remove DetectedUnit from the Set. + self:AddChangeUnit( DetectedItem, "RU", DetectedUnitName ) + DetectedItemSet:Remove( DetectedUnitName ) + end + end + if DetectedItemSet:Count() == 0 then + -- Now the Set is empty, meaning that a detected item has no units anymore. + -- Delete the DetectedItem from the detections + self:RemoveDetectedItem( DetectedItemKey ) + end + end + + + -- Now we need to loop through the unidentified detected units and add these... These are all new items. + for DetectedUnitName, DetectedObjectData in pairs( self.DetectedObjects ) do + + local DetectedObject = self:GetDetectedObject( DetectedUnitName ) + if DetectedObject then + self:T( { "Detected Unit #", DetectedUnitName } ) + + local DetectedUnit = UNIT:FindByName( DetectedUnitName ) -- Wrapper.Unit#UNIT + + if DetectedUnit then + local DetectedTypeName = DetectedUnit:GetTypeName() + local DetectedItem = self:GetDetectedItemByKey( DetectedTypeName ) + if not DetectedItem then + DetectedItem = self:AddDetectedItem( "TYPE", DetectedTypeName ) + DetectedItem.TypeName = DetectedTypeName + end + + DetectedItem.Set:AddUnit( DetectedUnit ) + self:AddChangeUnit( DetectedItem, "AU", DetectedTypeName ) + end + end + end + + + + -- Check if there are any friendlies nearby. + for DetectedItemID, DetectedItemData in pairs( self.DetectedItems ) do + + local DetectedItem = DetectedItemData -- #DETECTION_BASE.DetectedItem + local DetectedSet = DetectedItem.Set + + -- Set the last known coordinate. + local DetectedFirstUnit = DetectedSet:GetFirst() + local DetectedUnitCoord = DetectedFirstUnit:GetCoordinate() + self:SetDetectedItemCoordinate( DetectedItem, DetectedUnitCoord, DetectedFirstUnit ) + + self:ReportFriendliesNearBy( { DetectedItem = DetectedItem, ReportSetGroup = self.DetectionSet } ) -- Fill the Friendlies table + self:SetDetectedItemThreatLevel( DetectedItem ) + self:NearestRecce( DetectedItem ) + end + + + + end + + --- Report summary of a DetectedItem using a given numeric index. + -- @param #DETECTION_TYPES self + -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem. + -- @param Wrapper.Group#GROUP AttackGroup The group to generate the report for. + -- @param Core.Settings#SETTINGS Settings Message formatting settings to use. + -- @return Core.Report#REPORT The report of the detection items. + function DETECTION_TYPES:DetectedItemReportSummary( DetectedItem, AttackGroup, Settings ) + self:F( { DetectedItem = DetectedItem } ) + + local DetectedSet = self:GetDetectedItemSet( DetectedItem ) + local DetectedItemID = self:GetDetectedItemID( DetectedItem ) + + self:T( DetectedItem ) + if DetectedItem then + + local ThreatLevelA2G = self:GetDetectedItemThreatLevel( DetectedItem ) + local DetectedItemsCount = DetectedSet:Count() + local DetectedItemType = DetectedItem.TypeName + + local DetectedItemCoordinate = self:GetDetectedItemCoordinate( DetectedItem ) + local DetectedItemCoordText = DetectedItemCoordinate:ToString( AttackGroup, Settings ) + + local Report = REPORT:New() + Report:Add(DetectedItemID .. ", " .. DetectedItemCoordText) + Report:Add( string.format( "Threat: [%s%s]", string.rep( "■", ThreatLevelA2G ), string.rep( "□", 10-ThreatLevelA2G ) ) ) + Report:Add( string.format("Type: %2d of %s", DetectedItemsCount, DetectedItemType ) ) + return Report + end + end + + --- Report detailed of a detection result. + -- @param #DETECTION_TYPES self + -- @param Wrapper.Group#GROUP AttackGroup The group to generate the report for. + -- @return #string + function DETECTION_TYPES:DetectedReportDetailed( AttackGroup ) + self:F() + + local Report = REPORT:New() + for DetectedItemIndex, DetectedItem in pairs( self.DetectedItems ) do + local DetectedItem = DetectedItem -- #DETECTION_BASE.DetectedItem + local ReportSummary = self:DetectedItemReportSummary( DetectedItem, AttackGroup ) + Report:SetTitle( "Detected types:" ) + Report:Add( ReportSummary:Text() ) + end + + local ReportText = Report:Text() + + return ReportText + end + +end + + +do -- DETECTION_AREAS + + --- @type DETECTION_AREAS + -- @field DCS#Distance DetectionZoneRange The range till which targets are grouped upon the first detected target. + -- @field #DETECTION_BASE.DetectedItems DetectedItems A list of areas containing the set of @{Wrapper.Unit}s, @{Zone}s, the center @{Wrapper.Unit} within the zone, and ID of each area that was detected within a DetectionZoneRange. + -- @extends Functional.Detection#DETECTION_BASE + + --- Detect units within the battle zone for a list of @{Wrapper.Group}s detecting targets following (a) detection method(s), + -- and will build a list (table) of @{Core.Set#SET_UNIT}s containing the @{Wrapper.Unit#UNIT}s detected. + -- The class is group the detected units within zones given a DetectedZoneRange parameter. + -- A set with multiple detected zones will be created as there are groups of units detected. + -- + -- ## 4.1) Retrieve the Detected Unit Sets and Detected Zones + -- + -- The methods to manage the DetectedItems[].Set(s) are implemented in @{Functional.Detection#DECTECTION_BASE} and + -- the methods to manage the DetectedItems[].Zone(s) is implemented in @{Functional.Detection#DETECTION_AREAS}. + -- + -- Retrieve the DetectedItems[].Set with the method @{Functional.Detection#DETECTION_BASE.GetDetectedSet}(). A @{Core.Set#SET_UNIT} object will be returned. + -- + -- Retrieve the formed @{Zone@ZONE_UNIT}s as a result of the grouping the detected units within the DetectionZoneRange, use the method @{Functional.Detection#DETECTION_BASE.GetDetectionZones}(). + -- To understand the amount of zones created, use the method @{Functional.Detection#DETECTION_BASE.GetDetectionZoneCount}(). + -- If you want to obtain a specific zone from the DetectedZones, use the method @{Functional.Detection#DETECTION_BASE.GetDetectionZone}() with a given index. + -- + -- ## 4.4) Flare or Smoke detected units + -- + -- Use the methods @{Functional.Detection#DETECTION_AREAS.FlareDetectedUnits}() or @{Functional.Detection#DETECTION_AREAS.SmokeDetectedUnits}() to flare or smoke the detected units when a new detection has taken place. + -- + -- ## 4.5) Flare or Smoke or Bound detected zones + -- + -- Use the methods: + -- + -- * @{Functional.Detection#DETECTION_AREAS.FlareDetectedZones}() to flare in a color + -- * @{Functional.Detection#DETECTION_AREAS.SmokeDetectedZones}() to smoke in a color + -- * @{Functional.Detection#DETECTION_AREAS.SmokeDetectedZones}() to bound with a tire with a white flag + -- + -- the detected zones when a new detection has taken place. + -- + -- @field #DETECTION_AREAS + DETECTION_AREAS = { + ClassName = "DETECTION_AREAS", + DetectionZoneRange = nil, + } + + + --- DETECTION_AREAS constructor. + -- @param #DETECTION_AREAS self + -- @param Core.Set#SET_GROUP DetectionSetGroup The @{Set} of GROUPs in the Forward Air Controller role. + -- @param DCS#Distance DetectionZoneRange The range till which targets are grouped upon the first detected target. + -- @return #DETECTION_AREAS + function DETECTION_AREAS:New( DetectionSetGroup, DetectionZoneRange ) + + -- Inherits from DETECTION_BASE + local self = BASE:Inherit( self, DETECTION_BASE:New( DetectionSetGroup ) ) + + self.DetectionZoneRange = DetectionZoneRange + + self._SmokeDetectedUnits = false + self._FlareDetectedUnits = false + self._SmokeDetectedZones = false + self._FlareDetectedZones = false + self._BoundDetectedZones = false + + return self + end + + + --- Report summary of a detected item using a given numeric index. + -- @param #DETECTION_AREAS self + -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem. + -- @param Wrapper.Group#GROUP AttackGroup The group to get the settings for. + -- @param Core.Settings#SETTINGS Settings (Optional) Message formatting settings to use. + -- @return Core.Report#REPORT The report of the detection items. + function DETECTION_AREAS:DetectedItemReportMenu( DetectedItem, AttackGroup, Settings ) + self:F( { DetectedItem = DetectedItem } ) + + local DetectedItemID = self:GetDetectedItemID( DetectedItem ) + + if DetectedItem then + local DetectedSet = self:GetDetectedItemSet( DetectedItem ) + local ReportSummaryItem + + local DetectedZone = self:GetDetectedItemZone( DetectedItem ) + local DetectedItemCoordinate = DetectedZone:GetCoordinate() + local DetectedItemCoordText = DetectedItemCoordinate:ToString( AttackGroup, Settings ) + + local ThreatLevelA2G = self:GetDetectedItemThreatLevel( DetectedItem ) + + local Report = REPORT:New() + Report:Add( DetectedItemID ) + Report:Add( string.format( "Threat: [%s%s]", string.rep( "■", ThreatLevelA2G ), string.rep( "□", 10-ThreatLevelA2G ) ) ) + + return Report + end + + return nil + end + + --- Report summary of a detected item using a given numeric index. + -- @param #DETECTION_AREAS self + -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem. + -- @param Wrapper.Group#GROUP AttackGroup The group to get the settings for. + -- @param Core.Settings#SETTINGS Settings (Optional) Message formatting settings to use. + -- @return Core.Report#REPORT The report of the detection items. + function DETECTION_AREAS:DetectedItemReportSummary( DetectedItem, AttackGroup, Settings ) + self:F( { DetectedItem = DetectedItem } ) + + local DetectedItemID = self:GetDetectedItemID( DetectedItem ) + + if DetectedItem then + local DetectedSet = self:GetDetectedItemSet( DetectedItem ) + local ReportSummaryItem + + --local DetectedZone = self:GetDetectedItemZone( DetectedItem ) + local DetectedItemCoordinate = self:GetDetectedItemCoordinate( DetectedItem ) + local DetectedAir = DetectedSet:HasAirUnits() + local DetectedAltitude = self:GetDetectedItemCoordinate( DetectedItem ) + local DetectedItemCoordText = "" + if DetectedAir > 0 then + DetectedItemCoordText = DetectedItemCoordinate:ToStringA2A( AttackGroup, Settings ) + else + DetectedItemCoordText = DetectedItemCoordinate:ToStringA2G( AttackGroup, Settings ) + end + + + local ThreatLevelA2G = self:GetDetectedItemThreatLevel( DetectedItem ) + local DetectedItemsCount = DetectedSet:Count() + local DetectedItemsTypes = DetectedSet:GetTypeNames() + + local Report = REPORT:New() + Report:Add(DetectedItemID .. ", " .. DetectedItemCoordText) + Report:Add( string.format( "Threat: [%s%s]", string.rep( "■", ThreatLevelA2G ), string.rep( "□", 10-ThreatLevelA2G ) ) ) + Report:Add( string.format("Type: %2d of %s", DetectedItemsCount, DetectedItemsTypes ) ) + --Report:Add( string.format("Detected: %s", DetectedItem.IsDetected and "yes" or "no" ) ) + + return Report + end + + return nil + end + + --- Report detailed of a detection result. + -- @param #DETECTION_AREAS self + -- @param Wrapper.Group#GROUP AttackGroup The group to generate the report for. + -- @return #string + function DETECTION_AREAS:DetectedReportDetailed( AttackGroup ) --R2.1 Fixed missing report + self:F() + + local Report = REPORT:New() + for DetectedItemIndex, DetectedItem in pairs( self.DetectedItems ) do + local DetectedItem = DetectedItem -- #DETECTION_BASE.DetectedItem + local ReportSummary = self:DetectedItemReportSummary( DetectedItem, AttackGroup ) + Report:SetTitle( "Detected areas:" ) + Report:Add( ReportSummary:Text() ) + end + + local ReportText = Report:Text() + + return ReportText + end + + + --- Calculate the optimal intercept point of the DetectedItem. + -- @param #DETECTION_AREAS self + -- @param #DETECTION_BASE.DetectedItem DetectedItem + function DETECTION_AREAS:CalculateIntercept( DetectedItem ) + + local DetectedCoord = DetectedItem.Coordinate + local DetectedSpeed = DetectedCoord:GetVelocity() + local DetectedHeading = DetectedCoord:GetHeading() + + if self.Intercept then + local DetectedSet = DetectedItem.Set + -- todo: speed + + local TranslateDistance = DetectedSpeed * self.InterceptDelay + + local InterceptCoord = DetectedCoord:Translate( TranslateDistance, DetectedHeading ) + + DetectedItem.InterceptCoord = InterceptCoord + else + DetectedItem.InterceptCoord = DetectedCoord + end + + end + + + + --- Smoke the detected units + -- @param #DETECTION_AREAS self + -- @return #DETECTION_AREAS self + function DETECTION_AREAS:SmokeDetectedUnits() + self:F2() + + self._SmokeDetectedUnits = true + return self + end + + --- Flare the detected units + -- @param #DETECTION_AREAS self + -- @return #DETECTION_AREAS self + function DETECTION_AREAS:FlareDetectedUnits() + self:F2() + + self._FlareDetectedUnits = true + return self + end + + --- Smoke the detected zones + -- @param #DETECTION_AREAS self + -- @return #DETECTION_AREAS self + function DETECTION_AREAS:SmokeDetectedZones() + self:F2() + + self._SmokeDetectedZones = true + return self + end + + --- Flare the detected zones + -- @param #DETECTION_AREAS self + -- @return #DETECTION_AREAS self + function DETECTION_AREAS:FlareDetectedZones() + self:F2() + + self._FlareDetectedZones = true + return self + end + + --- Bound the detected zones + -- @param #DETECTION_AREAS self + -- @return #DETECTION_AREAS self + function DETECTION_AREAS:BoundDetectedZones() + self:F2() + + self._BoundDetectedZones = true + return self + end + + --- Make text documenting the changes of the detected zone. + -- @param #DETECTION_AREAS self + -- @param #DETECTION_BASE.DetectedItem DetectedItem + -- @return #string The Changes text + function DETECTION_AREAS:GetChangeText( DetectedItem ) + self:F( DetectedItem ) + + local MT = {} + + for ChangeCode, ChangeData in pairs( DetectedItem.Changes ) do + + if ChangeCode == "AA" then + MT[#MT+1] = "Detected new area " .. ChangeData.ID .. ". The center target is a " .. ChangeData.ItemUnitType .. "." + end + + if ChangeCode == "RAU" then + MT[#MT+1] = "Changed area " .. ChangeData.ID .. ". Removed the center target." + end + + if ChangeCode == "AAU" then + MT[#MT+1] = "Changed area " .. ChangeData.ID .. ". The new center target is a " .. ChangeData.ItemUnitType .. "." + end + + if ChangeCode == "RA" then + MT[#MT+1] = "Removed old area " .. ChangeData.ID .. ". No more targets in this area." + end + + if ChangeCode == "AU" then + local MTUT = {} + for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do + if ChangeUnitType ~= "ID" then + MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType + end + end + MT[#MT+1] = "Detected for area " .. ChangeData.ID .. " new target(s) " .. table.concat( MTUT, ", " ) .. "." + end + + if ChangeCode == "RU" then + local MTUT = {} + for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do + if ChangeUnitType ~= "ID" then + MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType + end + end + MT[#MT+1] = "Removed for area " .. ChangeData.ID .. " invisible or destroyed target(s) " .. table.concat( MTUT, ", " ) .. "." + end + + end + + return table.concat( MT, "\n" ) + + end + + + --- Make a DetectionSet table. This function will be overridden in the derived clsses. + -- @param #DETECTION_AREAS self + -- @return #DETECTION_AREAS self + function DETECTION_AREAS:CreateDetectionItems() + + + self:F( "Checking Detected Items for new Detected Units ..." ) + --self:F( { DetectedObjects = self.DetectedObjects } ) + + -- First go through all detected sets, and check if there are new detected units, match all existing detected units and identify undetected units. + -- Regroup when needed, split groups when needed. + for DetectedItemID, DetectedItemData in pairs( self.DetectedItems ) do + + local DetectedItem = DetectedItemData -- #DETECTION_BASE.DetectedItem + + if DetectedItem then + + self:T2( { "Detected Item ID: ", DetectedItemID } ) + + local DetectedSet = DetectedItem.Set + + local AreaExists = false -- This flag will determine of the detected area is still existing. + + -- First test if the center unit is detected in the detection area. + self:T3( { "Zone Center Unit:", DetectedItem.Zone.ZoneUNIT.UnitName } ) + local DetectedZoneObject = self:GetDetectedObject( DetectedItem.Zone.ZoneUNIT.UnitName ) + self:T3( { "Detected Zone Object:", DetectedItem.Zone:GetName(), DetectedZoneObject } ) + + if DetectedZoneObject then + + --self:IdentifyDetectedObject( DetectedZoneObject ) + AreaExists = true + + + + else + -- The center object of the detected area has not been detected. Find an other unit of the set to become the center of the area. + -- First remove the center unit from the set. + DetectedSet:RemoveUnitsByName( DetectedItem.Zone.ZoneUNIT.UnitName ) + + self:AddChangeItem( DetectedItem, 'RAU', self:GetDetectedUnitTypeName( DetectedItem.Zone.ZoneUNIT ) ) + + -- Then search for a new center area unit within the set. Note that the new area unit candidate must be within the area range. + for DetectedUnitName, DetectedUnitData in pairs( DetectedSet:GetSet() ) do + + local DetectedUnit = DetectedUnitData -- Wrapper.Unit#UNIT + local DetectedObject = self:GetDetectedObject( DetectedUnit.UnitName ) + local DetectedUnitTypeName = self:GetDetectedUnitTypeName( DetectedUnit ) + + -- The DetectedObject can be nil when the DetectedUnit is not alive anymore or it is not in the DetectedObjects map. + -- If the DetectedUnit was already identified, DetectedObject will be nil. + if DetectedObject then + self:IdentifyDetectedObject( DetectedObject ) + AreaExists = true + + --DetectedItem.Zone:BoundZone( 12, self.CountryID, true) + + -- Assign the Unit as the new center unit of the detected area. + DetectedItem.Zone = ZONE_UNIT:New( DetectedUnit:GetName(), DetectedUnit, self.DetectionZoneRange ) + + self:AddChangeItem( DetectedItem, "AAU", DetectedUnitTypeName ) + + -- We don't need to add the DetectedObject to the area set, because it is already there ... + break + else + DetectedSet:Remove( DetectedUnitName ) + self:AddChangeUnit( DetectedItem, "RU", DetectedUnitTypeName ) + end + end + end + + -- Now we've determined the center unit of the area, now we can iterate the units in the detected area. + -- Note that the position of the area may have moved due to the center unit repositioning. + -- If no center unit was identified, then the detected area does not exist anymore and should be deleted, as there are no valid units that can be the center unit. + if AreaExists then + + -- ok, we found the center unit of the area, now iterate through the detected area set and see which units are still within the center unit zone ... + -- Those units within the zone are flagged as Identified. + -- If a unit was not found in the set, remove it from the set. This may be added later to other existing or new sets. + for DetectedUnitName, DetectedUnitData in pairs( DetectedSet:GetSet() ) do + + local DetectedUnit = DetectedUnitData -- Wrapper.Unit#UNIT + local DetectedUnitTypeName = self:GetDetectedUnitTypeName( DetectedUnit ) + + local DetectedObject = nil + if DetectedUnit:IsAlive() then + --self:F(DetectedUnit:GetName()) + DetectedObject = self:GetDetectedObject( DetectedUnit:GetName() ) + end + if DetectedObject then + + -- Check if the DetectedUnit is within the DetectedItem.Zone + if DetectedUnit:IsInZone( DetectedItem.Zone ) then + + -- Yes, the DetectedUnit is within the DetectedItem.Zone, no changes, DetectedUnit can be kept within the Set. + self:IdentifyDetectedObject( DetectedObject ) + DetectedSet:AddUnit( DetectedUnit ) + + else + -- No, the DetectedUnit is not within the DetectedItem.Zone, remove DetectedUnit from the Set. + DetectedSet:Remove( DetectedUnitName ) + self:AddChangeUnit( DetectedItem, "RU", DetectedUnitTypeName ) + end + + else + -- There was no DetectedObject, remove DetectedUnit from the Set. + self:AddChangeUnit( DetectedItem, "RU", "destroyed target" ) + DetectedSet:Remove( DetectedUnitName ) + + -- The DetectedObject has been identified, because it does not exist ... + -- self:IdentifyDetectedObject( DetectedObject ) + end + end + else + --DetectedItem.Zone:BoundZone( 12, self.CountryID, true) + self:RemoveDetectedItem( DetectedItemID ) + self:AddChangeItem( DetectedItem, "RA" ) + end + end + end + + + + -- We iterated through the existing detection areas and: + -- - We checked which units are still detected in each detection area. Those units were flagged as Identified. + -- - We recentered the detection area to new center units where it was needed. + -- + -- Now we need to loop through the unidentified detected units and see where they belong: + -- - They can be added to a new detection area and become the new center unit. + -- - They can be added to a new detection area. + for DetectedUnitName, DetectedObjectData in pairs( self.DetectedObjects ) do + + local DetectedObject = self:GetDetectedObject( DetectedUnitName ) + + if DetectedObject then + + -- We found an unidentified unit outside of any existing detection area. + local DetectedUnit = UNIT:FindByName( DetectedUnitName ) -- Wrapper.Unit#UNIT + local DetectedUnitTypeName = self:GetDetectedUnitTypeName( DetectedUnit ) + + local AddedToDetectionArea = false + + for DetectedItemID, DetectedItemData in pairs( self.DetectedItems ) do + + local DetectedItem = DetectedItemData -- #DETECTION_BASE.DetectedItem + if DetectedItem then + local DetectedSet = DetectedItem.Set + if not self:IsDetectedObjectIdentified( DetectedObject ) and DetectedUnit:IsInZone( DetectedItem.Zone ) then + self:IdentifyDetectedObject( DetectedObject ) + DetectedSet:AddUnit( DetectedUnit ) + AddedToDetectionArea = true + self:AddChangeUnit( DetectedItem, "AU", DetectedUnitTypeName ) + end + end + end + + if AddedToDetectionArea == false then + + -- New detection area + local DetectedItem = self:AddDetectedItemZone( "AREA", nil, + SET_UNIT:New():FilterDeads():FilterCrashes(), + ZONE_UNIT:New( DetectedUnitName, DetectedUnit, self.DetectionZoneRange ) + ) + --self:F( DetectedItem.Zone.ZoneUNIT.UnitName ) + DetectedItem.Set:AddUnit( DetectedUnit ) + self:AddChangeItem( DetectedItem, "AA", DetectedUnitTypeName ) + end + end + end + + -- Now all the tests should have been build, now make some smoke and flares... + -- We also report here the friendlies within the detected areas. + + for DetectedItemID, DetectedItemData in pairs( self.DetectedItems ) do + + local DetectedItem = DetectedItemData -- #DETECTION_BASE.DetectedItem + local DetectedSet = DetectedItem.Set + local DetectedFirstUnit = DetectedSet:GetFirst() + local DetectedZone = DetectedItem.Zone + + -- Set the last known coordinate to the detection item. + local DetectedZoneCoord = DetectedZone:GetCoordinate() + self:SetDetectedItemCoordinate( DetectedItem, DetectedZoneCoord, DetectedFirstUnit ) + + self:CalculateIntercept( DetectedItem ) + + -- We search for friendlies nearby. + -- If there weren't any friendlies nearby, and now there are friendlies nearby, we flag the area as "changed". + -- If there were friendlies nearby, and now there aren't any friendlies nearby, we flag the area as "changed". + -- This is for the A2G dispatcher to detect if there is a change in the tactical situation. + local OldFriendliesNearbyGround = self:IsFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT ) + self:ReportFriendliesNearBy( { DetectedItem = DetectedItem, ReportSetGroup = self.DetectionSet } ) -- Fill the Friendlies table + local NewFriendliesNearbyGround = self:IsFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT ) + if OldFriendliesNearbyGround ~= NewFriendliesNearbyGround then + DetectedItem.Changed = true + end + + self:SetDetectedItemThreatLevel( DetectedItem ) -- Calculate A2G threat level + self:NearestRecce( DetectedItem ) + + + if DETECTION_AREAS._SmokeDetectedUnits or self._SmokeDetectedUnits then + DetectedZone.ZoneUNIT:SmokeRed() + end + + --DetectedSet:Flush( self ) + + DetectedSet:ForEachUnit( + --- @param Wrapper.Unit#UNIT DetectedUnit + function( DetectedUnit ) + if DetectedUnit:IsAlive() then + --self:T( "Detected Set #" .. DetectedItem.ID .. ":" .. DetectedUnit:GetName() ) + if DETECTION_AREAS._FlareDetectedUnits or self._FlareDetectedUnits then + DetectedUnit:FlareGreen() + end + if DETECTION_AREAS._SmokeDetectedUnits or self._SmokeDetectedUnits then + DetectedUnit:SmokeGreen() + end + end + end + ) + if DETECTION_AREAS._FlareDetectedZones or self._FlareDetectedZones then + DetectedZone:FlareZone( SMOKECOLOR.White, 30, math.random( 0,90 ) ) + end + if DETECTION_AREAS._SmokeDetectedZones or self._SmokeDetectedZones then + DetectedZone:SmokeZone( SMOKECOLOR.White, 30 ) + end + + if DETECTION_AREAS._BoundDetectedZones or self._BoundDetectedZones then + self.CountryID = DetectedSet:GetFirst():GetCountry() + DetectedZone:BoundZone( 12, self.CountryID ) + end + end + + end + +end + + +do -- DETECTION_ZONES + + --- @type DETECTION_ZONES + -- @field DCS#Distance DetectionZoneRange The range till which targets are grouped upon the first detected target. + -- @field #DETECTION_BASE.DetectedItems DetectedItems A list of areas containing the set of @{Wrapper.Unit}s, @{Zone}s, the center @{Wrapper.Unit} within the zone, and ID of each area that was detected within a DetectionZoneRange. + -- @extends Functional.Detection#DETECTION_BASE + + --- (old, to be revised ) Detect units within the battle zone for a list of @{Core.Zone}s detecting targets following (a) detection method(s), + -- and will build a list (table) of @{Core.Set#SET_UNIT}s containing the @{Wrapper.Unit#UNIT}s detected. + -- The class is group the detected units within zones given a DetectedZoneRange parameter. + -- A set with multiple detected zones will be created as there are groups of units detected. + -- + -- ## 4.1) Retrieve the Detected Unit Sets and Detected Zones + -- + -- The methods to manage the DetectedItems[].Set(s) are implemented in @{Functional.Detection#DECTECTION_BASE} and + -- the methods to manage the DetectedItems[].Zone(s) is implemented in @{Functional.Detection#DETECTION_ZONES}. + -- + -- Retrieve the DetectedItems[].Set with the method @{Functional.Detection#DETECTION_BASE.GetDetectedSet}(). A @{Core.Set#SET_UNIT} object will be returned. + -- + -- Retrieve the formed @{Zone@ZONE_UNIT}s as a result of the grouping the detected units within the DetectionZoneRange, use the method @{Functional.Detection#DETECTION_BASE.GetDetectionZones}(). + -- To understand the amount of zones created, use the method @{Functional.Detection#DETECTION_BASE.GetDetectionZoneCount}(). + -- If you want to obtain a specific zone from the DetectedZones, use the method @{Functional.Detection#DETECTION_BASE.GetDetectionZone}() with a given index. + -- + -- ## 4.4) Flare or Smoke detected units + -- + -- Use the methods @{Functional.Detection#DETECTION_ZONES.FlareDetectedUnits}() or @{Functional.Detection#DETECTION_ZONES.SmokeDetectedUnits}() to flare or smoke the detected units when a new detection has taken place. + -- + -- ## 4.5) Flare or Smoke or Bound detected zones + -- + -- Use the methods: + -- + -- * @{Functional.Detection#DETECTION_ZONES.FlareDetectedZones}() to flare in a color + -- * @{Functional.Detection#DETECTION_ZONES.SmokeDetectedZones}() to smoke in a color + -- * @{Functional.Detection#DETECTION_ZONES.SmokeDetectedZones}() to bound with a tire with a white flag + -- + -- the detected zones when a new detection has taken place. + -- + -- @field #DETECTION_ZONES + DETECTION_ZONES = { + ClassName = "DETECTION_ZONES", + DetectionZoneRange = nil, + } + + + --- DETECTION_ZONES constructor. + -- @param #DETECTION_ZONES self + -- @param Core.Set#SET_ZONE DetectionSetZone The @{Set} of ZONE_RADIUS. + -- @param DCS#Coalition.side DetectionCoalition The coalition of the detection. + -- @return #DETECTION_ZONES + function DETECTION_ZONES:New( DetectionSetZone, DetectionCoalition ) + + -- Inherits from DETECTION_BASE + local self = BASE:Inherit( self, DETECTION_BASE:New( DetectionSetZone ) ) -- #DETECTION_ZONES + + self.DetectionSetZone = DetectionSetZone -- Core.Set#SET_ZONE + self.DetectionCoalition = DetectionCoalition + + self._SmokeDetectedUnits = false + self._FlareDetectedUnits = false + self._SmokeDetectedZones = false + self._FlareDetectedZones = false + self._BoundDetectedZones = false + + return self + end + + --- @param #DETECTION_ZONES self + -- @param #number The amount of alive recce. + function DETECTION_ZONES:CountAliveRecce() + + return self.DetectionSetZone:Count() + + end + + --- @param #DETECTION_ZONES self + function DETECTION_ZONES:ForEachAliveRecce( IteratorFunction, ... ) + self:F2( arg ) + + self.DetectionSetZone:ForEachZone( IteratorFunction, arg ) + + return self + end + + --- Report summary of a detected item using a given numeric index. + -- @param #DETECTION_ZONES self + -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem. + -- @param Wrapper.Group#GROUP AttackGroup The group to get the settings for. + -- @param Core.Settings#SETTINGS Settings (Optional) Message formatting settings to use. + -- @return Core.Report#REPORT The report of the detection items. + function DETECTION_ZONES:DetectedItemReportSummary( DetectedItem, AttackGroup, Settings ) + self:F( { DetectedItem = DetectedItem } ) + + local DetectedItemID = self:GetDetectedItemID( DetectedItem ) + + if DetectedItem then + local DetectedSet = self:GetDetectedItemSet( DetectedItem ) + local ReportSummaryItem + + local DetectedZone = self:GetDetectedItemZone( DetectedItem ) + local DetectedItemCoordinate = DetectedZone:GetCoordinate() + local DetectedItemCoordText = DetectedItemCoordinate:ToString( AttackGroup, Settings ) + + local ThreatLevelA2G = self:GetDetectedItemThreatLevel( DetectedItem ) + local DetectedItemsCount = DetectedSet:Count() + local DetectedItemsTypes = DetectedSet:GetTypeNames() + + local Report = REPORT:New() + Report:Add(DetectedItemID .. ", " .. DetectedItemCoordText) + Report:Add( string.format( "Threat: [%s]", string.rep( "■", ThreatLevelA2G ), string.rep( "□", 10-ThreatLevelA2G ) ) ) + Report:Add( string.format("Type: %2d of %s", DetectedItemsCount, DetectedItemsTypes ) ) + Report:Add( string.format("Detected: %s", DetectedItem.IsDetected and "yes" or "no" ) ) + + return Report + end + + return nil + end + + --- Report detailed of a detection result. + -- @param #DETECTION_ZONES self + -- @param Wrapper.Group#GROUP AttackGroup The group to generate the report for. + -- @return #string + function DETECTION_ZONES:DetectedReportDetailed( AttackGroup ) --R2.1 Fixed missing report + self:F() + + local Report = REPORT:New() + for DetectedItemIndex, DetectedItem in pairs( self.DetectedItems ) do + local DetectedItem = DetectedItem -- #DETECTION_BASE.DetectedItem + local ReportSummary = self:DetectedItemReportSummary( DetectedItem, AttackGroup ) + Report:SetTitle( "Detected areas:" ) + Report:Add( ReportSummary:Text() ) + end + + local ReportText = Report:Text() + + return ReportText + end + + + --- Calculate the optimal intercept point of the DetectedItem. + -- @param #DETECTION_ZONES self + -- @param #DETECTION_BASE.DetectedItem DetectedItem + function DETECTION_ZONES:CalculateIntercept( DetectedItem ) + + local DetectedCoord = DetectedItem.Coordinate +-- local DetectedSpeed = DetectedCoord:GetVelocity() +-- local DetectedHeading = DetectedCoord:GetHeading() +-- +-- if self.Intercept then +-- local DetectedSet = DetectedItem.Set +-- -- todo: speed +-- +-- local TranslateDistance = DetectedSpeed * self.InterceptDelay +-- +-- local InterceptCoord = DetectedCoord:Translate( TranslateDistance, DetectedHeading ) +-- +-- DetectedItem.InterceptCoord = InterceptCoord +-- else +-- DetectedItem.InterceptCoord = DetectedCoord +-- end + DetectedItem.InterceptCoord = DetectedCoord + end + + + + --- Smoke the detected units + -- @param #DETECTION_ZONES self + -- @return #DETECTION_ZONES self + function DETECTION_ZONES:SmokeDetectedUnits() + self:F2() + + self._SmokeDetectedUnits = true + return self + end + + --- Flare the detected units + -- @param #DETECTION_ZONES self + -- @return #DETECTION_ZONES self + function DETECTION_ZONES:FlareDetectedUnits() + self:F2() + + self._FlareDetectedUnits = true + return self + end + + --- Smoke the detected zones + -- @param #DETECTION_ZONES self + -- @return #DETECTION_ZONES self + function DETECTION_ZONES:SmokeDetectedZones() + self:F2() + + self._SmokeDetectedZones = true + return self + end + + --- Flare the detected zones + -- @param #DETECTION_ZONES self + -- @return #DETECTION_ZONES self + function DETECTION_ZONES:FlareDetectedZones() + self:F2() + + self._FlareDetectedZones = true + return self + end + + --- Bound the detected zones + -- @param #DETECTION_ZONES self + -- @return #DETECTION_ZONES self + function DETECTION_ZONES:BoundDetectedZones() + self:F2() + + self._BoundDetectedZones = true + return self + end + + --- Make text documenting the changes of the detected zone. + -- @param #DETECTION_ZONES self + -- @param #DETECTION_BASE.DetectedItem DetectedItem + -- @return #string The Changes text + function DETECTION_ZONES:GetChangeText( DetectedItem ) + self:F( DetectedItem ) + + local MT = {} + + for ChangeCode, ChangeData in pairs( DetectedItem.Changes ) do + + if ChangeCode == "AA" then + MT[#MT+1] = "Detected new area " .. ChangeData.ID .. ". The center target is a " .. ChangeData.ItemUnitType .. "." + end + + if ChangeCode == "RAU" then + MT[#MT+1] = "Changed area " .. ChangeData.ID .. ". Removed the center target." + end + + if ChangeCode == "AAU" then + MT[#MT+1] = "Changed area " .. ChangeData.ID .. ". The new center target is a " .. ChangeData.ItemUnitType .. "." + end + + if ChangeCode == "RA" then + MT[#MT+1] = "Removed old area " .. ChangeData.ID .. ". No more targets in this area." + end + + if ChangeCode == "AU" then + local MTUT = {} + for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do + if ChangeUnitType ~= "ID" then + MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType + end + end + MT[#MT+1] = "Detected for area " .. ChangeData.ID .. " new target(s) " .. table.concat( MTUT, ", " ) .. "." + end + + if ChangeCode == "RU" then + local MTUT = {} + for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do + if ChangeUnitType ~= "ID" then + MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType + end + end + MT[#MT+1] = "Removed for area " .. ChangeData.ID .. " invisible or destroyed target(s) " .. table.concat( MTUT, ", " ) .. "." + end + + end + + return table.concat( MT, "\n" ) + + end + + + --- Make a DetectionSet table. This function will be overridden in the derived clsses. + -- @param #DETECTION_ZONES self + -- @return #DETECTION_ZONES self + function DETECTION_ZONES:CreateDetectionItems() + + + self:F( "Checking Detected Items for new Detected Units ..." ) + + local DetectedUnits = SET_UNIT:New() + + -- First go through all zones, and check if there are new Zones. + -- New Zones become a new DetectedItem. + for ZoneName, DetectionZone in pairs( self.DetectionSetZone:GetSet() ) do + + local DetectedItem = self:GetDetectedItemByKey( ZoneName ) + + if DetectedItem == nil then + DetectedItem = self:AddDetectedItemZone( "ZONE", ZoneName, nil, DetectionZone ) + end + + local DetectedItemSetUnit = self:GetDetectedItemSet( DetectedItem ) + + -- Scan the zone + DetectionZone:Scan( { Object.Category.UNIT }, { Unit.Category.GROUND_UNIT } ) + + -- For all the units in the zone, + -- check if they are of the same coalition to be included. + local ZoneUnits = DetectionZone:GetScannedUnits() + for DCSUnitID, DCSUnit in pairs( ZoneUnits ) do + local UnitName = DCSUnit:getName() + local ZoneUnit = UNIT:FindByName( UnitName ) + local ZoneUnitCoalition = ZoneUnit:GetCoalition() + if ZoneUnitCoalition == self.DetectionCoalition then + if DetectedItemSetUnit:FindUnit( UnitName ) == nil and DetectedUnits:FindUnit( UnitName ) == nil then + self:F( "Adding " .. UnitName ) + DetectedItemSetUnit:AddUnit( ZoneUnit ) + DetectedUnits:AddUnit( ZoneUnit ) + end + end + end + end + + + -- Now all the tests should have been build, now make some smoke and flares... + -- We also report here the friendlies within the detected areas. + + for DetectedItemID, DetectedItemData in pairs( self.DetectedItems ) do + + local DetectedItem = DetectedItemData -- #DETECTION_BASE.DetectedItem + local DetectedSet = self:GetDetectedItemSet( DetectedItem ) + local DetectedFirstUnit = DetectedSet:GetFirst() + local DetectedZone = self:GetDetectedItemZone( DetectedItem ) + + -- Set the last known coordinate to the detection item. + local DetectedZoneCoord = DetectedZone:GetCoordinate() + self:SetDetectedItemCoordinate( DetectedItem, DetectedZoneCoord, DetectedFirstUnit ) + + self:CalculateIntercept( DetectedItem ) + + -- We search for friendlies nearby. + -- If there weren't any friendlies nearby, and now there are friendlies nearby, we flag the area as "changed". + -- If there were friendlies nearby, and now there aren't any friendlies nearby, we flag the area as "changed". + -- This is for the A2G dispatcher to detect if there is a change in the tactical situation. + local OldFriendliesNearbyGround = self:IsFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT ) + self:ReportFriendliesNearBy( { DetectedItem = DetectedItem, ReportSetGroup = self.DetectionSetGroup } ) -- Fill the Friendlies table + local NewFriendliesNearbyGround = self:IsFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT ) + if OldFriendliesNearbyGround ~= NewFriendliesNearbyGround then + DetectedItem.Changed = true + end + + self:SetDetectedItemThreatLevel( DetectedItem ) -- Calculate A2G threat level + --self:NearestRecce( DetectedItem ) + + + if DETECTION_ZONES._SmokeDetectedUnits or self._SmokeDetectedUnits then + DetectedZone:SmokeZone( SMOKECOLOR.Red, 30 ) + end + + --DetectedSet:Flush( self ) + + DetectedSet:ForEachUnit( + --- @param Wrapper.Unit#UNIT DetectedUnit + function( DetectedUnit ) + if DetectedUnit:IsAlive() then + --self:T( "Detected Set #" .. DetectedItem.ID .. ":" .. DetectedUnit:GetName() ) + if DETECTION_ZONES._FlareDetectedUnits or self._FlareDetectedUnits then + DetectedUnit:FlareGreen() + end + if DETECTION_ZONES._SmokeDetectedUnits or self._SmokeDetectedUnits then + DetectedUnit:SmokeGreen() + end + end + end + ) + if DETECTION_ZONES._FlareDetectedZones or self._FlareDetectedZones then + DetectedZone:FlareZone( SMOKECOLOR.White, 30, math.random( 0,90 ) ) + end + if DETECTION_ZONES._SmokeDetectedZones or self._SmokeDetectedZones then + DetectedZone:SmokeZone( SMOKECOLOR.White, 30 ) + end + + if DETECTION_ZONES._BoundDetectedZones or self._BoundDetectedZones then + self.CountryID = DetectedSet:GetFirst():GetCountry() + DetectedZone:BoundZone( 12, self.CountryID ) + end + end + + end + + --- @param #DETECTION_ZONES self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @param Detection The element on which the detection is based. + -- @param #number DetectionTimeStamp Time stamp of detection event. + function DETECTION_ZONES:onafterDetection( From, Event, To, Detection, DetectionTimeStamp ) + + self.DetectionRun = self.DetectionRun + 1 + if self.DetectionCount > 0 and self.DetectionRun == self.DetectionCount then + self:CreateDetectionItems() -- Polymorphic call to Create/Update the DetectionItems list for the DETECTION_ class grouping method. + + for DetectedItemID, DetectedItem in pairs( self.DetectedItems ) do + self:UpdateDetectedItemDetection( DetectedItem ) + self:CleanDetectionItem( DetectedItem, DetectedItemID ) -- Any DetectionItem that has a Set with zero elements in it, must be removed from the DetectionItems list. + if DetectedItem then + self:__DetectedItem( 0.1, DetectedItem ) + end + end + self:__Detect( -self.RefreshTimeInterval ) + end + end + + + --- Set IsDetected flag for the DetectedItem, which can have more units. + -- @param #DETECTION_ZONES self + -- @return #DETECTION_ZONES.DetectedItem DetectedItem + -- @return #boolean true if at least one UNIT is detected from the DetectedSet, false if no UNIT was detected from the DetectedSet. + function DETECTION_ZONES:UpdateDetectedItemDetection( DetectedItem ) + + local IsDetected = true + + DetectedItem.IsDetected = true + + return IsDetected + end + +end --- **Functional** -- Management of target **Designation**. Lase, smoke and illuminate targets. +-- +-- === +-- +-- ## Features: +-- +-- * Faciliate the communication of detected targets to players. +-- * Designate targets using lasers, through a menu system. +-- * Designate targets using smoking, through a menu system. +-- * Designate targets using illumination, through a menu system. +-- * Auto lase targets. +-- * Refresh detection upon specified time intervals. +-- * Prioritization on threat levels. +-- * Reporting system of threats. +-- +-- === +-- +-- ## Missions: +-- +-- [DES - Designation](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/DES%20-%20Designation) +-- +-- === +-- +-- Targets detected by recce will be communicated to a group of attacking players. +-- A menu system is made available that allows to: +-- +-- * **Lased** for a period of time. +-- * **Smoked**. Artillery or airplanes with Illuminatino ordonance need to be present. (WIP, but early demo ready.) +-- * **Illuminated** through an illumination bomb. Artillery or airplanes with Illuminatino ordonance need to be present. (WIP, but early demo ready. +-- +-- The following terminology is being used throughout this document: +-- +-- * The **DesignateObject** is the object of the DESIGNATE class, which is this class explained in the document. +-- * The **DetectionObject** is the object of a DETECTION_ class (DETECTION_TYPES, DETECTION_AREAS, DETECTION_UNITS), which is executing the detection and grouping of Targets into _DetectionItems_. +-- * **TargetGroups** is the list of detected target groupings by the _DetectionObject_. Each _TargetGroup_ contains a _TargetSet_. +-- * **TargetGroup** is one element of the __TargetGroups__ list, and contains a _TargetSet_. +-- * The **TargetSet** is a SET_UNITS collection of _Targets_, that have been detected by the _DetectionObject_. +-- * A **Target** is a detected UNIT object by the _DetectionObject_. +-- * A **Threat Level** is a number from 0 to 10 that is calculated based on the threat of the Target in an Air to Ground battle scenario. +-- * The **RecceSet** is a SET_GROUP collection that contains the **RecceGroups**. +-- * A **RecceGroup** is a GROUP object containing the **Recces**. +-- * A **Recce** is a UNIT object executing the reconnaissance as part the _DetectionObject_. A Recce can be of any UNIT type. +-- * An **AttackGroup** is a GROUP object that contain _Players_. +-- * A **Player** is an active CLIENT object containing a human player. +-- * A **Designate Menu** is the menu that is dynamically created during the designation process for each _AttackGroup_. +-- +-- # Player Manual +-- +-- ![Banner Image](..\Presentations\DESIGNATE\Dia3.JPG) +-- +-- A typical mission setup would require Recce (a @{Set} of Recce) to be detecting potential targets. +-- The DetectionObject will group the detected targets based on the detection method being used. +-- Possible detection methods could be by Area, by Type or by Unit. +-- Each grouping will result in a **TargetGroup**, for terminology and clarity we will use this term throughout the document. +-- +-- **Recce** require to have Line of Sight (LOS) towards the targets. +-- The **Recce** will report any detected targets to the Players (on the picture Observers). +-- When targets are detected, a menu will be made available that allows those **TargetGroups** to be designated. +-- Designation can be done by Lasing, Smoking and Illumination. +-- Smoking is useful during the day, while illumination is recommended to be used during the night. +-- Smoking can designate specific targets, but not very precise, while lasing is very accurate and allows to +-- players to attack the targets using laser guided bombs or rockets. +-- Illumination will lighten up the Target Area. +-- +-- **Recce** can be ground based or airborne. Airborne **Recce** (AFAC) can be really useful to designate a large amount of targets +-- in a wide open area, as airborne **Recce** has a large LOS. +-- However, ground based **Recce** are very useful to smoke or illuminate targets, as they can be much closer +-- to the Target Area. +-- +-- It is recommended to make the **Recce** invisible and immortal using the Mission Editor in DCS World. +-- This will ensure that the detection process won't be interrupted and that targets can be designated. +-- However, you don't have to, so to simulate a more real-word situation or simulation, **Recce can also be destroyed**! +-- +-- ## 1. Player View (Observer) +-- +-- ![Banner Image](..\Presentations\DESIGNATE\Dia4.JPG) +-- +-- The RecceSet is continuously detecting for potential Targets, +-- executing its task as part of the DetectionObject. +-- Once Targets have been detected, the DesignateObject will trigger the **Detect Event**. +-- +-- In order to prevent an overflow in the DesignateObject of detected targets, +-- there is a maximum amount of TargetGroups +-- that can be put in **scope** of the DesignateObject. +-- We call this the **MaximumDesignations** term. +-- +-- ## 2. Designate Menu +-- +-- ![Banner Image](..\Presentations\DESIGNATE\Dia5.JPG) +-- +-- For each detected TargetGroup, there is: +-- +-- * A **Designate Menu** are created and continuously refreshed, containing the **DesignationID** and the **Designation Status**. +-- * The RecceGroups are reporting to each AttackGroup, sending **Messages** containing the Threat Level and the TargetSet composition. +-- +-- A Player can then select an action from the **Designate Menu**. +-- The Designation Status is shown between the ( ). +-- +-- It indicates for each TargetGroup the current active designation action applied: +-- +-- * An "I" for Illumnation designation. +-- * An "S" for Smoking designation. +-- * An "L" for Lasing designation. +-- +-- Note that multiple designation methods can be active at the same time! +-- Note the **Auto Lase** option. When switched on, the available **Recce** will lase +-- Targets when detected. +-- +-- Targets are designated per **Threat Level**. +-- The most threatening targets from an Air to Ground perspective, are designated first! +-- This is for all designation methods. +-- +-- ![Banner Image](..\Presentations\DESIGNATE\Dia6.JPG) +-- +-- Each Designate Menu has a sub menu structure, which allows specific actions to be triggered: +-- +-- * Lase Targets using a specific laser code. +-- * Smoke Targets using a specific smoke color. +-- * Illuminate areas. +-- +-- ## 3. Lasing Targets +-- +-- ![Banner Image](..\Presentations\DESIGNATE\Dia7.JPG) +-- +-- Lasing targets is done as expected. Each available Recce can lase only ONE target through! +-- +-- ![Banner Image](..\Presentations\DESIGNATE\Dia8.JPG) +-- +-- Lasing can be done for specific laser codes. The Su-25T requires laser code 1113, while the A-10A requires laser code 1680. +-- For those, specific menu options can be made available for players to lase with these codes. +-- Auto Lase (as explained above), will ensure continuous lasing of available targets. +-- The status report shows which targets are being designated. +-- +-- The following logic is executed when a TargetGroup is selected to be *lased* from the Designation Menu: +-- +-- * The RecceSet is searched for any Recce that is within *designation distance* from a Target in the TargetGroup that is currently not being designated. +-- * If there is a Recce found that is currently no designating a target, and is within designation distance from the Target, then that Target will be designated. +-- * During designation, any Recce that does not have Line of Sight (LOS) and is not within disignation distance from the Target, will stop designating the Target, and a report is given. +-- * When a Recce is designating a Target, and that Target is destroyed, then the Recce will stop designating the Target, and will report the event. +-- * When a Recce is designating a Target, and that Recce is destroyed, then the Recce will be removed from the RecceSet and designation will stop without reporting. +-- * When all RecceGroups are destroyed from the RecceSet, then the DesignationObject will stop functioning, and nothing will be reported. +-- +-- In this way, DESIGNATE assists players to designate ground targets for a coordinated attack! +-- +-- ## 4. Illuminating Targets +-- +-- ![Banner Image](..\Presentations\DESIGNATE\Dia9.JPG) +-- +-- Illumination bombs are fired between 500 and 700 meters altitude and will burn about 2 minutes, while slowly decending. +-- Each available recce within range will fire an illumination bomb. +-- Illumination bombs can be fired in while lasing targets. +-- When illumination bombs are fired, it will take about 2 minutes until a sequent bomb run can be requested using the menus. +-- +-- ## 5. Smoking Targets +-- +-- ![Banner Image](..\Presentations\DESIGNATE\Dia10.JPG) +-- +-- Smoke will fire for 5 minutes. +-- Each available recce within range will smoke a target. +-- Smoking can be requested while lasing targets. +-- Smoke will appear "around" the targets, because of accuracy limitations. +-- +-- +-- Have FUN! +-- +-- === +-- +-- ### Contributions: +-- +-- * [**Ciribob**](https://forums.eagle.ru/member.php?u=112175): Showing the way how to lase targets + how laser codes work!!! Explained the autolase script. +-- * [**EasyEB**](https://forums.eagle.ru/member.php?u=112055): Ideas and Beta Testing +-- * [**Wingthor**](https://forums.eagle.ru/member.php?u=123698): Beta Testing +-- +-- ### Authors: +-- +-- * **FlightControl**: Design & Programming +-- +-- === +-- +-- @module Functional.Designate +-- @image Designation.JPG + +do -- DESIGNATE + + --- @type DESIGNATE + -- @extends Core.Fsm#FSM_PROCESS + + --- Manage the designation of detected targets. + -- + -- + -- # 1. DESIGNATE constructor + -- + -- * @{#DESIGNATE.New}(): Creates a new DESIGNATE object. + -- + -- # 2. DESIGNATE is a FSM + -- + -- Designate is a finite state machine, which allows for controlled transitions of states. + -- + -- ## 2.1 DESIGNATE States + -- + -- * **Designating** ( Group ): The designation process. + -- + -- ## 2.2 DESIGNATE Events + -- + -- * **@{#DESIGNATE.Detect}**: Detect targets. + -- * **@{#DESIGNATE.LaseOn}**: Lase the targets with the specified Index. + -- * **@{#DESIGNATE.LaseOff}**: Stop lasing the targets with the specified Index. + -- * **@{#DESIGNATE.Smoke}**: Smoke the targets with the specified Index. + -- * **@{#DESIGNATE.Status}**: Report designation status. + -- + -- # 3. Maximum Designations + -- + -- In order to prevent an overflow of designations due to many Detected Targets, there is a + -- Maximum Designations scope that is set in the DesignationObject. + -- + -- The method @{#DESIGNATE.SetMaximumDesignations}() will put a limit on the amount of designations put in scope of the DesignationObject. + -- Using the menu system, the player can "forget" a designation, so that gradually a new designation can be put in scope when detected. + -- + -- # 4. Laser codes + -- + -- ## 4.1. Set possible laser codes + -- + -- An array of laser codes can be provided, that will be used by the DESIGNATE when lasing. + -- The laser code is communicated by the Recce when it is lasing a larget. + -- Note that the default laser code is 1113. + -- Working known laser codes are: 1113,1462,1483,1537,1362,1214,1131,1182,1644,1614,1515,1411,1621,1138,1542,1678,1573,1314,1643,1257,1467,1375,1341,1275,1237 + -- + -- Use the method @{#DESIGNATE.SetLaserCodes}() to set the possible laser codes to be selected from. + -- One laser code can be given or an sequence of laser codes through an table... + -- + -- Designate:SetLaserCodes( 1214 ) + -- + -- The above sets one laser code with the value 1214. + -- + -- Designate:SetLaserCodes( { 1214, 1131, 1614, 1138 } ) + -- + -- The above sets a collection of possible laser codes that can be assigned. **Note the { } notation!** + -- + -- ## 4.2. Auto generate laser codes + -- + -- Use the method @{#DESIGNATE.GenerateLaserCodes}() to generate all possible laser codes. Logic implemented and advised by Ciribob! + -- + -- ## 4.3. Add specific lase codes to the lase menu + -- + -- Certain plane types can only drop laser guided ordonnance when targets are lased with specific laser codes. + -- The SU-25T needs targets to be lased using laser code 1113. + -- The A-10A needs targets to be lased using laser code 1680. + -- + -- The method @{#DESIGNATE.AddMenuLaserCode}() to allow a player to lase a target using a specific laser code. + -- Remove such a lase menu option using @{#DESIGNATE.RemoveMenuLaserCode}(). + -- + -- # 5. Autolase to automatically lase detected targets. + -- + -- DetectionItems can be auto lased once detected by Recces. As such, there is almost no action required from the Players using the Designate Menu. + -- The **auto lase** function can be activated through the Designation Menu. + -- Use the method @{#DESIGNATE.SetAutoLase}() to activate or deactivate the auto lase function programmatically. + -- Note that autolase will automatically activate lasing for ALL DetectedItems. Individual items can be switched-off if required using the Designation Menu. + -- + -- Designate:SetAutoLase( true ) + -- + -- Activate the auto lasing. + -- + -- # 6. Target prioritization on threat level + -- + -- Targets can be detected of different types in one DetectionItem. Depending on the type of the Target, a different threat level applies in an Air to Ground combat context. + -- SAMs are of a higher threat than normal tanks. So, if the Target type was recognized, the Recces will select those targets that form the biggest threat first, + -- and will continue this until the remaining vehicles with the lowest threat have been reached. + -- + -- This threat level prioritization can be activated using the method @{#DESIGNATE.SetThreatLevelPrioritization}(). + -- If not activated, Targets will be selected in a random order, but most like those first which are the closest to the Recce marking the Target. + -- + -- Designate:SetThreatLevelPrioritization( true ) + -- + -- The example will activate the threat level prioritization for this the Designate object. Threats will be marked based on the threat level of the Target. + -- + -- # 7. Designate Menu Location for a Mission + -- + -- You can make DESIGNATE work for a @{Tasking.Mission#MISSION} object. In this way, the designate menu will not appear in the root of the radio menu, but in the menu of the Mission. + -- Use the method @{#DESIGNATE.SetMission}() to set the @{Mission} object for the designate function. + -- + -- # 8. Status Report + -- + -- A status report is available that displays the current Targets detected, grouped per DetectionItem, and a list of which Targets are currently being marked. + -- + -- * The status report can be shown by selecting "Status" -> "Report Status" from the Designation menu . + -- * The status report can be automatically flashed by selecting "Status" -> "Flash Status On". + -- * The automatic flashing of the status report can be deactivated by selecting "Status" -> "Flash Status Off". + -- * The flashing of the status menu is disabled by default. + -- * The method @{#DESIGNATE.SetFlashStatusMenu}() can be used to enable or disable to flashing of the status menu. + -- + -- Designate:SetFlashStatusMenu( true ) + -- + -- The example will activate the flashing of the status menu for this Designate object. + -- + -- @field #DESIGNATE + DESIGNATE = { + ClassName = "DESIGNATE", + } + + --- DESIGNATE Constructor. This class is an abstract class and should not be instantiated. + -- @param #DESIGNATE self + -- @param Tasking.CommandCenter#COMMANDCENTER CC + -- @param Functional.Detection#DETECTION_BASE Detection + -- @param Core.Set#SET_GROUP AttackSet The Attack collection of GROUP objects to designate and report for. + -- @param Tasking.Mission#MISSION Mission (Optional) The Mission where the menu needs to be attached. + -- @return #DESIGNATE + function DESIGNATE:New( CC, Detection, AttackSet, Mission ) + + local self = BASE:Inherit( self, FSM:New() ) -- #DESIGNATE + self:F( { Detection } ) + + self:SetStartState( "Designating" ) + + self:AddTransition( "*", "Detect", "*" ) + --- Detect Handler OnBefore for DESIGNATE + -- @function [parent=#DESIGNATE] OnBeforeDetect + -- @param #DESIGNATE self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Detect Handler OnAfter for DESIGNATE + -- @function [parent=#DESIGNATE] OnAfterDetect + -- @param #DESIGNATE self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Detect Trigger for DESIGNATE + -- @function [parent=#DESIGNATE] Detect + -- @param #DESIGNATE self + + --- Detect Asynchronous Trigger for DESIGNATE + -- @function [parent=#DESIGNATE] __Detect + -- @param #DESIGNATE self + -- @param #number Delay + + self:AddTransition( "*", "LaseOn", "Lasing" ) + --- LaseOn Handler OnBefore for DESIGNATE + -- @function [parent=#DESIGNATE ] OnBeforeLaseOn + -- @param #DESIGNATE self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- LaseOn Handler OnAfter for DESIGNATE + -- @function [parent=#DESIGNATE ] OnAfterLaseOn + -- @param #DESIGNATE self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- LaseOn Trigger for DESIGNATE + -- @function [parent=#DESIGNATE ] LaseOn + -- @param #DESIGNATE self + + --- LaseOn Asynchronous Trigger for DESIGNATE + -- @function [parent=#DESIGNATE ] __LaseOn + -- @param #DESIGNATE self + -- @param #number Delay + + self:AddTransition( "Lasing", "Lasing", "Lasing" ) + + self:AddTransition( "*", "LaseOff", "Designate" ) + --- LaseOff Handler OnBefore for DESIGNATE + -- @function [parent=#DESIGNATE ] OnBeforeLaseOff + -- @param #DESIGNATE self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- LaseOff Handler OnAfter for DESIGNATE + -- @function [parent=#DESIGNATE ] OnAfterLaseOff + -- @param #DESIGNATE self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- LaseOff Trigger for DESIGNATE + -- @function [parent=#DESIGNATE ] LaseOff + -- @param #DESIGNATE self + + --- LaseOff Asynchronous Trigger for DESIGNATE + -- @function [parent=#DESIGNATE ] __LaseOff + -- @param #DESIGNATE self + -- @param #number Delay + + self:AddTransition( "*", "Smoke", "*" ) + --- Smoke Handler OnBefore for DESIGNATE + -- @function [parent=#DESIGNATE ] OnBeforeSmoke + -- @param #DESIGNATE self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Smoke Handler OnAfter for DESIGNATE + -- @function [parent=#DESIGNATE ] OnAfterSmoke + -- @param #DESIGNATE self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Smoke Trigger for DESIGNATE + -- @function [parent=#DESIGNATE ] Smoke + -- @param #DESIGNATE self + + --- Smoke Asynchronous Trigger for DESIGNATE + -- @function [parent=#DESIGNATE ] __Smoke + -- @param #DESIGNATE self + -- @param #number Delay + + self:AddTransition( "*", "Illuminate", "*" ) + --- Illuminate Handler OnBefore for DESIGNATE + -- @function [parent=#DESIGNATE] OnBeforeIlluminate + -- @param #DESIGNATE self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Illuminate Handler OnAfter for DESIGNATE + -- @function [parent=#DESIGNATE] OnAfterIlluminate + -- @param #DESIGNATE self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Illuminate Trigger for DESIGNATE + -- @function [parent=#DESIGNATE] Illuminate + -- @param #DESIGNATE self + + --- Illuminate Asynchronous Trigger for DESIGNATE + -- @function [parent=#DESIGNATE] __Illuminate + -- @param #DESIGNATE self + -- @param #number Delay + + self:AddTransition( "*", "DoneSmoking", "*" ) + self:AddTransition( "*", "DoneIlluminating", "*" ) + + self:AddTransition( "*", "Status", "*" ) + --- Status Handler OnBefore for DESIGNATE + -- @function [parent=#DESIGNATE ] OnBeforeStatus + -- @param #DESIGNATE self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Status Handler OnAfter for DESIGNATE + -- @function [parent=#DESIGNATE ] OnAfterStatus + -- @param #DESIGNATE self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Status Trigger for DESIGNATE + -- @function [parent=#DESIGNATE ] Status + -- @param #DESIGNATE self + + --- Status Asynchronous Trigger for DESIGNATE + -- @function [parent=#DESIGNATE ] __Status + -- @param #DESIGNATE self + -- @param #number Delay + + self.CC = CC + self.Detection = Detection + self.AttackSet = AttackSet + self.RecceSet = Detection:GetDetectionSet() + self.Recces = {} + self.Designating = {} + self:SetDesignateName() + + self:SetLaseDuration() -- Default is 120 seconds. + + self:SetFlashStatusMenu( false ) + self:SetFlashDetectionMessages( true ) + self:SetMission( Mission ) + + self:SetLaserCodes( { 1688, 1130, 4785, 6547, 1465, 4578 } ) -- set self.LaserCodes + self:SetAutoLase( false, false ) -- set self.Autolase and don't send message. + + self:SetThreatLevelPrioritization( false ) -- self.ThreatLevelPrioritization, default is threat level priorization off + self:SetMaximumDesignations( 5 ) -- Sets the maximum designations. The default is 5 designations. + self:SetMaximumDistanceDesignations( 8000 ) -- Sets the maximum distance on which designations can be accepted. The default is 8000 meters. + self:SetMaximumMarkings( 2 ) -- Per target group, a maximum of 2 markings will be made by default. + + self:SetDesignateMenu() + + self.LaserCodesUsed = {} + + self.MenuLaserCodes = {} -- This map contains the laser codes that will be shown in the designate menu to lase with specific laser codes. + + self.Detection:__Start( 2 ) + + self:__Detect( -15 ) + + self.MarkScheduler = SCHEDULER:New( self ) + + return self + end + + --- Set the flashing of the status menu for all AttackGroups. + -- @param #DESIGNATE self + -- @param #boolean FlashMenu true: the status menu will be flashed every detection run; false: no flashing of the menu. + -- @return #DESIGNATE + -- @usage + -- + -- -- Enable the designate status message flashing... + -- Designate:SetFlashStatusMenu( true ) + -- + -- -- Disable the designate statusmessage flashing... + -- Designate:SetFlashStatusMenu() + -- + -- -- Disable the designate status message flashing... + -- Designate:SetFlashStatusMenu( false ) + function DESIGNATE:SetFlashStatusMenu( FlashMenu ) --R2.1 + + self.FlashStatusMenu = {} + + self.AttackSet:ForEachGroupAlive( + + --- @param Wrapper.Group#GROUP AttackGroup + function( AttackGroup ) + self.FlashStatusMenu[AttackGroup] = FlashMenu + end + ) + + return self + end + + --- Set the flashing of the new detection messages. + -- @param #DESIGNATE self + -- @param #boolean FlashDetectionMessage true: The detection message will be flashed every time a new detection was done; false: no messages will be displayed. + -- @return #DESIGNATE + -- @usage + -- + -- -- Enable the message flashing... + -- Designate:SetFlashDetectionMessages( true ) + -- + -- -- Disable the message flashing... + -- Designate:SetFlashDetectionMessages() + -- + -- -- Disable the message flashing... + -- Designate:SetFlashDetectionMessages( false ) + function DESIGNATE:SetFlashDetectionMessages( FlashDetectionMessage ) + + self.FlashDetectionMessage = {} + + self.AttackSet:ForEachGroupAlive( + + --- @param Wrapper.Group#GROUP AttackGroup + function( AttackGroup ) + self.FlashDetectionMessage[AttackGroup] = FlashDetectionMessage + end + ) + + return self + end + + + --- Set the maximum amount of designations. + -- @param #DESIGNATE self + -- @param #number MaximumDesignations + -- @return #DESIGNATE + function DESIGNATE:SetMaximumDesignations( MaximumDesignations ) + self.MaximumDesignations = MaximumDesignations + return self + end + + + --- Set the maximum ground designation distance. + -- @param #DESIGNATE self + -- @param #number MaximumDistanceGroundDesignation Maximum ground designation distance in meters. + -- @return #DESIGNATE + function DESIGNATE:SetMaximumDistanceGroundDesignation( MaximumDistanceGroundDesignation ) + self.MaximumDistanceGroundDesignation = MaximumDistanceGroundDesignation + return self + end + + + --- Set the maximum air designation distance. + -- @param #DESIGNATE self + -- @param #number MaximumDistanceAirDesignation Maximum air designation distance in meters. + -- @return #DESIGNATE + function DESIGNATE:SetMaximumDistanceAirDesignation( MaximumDistanceAirDesignation ) + self.MaximumDistanceAirDesignation = MaximumDistanceAirDesignation + return self + end + + + --- Set the overall maximum distance when designations can be accepted. + -- @param #DESIGNATE self + -- @param #number MaximumDistanceDesignations Maximum distance in meters to accept designations. + -- @return #DESIGNATE + function DESIGNATE:SetMaximumDistanceDesignations( MaximumDistanceDesignations ) + self.MaximumDistanceDesignations = MaximumDistanceDesignations + return self + end + + + --- Set the maximum amount of markings FACs will do, per designated target group. + -- @param #DESIGNATE self + -- @param #number MaximumMarkings Maximum markings FACs will do, per designated target group. + -- @return #DESIGNATE + function DESIGNATE:SetMaximumMarkings( MaximumMarkings ) + self.MaximumMarkings = MaximumMarkings + return self + end + + + --- Set an array of possible laser codes. + -- Each new lase will select a code from this table. + -- @param #DESIGNATE self + -- @param #list<#number> LaserCodes + -- @return #DESIGNATE + function DESIGNATE:SetLaserCodes( LaserCodes ) --R2.1 + + self.LaserCodes = ( type( LaserCodes ) == "table" ) and LaserCodes or { LaserCodes } + self:F( { LaserCodes = self.LaserCodes } ) + + self.LaserCodesUsed = {} + + return self + end + + + --- Add a specific lase code to the designate lase menu to lase targets with a specific laser code. + -- The MenuText will appear in the lase menu. + -- @param #DESIGNATE self + -- @param #number LaserCode The specific laser code to be added to the lase menu. + -- @param #string MenuText The text to be shown to the player. If you specify a %d in the MenuText, the %d will be replaced with the LaserCode specified. + -- @return #DESIGNATE + -- @usage + -- RecceDesignation:AddMenuLaserCode( 1113, "Lase with %d for Su-25T" ) + -- RecceDesignation:AddMenuLaserCode( 1680, "Lase with %d for A-10A" ) + -- + function DESIGNATE:AddMenuLaserCode( LaserCode, MenuText ) + + self.MenuLaserCodes[LaserCode] = MenuText + self:SetDesignateMenu() + + return self + end + + + --- Removes a specific lase code from the designate lase menu. + -- @param #DESIGNATE self + -- @param #number LaserCode The specific laser code that was set to be added to the lase menu. + -- @return #DESIGNATE + -- @usage + -- RecceDesignation:RemoveMenuLaserCode( 1113 ) + -- + function DESIGNATE:RemoveMenuLaserCode( LaserCode ) + + self.MenuLaserCodes[LaserCode] = nil + self:SetDesignateMenu() + + return self + end + + + + + --- Set the name of the designation. The name will appear in the menu. + -- This method can be used to control different designations for different plane types. + -- @param #DESIGNATE self + -- @param #string DesignateName + -- @return #DESIGNATE + function DESIGNATE:SetDesignateName( DesignateName ) + + self.DesignateName = "Designation" .. ( DesignateName and ( " for " .. DesignateName ) or "" ) + + return self + end + + --- Set the lase duration for designations. + -- @param #DESIGNATE self + -- @param #number LaseDuration The time in seconds a lase will continue to hold on target. The default is 120 seconds. + -- @return #DESIGNATE + function DESIGNATE:SetLaseDuration( LaseDuration ) + self.LaseDuration = LaseDuration or 120 + return self + end + + --- Generate an array of possible laser codes. + -- Each new lase will select a code from this table. + -- The entered value can range from 1111 - 1788, + -- -- but the first digit of the series must be a 1 or 2 + -- -- and the last three digits must be between 1 and 8. + -- The range used to be bugged so its not 1 - 8 but 0 - 7. + -- function below will use the range 1-7 just in case + -- @param #DESIGNATE self + -- @return #DESIGNATE + function DESIGNATE:GenerateLaserCodes() --R2.1 + + self.LaserCodes = {} + + 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 containsDigit(_code, 8) + and not containsDigit(_code, 9) + and not containsDigit(_code, 0) then + self:T(_code) + table.insert( self.LaserCodes, _code ) + break + end + end + _count = _count + 1 + end + + self.LaserCodesUsed = {} + + return self + end + + + + --- Set auto lase. + -- Auto lase will start lasing targets immediately when these are in range. + -- @param #DESIGNATE self + -- @param #boolean AutoLase (optional) true sets autolase on, false off. Default is off. + -- @param #boolean Message (optional) true is send message, false or nil won't send a message. Default is no message sent. + -- @return #DESIGNATE + function DESIGNATE:SetAutoLase( AutoLase, Message ) + + self.AutoLase = AutoLase or false + + if Message then + local AutoLaseOnOff = ( self.AutoLase == true ) and "On" or "Off" + local CC = self.CC:GetPositionable() + if CC then + CC:MessageToSetGroup( self.DesignateName .. ": Auto Lase " .. AutoLaseOnOff .. ".", 15, self.AttackSet ) + end + end + + self:CoordinateLase() + self:SetDesignateMenu() + + return self + end + + --- Set priorization of Targets based on the **Threat Level of the Target** in an Air to Ground context. + -- @param #DESIGNATE self + -- @param #boolean Prioritize + -- @return #DESIGNATE + function DESIGNATE:SetThreatLevelPrioritization( Prioritize ) --R2.1 + + self.ThreatLevelPrioritization = Prioritize + + return self + end + + --- Set the MISSION object for which designate will function. + -- When a MISSION object is assigned, the menu for the designation will be located at the Mission Menu. + -- @param #DESIGNATE self + -- @param Tasking.Mission#MISSION Mission The MISSION object. + -- @return #DESIGNATE + function DESIGNATE:SetMission( Mission ) --R2.2 + + self.Mission = Mission + + return self + end + + + --- + -- @param #DESIGNATE self + -- @return #DESIGNATE + function DESIGNATE:onafterDetect() + + self:__Detect( -math.random( 60 ) ) + + self:DesignationScope() + self:CoordinateLase() + self:SendStatus() + self:SetDesignateMenu() + + return self + end + + + --- Adapt the designation scope according the detected items. + -- @param #DESIGNATE self + -- @return #DESIGNATE + function DESIGNATE:DesignationScope() + + local DetectedItems = self.Detection:GetDetectedItemsByIndex() + + local DetectedItemCount = 0 + + for DesignateIndex, Designating in pairs( self.Designating ) do + local DetectedItem = self.Detection:GetDetectedItemByIndex( DesignateIndex ) + if DetectedItem then + -- Check LOS... + local IsDetected = self.Detection:IsDetectedItemDetected( DetectedItem ) + self:F({IsDetected = IsDetected }) + if IsDetected == false then + self:F("Removing") + -- This Detection is obsolete, remove from the designate scope + self.Designating[DesignateIndex] = nil + self.AttackSet:ForEachGroupAlive( + --- @param Wrapper.Group#GROUP AttackGroup + function( AttackGroup ) + if AttackGroup:IsAlive() == true then + local DetectionText = self.Detection:DetectedItemReportSummary( DetectedItem, AttackGroup ):Text( ", " ) + self.CC:GetPositionable():MessageToGroup( "Targets out of LOS\n" .. DetectionText, 10, AttackGroup, self.DesignateName ) + end + end + ) + else + DetectedItemCount = DetectedItemCount + 1 + end + else + -- This Detection is obsolete, remove from the designate scope + self.Designating[DesignateIndex] = nil + end + end + + if DetectedItemCount < 5 then + for DesignateIndex, DetectedItem in pairs( DetectedItems ) do + local IsDetected = self.Detection:IsDetectedItemDetected( DetectedItem ) + if IsDetected == true then + self:F( { DistanceRecce = DetectedItem.DistanceRecce } ) + if DetectedItem.DistanceRecce <= self.MaximumDistanceDesignations then + if self.Designating[DesignateIndex] == nil then + -- ok, we added one item to the designate scope. + self.AttackSet:ForEachGroupAlive( + function( AttackGroup ) + if self.FlashDetectionMessage[AttackGroup] == true then + local DetectionText = self.Detection:DetectedItemReportSummary( DetectedItem, AttackGroup ):Text( ", " ) + self.CC:GetPositionable():MessageToGroup( "Targets detected at \n" .. DetectionText, 10, AttackGroup, self.DesignateName ) + end + end + ) + self.Designating[DesignateIndex] = "" + + -- When we found an item for designation, we stop the loop. + -- So each iteration over the detected items, a new detected item will be selected to be designated. + -- Until all detected items were found or until there are about 5 designations allocated. + break + end + end + end + end + end + + return self + end + + --- Coordinates the Auto Lase. + -- @param #DESIGNATE self + -- @return #DESIGNATE + function DESIGNATE:CoordinateLase() + + local DetectedItems = self.Detection:GetDetectedItemsByIndex() + + for DesignateIndex, Designating in pairs( self.Designating ) do + local DetectedItem = DetectedItems[DesignateIndex] + if DetectedItem then + if self.AutoLase then + self:LaseOn( DesignateIndex, self.LaseDuration ) + end + end + end + + return self + end + + + --- Sends the status to the Attack Groups. + -- @param #DESIGNATE self + -- @param Wrapper.Group#GROUP AttackGroup + -- @param #number Duration The time in seconds the report should be visible. + -- @return #DESIGNATE + function DESIGNATE:SendStatus( MenuAttackGroup ) + + self.AttackSet:ForEachGroupAlive( + + --- @param Wrapper.Group#GROUP GroupReport + function( AttackGroup ) + + if self.FlashStatusMenu[AttackGroup] or ( MenuAttackGroup and ( AttackGroup:GetName() == MenuAttackGroup:GetName() ) ) then + + local DetectedReport = REPORT:New( "Targets ready for Designation:" ) + local DetectedItems = self.Detection:GetDetectedItemsByIndex() + + for DesignateIndex, Designating in pairs( self.Designating ) do + local DetectedItem = DetectedItems[DesignateIndex] + if DetectedItem then + local Report = self.Detection:DetectedItemReportSummary( DetectedItem, AttackGroup ):Text( ", " ) + DetectedReport:Add( string.rep( "-", 140 ) ) + DetectedReport:Add( " - " .. Report ) + if string.find( Designating, "L" ) then + DetectedReport:Add( " - " .. "Lasing Targets" ) + end + if string.find( Designating, "S" ) then + DetectedReport:Add( " - " .. "Smoking Targets" ) + end + if string.find( Designating, "I" ) then + DetectedReport:Add( " - " .. "Illuminating Area" ) + end + end + end + + local CC = self.CC:GetPositionable() + + CC:MessageTypeToGroup( DetectedReport:Text( "\n" ), MESSAGE.Type.Information, AttackGroup, self.DesignateName ) + + local DesignationReport = REPORT:New( "Marking Targets:" ) + + self.RecceSet:ForEachGroupAlive( + function( RecceGroup ) + local RecceUnits = RecceGroup:GetUnits() + for UnitID, RecceData in pairs( RecceUnits ) do + local Recce = RecceData -- Wrapper.Unit#UNIT + if Recce:IsLasing() then + DesignationReport:Add( " - " .. Recce:GetMessageText( "Marking " .. Recce:GetSpot().Target:GetTypeName() .. " with laser " .. Recce:GetSpot().LaserCode .. "." ) ) + end + end + end + ) + + CC:MessageTypeToGroup( DesignationReport:Text(), MESSAGE.Type.Information, AttackGroup, self.DesignateName ) + end + end + ) + + return self + end + + + --- Sets the Designate Menu for one attack groups. + -- @param #DESIGNATE self + -- @return #DESIGNATE + function DESIGNATE:SetMenu( AttackGroup ) + + self.MenuDesignate = self.MenuDesignate or {} + + local MissionMenu = nil + + if self.Mission then + --MissionMenu = self.Mission:GetRootMenu( AttackGroup ) + MissionMenu = self.Mission:GetMenu( AttackGroup ) + end + + local MenuTime = timer.getTime() + + self.MenuDesignate[AttackGroup] = MENU_GROUP_DELAYED:New( AttackGroup, self.DesignateName, MissionMenu ):SetTime( MenuTime ):SetTag( self.DesignateName ) + local MenuDesignate = self.MenuDesignate[AttackGroup] -- Core.Menu#MENU_GROUP_DELAYED + + -- Set Menu option for auto lase + + if self.AutoLase then + MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Auto Lase Off", MenuDesignate, self.MenuAutoLase, self, false ):SetTime( MenuTime ):SetTag( self.DesignateName ) + else + MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Auto Lase On", MenuDesignate, self.MenuAutoLase, self, true ):SetTime( MenuTime ):SetTag( self.DesignateName ) + end + + local StatusMenu = MENU_GROUP_DELAYED:New( AttackGroup, "Status", MenuDesignate ):SetTime( MenuTime ):SetTag( self.DesignateName ) + MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Report Status", StatusMenu, self.MenuStatus, self, AttackGroup ):SetTime( MenuTime ):SetTag( self.DesignateName ) + + if self.FlashStatusMenu[AttackGroup] then + MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Flash Status Report Off", StatusMenu, self.MenuFlashStatus, self, AttackGroup, false ):SetTime( MenuTime ):SetTag( self.DesignateName ) + else + MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Flash Status Report On", StatusMenu, self.MenuFlashStatus, self, AttackGroup, true ):SetTime( MenuTime ):SetTag( self.DesignateName ) + end + + local DesignateCount = 0 + + for DesignateIndex, Designating in pairs( self.Designating ) do + + local DetectedItem = self.Detection:GetDetectedItemByIndex( DesignateIndex ) + + if DetectedItem then + + local Coord = self.Detection:GetDetectedItemCoordinate( DetectedItem ) + local ID = self.Detection:GetDetectedItemID( DetectedItem ) + local MenuText = ID --.. ", " .. Coord:ToStringA2G( AttackGroup ) + + -- Use injected MenuName from TaskA2GDispatcher if using same Detection Object + if DetectedItem.DesignateMenuName then + MenuText = string.format( "(%3s) %s", Designating, DetectedItem.DesignateMenuName ) + else + MenuText = string.format( "(%3s) %s", Designating, MenuText ) + end + + local DetectedMenu = MENU_GROUP_DELAYED:New( AttackGroup, MenuText, MenuDesignate ):SetTime( MenuTime ):SetTag( self.DesignateName ) + + -- Build the Lasing menu. + if string.find( Designating, "L", 1, true ) == nil then + MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Search other target", DetectedMenu, self.MenuForget, self, DesignateIndex ):SetTime( MenuTime ):SetTag( self.DesignateName ) + for LaserCode, MenuText in pairs( self.MenuLaserCodes ) do + MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, string.format( MenuText, LaserCode ), DetectedMenu, self.MenuLaseCode, self, DesignateIndex, self.LaseDuration, LaserCode ):SetTime( MenuTime ):SetTag( self.DesignateName ) + end + MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Lase with random laser code(s)", DetectedMenu, self.MenuLaseOn, self, DesignateIndex, self.LaseDuration ):SetTime( MenuTime ):SetTag( self.DesignateName ) + else + MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Stop lasing", DetectedMenu, self.MenuLaseOff, self, DesignateIndex ):SetTime( MenuTime ):SetTag( self.DesignateName ) + end + + -- Build the Smoking menu. + if string.find( Designating, "S", 1, true ) == nil then + MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Smoke red", DetectedMenu, self.MenuSmoke, self, DesignateIndex, SMOKECOLOR.Red ):SetTime( MenuTime ):SetTag( self.DesignateName ) + MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Smoke blue", DetectedMenu, self.MenuSmoke, self, DesignateIndex, SMOKECOLOR.Blue ):SetTime( MenuTime ):SetTag( self.DesignateName ) + MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Smoke green", DetectedMenu, self.MenuSmoke, self, DesignateIndex, SMOKECOLOR.Green ):SetTime( MenuTime ):SetTag( self.DesignateName ) + MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Smoke white", DetectedMenu, self.MenuSmoke, self, DesignateIndex, SMOKECOLOR.White ):SetTime( MenuTime ):SetTag( self.DesignateName ) + MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Smoke orange", DetectedMenu, self.MenuSmoke, self, DesignateIndex, SMOKECOLOR.Orange ):SetTime( MenuTime ):SetTag( self.DesignateName ) + end + + -- Build the Illuminate menu. + if string.find( Designating, "I", 1, true ) == nil then + MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Illuminate", DetectedMenu, self.MenuIlluminate, self, DesignateIndex ):SetTime( MenuTime ):SetTag( self.DesignateName ) + end + end + + DesignateCount = DesignateCount + 1 + if DesignateCount > 10 then + break + end + end + MenuDesignate:Remove( MenuTime, self.DesignateName ) + MenuDesignate:Set() + end + + + --- Sets the Designate Menu for all the attack groups. + -- @param #DESIGNATE self + -- @return #DESIGNATE + function DESIGNATE:SetDesignateMenu() + + self.AttackSet:Flush( self ) + + local Delay = 1 + + self.AttackSet:ForEachGroupAlive( + + --- @param Wrapper.Group#GROUP GroupReport + function( AttackGroup ) + + self:ScheduleOnce( Delay, self.SetMenu, self, AttackGroup ) + Delay = Delay + 1 + end + + ) + + return self + end + + --- + -- @param #DESIGNATE self + function DESIGNATE:MenuStatus( AttackGroup ) + + self:F("Status") + + self:SendStatus( AttackGroup ) + end + + --- + -- @param #DESIGNATE self + function DESIGNATE:MenuFlashStatus( AttackGroup, Flash ) + + self:F("Flash Status") + + self.FlashStatusMenu[AttackGroup] = Flash + self:SetDesignateMenu() + end + + + --- + -- @param #DESIGNATE self + function DESIGNATE:MenuForget( Index ) + + self:F("Forget") + + self.Designating[Index] = "" + self:SetDesignateMenu() + end + + --- + -- @param #DESIGNATE self + function DESIGNATE:MenuAutoLase( AutoLase ) + + self:F("AutoLase") + + self:SetAutoLase( AutoLase, true ) + end + + --- + -- @param #DESIGNATE self + function DESIGNATE:MenuSmoke( Index, Color ) + + self:F("Designate through Smoke") + + if string.find( self.Designating[Index], "S" ) == nil then + self.Designating[Index] = self.Designating[Index] .. "S" + end + self:Smoke( Index, Color ) + self:SetDesignateMenu() + end + + --- + -- @param #DESIGNATE self + function DESIGNATE:MenuIlluminate( Index ) + + self:F("Designate through Illumination") + + if string.find( self.Designating[Index], "I", 1, true ) == nil then + self.Designating[Index] = self.Designating[Index] .. "I" + end + + self:__Illuminate( 1, Index ) + self:SetDesignateMenu() + end + + --- + -- @param #DESIGNATE self + function DESIGNATE:MenuLaseOn( Index, Duration ) + + self:F("Designate through Lase") + + self:__LaseOn( 1, Index, Duration ) + self:SetDesignateMenu() + end + + + --- + -- @param #DESIGNATE self + function DESIGNATE:MenuLaseCode( Index, Duration, LaserCode ) + + self:F( "Designate through Lase using " .. LaserCode ) + + self:__LaseOn( 1, Index, Duration, LaserCode ) + self:SetDesignateMenu() + end + + + --- + -- @param #DESIGNATE self + function DESIGNATE:MenuLaseOff( Index, Duration ) + + self:F("Lasing off") + + self.Designating[Index] = string.gsub( self.Designating[Index], "L", "" ) + self:__LaseOff( 1, Index ) + self:SetDesignateMenu() + end + + --- + -- @param #DESIGNATE self + function DESIGNATE:onafterLaseOn( From, Event, To, Index, Duration, LaserCode ) + + if string.find( self.Designating[Index], "L", 1, true ) == nil then + self.Designating[Index] = self.Designating[Index] .. "L" + self.LaseStart = timer.getTime() + self.LaseDuration = Duration + self:Lasing( Index, Duration, LaserCode ) + end + end + + + --- + -- @param #DESIGNATE self + -- @return #DESIGNATE + function DESIGNATE:onafterLasing( From, Event, To, Index, Duration, LaserCodeRequested ) + + + local DetectedItem = self.Detection:GetDetectedItemByIndex( Index ) + local TargetSetUnit = self.Detection:GetDetectedItemSet( DetectedItem ) + + local MarkingCount = 0 + local MarkedTypes = {} + local ReportTypes = REPORT:New() + local ReportLaserCodes = REPORT:New() + + TargetSetUnit:Flush( self ) + + --self:F( { Recces = self.Recces } ) + for TargetUnit, RecceData in pairs( self.Recces ) do + local Recce = RecceData -- Wrapper.Unit#UNIT + self:F( { TargetUnit = TargetUnit, Recce = Recce:GetName() } ) + if not Recce:IsLasing() then + local LaserCode = Recce:GetLaserCode() -- (Not deleted when stopping with lasing). + self:F( { ClearingLaserCode = LaserCode } ) + self.LaserCodesUsed[LaserCode] = nil + self.Recces[TargetUnit] = nil + end + end + + -- If a specific lasercode is requested, we disable one active lase! + if LaserCodeRequested then + for TargetUnit, RecceData in pairs( self.Recces ) do -- We break after the first has been processed. + local Recce = RecceData -- Wrapper.Unit#UNIT + self:F( { TargetUnit = TargetUnit, Recce = Recce:GetName() } ) + if Recce:IsLasing() then + -- When a Recce is lasing, we switch the lasing off, and clear the references to the lasing in the DESIGNATE class. + Recce:LaseOff() -- Switch off the lasing. + local LaserCode = Recce:GetLaserCode() -- (Not deleted when stopping with lasing). + self:F( { ClearingLaserCode = LaserCode } ) + self.LaserCodesUsed[LaserCode] = nil + self.Recces[TargetUnit] = nil + break + end + end + end + + if self.AutoLase or ( not self.AutoLase and ( self.LaseStart + Duration >= timer.getTime() ) ) then + + TargetSetUnit:ForEachUnitPerThreatLevel( 10, 0, + --- @param Wrapper.Unit#UNIT SmokeUnit + function( TargetUnit ) + + self:F( { TargetUnit = TargetUnit:GetName() } ) + + if MarkingCount < self.MaximumMarkings then + + if TargetUnit:IsAlive() then + + local Recce = self.Recces[TargetUnit] + + if not Recce then + + self:F( "Lasing..." ) + self.RecceSet:Flush( self) + + for RecceGroupID, RecceGroup in pairs( self.RecceSet:GetSet() ) do + for UnitID, UnitData in pairs( RecceGroup:GetUnits() or {} ) do + + local RecceUnit = UnitData -- Wrapper.Unit#UNIT + local RecceUnitDesc = RecceUnit:GetDesc() + --self:F( { RecceUnit = RecceUnit:GetName(), RecceDescription = RecceUnitDesc } ) + + if RecceUnit:IsLasing() == false then + --self:F( { IsDetected = RecceUnit:IsDetected( TargetUnit ), IsLOS = RecceUnit:IsLOS( TargetUnit ) } ) + + if RecceUnit:IsDetected( TargetUnit ) and RecceUnit:IsLOS( TargetUnit ) then + + local LaserCodeIndex = math.random( 1, #self.LaserCodes ) + local LaserCode = self.LaserCodes[LaserCodeIndex] + --self:F( { LaserCode = LaserCode, LaserCodeUsed = self.LaserCodesUsed[LaserCode] } ) + + if LaserCodeRequested and LaserCodeRequested ~= LaserCode then + LaserCode = LaserCodeRequested + LaserCodeRequested = nil + end + + if not self.LaserCodesUsed[LaserCode] then + + self.LaserCodesUsed[LaserCode] = LaserCodeIndex + local Spot = RecceUnit:LaseUnit( TargetUnit, LaserCode, Duration ) + local AttackSet = self.AttackSet + local DesignateName = self.DesignateName + + function Spot:OnAfterDestroyed( From, Event, To ) + self.Recce:MessageToSetGroup( "Target " .. TargetUnit:GetTypeName() .. " destroyed. " .. TargetSetUnit:Count() .. " targets left.", + 5, AttackSet, self.DesignateName ) + end + + self.Recces[TargetUnit] = RecceUnit + -- OK. We have assigned for the Recce a TargetUnit. We can exit the function. + MarkingCount = MarkingCount + 1 + local TargetUnitType = TargetUnit:GetTypeName() + --RecceUnit:MessageToSetGroup( "Marking " .. TargetUnit:GetTypeName() .. " with laser " .. RecceUnit:GetSpot().LaserCode .. " for " .. Duration .. "s.", + -- 5, self.AttackSet, DesignateName ) + if not MarkedTypes[TargetUnitType] then + MarkedTypes[TargetUnitType] = true + ReportTypes:Add(TargetUnitType) + end + ReportLaserCodes:Add(RecceUnit.LaserCode) + return + end + else + --RecceUnit:MessageToSetGroup( "Can't mark " .. TargetUnit:GetTypeName(), 5, self.AttackSet ) + end + else + -- The Recce is lasing, but the Target is not detected or within LOS. So stop lasing and send a report. + + if not RecceUnit:IsDetected( TargetUnit ) or not RecceUnit:IsLOS( TargetUnit ) then + + local Recce = self.Recces[TargetUnit] -- Wrapper.Unit#UNIT + + if Recce then + Recce:LaseOff() + Recce:MessageToSetGroup( "Target " .. TargetUnit:GetTypeName() "out of LOS. Cancelling lase!", 5, self.AttackSet, self.DesignateName ) + end + else + --MarkingCount = MarkingCount + 1 + local TargetUnitType = TargetUnit:GetTypeName() + if not MarkedTypes[TargetUnitType] then + MarkedTypes[TargetUnitType] = true + ReportTypes:Add(TargetUnitType) + end + ReportLaserCodes:Add(RecceUnit.LaserCode) + end + end + end + end + else + MarkingCount = MarkingCount + 1 + local TargetUnitType = TargetUnit:GetTypeName() + if not MarkedTypes[TargetUnitType] then + MarkedTypes[TargetUnitType] = true + ReportTypes:Add(TargetUnitType) + end + ReportLaserCodes:Add(Recce.LaserCode) + --Recce:MessageToSetGroup( self.DesignateName .. ": Marking " .. TargetUnit:GetTypeName() .. " with laser " .. Recce.LaserCode .. ".", 5, self.AttackSet ) + end + end + end + end + ) + + local MarkedTypesText = ReportTypes:Text(', ') + local MarkedLaserCodesText = ReportLaserCodes:Text(', ') + self.CC:GetPositionable():MessageToSetGroup( "Marking " .. MarkingCount .. " x " .. MarkedTypesText .. ", code " .. MarkedLaserCodesText .. ".", 5, self.AttackSet, self.DesignateName ) + + self:__Lasing( -self.LaseDuration, Index, Duration, LaserCodeRequested ) + + self:SetDesignateMenu() + + else + self:LaseOff( Index ) + end + + end + + --- + -- @param #DESIGNATE self + -- @return #DESIGNATE + function DESIGNATE:onafterLaseOff( From, Event, To, Index ) + + local CC = self.CC:GetPositionable() + + if CC then + CC:MessageToSetGroup( "Stopped lasing.", 5, self.AttackSet, self.DesignateName ) + end + + local DetectedItem = self.Detection:GetDetectedItemByIndex( Index ) + local TargetSetUnit = self.Detection:GetDetectedItemSet( DetectedItem ) + + local Recces = self.Recces + + for TargetID, RecceData in pairs( Recces ) do + local Recce = RecceData -- Wrapper.Unit#UNIT + Recce:MessageToSetGroup( "Stopped lasing " .. Recce:GetSpot().Target:GetTypeName() .. ".", 5, self.AttackSet, self.DesignateName ) + Recce:LaseOff() + end + + Recces = nil + self.Recces = {} + self.LaserCodesUsed = {} + + self.Designating[Index] = string.gsub( self.Designating[Index], "L", "" ) + self:SetDesignateMenu() + end + + + --- + -- @param #DESIGNATE self + -- @return #DESIGNATE + function DESIGNATE:onafterSmoke( From, Event, To, Index, Color ) + + local DetectedItem = self.Detection:GetDetectedItemByIndex( Index ) + local TargetSetUnit = self.Detection:GetDetectedItemSet( DetectedItem ) + local TargetSetUnitCount = TargetSetUnit:Count() + + local MarkedCount = 0 + + TargetSetUnit:ForEachUnitPerThreatLevel( 10, 0, + --- @param Wrapper.Unit#UNIT SmokeUnit + function( SmokeUnit ) + + if MarkedCount < self.MaximumMarkings then + + MarkedCount = MarkedCount + 1 + + self:F( "Smoking ..." ) + + local RecceGroup = self.RecceSet:FindNearestGroupFromPointVec2(SmokeUnit:GetPointVec2()) + local RecceUnit = RecceGroup:GetUnit( 1 ) -- Wrapper.Unit#UNIT + + if RecceUnit then + + RecceUnit:MessageToSetGroup( "Smoking " .. SmokeUnit:GetTypeName() .. ".", 5, self.AttackSet, self.DesignateName ) + + if SmokeUnit:IsAlive() then + SmokeUnit:Smoke( Color, 50, 2 ) + end + + self.MarkScheduler:Schedule( self, + function() + self:DoneSmoking( Index ) + end, {}, math.random( 180, 240 ) + ) + end + end + end + ) + + + end + + --- Illuminating + -- @param #DESIGNATE self + -- @return #DESIGNATE + function DESIGNATE:onafterIlluminate( From, Event, To, Index ) + + local DetectedItem = self.Detection:GetDetectedItemByIndex( Index ) + local TargetSetUnit = self.Detection:GetDetectedItemSet( DetectedItem ) + local TargetUnit = TargetSetUnit:GetFirst() + + if TargetUnit then + local RecceGroup = self.RecceSet:FindNearestGroupFromPointVec2(TargetUnit:GetPointVec2()) + local RecceUnit = RecceGroup:GetUnit( 1 ) + if RecceUnit then + RecceUnit:MessageToSetGroup( "Illuminating " .. TargetUnit:GetTypeName() .. ".", 5, self.AttackSet, self.DesignateName ) + if TargetUnit:IsAlive() then + -- Fire 2 illumination bombs at random locations. + TargetUnit:GetPointVec3():AddY(math.random( 350, 500) ):AddX(math.random(-50,50) ):AddZ(math.random(-50,50) ):IlluminationBomb() + TargetUnit:GetPointVec3():AddY(math.random( 350, 500) ):AddX(math.random(-50,50) ):AddZ(math.random(-50,50) ):IlluminationBomb() + end + self.MarkScheduler:Schedule( self, + function() + self:DoneIlluminating( Index ) + end, {}, math.random( 60, 90 ) + ) + end + end + end + + --- DoneSmoking + -- @param #DESIGNATE self + -- @return #DESIGNATE + function DESIGNATE:onafterDoneSmoking( From, Event, To, Index ) + + self.Designating[Index] = string.gsub( self.Designating[Index], "S", "" ) + self:SetDesignateMenu() + end + + --- DoneIlluminating + -- @param #DESIGNATE self + -- @return #DESIGNATE + function DESIGNATE:onafterDoneIlluminating( From, Event, To, Index ) + + self.Designating[Index] = string.gsub( self.Designating[Index], "I", "" ) + self:SetDesignateMenu() + end + +end + + +--- **Functional** - Create random airtraffic in your missions. +-- +-- === +-- +-- The aim of the RAT class is to fill the empty DCS world with randomized air traffic and bring more life to your airports. +-- In particular, it is designed to spawn AI air units at random airports. These units will be assigned a random flight path to another random airport on the map. +-- Even the mission designer will not know where aircraft will be spawned and which route they follow. +-- +-- ## Features: +-- +-- * Very simple interface. Just one unit and two lines of Lua code needed to fill your map. +-- * High degree of randomization. Aircraft will spawn at random airports, have random routes and random destinations. +-- * Specific departure and/or destination airports can be chosen. +-- * Departure and destination airports can be restricted by coalition. +-- * Planes and helicopters supported. Helicopters can also be send to FARPs and ships. +-- * Units can also be spawned in air within pre-defined zones of the map. +-- * Aircraft will be removed when they arrive at their destination (or get stuck on the ground). +-- * When a unit is removed a new unit with a different flight plan is respawned. +-- * Aircraft can report their status during the route. +-- * All of the above can be customized by the user if necessary. +-- * All current (Caucasus, Nevada, Normandy, Persian Gulf) and future maps are supported. +-- +-- The RAT class creates an entry in the F10 radio menu which allows to: +-- +-- * Create new groups on-the-fly, i.e. at run time within the mission, +-- * Destroy specific groups (e.g. if they get stuck or damaged and block a runway), +-- * Request the status of all RAT aircraft or individual groups, +-- * Place markers at waypoints on the F10 map for each group. +-- +-- Note that by its very nature, this class is suited best for civil or transport aircraft. However, it also works perfectly fine for military aircraft of any kind. +-- +-- More of the documentation include some simple examples can be found further down this page. +-- +-- === +-- +-- ## Missions: +-- +-- ### [RAT - Random Air Traffic](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/RAT%20-%20Random%20Air%20Traffic) +-- +-- === +-- +-- # YouTube Channel +-- +-- ### [MOOSE YouTube Channel](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg) +-- ### [MOOSE - RAT - Random Air Traffic](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl0u4Zxywtg-mx_ov4vi68CO) +-- +-- === +-- +-- ### Author: **[funkyfranky](https://forums.eagle.ru/member.php?u=115026)** +-- +-- ### Contributions: [FlightControl](https://forums.eagle.ru/member.php?u=89536) +-- +-- === +-- @module Functional.Rat +-- @image RAT.JPG + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- RAT class +-- @type RAT +-- @field #string ClassName Name of the Class. +-- @field #boolean Debug Turn debug messages on or off. +-- @field Wrapper.Group#GROUP templategroup Group serving as template for the RAT aircraft. +-- @field #string alias Alias for spawned group. +-- @field #boolean spawninitialized If RAT:Spawn() was already called this RAT object is set to true to prevent users to call it again. +-- @field #number spawndelay Delay time in seconds before first spawning happens. +-- @field #number spawninterval Interval between spawning units/groups. Note that we add a randomization of 50%. +-- @field #number coalition Coalition of spawn group template. +-- @field #number country Country of spawn group template. +-- @field #string category Category of aircarft: "plane" or "heli". +-- @field #number groupsize Number of aircraft in group. +-- @field #string friendly Possible departure/destination airport: all=blue+red+neutral, same=spawn+neutral, spawnonly=spawn, blue=blue+neutral, blueonly=blue, red=red+neutral, redonly=red. +-- @field #table ctable Table with the valid coalitons from choice self.friendly. +-- @field #table aircraft Table which holds the basic aircraft properties (speed, range, ...). +-- @field #number Vcruisemax Max cruise speed in m/s (250 m/s = 900 km/h = 486 kt) set by user. +-- @field #number Vclimb Default climb rate in ft/min. +-- @field #number AlphaDescent Default angle of descenti in degrees. A value of 3.6 follows the 3:1 rule of 3 miles of travel and 1000 ft descent. +-- @field #string roe ROE of spawned groups, default is weapon hold (this is a peaceful class for civil aircraft or ferry missions). Possible: "hold", "return", "free". +-- @field #string rot ROT of spawned groups, default is no reaction. Possible: "noreaction", "passive", "evade". +-- @field #number takeoff Takeoff type. 0=coldorhot. +-- @field #number landing Landing type. Determines if we actually land at an airport or treat it as zone. +-- @field #number mindist Min distance from departure to destination in meters. Default 5 km. +-- @field #number maxdist Max distance from departure to destination in meters. Default 5000 km. +-- @field #table airports_map All airports available on current map (Caucasus, Nevada, Normandy, ...). +-- @field #table airports All airports of friedly coalitions. +-- @field #boolean random_departure By default a random friendly airport is chosen as departure. +-- @field #boolean random_destination By default a random friendly airport is chosen as destination. +-- @field #table departure_ports Array containing the names of the destination airports or zones. +-- @field #table destination_ports Array containing the names of the destination airports or zones. +-- @field #number Ndestination_Airports Number of destination airports set via SetDestination(). +-- @field #number Ndestination_Zones Number of destination zones set via SetDestination(). +-- @field #number Ndeparture_Airports Number of departure airports set via SetDeparture(). +-- @field #number Ndeparture_Zones Number of departure zones set via SetDeparture. +-- @field #table excluded_ports Array containing the names of explicitly excluded airports. +-- @field #boolean destinationzone Destination is a zone and not an airport. +-- @field #table return_zones Array containing the names of the return zones. +-- @field #boolean returnzone Zone where aircraft will fly to before returning to their departure airport. +-- @field Core.Zone#ZONE departure_Azone Zone containing the departure airports. +-- @field Core.Zone#ZONE destination_Azone Zone containing the destination airports. +-- @field #boolean addfriendlydepartures Add all friendly airports to departures. +-- @field #boolean addfriendlydestinations Add all friendly airports to destinations. +-- @field #table ratcraft Array with the spawned RAT aircraft. +-- @field #number Tinactive Time in seconds after which inactive units will be destroyed. Default is 300 seconds. +-- @field #boolean reportstatus Aircraft report status. +-- @field #number statusinterval Intervall between status checks (and reports if enabled). +-- @field #boolean placemarkers Place markers of waypoints on F10 map. +-- @field #number FLcruise Cruise altitude of aircraft. Default FL200 for planes and F005 for helos. +-- @field #number FLuser Flight level set by users explicitly. +-- @field #number FLminuser Minimum flight level set by user. +-- @field #number FLmaxuser Maximum flight level set by user. +-- @field #boolean commute Aircraft commute between departure and destination, i.e. when respawned the departure airport becomes the new destiation. +-- @field #boolean starshape If true, aircraft travel A-->B-->A-->C-->A-->D... for commute. +-- @field #string homebase Home base for commute and return zone. Aircraft will always return to this base but otherwise travel in a star shaped way. +-- @field #boolean continuejourney Aircraft will continue their journey, i.e. get respawned at their destination with a new random destination. +-- @field #number ngroups Number of groups to be spawned in total. +-- @field #number alive Number of groups which are alive. +-- @field #boolean f10menu If true, add an F10 radiomenu for RAT. Default is false. +-- @field #table Menu F10 menu items for this RAT object. +-- @field #string SubMenuName Submenu name for RAT object. +-- @field #boolean respawn_at_landing Respawn aircraft the moment they land rather than at engine shutdown. +-- @field #boolean norespawn Aircraft will not be respawned after they have finished their route. +-- @field #boolean respawn_after_takeoff Aircraft will be respawned directly after take-off. +-- @field #boolean respawn_after_crash Aircraft will be respawned after a crash, e.g. when they get shot down. +-- @field #boolean respawn_inair Aircraft are allowed to spawned in air if they cannot be respawned on ground because there is not free parking spot. Default is true. +-- @field #number respawn_delay Delay in seconds until a repawn happens. +-- @field #table markerids Array with marker IDs. +-- @field #table waypointdescriptions Table with strings for waypoint descriptions of markers. +-- @field #table waypointstatus Table with strings of waypoint status. +-- @field #string livery Livery of the aircraft set by user. +-- @field #string skill Skill of AI. +-- @field #boolean ATCswitch Enable/disable ATC if set to true/false. +-- @field #boolean radio If true/false disables radio messages from the RAT groups. +-- @field #number frequency Radio frequency used by the RAT groups. +-- @field #string modulation Ratio modulation. Either "FM" or "AM". +-- @field #boolean uncontrolled If true aircraft are spawned in uncontrolled state and will only sit on their parking spots. They can later be activated. +-- @field #boolean invisible If true aircraft are set to invisible for other AI forces. +-- @field #boolean immortal If true, aircraft are spawned as immortal. +-- @field #boolean activate_uncontrolled If true, uncontrolled are activated randomly after certain time intervals. +-- @field #number activate_delay Delay in seconds before first uncontrolled group is activated. Default is 5 seconds. +-- @field #number activate_delta Time interval in seconds between activation of uncontrolled groups. Default is 5 seconds. +-- @field #number activate_frand Randomization factor of time interval (activate_delta) between activating uncontrolled groups. Default is 0. +-- @field #number activate_max Maximum number of uncontrolled aircraft, which will be activated at the same time. Default is 1. +-- @field #string onboardnum Sets the onboard number prefix. Same as setting "TAIL #" in the mission editor. +-- @field #number onboardnum0 (Optional) Starting value of the automatically appended numbering of aircraft within a flight. Default is 1. +-- @field #boolean checkonrunway Aircraft are checked if they were accidentally spawned on the runway. Default is true. +-- @field #number onrunwayradius Distance (in meters) from a runway spawn point until a unit is considered to have accidentally been spawned on a runway. Default is 75 m. +-- @field #number onrunwaymaxretry Number of respawn retries (on ground) at other airports if a group gets accidentally spawned on the runway. Default is 3. +-- @field #boolean checkontop Aircraft are checked if they were accidentally spawned on top of another unit. Default is true. +-- @field #number ontopradius Radius in meters until which a unit is considered to be on top of another. Default is 2 m. +-- @field Wrapper.Airbase#AIRBASE.TerminalType termtype Type of terminal to be used when spawning at an airbase. +-- @field #number parkingscanradius Radius in meters until which parking spots are scanned for obstacles like other units, statics or scenery. +-- @field #boolean parkingscanscenery If true, area around parking spots is scanned for scenery objects. Default is false. +-- @field #boolean parkingverysafe If true, parking spots are considered as non-free until a possible aircraft has left and taken off. Default false. +-- @field #boolean despawnair If true, aircraft are despawned when they reach their destination zone. Default. +-- @field #boolean eplrs If true, turn on EPLSR datalink for the RAT group. +-- @extends Core.Spawn#SPAWN + +--- Implements an easy to use way to randomly fill your map with AI aircraft. +-- +-- ## Airport Selection +-- +-- ![Process](..\Presentations\RAT\RAT_Airport_Selection.png) +-- +-- ### Default settings: +-- +-- * By default, aircraft are spawned at airports of their own coalition (blue or red) or neutral airports. +-- * Destination airports are by default also of neutral or of the same coalition as the template group of the spawned aircraft. +-- * Possible destinations are restricted by their distance to the departure airport. The maximal distance depends on the max range of spawned aircraft type and its initial fuel amount. +-- +-- ### The default behavior can be changed: +-- +-- * A specific departure and/or destination airport can be chosen. +-- * Valid coalitions can be set, e.g. only red, blue or neutral, all three "colours". +-- * It is possible to start in air within a zone defined in the mission editor or within a zone above an airport of the map. +-- +-- ## Flight Plan +-- +-- ![Process](..\Presentations\RAT\RAT_Flight_Plan.png) +-- +-- * A general flight plan has five main airborne segments: Climb, cruise, descent, holding and final approach. +-- * Events monitored during the flight are: birth, engine-start, take-off, landing and engine-shutdown. +-- * The default flight level (FL) is set to ~FL200, i.e. 20000 feet ASL but randomized for each aircraft. +-- Service ceiling of aircraft type is into account for max FL as well as the distance between departure and destination. +-- * Maximal distance between destination and departure airports depends on range and initial fuel of aircraft. +-- * Climb rate is set to a moderate value of ~1500 ft/min. +-- * The standard descent rate follows the 3:1 rule, i.e. 1000 ft decent per 3 miles of travel. Hence, angle of descent is ~3.6 degrees. +-- * A holding point is randomly selected at a distance between 5 and 10 km away from destination airport. +-- * The altitude of theholding point is ~1200 m AGL. Holding patterns might or might not happen with variable duration. +-- * If an aircraft is spawned in air, the procedure omitts taxi and take-off and starts with the climb/cruising part. +-- * All values are randomized for each spawned aircraft. +-- +-- ## Mission Editor Setup +-- +-- ![Process](..\Presentations\RAT\RAT_Mission_Setup.png) +-- +-- Basic mission setup is very simple and essentially a three step process: +-- +-- * Place your aircraft **anywhere** on the map. It really does not matter where you put it. +-- * Give the group a good name. In the example above the group is named "RAT_YAK". +-- * Activate the "LATE ACTIVATION" tick box. Note that this aircraft will not be spawned itself but serves a template for each RAT aircraft spawned when the mission starts. +-- +-- Voilà, your already done! +-- +-- Optionally, you can set a specific livery for the aircraft or give it some weapons. +-- However, the aircraft will by default not engage any enemies. Think of them as beeing on a peaceful or ferry mission. +-- +-- ## Basic Lua Script +-- +-- ![Process](..\Presentations\RAT\RAT_Basic_Lua_Script.png) +-- +-- The basic Lua script for one template group consits of two simple lines as shown in the picture above. +-- +-- * **Line 2** creates a new RAT object "yak". The only required parameter for the constructor @{#RAT.New}() is the name of the group as defined in the mission editor. In this example it is "RAT_YAK". +-- * **Line 5** trigger the command to spawn the aircraft. The (optional) parameter for the @{#RAT.Spawn}() function is the number of aircraft to be spawned of this object. +-- By default each of these aircraft gets a random departure airport anywhere on the map and a random destination airport, which lies within range of the of the selected aircraft type. +-- +-- In this simple example aircraft are respawned with a completely new flightplan when they have reached their destination airport. +-- The "old" aircraft is despawned (destroyed) after it has shut-down its engines and a new aircraft of the same type is spawned at a random departure airport anywhere on the map. +-- Hence, the default flight plan for a RAT aircraft will be: Fly from airport A to B, get respawned at C and fly to D, get respawned at E and fly to F, ... +-- This ensures that you always have a constant number of AI aircraft on your map. +-- +-- ## Parking Problems +-- +-- One big issue in DCS is that not all aircraft can be spawned on every airport or airbase. In particular, bigger aircraft might not have a valid parking spot at smaller airports and +-- airstripes. This can lead to multiple problems in DCS. +-- +-- * Landing: When an aircraft tries to land at an airport where it does not have a valid parking spot, it is immidiately despawned the moment its wheels touch the runway, i.e. +-- when a landing event is triggered. This leads to the loss of the RAT aircraft. On possible way to circumvent the this problem is to let another RAT aircraft spawn at landing +-- and not when it shuts down its engines. See the @{RAT.RespawnAfterLanding}() function. +-- * Spawning: When a big aircraft is dynamically spawned on a small airbase a few things can go wrong. For example, it could be spawned at a parking spot with a shelter. +-- Or it could be damaged by a scenery object when it is taxiing out to the runway, or it could overlap with other aircraft on parking spots near by. +-- +-- You can check yourself if an aircraft has a valid parking spot at an airbase by dragging its group on the airport in the mission editor and set it to start from ramp. +-- If it stays at the airport, it has a valid parking spot, if it jumps to another airport, it does not have a valid parking spot on that airbase. +-- +-- ### Setting the Terminal Type +-- Each parking spot has a specific type depending on its size or if a helicopter spot or a shelter etc. The classification is not perfect but it is the best we have. +-- If you encounter problems described above, you can request a specific terminal type for the RAT aircraft. This can be done by the @{#RAT.SetTerminalType}(*terminaltype*) +-- function. The parameter *terminaltype* can be set as follows +-- +-- * AIRBASE.TerminalType.HelicopterOnly: Special spots for Helicopers. +-- * AIRBASE.TerminalType.Shelter: Hardened Air Shelter. Currently only on Caucaus map. +-- * AIRBASE.TerminalType.OpenMed: Open/Shelter air airplane only. +-- * AIRBASE.TerminalType.OpenBig: Open air spawn points. Generally larger but does not guarantee large aircraft are capable of spawning there. +-- * AIRBASE.TerminalType.OpenMedOrBig: Combines OpenMed and OpenBig spots. +-- * AIRBASE.TerminalType.HelicopterUsable: Combines HelicopterOnly, OpenMed and OpenBig. +-- * AIRBASE.TerminalType.FighterAircraft: Combines Shelter, OpenMed and OpenBig spots. So effectively all spots usable by fixed wing aircraft. +-- +-- So for example +-- c17=RAT:New("C-17") +-- c17:SetTerminalType(AIRBASE.TerminalType.OpenBig) +-- c17:Spawn(5) +-- +-- This would randomly spawn five C-17s but only on airports which have big open air parking spots. Note that also only destination airports are allowed +-- which do have this type of parking spot. This should ensure that the aircraft is able to land at the destination without beeing despawned immidiately. +-- +-- Also, the aircraft are spawned only on the requested parking spot types and not on any other type. If no parking spot of this type is availabe at the +-- moment of spawning, the group is automatically spawned in air above the selected airport. +-- +-- ## Examples +-- +-- Here are a few examples, how you can modify the default settings of RAT class objects. +-- +-- ### Specify Departure and Destinations +-- +-- ![Process](..\Presentations\RAT\RAT_Examples_Specify_Departure_and_Destination.png) +-- +-- In the picture above you find a few possibilities how to modify the default behaviour to spawn at random airports and fly to random destinations. +-- +-- In particular, you can specify fixed departure and/or destination airports. This is done via the @{#RAT.SetDeparture}() or @{#RAT.SetDestination}() functions, respectively. +-- +-- * If you only fix a specific departure airport via @{#RAT.SetDeparture}() all aircraft will be spawned at that airport and get random destination airports. +-- * If you only fix the destination airport via @{#RAT.SetDestination}(), aircraft a spawned at random departure airports but will all fly to the destination airport. +-- * If you fix departure and destination airports, aircraft will only travel from between those airports. +-- When the aircraft reaches its destination, it will be respawned at its departure and fly again to its destination. +-- +-- There is also an option that allows aircraft to "continue their journey" from their destination. This is achieved by the @{#RAT.ContinueJourney}() function. +-- In that case, when the aircraft arrives at its first destination it will be respawned at that very airport and get a new random destination. +-- So the flight plan in this case would be: Fly from airport A to B, then from B to C, then from C to D, ... +-- +-- It is also possible to make aircraft "commute" between two airports, i.e. flying from airport A to B and then back from B to A, etc. +-- This can be done by the @{#RAT.Commute}() function. Note that if no departure or destination airports are specified, the first departure and destination are chosen randomly. +-- Then the aircraft will fly back and forth between those two airports indefinetly. +-- +-- +-- ### Spawn in Air +-- +-- ![Process](..\Presentations\RAT\RAT_Examples_Spawn_in_Air.png) +-- +-- Aircraft can also be spawned in air rather than at airports on the ground. This is done by setting @{#RAT.SetTakeoff}() to "air". +-- +-- By default, aircraft are spawned randomly above airports of the map. +-- +-- The @{#RAT.SetDeparture}() option can be used to specify zones, which have been defined in the mission editor as departure zones. +-- Aircraft will then be spawned at a random point within the zone or zones. +-- +-- Note that @{#RAT.SetDeparture}() also accepts airport names. For an air takeoff these are treated like zones with a radius of XX kilometers. +-- Again, aircraft are spawned at random points within these zones around the airport. +-- +-- ### Misc Options +-- +-- ![Process](..\Presentations\RAT\RAT_Examples_Misc.png) +-- +-- The default "takeoff" type of RAT aircraft is that they are spawned with hot or cold engines. +-- The choice is random, so 50% of aircraft will be spawned with hot engines while the other 50% will be spawned with cold engines. +-- This setting can be changed using the @{#RAT.SetTakeoff}() function. The possible parameters for starting on ground are: +-- +-- * @{#RAT.SetTakeoff}("cold"), which means that all aircraft are spawned with their engines off, +-- * @{#RAT.SetTakeoff}("hot"), which means that all aircraft are spawned with their engines on, +-- * @{#RAT.SetTakeoff}("runway"), which means that all aircraft are spawned already at the runway ready to takeoff. +-- Note that in this case the default spawn intervall is set to 180 seconds in order to avoid aircraft jamms on the runway. Generally, this takeoff at runways should be used with care and problems are to be expected. +-- +-- +-- The options @{#RAT.SetMinDistance}() and @{#RAT.SetMaxDistance}() can be used to restrict the range from departure to destination. For example +-- +-- * @{#RAT.SetMinDistance}(100) will cause only random destination airports to be selected which are **at least** 100 km away from the departure airport. +-- * @{#RAT.SetMaxDistance}(150) will allow only destination airports which are **less than** 150 km away from the departure airport. +-- +-- ![Process](..\Presentations\RAT\RAT_Gaussian.png) +-- +-- By default planes get a cruise altitude of ~20,000 ft ASL. The actual altitude is sampled from a Gaussian distribution. The picture shows this distribution +-- if one would spawn 1000 planes. As can be seen most planes get a cruising alt of around FL200. Other values are possible but less likely the further away +-- one gets from the expectation value. +-- +-- The expectation value, i.e. the altitude most aircraft get, can be set with the function @{#RAT.SetFLcruise}(). +-- It is possible to restrict the minimum cruise altitude by @{#RAT.SetFLmin}() and the maximum cruise altitude by @{#RAT.SetFLmax}() +-- +-- The cruise altitude can also be given in meters ASL by the functions @{#RAT.SetCruiseAltitude}(), @{#RAT.SetMinCruiseAltitude}() and @{#RAT.SetMaxCruiseAltitude}(). +-- +-- For example: +-- +-- * @{#RAT.SetFLcruise}(300) will cause most planes fly around FL300. +-- * @{#RAT.SetFLmin}(100) restricts the cruising alt such that no plane will fly below FL100. Note that this automatically changes the minimum distance from departure to destination. +-- That means that only destinations are possible for which the aircraft has had enought time to reach that flight level and descent again. +-- * @{#RAT.SetFLmax}(200) will restrict the cruise alt to maximum FL200, i.e. no aircraft will travel above this height. +-- +-- +-- @field #RAT +RAT={ + ClassName = "RAT", -- Name of class: RAT = Random Air Traffic. + Debug=false, -- Turn debug messages on or off. + templategroup=nil, -- Template group for the RAT aircraft. + alias=nil, -- Alias for spawned group. + spawninitialized=false, -- If RAT:Spawn() was already called this is set to true to prevent users to call it again. + spawndelay=5, -- Delay time in seconds before first spawning happens. + spawninterval=5, -- Interval between spawning units/groups. Note that we add a randomization of 50%. + coalition = nil, -- Coalition of spawn group template. + country = nil, -- Country of the group template. + category = nil, -- Category of aircarft: "plane" or "heli". + groupsize=nil, -- Number of aircraft in the group. + friendly = "same", -- Possible departure/destination airport: same=spawn+neutral, spawnonly=spawn, blue=blue+neutral, blueonly=blue, red=red+neutral, redonly=red, neutral. + ctable = {}, -- Table with the valid coalitons from choice self.friendly. + aircraft = {}, -- Table which holds the basic aircraft properties (speed, range, ...). + Vcruisemax=nil, -- Max cruise speed in set by user. + Vclimb=1500, -- Default climb rate in ft/min. + AlphaDescent=3.6, -- Default angle of descenti in degrees. A value of 3.6 follows the 3:1 rule of 3 miles of travel and 1000 ft descent. + roe = "hold", -- ROE of spawned groups, default is weapon hold (this is a peaceful class for civil aircraft or ferry missions). Possible: "hold", "return", "free". + rot = "noreaction", -- ROT of spawned groups, default is no reaction. Possible: "noreaction", "passive", "evade". + takeoff = 0, -- Takeoff type. 0=coldorhot. + landing = 9, -- Landing type. 9=landing. + mindist = 5000, -- Min distance from departure to destination in meters. Default 5 km. + maxdist = 5000000, -- Max distance from departure to destination in meters. Default 5000 km. + airports_map={}, -- All airports available on current map (Caucasus, Nevada, Normandy, ...). + airports={}, -- All airports of friedly coalitions. + random_departure=true, -- By default a random friendly airport is chosen as departure. + random_destination=true, -- By default a random friendly airport is chosen as destination. + departure_ports={}, -- Array containing the names of the departure airports or zones. + destination_ports={}, -- Array containing the names of the destination airports or zones. + Ndestination_Airports=0, -- Number of destination airports set via SetDestination(). + Ndestination_Zones=0, -- Number of destination zones set via SetDestination(). + Ndeparture_Airports=0, -- Number of departure airports set via SetDeparture(). + Ndeparture_Zones=0, -- Number of departure zones set via SetDeparture. + destinationzone=false, -- Destination is a zone and not an airport. + return_zones={}, -- Array containing the names of return zones. + returnzone=false, -- Aircraft will fly to a zone and back. + excluded_ports={}, -- Array containing the names of explicitly excluded airports. + departure_Azone=nil, -- Zone containing the departure airports. + destination_Azone=nil, -- Zone containing the destination airports. + addfriendlydepartures=false, -- Add all friendly airports to departures. + addfriendlydestinations=false, -- Add all friendly airports to destinations. + ratcraft={}, -- Array with the spawned RAT aircraft. + Tinactive=600, -- Time in seconds after which inactive units will be destroyed. Default is 600 seconds. + reportstatus=false, -- Aircraft report status. + statusinterval=30, -- Intervall between status checks (and reports if enabled). + placemarkers=false, -- Place markers of waypoints on F10 map. + FLcruise=nil, -- Cruise altitude of aircraft. Default FL200 for planes and F005 for helos. + FLminuser=nil, -- Minimum flight level set by user. + FLmaxuser=nil, -- Maximum flight level set by user. + FLuser=nil, -- Flight level set by users explicitly. + commute=false, -- Aircraft commute between departure and destination, i.e. when respawned the departure airport becomes the new destiation. + starshape=false, -- If true, aircraft travel A-->B-->A-->C-->A-->D... for commute. + homebase=nil, -- Home base for commute. + continuejourney=false, -- Aircraft will continue their journey, i.e. get respawned at their destination with a new random destination. + alive=0, -- Number of groups which are alive. + ngroups=nil, -- Number of groups to be spawned in total. + f10menu=false, -- Add an F10 menu for RAT. + Menu={}, -- F10 menu items for this RAT object. + SubMenuName=nil, -- Submenu name for RAT object. + respawn_at_landing=false, -- Respawn aircraft the moment they land rather than at engine shutdown. + norespawn=false, -- Aircraft will not get respawned. + respawn_after_takeoff=false, -- Aircraft will be respawned directly after takeoff. + respawn_after_crash=true, -- Aircraft will be respawned after a crash. + respawn_inair=true, -- Aircraft are spawned in air if there is no free parking spot on the ground. + respawn_delay=0, -- Delay in seconds until repawn happens after landing. + markerids={}, -- Array with marker IDs. + waypointdescriptions={}, -- Array with descriptions for waypoint markers. + waypointstatus={}, -- Array with status info on waypoints. + livery=nil, -- Livery of the aircraft. + skill="High", -- Skill of AI. + ATCswitch=true, -- Enable ATC. + radio=nil, -- If true/false disables radio messages from the RAT groups. + frequency=nil, -- Radio frequency used by the RAT groups. + modulation=nil, -- Ratio modulation. Either "FM" or "AM". + actype=nil, -- Aircraft type set by user. Changes the type of the template group. + uncontrolled=false, -- Spawn uncontrolled aircraft. + invisible=false, -- Spawn aircraft as invisible. + immortal=false, -- Spawn aircraft as indestructible. + activate_uncontrolled=false, -- Activate uncontrolled aircraft (randomly). + activate_delay=5, -- Delay in seconds before first uncontrolled group is activated. + activate_delta=5, -- Time interval in seconds between activation of uncontrolled groups. + activate_frand=0, -- Randomization factor of time interval (activate_delta) between activating uncontrolled groups. + activate_max=1, -- Max number of uncontrolle aircraft, which will be activated at a time. + onboardnum=nil, -- Tail number. + onboardnum0=1, -- (Optional) Starting value of the automatically appended numbering of aircraft within a flight. Default is one. + checkonrunway=true, -- Check whether aircraft have been spawned on the runway. + onrunwayradius=75, -- Distance from a runway spawn point until a unit is considered to have accidentally been spawned on a runway. + onrunwaymaxretry=3, -- Number of respawn retries (on ground) at other airports if a group gets accidentally spawned on the runway. + checkontop=false, -- Check whether aircraft have been spawned on top of another unit. + ontopradius=2, -- Radius in meters until which a unit is considered to be on top of another. + termtype=nil, -- Terminal type. + parkingscanradius=40, -- Scan radius. + parkingscanscenery=false, -- Scan parking spots for scenery obstacles. + parkingverysafe=false, -- Very safe option. + despawnair=true, + eplrs=false, +} + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Categories of the RAT class. +-- @list cat +-- @field #string plane Plane. +-- @field #string heli Heli. +RAT.cat={ + plane="plane", + heli="heli", +} + +--- RAT waypoint type. +-- @list wp +RAT.wp={ + coldorhot=0, + air=1, + runway=2, + hot=3, + cold=4, + climb=5, + cruise=6, + descent=7, + holding=8, + landing=9, + finalwp=10, +} + +--- RAT aircraft status. +-- @list status +RAT.status={ + -- Waypoint states. + Departure="At departure point", + Climb="Climbing", + Cruise="Cruising", + Uturn="Flying back home", + Descent="Descending", + DescentHolding="Descend to holding point", + Holding="Holding", + Destination="Arrived at destination", + -- Spawn states. + Uncontrolled="Uncontrolled", + Spawned="Spawned", + -- Event states. + EventBirthAir="Born in air", + EventBirth="Ready and starting engines", + EventEngineStartAir="On journey", -- Started engines (in air) + EventEngineStart="Started engines and taxiing", + EventTakeoff="Airborne after take-off", + EventLand="Landed and taxiing", + EventEngineShutdown="Engines off", + EventDead="Dead", + EventCrash="Crashed", +} + +--- RAT friendly coalitions. +-- @list coal +RAT.coal={ + same="same", + sameonly="sameonly", + neutral="neutral", +} + +--- RAT unit conversions. +-- @list unit +RAT.unit={ + ft2meter=0.305, + kmh2ms=0.278, + FL2m=30.48, + nm2km=1.852, + nm2m=1852, +} + +--- RAT rules of engagement. +-- @list ROE +RAT.ROE={ + weaponhold="hold", + weaponfree="free", + returnfire="return", +} + +--- RAT reaction to threat. +-- @list ROT +RAT.ROT={ + evade="evade", + passive="passive", + noreaction="noreaction", +} + +--- RAT ATC. +-- @list ATC +RAT.ATC={ + init=false, + flight={}, + airport={}, + unregistered=-1, + onfinal=-100, + Nclearance=2, + delay=240, + messages=true, +} + +--- Running number of placed markers on the F10 map. +-- @field #number markerid +RAT.markerid=0 + +--- Main F10 menu. +-- @field #string MenuF10 +RAT.MenuF10=nil + +--- Some ID to identify who we are in output of the DCS.log file. +-- @field #string id +RAT.id="RAT | " + +--- RAT version. +-- @list version +RAT.version={ + version = "2.3.9", + print = true, +} + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--TODO list: +--DONE: Add scheduled spawn. +--DONE: Add possibility to spawn in air. +--DONE: Add departure zones for air start. +--DONE: Make more functions to adjust/set RAT parameters. +--DONE: Clean up debug messages. +--DONE: Improve flight plan. Especially check FL against route length. +--DONE: Add event handlers. +--DONE: Respawn units when they have landed. +--DONE: Change ROE state. +--DONE: Make ROE state user function +--DONE: Improve status reports. +--DONE: Check compatibility with other #SPAWN functions. nope, not all! +--DONE: Add possibility to continue journey at destination. Need "place" in event data for that. +--DONE: Add enumerators and get rid off error prone string comparisons. +--DONE: Check that FARPS are not used as airbases for planes. +--DONE: Add special cases for ships (similar to FARPs). +--DONE: Add cases for helicopters. +--DONE: Add F10 menu. +--DONE: Add markers to F10 menu. +--DONE: Add respawn limit. Later... +--DONE: Make takeoff method random between cold and hot start. +--DONE: Check out uncontrolled spawning. Not now! +--DONE: Check aircraft spawning in air at Sochi after third aircraft was spawned. ==> DCS behaviour. +--DONE: Improve despawn after stationary. Might lead to despawning if many aircraft spawn at the same time. +--DONE: Check why birth event is not handled. ==> Seems to be okay if it is called _OnBirth rather than _OnBirthday. Dont know why actually!? +--DONE: Improve behaviour when no destination or departure airports were found. Leads to crash, e.g. 1184: attempt to get length of local 'destinations' (a nil value) +--DONE: Check cases where aircraft get shot down. +--DONE: Handle the case where more than 10 RAT objects are spawned. Likewise, more than 10 groups of one object. Causes problems with the number of menu items! ==> not now! +--DONE: Add custom livery choice if possible. +--DONE: Add function to include all airports to selected destinations/departures. +--DONE: Find way to respawn aircraft at same position where the last was despawned for commute and journey. +--TODO: Check that same alias is not given twice. Need to store previous ones and compare. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor New +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new RAT object. +-- @param #RAT self +-- @param #string groupname Name of the group as defined in the mission editor. This group is serving as a template for all spawned units. +-- @param #string alias (Optional) Alias of the group. This is and optional parameter but must(!) be used if the same template group is used for more than one RAT object. +-- @return #RAT Object of RAT class or nil if the group does not exist in the mission editor. +-- @usage yak1:RAT("RAT_YAK") will create a RAT object called "yak1". The template group in the mission editor must have the name "RAT_YAK". +-- @usage yak2:RAT("RAT_YAK", "Yak2") will create a RAT object "yak2". The template group in the mission editor must have the name "RAT_YAK" but the group will be called "Yak2" in e.g. the F10 menu. +function RAT:New(groupname, alias) + BASE:F({groupname=groupname, alias=alias}) + + -- Inherit SPAWN class. + self=BASE:Inherit(self, SPAWN:NewWithAlias(groupname, alias)) -- #RAT + + -- Version info. + if RAT.version.print then + env.info(RAT.id.."Version "..RAT.version.version) + RAT.version.print=false + end + + -- Welcome message. + self:F(RAT.id..string.format("Creating new RAT object from template: %s.", groupname)) + + -- Set alias. + alias=alias or groupname + + -- Alias of groupname. + self.alias=alias + + -- Get template group defined in the mission editor. + local DCSgroup=Group.getByName(groupname) + + -- Check the group actually exists. + if DCSgroup==nil then + self:E(RAT.id..string.format("ERROR: Group with name %s does not exist in the mission editor!", groupname)) + return nil + end + + -- Store template group. + self.templategroup=GROUP:FindByName(groupname) + + -- Get number of aircraft in group. + self.groupsize=self.templategroup:GetSize() + + -- Set own coalition. + self.coalition=DCSgroup:getCoalition() + + -- Initialize aircraft parameters based on ME group template. + self:_InitAircraft(DCSgroup) + + -- Get all airports of current map (Caucasus, NTTR, Normandy, ...). + self:_GetAirportsOfMap() + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Spawn function +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Triggers the spawning of AI aircraft. Note that all additional options should be set before giving the spawn command. +-- @param #RAT self +-- @param #number naircraft (Optional) Number of aircraft to spawn. Default is one aircraft. +-- @return #boolean True if spawning was successful or nil if nothing was spawned. +-- @usage yak:Spawn(5) will spawn five aircraft. By default aircraft will spawn at neutral and red airports if the template group is part of the red coaliton. +function RAT:Spawn(naircraft) + + -- Make sure that this function is only been called once per RAT object. + if self.spawninitialized==true then + self:E("ERROR: Spawn function should only be called once per RAT object! Exiting and returning nil.") + return nil + else + self.spawninitialized=true + end + + -- Number of aircraft to spawn. Default is one. + self.ngroups=naircraft or 1 + + -- Init RAT ATC if not already done. + if self.ATCswitch and not RAT.ATC.init then + self:_ATCInit(self.airports_map) + end + + -- Create F10 main menu if it does not exists yet. + if self.f10menu and not RAT.MenuF10 then + RAT.MenuF10 = MENU_MISSION:New("RAT") + end + + -- Set the coalition table based on choice of self.coalition and self.friendly. + self:_SetCoalitionTable() + + -- Get all airports of this map beloning to friendly coalition(s). + self:_GetAirportsOfCoalition() + + -- Set submenuname if it has not been set by user. + if not self.SubMenuName then + self.SubMenuName=self.alias + end + + -- Get all departure airports inside a Moose zone. + if self.departure_Azone~=nil then + self.departure_ports=self:_GetAirportsInZone(self.departure_Azone) + end + + -- Get all destination airports inside a Moose zone. + if self.destination_Azone~=nil then + self.destination_ports=self:_GetAirportsInZone(self.destination_Azone) + end + + -- Add all friendly airports to possible departures/destinations + if self.addfriendlydepartures then + self:_AddFriendlyAirports(self.departure_ports) + end + if self.addfriendlydestinations then + self:_AddFriendlyAirports(self.destination_ports) + end + + -- Setting and possibly correction min/max/cruise flight levels. + if self.FLcruise==nil then + -- Default flight level (ASL). + if self.category==RAT.cat.plane then + -- For planes: FL200 = 20000 ft = 6096 m. + self.FLcruise=200*RAT.unit.FL2m + else + -- For helos: FL005 = 500 ft = 152 m. + self.FLcruise=005*RAT.unit.FL2m + end + end + + -- Enable helos to go to destinations 100 meters away. + if self.category==RAT.cat.heli then + self.mindist=50 + end + + -- Run consistency checks. + self:_CheckConsistency() + + -- Settings info + local text=string.format("\n******************************************************\n") + text=text..string.format("Spawning %i aircraft from template %s of type %s.\n", self.ngroups, self.SpawnTemplatePrefix, self.aircraft.type) + text=text..string.format("Alias: %s\n", self.alias) + text=text..string.format("Category: %s\n", self.category) + text=text..string.format("Friendly coalitions: %s\n", self.friendly) + text=text..string.format("Number of airports on map : %i\n", #self.airports_map) + text=text..string.format("Number of friendly airports: %i\n", #self.airports) + text=text..string.format("Totally random departure: %s\n", tostring(self.random_departure)) + if not self.random_departure then + text=text..string.format("Number of departure airports: %d\n", self.Ndeparture_Airports) + text=text..string.format("Number of departure zones : %d\n", self.Ndeparture_Zones) + end + text=text..string.format("Totally random destination: %s\n", tostring(self.random_destination)) + if not self.random_destination then + text=text..string.format("Number of destination airports: %d\n", self.Ndestination_Airports) + text=text..string.format("Number of destination zones : %d\n", self.Ndestination_Zones) + end + text=text..string.format("Min dist to destination: %4.1f\n", self.mindist) + text=text..string.format("Max dist to destination: %4.1f\n", self.maxdist) + text=text..string.format("Terminal type: %s\n", tostring(self.termtype)) + text=text..string.format("Takeoff type: %i\n", self.takeoff) + text=text..string.format("Landing type: %i\n", self.landing) + text=text..string.format("Commute: %s\n", tostring(self.commute)) + text=text..string.format("Journey: %s\n", tostring(self.continuejourney)) + text=text..string.format("Destination Zone: %s\n", tostring(self.destinationzone)) + text=text..string.format("Return Zone: %s\n", tostring(self.returnzone)) + text=text..string.format("Spawn delay: %4.1f\n", self.spawndelay) + text=text..string.format("Spawn interval: %4.1f\n", self.spawninterval) + text=text..string.format("Respawn delay: %s\n", tostring(self.respawn_delay)) + text=text..string.format("Respawn off: %s\n", tostring(self.norespawn)) + text=text..string.format("Respawn after landing: %s\n", tostring(self.respawn_at_landing)) + text=text..string.format("Respawn after take-off: %s\n", tostring(self.respawn_after_takeoff)) + text=text..string.format("Respawn after crash: %s\n", tostring(self.respawn_after_crash)) + text=text..string.format("Respawn in air: %s\n", tostring(self.respawn_inair)) + text=text..string.format("ROE: %s\n", tostring(self.roe)) + text=text..string.format("ROT: %s\n", tostring(self.rot)) + text=text..string.format("Immortal: %s\n", tostring(self.immortal)) + text=text..string.format("Invisible: %s\n", tostring(self.invisible)) + text=text..string.format("Vclimb: %4.1f\n", self.Vclimb) + text=text..string.format("AlphaDescent: %4.2f\n", self.AlphaDescent) + text=text..string.format("Vcruisemax: %s\n", tostring(self.Vcruisemax)) + text=text..string.format("FLcruise = %6.1f km = FL%3.0f\n", self.FLcruise/1000, self.FLcruise/RAT.unit.FL2m) + text=text..string.format("FLuser: %s\n", tostring(self.Fluser)) + text=text..string.format("FLminuser: %s\n", tostring(self.FLminuser)) + text=text..string.format("FLmaxuser: %s\n", tostring(self.FLmaxuser)) + text=text..string.format("Place markers: %s\n", tostring(self.placemarkers)) + text=text..string.format("Report status: %s\n", tostring(self.reportstatus)) + text=text..string.format("Status interval: %4.1f\n", self.statusinterval) + text=text..string.format("Time inactive: %4.1f\n", self.Tinactive) + text=text..string.format("Create F10 menu : %s\n", tostring(self.f10menu)) + text=text..string.format("F10 submenu name: %s\n", self.SubMenuName) + text=text..string.format("ATC enabled : %s\n", tostring(self.ATCswitch)) + text=text..string.format("Radio comms : %s\n", tostring(self.radio)) + text=text..string.format("Radio frequency : %s\n", tostring(self.frequency)) + text=text..string.format("Radio modulation : %s\n", tostring(self.frequency)) + text=text..string.format("Tail # prefix : %s\n", tostring(self.onboardnum)) + text=text..string.format("Check on runway: %s\n", tostring(self.checkonrunway)) + text=text..string.format("Max respawn attempts: %s\n", tostring(self.onrunwaymaxretry)) + text=text..string.format("Check on top: %s\n", tostring(self.checkontop)) + text=text..string.format("Uncontrolled: %s\n", tostring(self.uncontrolled)) + if self.uncontrolled and self.activate_uncontrolled then + text=text..string.format("Uncontrolled max : %4.1f\n", self.activate_max) + text=text..string.format("Uncontrolled delay: %4.1f\n", self.activate_delay) + text=text..string.format("Uncontrolled delta: %4.1f\n", self.activate_delta) + text=text..string.format("Uncontrolled frand: %4.1f\n", self.activate_frand) + end + if self.livery then + text=text..string.format("Available liveries:\n") + for _,livery in pairs(self.livery) do + text=text..string.format("- %s\n", livery) + end + end + text=text..string.format("******************************************************\n") + self:T(RAT.id..text) + + -- Create submenus. + if self.f10menu then + self.Menu[self.SubMenuName]=MENU_MISSION:New(self.SubMenuName, RAT.MenuF10) + self.Menu[self.SubMenuName]["groups"]=MENU_MISSION:New("Groups", self.Menu[self.SubMenuName]) + MENU_MISSION_COMMAND:New("Spawn new group", self.Menu[self.SubMenuName], self._SpawnWithRoute, self) + MENU_MISSION_COMMAND:New("Delete markers", self.Menu[self.SubMenuName], self._DeleteMarkers, self) + MENU_MISSION_COMMAND:New("Status report", self.Menu[self.SubMenuName], self.Status, self, true) + end + + -- Schedule spawning of aircraft. + local Tstart=self.spawndelay + local dt=self.spawninterval + -- Ensure that interval is >= 180 seconds if spawn at runway is chosen. Aircraft need time to takeoff or the runway gets jammed. + if self.takeoff==RAT.wp.runway and not self.random_departure then + dt=math.max(dt, 180) + end + local Tstop=Tstart+dt*(self.ngroups-1) + + -- Status check and report scheduler. + SCHEDULER:New(nil, self.Status, {self}, Tstart+1, self.statusinterval) + + -- Handle events. + self:HandleEvent(EVENTS.Birth, self._OnBirth) + self:HandleEvent(EVENTS.EngineStartup, self._OnEngineStartup) + self:HandleEvent(EVENTS.Takeoff, self._OnTakeoff) + self:HandleEvent(EVENTS.Land, self._OnLand) + self:HandleEvent(EVENTS.EngineShutdown, self._OnEngineShutdown) + self:HandleEvent(EVENTS.Dead, self._OnDeadOrCrash) + self:HandleEvent(EVENTS.Crash, self._OnDeadOrCrash) + self:HandleEvent(EVENTS.Hit, self._OnHit) + + -- No groups should be spawned. + if self.ngroups==0 then + return nil + end + + -- Start scheduled spawning. + SCHEDULER:New(nil, self._SpawnWithRoute, {self}, Tstart, dt, 0.0, Tstop) + + -- Start scheduled activation of uncontrolled groups. + if self.uncontrolled and self.activate_uncontrolled then + SCHEDULER:New(nil, self._ActivateUncontrolled, {self}, self.activate_delay, self.activate_delta, self.activate_frand) + end + + return true +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Consistency Check +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Function checks consistency of user input and automatically adjusts parameters if necessary. +-- @param #RAT self +function RAT:_CheckConsistency() + self:F2() + + -- User has used SetDeparture() + if not self.random_departure then + + -- Count departure airports and zones. + for _,name in pairs(self.departure_ports) do + if self:_AirportExists(name) then + self.Ndeparture_Airports=self.Ndeparture_Airports+1 + elseif self:_ZoneExists(name) then + self.Ndeparture_Zones=self.Ndeparture_Zones+1 + end + end + + -- What can go wrong? + -- Only zones but not takeoff air == > Enable takeoff air. + if self.Ndeparture_Zones>0 and self.takeoff~=RAT.wp.air then + self.takeoff=RAT.wp.air + self:E(RAT.id..string.format("ERROR: At least one zone defined as departure and takeoff is NOT set to air. Enabling air start for RAT group %s!", self.alias)) + end + -- No airport and no zone specified. + if self.Ndeparture_Airports==0 and self.Ndeparture_Zone==0 then + self.random_departure=true + local text=string.format("No airports or zones found given in SetDeparture(). Enabling random departure airports for RAT group %s!", self.alias) + self:E(RAT.id.."ERROR: "..text) + MESSAGE:New(text, 30):ToAll() + end + end + + -- User has used SetDestination() + if not self.random_destination then + + -- Count destination airports and zones. + for _,name in pairs(self.destination_ports) do + if self:_AirportExists(name) then + self.Ndestination_Airports=self.Ndestination_Airports+1 + elseif self:_ZoneExists(name) then + self.Ndestination_Zones=self.Ndestination_Zones+1 + end + end + + -- One zone specified as destination ==> Enable destination zone. + -- This does not apply to return zone because the destination is the zone and not the final destination which can be an airport. + if self.Ndestination_Zones>0 and self.landing~=RAT.wp.air and not self.returnzone then + self.landing=RAT.wp.air + self.destinationzone=true + self:E(RAT.id.."ERROR: At least one zone defined as destination and landing is NOT set to air. Enabling destination zone!") + end + -- No specified airport and no zone found at all. + if self.Ndestination_Airports==0 and self.Ndestination_Zones==0 then + self.random_destination=true + local text="No airports or zones found given in SetDestination(). Enabling random destination airports!" + self:E(RAT.id.."ERROR: "..text) + MESSAGE:New(text, 30):ToAll() + end + end + + -- Destination zone and return zone should not be used together. + if self.destinationzone and self.returnzone then + self:E(RAT.id.."ERROR: Destination zone _and_ return to zone not possible! Disabling return to zone.") + self.returnzone=false + end + -- If returning to a zone, we set the landing type to "air" if takeoff is in air. + -- Because if we start in air we want to end in air. But default landing is ground. + if self.returnzone and self.takeoff==RAT.wp.air then + self.landing=RAT.wp.air + end + + -- Ensure that neither FLmin nor FLmax are above the aircrafts service ceiling. + if self.FLminuser then + self.FLminuser=math.min(self.FLminuser, self.aircraft.ceiling) + end + if self.FLmaxuser then + self.FLmaxuser=math.min(self.FLmaxuser, self.aircraft.ceiling) + end + if self.FLcruise then + self.FLcruise=math.min(self.FLcruise, self.aircraft.ceiling) + end + + -- FL min > FL max case ==> spaw values + if self.FLminuser and self.FLmaxuser then + if self.FLminuser > self.FLmaxuser then + local min=self.FLminuser + local max=self.FLmaxuser + self.FLminuser=max + self.FLmaxuser=min + end + end + + -- Cruise alt < FL min + if self.FLminuser and self.FLcruise FL max + if self.FLmaxuser and self.FLcruise>self.FLmaxuser then + self.FLcruise=self.FLmaxuser + end + + -- Uncontrolled aircraft must start with engines off. + if self.uncontrolled then + -- SOLVED: Strangly, it does not work with RAT.wp.cold only with RAT.wp.hot! + -- Figured out why. SPAWN:SpawnWithIndex is overwriting some values. Now it should work with cold as expected! + self.takeoff=RAT.wp.cold + end +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set the friendly coalitions from which the airports can be used as departure and destination. +-- @param #RAT self +-- @param #string friendly "same"=own coalition+neutral (default), "sameonly"=own coalition only, "neutral"=all neutral airports. +-- Default is "same", so aircraft will use airports of the coalition their spawn template has plus all neutral airports. +-- @return #RAT RAT self object. +-- @usage yak:SetCoalition("neutral") will spawn aircraft randomly on all neutral airports. +-- @usage yak:SetCoalition("sameonly") will spawn aircraft randomly on airports belonging to the same coalition only as the template. +function RAT:SetCoalition(friendly) + self:F2(friendly) + if friendly:lower()=="sameonly" then + self.friendly=RAT.coal.sameonly + elseif friendly:lower()=="neutral" then + self.friendly=RAT.coal.neutral + else + self.friendly=RAT.coal.same + end + return self +end + +--- Set coalition of RAT group. You can make red templates blue and vice versa. +-- Note that a country is also set automatically if it has not done before via RAT:SetCountry. +-- +-- * For blue, the country is set to USA. +-- * For red, the country is set to RUSSIA. +-- * For neutral, the country is set to SWITZERLAND. +-- +-- This is important, since it is ultimately the COUNTRY that determines the coalition of the aircraft. +-- You can set the country explicitly via the RAT:SetCountry() function if necessary. +-- @param #RAT self +-- @param #string color Color of coalition, i.e. "red" or blue" or "neutral". +-- @return #RAT RAT self object. +function RAT:SetCoalitionAircraft(color) + self:F2(color) + if color:lower()=="blue" then + self.coalition=coalition.side.BLUE + if not self.country then + self.country=country.id.USA + end + elseif color:lower()=="red" then + self.coalition=coalition.side.RED + if not self.country then + self.country=country.id.RUSSIA + end + elseif color:lower()=="neutral" then + self.coalition=coalition.side.NEUTRAL + if not self.country then + self.country=country.id.SWITZERLAND + end + end + return self +end + +--- Set country of RAT group. +-- See [DCS_enum_country](https://wiki.hoggitworld.com/view/DCS_enum_country). +-- +-- This overrules the coalition settings. So if you want your group to be of a specific coalition, you have to set a country that is part of that coalition. +-- @param #RAT self +-- @param DCS#country.id id DCS country enumerator ID. For example country.id.USA or country.id.RUSSIA. +-- @return #RAT RAT self object. +function RAT:SetCountry(id) + self:F2(id) + self.country=id + return self +end + +--- Set the terminal type the aircraft use when spawning at an airbase. See [DCS_func_getParking](https://wiki.hoggitworld.com/view/DCS_func_getParking). +-- Note that some additional terminal types have been introduced. Check @{Wrapper.Airbase#AIRBASE} class for details. +-- Also note that only airports which have this kind of terminal are possible departures and/or destinations. +-- @param #RAT self +-- @param Wrapper.Airbase#AIRBASE.TerminalType termtype Type of terminal. Use enumerator AIRBASE.TerminalType.XXX. +-- @return #RAT RAT self object. +-- +-- @usage +-- c17=RAT:New("C-17 BIG Plane") +-- c17:SetTerminalType(AIRBASE.TerminalType.OpenBig) -- Only very big parking spots are used. +-- c17:Spawn(5) +function RAT:SetTerminalType(termtype) + self:F2(termtype) + self.termtype=termtype + return self +end + +--- Set the scan radius around parking spots. Parking spot is considered to be occupied if any obstacle is found with the radius. +-- @param #RAT self +-- @param #number radius Radius in meters. Default 50 m. +-- @return #RAT RAT self object. +function RAT:SetParkingScanRadius(radius) + self:F2(radius) + self.parkingscanradius=radius or 50 + return self +end + +--- Enables scanning for scenery objects around parking spots which might block the spot. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:SetParkingScanSceneryON() + self:F2() + self.parkingscanscenery=true + return self +end + +--- Disables scanning for scenery objects around parking spots which might block the spot. This is also the default setting. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:SetParkingScanSceneryOFF() + self:F2() + self.parkingscanscenery=false + return self +end + +--- A parking spot is not free until a possible aircraft has left and taken off. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:SetParkingSpotSafeON() + self:F2() + self.parkingverysafe=true + return self +end + +--- A parking spot is free as soon as possible aircraft has left the place. This is the default. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:SetParkingSpotSafeOFF() + self:F2() + self.parkingverysafe=false + return self +end + +--- Aircraft that reach their destination zone are not despawned. They will probably go the the nearest airbase and try to land. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:SetDespawnAirOFF() + self.despawnair=false + return self +end + +--- Set takeoff type. Starting cold at airport, starting hot at airport, starting at runway, starting in the air. +-- Default is "takeoff-coldorhot". So there is a 50% chance that the aircraft starts with cold engines and 50% that it starts with hot engines. +-- @param #RAT self +-- @param #string type Type can be "takeoff-cold" or "cold", "takeoff-hot" or "hot", "takeoff-runway" or "runway", "air". +-- @return #RAT RAT self object. +-- @usage RAT:Takeoff("hot") will spawn RAT objects at airports with engines started. +-- @usage RAT:Takeoff("cold") will spawn RAT objects at airports with engines off. +-- @usage RAT:Takeoff("air") will spawn RAT objects in air over random airports or within pre-defined zones. +function RAT:SetTakeoff(type) + self:F2(type) + + local _Type + if type:lower()=="takeoff-cold" or type:lower()=="cold" then + _Type=RAT.wp.cold + elseif type:lower()=="takeoff-hot" or type:lower()=="hot" then + _Type=RAT.wp.hot + elseif type:lower()=="takeoff-runway" or type:lower()=="runway" then + _Type=RAT.wp.runway + elseif type:lower()=="air" then + _Type=RAT.wp.air + else + _Type=RAT.wp.coldorhot + end + + self.takeoff=_Type + + return self +end + +--- Set takeoff type cold. Aircraft will spawn at a parking spot with engines off. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:SetTakeoffCold() + self.takeoff=RAT.wp.cold + return self +end + +--- Set takeoff type to hot. Aircraft will spawn at a parking spot with engines on. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:SetTakeoffHot() + self.takeoff=RAT.wp.hot + return self +end + +--- Set takeoff type to runway. Aircraft will spawn directly on the runway. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:SetTakeoffRunway() + self.takeoff=RAT.wp.runway + return self +end + +--- Set takeoff type to cold or hot. Aircraft will spawn at a parking spot with 50:50 change of engines on or off. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:SetTakeoffColdOrHot() + self.takeoff=RAT.wp.coldorhot + return self +end + +--- Set takeoff type to air. Aircraft will spawn in the air. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:SetTakeoffAir() + self.takeoff=RAT.wp.air + return self +end + +--- Set possible departure ports. This can be an airport or a zone defined in the mission editor. +-- @param #RAT self +-- @param #string departurenames Name or table of names of departure airports or zones. +-- @return #RAT RAT self object. +-- @usage RAT:SetDeparture("Sochi-Adler") will spawn RAT objects at Sochi-Adler airport. +-- @usage RAT:SetDeparture({"Sochi-Adler", "Gudauta"}) will spawn RAT aircraft radomly at Sochi-Adler or Gudauta airport. +-- @usage RAT:SetDeparture({"Zone A", "Gudauta"}) will spawn RAT aircraft in air randomly within Zone A, which has to be defined in the mission editor, or within a zone around Gudauta airport. Note that this also requires RAT:takeoff("air") to be set. +function RAT:SetDeparture(departurenames) + self:F2(departurenames) + + -- Random departure is deactivated now that user specified departure ports. + self.random_departure=false + + -- Convert input to table. + local names + if type(departurenames)=="table" then + names=departurenames + elseif type(departurenames)=="string" then + names={departurenames} + else + -- error message + self:E(RAT.id.."ERROR: Input parameter must be a string or a table in SetDeparture()!") + end + + -- Put names into arrays. + for _,name in pairs(names) do + + if self:_AirportExists(name) then + -- If an airport with this name exists, we put it in the ports array. + table.insert(self.departure_ports, name) + elseif self:_ZoneExists(name) then + -- If it is not an airport, we assume it is a zone. + table.insert(self.departure_ports, name) + else + self:E(RAT.id.."ERROR: No departure airport or zone found with name "..name) + end + + end + + return self +end + +--- Set name of destination airports or zones for the AI aircraft. +-- @param #RAT self +-- @param #string destinationnames Name of the destination airport or table of destination airports. +-- @return #RAT RAT self object. +-- @usage RAT:SetDestination("Krymsk") makes all aircraft of this RAT oject fly to Krymsk airport. +function RAT:SetDestination(destinationnames) + self:F2(destinationnames) + + -- Random departure is deactivated now that user specified departure ports. + self.random_destination=false + + -- Convert input to table + local names + if type(destinationnames)=="table" then + names=destinationnames + elseif type(destinationnames)=="string" then + names={destinationnames} + else + -- Error message. + self:E(RAT.id.."ERROR: Input parameter must be a string or a table in SetDestination()!") + end + + -- Put names into arrays. + for _,name in pairs(names) do + + if self:_AirportExists(name) then + -- If an airport with this name exists, we put it in the ports array. + table.insert(self.destination_ports, name) + elseif self:_ZoneExists(name) then + -- If it is not an airport, we assume it is a zone. + table.insert(self.destination_ports, name) + else + self:E(RAT.id.."ERROR: No destination airport or zone found with name "..name) + end + + end + + return self +end + +--- Destinations are treated as zones. Aircraft will not land but rather be despawned when they reach a random point in the zone. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:DestinationZone() + self:F2() + + -- Destination is a zone. Needs special care. + self.destinationzone=true + + -- Landing type is "air" because we don't actually land at the airport. + self.landing=RAT.wp.air + + return self +end + +--- Aircraft will fly to a random point within a zone and then return to its departure airport or zone. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:ReturnZone() + self:F2() + -- Destination is a zone. Needs special care. + self.returnzone=true + return self +end + + +--- Include all airports which lie in a zone as possible destinations. +-- @param #RAT self +-- @param Core.Zone#ZONE zone Zone in which the departure airports lie. Has to be a MOOSE zone. +-- @return #RAT RAT self object. +function RAT:SetDestinationsFromZone(zone) + self:F2(zone) + + -- Random departure is deactivated now that user specified departure ports. + self.random_destination=false + + -- Set zone. + self.destination_Azone=zone + + return self +end + +--- Include all airports which lie in a zone as possible destinations. +-- @param #RAT self +-- @param Core.Zone#ZONE zone Zone in which the destination airports lie. Has to be a MOOSE zone. +-- @return #RAT RAT self object. +function RAT:SetDeparturesFromZone(zone) + self:F2(zone) + + -- Random departure is deactivated now that user specified departure ports. + self.random_departure=false + + -- Set zone. + self.departure_Azone=zone + + return self +end + +--- Add all friendly airports to the list of possible departures. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:AddFriendlyAirportsToDepartures() + self:F2() + self.addfriendlydepartures=true + return self +end + +--- Add all friendly airports to the list of possible destinations +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:AddFriendlyAirportsToDestinations() + self:F2() + self.addfriendlydestinations=true + return self +end + +--- Airports, FARPs and ships explicitly excluded as departures and destinations. +-- @param #RAT self +-- @param #string ports Name or table of names of excluded airports. +-- @return #RAT RAT self object. +function RAT:ExcludedAirports(ports) + self:F2(ports) + if type(ports)=="string" then + self.excluded_ports={ports} + else + self.excluded_ports=ports + end + return self +end + +--- Set skill of AI aircraft. Default is "High". +-- @param #RAT self +-- @param #string skill Skill, options are "Average", "Good", "High", "Excellent" and "Random". Parameter is case insensitive. +-- @return #RAT RAT self object. +function RAT:SetAISkill(skill) + self:F2(skill) + if skill:lower()=="average" then + self.skill="Average" + elseif skill:lower()=="good" then + self.skill="Good" + elseif skill:lower()=="excellent" then + self.skill="Excellent" + elseif skill:lower()=="random" then + self.skill="Random" + else + self.skill="High" + end + return self +end + +--- Set livery of aircraft. If more than one livery is specified in a table, the actually used one is chosen randomly from the selection. +-- @param #RAT self +-- @param #table skins Name of livery or table of names of liveries. +-- @return #RAT RAT self object. +function RAT:Livery(skins) + self:F2(skins) + if type(skins)=="string" then + self.livery={skins} + else + self.livery=skins + end + return self +end + +--- Change aircraft type. This is a dirty hack which allows to change the aircraft type of the template group. +-- Note that all parameters like cruise speed, climb rate, range etc are still taken from the template group which likely leads to strange behaviour. +-- @param #RAT self +-- @param #string actype Type of aircraft which is spawned independent of the template group. Use with care and expect problems! +-- @return #RAT RAT self object. +function RAT:ChangeAircraft(actype) + self:F2(actype) + self.actype=actype + return self +end + +--- Aircraft will continue their journey from their destination. This means they are respawned at their destination and get a new random destination. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:ContinueJourney() + self:F2() + self.continuejourney=true + self.commute=false + return self +end + +--- Aircraft will commute between their departure and destination airports or zones. +-- @param #RAT self +-- @param #boolean starshape If true, keep homebase, i.e. travel A-->B-->A-->C-->A-->D... instead of A-->B-->A-->B-->A... +-- @return #RAT RAT self object. +function RAT:Commute(starshape) + self:F2() + self.commute=true + self.continuejourney=false + if starshape then + self.starshape=starshape + else + self.starshape=false + end + return self +end + +--- Set the delay before first group is spawned. +-- @param #RAT self +-- @param #number delay Delay in seconds. Default is 5 seconds. Minimum delay is 0.5 seconds. +-- @return #RAT RAT self object. +function RAT:SetSpawnDelay(delay) + self:F2(delay) + delay=delay or 5 + self.spawndelay=math.max(0.5, delay) + return self +end + +--- Set the interval between spawnings of the template group. +-- @param #RAT self +-- @param #number interval Interval in seconds. Default is 5 seconds. Minimum is 0.5 seconds. +-- @return #RAT RAT self object. +function RAT:SetSpawnInterval(interval) + self:F2(interval) + interval=interval or 5 + self.spawninterval=math.max(0.5, interval) + return self +end + +--- Make aircraft respawn the moment they land rather than at engine shut down. +-- @param #RAT self +-- @param #number delay (Optional) Delay in seconds until respawn happens after landing. Default is 180 seconds. Minimum is 1.0 seconds. +-- @return #RAT RAT self object. +function RAT:RespawnAfterLanding(delay) + self:F2(delay) + delay = delay or 180 + self.respawn_at_landing=true + delay=math.max(1.0, delay) + self.respawn_delay=delay + return self +end + +--- Sets the delay between despawning and respawning aircraft. +-- @param #RAT self +-- @param #number delay Delay in seconds until respawn happens. Default is 1 second. Minimum is 1 second. +-- @return #RAT RAT self object. +function RAT:SetRespawnDelay(delay) + self:F2(delay) + delay = delay or 1.0 + delay=math.max(1.0, delay) + self.respawn_delay=delay + return self +end + +--- Aircraft will not get respawned when they finished their route. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:NoRespawn() + self:F2() + self.norespawn=true + return self +end + +--- Number of tries to respawn an aircraft in case it has accitentally been spawned on runway. +-- @param #RAT self +-- @param #number n Number of retries. Default is 3. +-- @return #RAT RAT self object. +function RAT:SetMaxRespawnTriedWhenSpawnedOnRunway(n) + self:F2(n) + n=n or 3 + self.onrunwaymaxretry=n + return self +end + +--- Aircraft will be respawned directly after take-off. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:RespawnAfterTakeoff() + self:F2() + self.respawn_after_takeoff=true + return self +end + +--- Aircraft will be respawned after they crashed or get shot down. This is the default behavior. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:RespawnAfterCrashON() + self:F2() + self.respawn_after_crash=true + return self +end + +--- Aircraft will not be respawned after they crashed or get shot down. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:RespawnAfterCrashOFF() + self:F2() + self.respawn_after_crash=false + return self +end + +--- If aircraft cannot be spawned on parking spots, it is allowed to spawn them in air above the same airport. Note that this is also the default behavior. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:RespawnInAirAllowed() + self:F2() + self.respawn_inair=true + return self +end + +--- If aircraft cannot be spawned on parking spots, it is NOT allowed to spawn them in air. This has only impact if aircraft are supposed to be spawned on the ground (and not in a zone). +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:RespawnInAirNotAllowed() + self:F2() + self.respawn_inair=false + return self +end + +--- Check if aircraft have accidentally been spawned on the runway. If so they will be removed immediatly. +-- @param #RAT self +-- @param #boolean switch If true, check is performed. If false, this check is omitted. +-- @param #number radius Distance in meters until a unit is considered to have spawned accidentally on the runway. Default is 75 m. +-- @return #RAT RAT self object. +function RAT:CheckOnRunway(switch, distance) + self:F2(switch) + if switch==nil then + switch=true + end + self.checkonrunway=switch + self.onrunwayradius=distance or 75 + return self +end + +--- Check if aircraft have accidentally been spawned on top of each other. If yes, they will be removed immediately. +-- @param #RAT self +-- @param #boolean switch If true, check is performed. If false, this check is omitted. +-- @param #number radius Radius in meters until which a unit is considered to be on top of each other. Default is 2 m. +-- @return #RAT RAT self object. +function RAT:CheckOnTop(switch, radius) + self:F2(switch) + if switch==nil then + switch=true + end + self.checkontop=switch + self.ontopradius=radius or 2 + return self +end + +--- Put parking spot coordinates in a data base for future use of aircraft. (Obsolete! API function will be removed soon.) +-- @param #RAT self +-- @param #boolean switch If true, parking spots are memorized. This is also the default setting. +-- @return #RAT RAT self object. +function RAT:ParkingSpotDB(switch) + self:E("RAT ParkingSpotDB function is obsolete and will be removed soon!") + return self +end + +--- Enable Radio. Overrules the ME setting. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:RadioON() + self:F2() + self.radio=true + return self +end + +--- Disable Radio. Overrules the ME setting. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:RadioOFF() + self:F2() + self.radio=false + return self +end + +--- Set radio frequency. +-- @param #RAT self +-- @param #number frequency Radio frequency. +-- @return #RAT RAT self object. +function RAT:RadioFrequency(frequency) + self:F2(frequency) + self.frequency=frequency + return self +end + +--- Set radio modulation. Default is AM. +-- @param #RAT self +-- @param #string modulation Either "FM" or "AM". If no value is given, modulation is set to AM. +-- @return #RAT RAT self object. +function RAT:RadioModulation(modulation) + self:F2(modulation) + if modulation=="AM" then + self.modulation=radio.modulation.AM + elseif modulation=="FM" then + self.modulation=radio.modulation.FM + else + self.modulation=radio.modulation.AM + end + return self +end + +--- Radio menu On. Default is off. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:RadioMenuON() + self:F2() + self.f10menu=true + return self +end + +--- Radio menu Off. This is the default setting. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:RadioMenuOFF() + self:F2() + self.f10menu=false + return self +end + +--- Aircraft are invisible. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:Invisible() + self:F2() + self.invisible=true + return self +end + +--- Turn EPLRS datalink on/off. +-- @param #RAT self +-- @param #boolean switch If true (or nil), turn EPLRS on. +-- @return #RAT RAT self object. +function RAT:SetEPLRS(switch) + if switch==nil or switch==true then + self.eplrs=true + else + self.eplrs=false + end + return self +end + +--- Aircraft are immortal. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:Immortal() + self:F2() + self.immortal=true + return self +end + +--- Spawn aircraft in uncontrolled state. Aircraft will only sit at their parking spots. They can be activated randomly by the RAT:ActivateUncontrolled() function. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:Uncontrolled() + self:F2() + self.uncontrolled=true + return self +end + +--- Activate uncontrolled aircraft. +-- @param #RAT self +-- @param #number maxactivated Maximal numnber of activated aircraft. Absolute maximum will be the number of spawned groups. Default is 1. +-- @param #number delay Time delay in seconds before (first) aircraft is activated. Default is 1 second. +-- @param #number delta Time difference in seconds before next aircraft is activated. Default is 1 second. +-- @param #number frand Factor [0,...,1] for randomization of time difference between aircraft activations. Default is 0, i.e. no randomization. +-- @return #RAT RAT self object. +function RAT:ActivateUncontrolled(maxactivated, delay, delta, frand) + self:F2({max=maxactivated, delay=delay, delta=delta, rand=frand}) + + self.activate_uncontrolled=true + self.activate_max=maxactivated or 1 + self.activate_delay=delay or 1 + self.activate_delta=delta or 1 + self.activate_frand=frand or 0 + + -- Ensure min delay is one second. + self.activate_delay=math.max(self.activate_delay,1) + + -- Ensure min delta is one second. + self.activate_delta=math.max(self.activate_delta,0) + + -- Ensure frand is in [0,...,1] + self.activate_frand=math.max(self.activate_frand,0) + self.activate_frand=math.min(self.activate_frand,1) + + return self +end + +--- Set the time after which inactive groups will be destroyed. +-- @param #RAT self +-- @param #number time Time in seconds. Default is 600 seconds = 10 minutes. Minimum is 60 seconds. +-- @return #RAT RAT self object. +function RAT:TimeDestroyInactive(time) + self:F2(time) + time=time or self.Tinactive + time=math.max(time, 60) + self.Tinactive=time + return self +end + +--- Set the maximum cruise speed of the aircraft. +-- @param #RAT self +-- @param #number speed Speed in km/h. +-- @return #RAT RAT self object. +function RAT:SetMaxCruiseSpeed(speed) + self:F2(speed) + -- Convert to m/s. + self.Vcruisemax=speed/3.6 + return self +end + +--- Set the climb rate. This automatically sets the climb angle. +-- @param #RAT self +-- @param #number rate Climb rate in ft/min. Default is 1500 ft/min. Minimum is 100 ft/min. Maximum is 15,000 ft/min. +-- @return #RAT RAT self object. +function RAT:SetClimbRate(rate) + self:F2(rate) + rate=rate or self.Vclimb + rate=math.max(rate, 100) + rate=math.min(rate, 15000) + self.Vclimb=rate + return self +end + +--- Set the angle of descent. Default is 3.6 degrees, which corresponds to 3000 ft descent after one mile of travel. +-- @param #RAT self +-- @param #number angle Angle of descent in degrees. Minimum is 0.5 deg. Maximum 50 deg. +-- @return #RAT RAT self object. +function RAT:SetDescentAngle(angle) + self:F2(angle) + angle=angle or self.AlphaDescent + angle=math.max(angle, 0.5) + angle=math.min(angle, 50) + self.AlphaDescent=angle + return self +end + +--- Set rules of engagement (ROE). Default is weapon hold. This is a peaceful class. +-- @param #RAT self +-- @param #string roe "hold" = weapon hold, "return" = return fire, "free" = weapons free. +-- @return #RAT RAT self object. +function RAT:SetROE(roe) + self:F2(roe) + if roe=="return" then + self.roe=RAT.ROE.returnfire + elseif roe=="free" then + self.roe=RAT.ROE.weaponfree + else + self.roe=RAT.ROE.weaponhold + end + return self +end + +--- Set reaction to threat (ROT). Default is no reaction, i.e. aircraft will simply ignore all enemies. +-- @param #RAT self +-- @param #string rot "noreaction" = no reaction to threats, "passive" = passive defence, "evade" = evade enemy attacks. +-- @return #RAT RAT self object. +function RAT:SetROT(rot) + self:F2(rot) + if rot=="passive" then + self.rot=RAT.ROT.passive + elseif rot=="evade" then + self.rot=RAT.ROT.evade + else + self.rot=RAT.ROT.noreaction + end + return self +end + +--- Set the name of the F10 submenu. Default is the name of the template group. +-- @param #RAT self +-- @param #string name Submenu name. +-- @return #RAT RAT self object. +function RAT:MenuName(name) + self:F2(name) + self.SubMenuName=tostring(name) + return self +end + +--- Enable ATC, which manages the landing queue for RAT aircraft if they arrive simultaniously at the same airport. +-- @param #RAT self +-- @param #boolean switch Enable ATC (true) or Disable ATC (false). No argument means ATC enabled. +-- @return #RAT RAT self object. +function RAT:EnableATC(switch) + self:F2(switch) + if switch==nil then + switch=true + end + self.ATCswitch=switch + return self +end + +--- Turn messages from ATC on or off. Default is on. This setting effects all RAT objects and groups! +-- @param #RAT self +-- @param #boolean switch Enable (true) or disable (false) messages from ATC. +-- @return #RAT RAT self object. +function RAT:ATC_Messages(switch) + self:F2(switch) + if switch==nil then + switch=true + end + RAT.ATC.messages=switch + return self +end + +--- Max number of planes that get landing clearance of the RAT ATC. This setting effects all RAT objects and groups! +-- @param #RAT self +-- @param #number n Number of aircraft that are allowed to land simultaniously. Default is 2. +-- @return #RAT RAT self object. +function RAT:ATC_Clearance(n) + self:F2(n) + RAT.ATC.Nclearance=n or 2 + return self +end + +--- Delay between granting landing clearance for simultanious landings. This setting effects all RAT objects and groups! +-- @param #RAT self +-- @param #number time Delay time when the next aircraft will get landing clearance event if the previous one did not land yet. Default is 240 sec. +-- @return #RAT RAT self object. +function RAT:ATC_Delay(time) + self:F2(time) + RAT.ATC.delay=time or 240 + return self +end + +--- Set minimum distance between departure and destination. Default is 5 km. +-- Minimum distance should not be smaller than maybe ~100 meters to ensure that departure and destination are different. +-- @param #RAT self +-- @param #number dist Distance in km. +-- @return #RAT RAT self object. +function RAT:SetMinDistance(dist) + self:F2(dist) + -- Distance in meters. Absolute minimum is 500 m. + self.mindist=math.max(100, dist*1000) + return self +end + +--- Set maximum distance between departure and destination. Default is 5000 km but aircarft range is also taken into account automatically. +-- @param #RAT self +-- @param #number dist Distance in km. +-- @return #RAT RAT self object. +function RAT:SetMaxDistance(dist) + self:F2(dist) + -- Distance in meters. + self.maxdist=dist*1000 + return self +end + +--- Turn debug messages on or off. Default is off. +-- @param #RAT self +-- @param #boolean switch Turn debug on=true or off=false. No argument means on. +-- @return #RAT RAT self object. +function RAT:_Debug(switch) + self:F2(switch) + if switch==nil then + switch=true + end + self.Debug=switch + return self +end + +--- Enable debug mode. More output in dcs.log file and onscreen messages to all. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:Debugmode() + self:F2() + self.Debug=true + return self +end + +--- Aircraft report status update messages along the route. +-- @param #RAT self +-- @param #boolean switch Swtich reports on (true) or off (false). No argument is on. +-- @return #RAT RAT self object. +function RAT:StatusReports(switch) + self:F2(switch) + if switch==nil then + switch=true + end + self.reportstatus=switch + return self +end + +--- Place markers of waypoints on the F10 map. Default is off. +-- @param #RAT self +-- @param #boolean switch true=yes, false=no. +-- @return #RAT RAT self object. +function RAT:PlaceMarkers(switch) + self:F2(switch) + if switch==nil then + switch=true + end + self.placemarkers=switch + return self +end + +--- Set flight level. Setting this value will overrule all other logic. Aircraft will try to fly at this height regardless. +-- @param #RAT self +-- @param #number FL Fight Level in hundrets of feet. E.g. FL200 = 20000 ft ASL. +-- @return #RAT RAT self object. +function RAT:SetFL(FL) + self:F2(FL) + FL=FL or self.FLcruise + FL=math.max(FL,0) + self.FLuser=FL*RAT.unit.FL2m + return self +end + +--- Set max flight level. Setting this value will overrule all other logic. Aircraft will try to fly at less than this FL regardless. +-- @param #RAT self +-- @param #number FL Maximum Fight Level in hundrets of feet. +-- @return #RAT RAT self object. +function RAT:SetFLmax(FL) + self:F2(FL) + self.FLmaxuser=FL*RAT.unit.FL2m + return self +end + +--- Set max cruising altitude above sea level. +-- @param #RAT self +-- @param #number alt Altitude ASL in meters. +-- @return #RAT RAT self object. +function RAT:SetMaxCruiseAltitude(alt) + self:F2(alt) + self.FLmaxuser=alt + return self +end + +--- Set min flight level. Setting this value will overrule all other logic. Aircraft will try to fly at higher than this FL regardless. +-- @param #RAT self +-- @param #number FL Maximum Fight Level in hundrets of feet. +-- @return #RAT RAT self object. +function RAT:SetFLmin(FL) + self:F2(FL) + self.FLminuser=FL*RAT.unit.FL2m + return self +end + +--- Set min cruising altitude above sea level. +-- @param #RAT self +-- @param #number alt Altitude ASL in meters. +-- @return #RAT RAT self object. +function RAT:SetMinCruiseAltitude(alt) + self:F2(alt) + self.FLminuser=alt + return self +end + +--- Set flight level of cruising part. This is still be checked for consitancy with selected route and prone to radomization. +-- Default is FL200 for planes and FL005 for helicopters. +-- @param #RAT self +-- @param #number FL Flight level in hundrets of feet. E.g. FL200 = 20000 ft ASL. +-- @return #RAT RAT self object. +function RAT:SetFLcruise(FL) + self:F2(FL) + self.FLcruise=FL*RAT.unit.FL2m + return self +end + +--- Set cruising altitude. This is still be checked for consitancy with selected route and prone to radomization. +-- @param #RAT self +-- @param #number alt Cruising altitude ASL in meters. +-- @return #RAT RAT self object. +function RAT:SetCruiseAltitude(alt) + self:F2(alt) + self.FLcruise=alt + return self +end + +--- Set onboard number prefix. Same as setting "TAIL #" in the mission editor. Note that if you dont use this function, the values defined in the template group of the ME are taken. +-- @param #RAT self +-- @param #string tailnumprefix String of the tail number prefix. If flight consists of more than one aircraft, two digits are appended automatically, i.e. 001, 002, ... +-- @param #number zero (Optional) Starting value of the automatically appended numbering of aircraft within a flight. Default is 0. +-- @return #RAT RAT self object. +function RAT:SetOnboardNum(tailnumprefix, zero) + self:F2({tailnumprefix=tailnumprefix, zero=zero}) + self.onboardnum=tailnumprefix + if zero ~= nil then + self.onboardnum0=zero + end + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Private functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Initialize basic parameters of the aircraft based on its (template) group in the mission editor. +-- @param #RAT self +-- @param DCS#Group DCSgroup Group of the aircraft in the mission editor. +function RAT:_InitAircraft(DCSgroup) + self:F2(DCSgroup) + + local DCSunit=DCSgroup:getUnit(1) + local DCSdesc=DCSunit:getDesc() + local DCScategory=DCSgroup:getCategory() + local DCStype=DCSunit:getTypeName() + + -- set category + if DCScategory==Group.Category.AIRPLANE then + self.category=RAT.cat.plane + elseif DCScategory==Group.Category.HELICOPTER then + self.category=RAT.cat.heli + else + self.category="other" + self:E(RAT.id.."ERROR: Group of RAT is neither airplane nor helicopter!") + end + + -- Get type of aircraft. + self.aircraft.type=DCStype + + -- inital fuel in % + self.aircraft.fuel=DCSunit:getFuel() + + -- operational range in NM converted to m + self.aircraft.Rmax = DCSdesc.range*RAT.unit.nm2m + + -- effective range taking fuel into accound and a 5% reserve + self.aircraft.Reff = self.aircraft.Rmax*self.aircraft.fuel*0.95 + + -- max airspeed from group + self.aircraft.Vmax = DCSdesc.speedMax + + -- max climb speed in m/s + self.aircraft.Vymax=DCSdesc.VyMax + + -- service ceiling in meters + self.aircraft.ceiling=DCSdesc.Hmax + + -- Store all descriptors. + --self.aircraft.descriptors=DCSdesc + + -- aircraft dimensions + self.aircraft.length=DCSdesc.box.max.x + self.aircraft.height=DCSdesc.box.max.y + self.aircraft.width=DCSdesc.box.max.z + self.aircraft.box=math.max(self.aircraft.length,self.aircraft.width) + + -- info message + local text=string.format("\n******************************************************\n") + text=text..string.format("Aircraft parameters:\n") + text=text..string.format("Template group = %s\n", self.SpawnTemplatePrefix) + text=text..string.format("Alias = %s\n", self.alias) + text=text..string.format("Category = %s\n", self.category) + text=text..string.format("Type = %s\n", self.aircraft.type) + text=text..string.format("Length (x) = %6.1f m\n", self.aircraft.length) + text=text..string.format("Width (z) = %6.1f m\n", self.aircraft.width) + text=text..string.format("Height (y) = %6.1f m\n", self.aircraft.height) + text=text..string.format("Max air speed = %6.1f m/s\n", self.aircraft.Vmax) + text=text..string.format("Max climb speed = %6.1f m/s\n", self.aircraft.Vymax) + text=text..string.format("Initial Fuel = %6.1f\n", self.aircraft.fuel*100) + text=text..string.format("Max range = %6.1f km\n", self.aircraft.Rmax/1000) + text=text..string.format("Eff range = %6.1f km (with 95 percent initial fuel amount)\n", self.aircraft.Reff/1000) + text=text..string.format("Ceiling = %6.1f km = FL%3.0f\n", self.aircraft.ceiling/1000, self.aircraft.ceiling/RAT.unit.FL2m) + text=text..string.format("******************************************************\n") + self:T(RAT.id..text) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Spawn the AI aircraft with a route. +-- Sets the departure and destination airports and waypoints. +-- Modifies the spawn template. +-- Sets ROE/ROT. +-- Initializes the ratcraft array and group menu. +-- @param #RAT self +-- @param #string _departure (Optional) Name of departure airbase. +-- @param #string _destination (Optional) Name of destination airbase. +-- @param #number _takeoff Takeoff type id. +-- @param #number _landing Landing type id. +-- @param #string _livery Livery to use for this group. +-- @param #table _waypoint First waypoint to be used (for continue journey, commute, etc). +-- @param Core.Point#COORDINATE _lastpos (Optional) Position where the aircraft will be spawned. +-- @param #number _nrespawn Number of already performed respawn attempts (e.g. spawning on runway bug). +-- @param #table parkingdata Explicitly specify the parking spots when spawning at an airport. +-- @return #number Spawn index. +function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _livery, _waypoint, _lastpos, _nrespawn, parkingdata) + self:F({rat=RAT.id, departure=_departure, destination=_destination, takeoff=_takeoff, landing=_landing, livery=_livery, waypoint=_waypoint, lastpos=_lastpos, nrespawn=_nrespawn}) + + -- Set takeoff type. + local takeoff=self.takeoff + local landing=self.landing + + -- Overrule takeoff/landing by what comes in. + if _takeoff then + takeoff=_takeoff + end + if _landing then + landing=_landing + end + + -- Random choice between cold and hot. + if takeoff==RAT.wp.coldorhot then + local temp={RAT.wp.cold, RAT.wp.hot} + takeoff=temp[math.random(2)] + end + + -- Number of respawn attempts after spawning on runway. + local nrespawn=0 + if _nrespawn then + nrespawn=_nrespawn + end + + -- Set flight plan. + local departure, destination, waypoints, WPholding, WPfinal = self:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) + + -- Return nil if we could not find a departure destination or waypoints + if not (departure and destination and waypoints) then + return nil + end + + -- Set (another) livery. + local livery + if _livery then + -- Take livery from previous flight (continue journey). + livery=_livery + elseif self.livery then + -- Choose random livery. + livery=self.livery[math.random(#self.livery)] + local text=string.format("Chosen livery for group %s: %s", self:_AnticipatedGroupName(), livery) + self:T(RAT.id..text) + else + livery=nil + end + + -- Modify the spawn template to follow the flight plan. + local successful=self:_ModifySpawnTemplate(waypoints, livery, _lastpos, departure, takeoff, parkingdata) + if not successful then + return nil + end + + -- Actually spawn the group. + local group=self:SpawnWithIndex(self.SpawnIndex) -- Wrapper.Group#GROUP + + -- Increase counter of alive groups (also uncontrolled ones). + self.alive=self.alive+1 + self:T(RAT.id..string.format("Alive groups counter now = %d.",self.alive)) + + -- ATC is monitoring this flight (if it is supposed to land). + if self.ATCswitch and landing==RAT.wp.landing then + if self.returnzone then + self:_ATCAddFlight(group:GetName(), departure:GetName()) + else + self:_ATCAddFlight(group:GetName(), destination:GetName()) + end + end + + -- Place markers of waypoints on F10 map. + if self.placemarkers then + self:_PlaceMarkers(waypoints, self.SpawnIndex) + end + + -- Set group to be invisible. + if self.invisible then + self:_CommandInvisible(group, true) + end + + -- Set group to be immortal. + if self.immortal then + self:_CommandImmortal(group, true) + end + + -- Set group to be immortal. + if self.eplrs then + group:CommandEPLRS(true, 1) + end + + -- Set ROE, default is "weapon hold". + self:_SetROE(group, self.roe) + + -- Set ROT, default is "no reaction". + self:_SetROT(group, self.rot) + + -- Init ratcraft array. + self.ratcraft[self.SpawnIndex]={} + self.ratcraft[self.SpawnIndex]["group"]=group + self.ratcraft[self.SpawnIndex]["destination"]=destination + self.ratcraft[self.SpawnIndex]["departure"]=departure + self.ratcraft[self.SpawnIndex]["waypoints"]=waypoints + self.ratcraft[self.SpawnIndex]["airborne"]=group:InAir() + self.ratcraft[self.SpawnIndex]["nunits"]=group:GetInitialSize() + -- Time and position on ground. For check if aircraft is stuck somewhere. + if group:InAir() then + self.ratcraft[self.SpawnIndex]["Tground"]=nil + self.ratcraft[self.SpawnIndex]["Pground"]=nil + self.ratcraft[self.SpawnIndex]["Uground"]=nil + self.ratcraft[self.SpawnIndex]["Tlastcheck"]=nil + else + self.ratcraft[self.SpawnIndex]["Tground"]=timer.getTime() + self.ratcraft[self.SpawnIndex]["Pground"]=group:GetCoordinate() + self.ratcraft[self.SpawnIndex]["Uground"]={} + for _,_unit in pairs(group:GetUnits()) do + local _unitname=_unit:GetName() + self.ratcraft[self.SpawnIndex]["Uground"][_unitname]=_unit:GetCoordinate() + end + self.ratcraft[self.SpawnIndex]["Tlastcheck"]=timer.getTime() + end + -- Initial and current position. For calculating the travelled distance. + self.ratcraft[self.SpawnIndex]["P0"]=group:GetCoordinate() + self.ratcraft[self.SpawnIndex]["Pnow"]=group:GetCoordinate() + self.ratcraft[self.SpawnIndex]["Distance"]=0 + + -- Each aircraft gets its own takeoff type. + self.ratcraft[self.SpawnIndex].takeoff=takeoff + self.ratcraft[self.SpawnIndex].landing=landing + self.ratcraft[self.SpawnIndex].wpholding=WPholding + self.ratcraft[self.SpawnIndex].wpfinal=WPfinal + + -- Aircraft is active or spawned in uncontrolled state. + self.ratcraft[self.SpawnIndex].active=not self.uncontrolled + + -- Set status to spawned. This will be overwritten in birth event. + self.ratcraft[self.SpawnIndex]["status"]=RAT.status.Spawned + + -- Livery + self.ratcraft[self.SpawnIndex].livery=livery + + -- If this switch is set to true, the aircraft will be despawned the next time the status function is called. + self.ratcraft[self.SpawnIndex].despawnme=false + + -- Number of preformed spawn attempts for this group. + self.ratcraft[self.SpawnIndex].nrespawn=nrespawn + + -- Create submenu for this group. + if self.f10menu then + local name=self.aircraft.type.." ID "..tostring(self.SpawnIndex) + -- F10/RAT//Group X + self.Menu[self.SubMenuName].groups[self.SpawnIndex]=MENU_MISSION:New(name, self.Menu[self.SubMenuName].groups) + -- F10/RAT//Group X/Set ROE + self.Menu[self.SubMenuName].groups[self.SpawnIndex]["roe"]=MENU_MISSION:New("Set ROE", self.Menu[self.SubMenuName].groups[self.SpawnIndex]) + MENU_MISSION_COMMAND:New("Weapons hold", self.Menu[self.SubMenuName].groups[self.SpawnIndex]["roe"], self._SetROE, self, group, RAT.ROE.weaponhold) + MENU_MISSION_COMMAND:New("Weapons free", self.Menu[self.SubMenuName].groups[self.SpawnIndex]["roe"], self._SetROE, self, group, RAT.ROE.weaponfree) + MENU_MISSION_COMMAND:New("Return fire", self.Menu[self.SubMenuName].groups[self.SpawnIndex]["roe"], self._SetROE, self, group, RAT.ROE.returnfire) + -- F10/RAT//Group X/Set ROT + self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"]=MENU_MISSION:New("Set ROT", self.Menu[self.SubMenuName].groups[self.SpawnIndex]) + MENU_MISSION_COMMAND:New("No reaction", self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"], self._SetROT, self, group, RAT.ROT.noreaction) + MENU_MISSION_COMMAND:New("Passive defense", self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"], self._SetROT, self, group, RAT.ROT.passive) + MENU_MISSION_COMMAND:New("Evade on fire", self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"], self._SetROT, self, group, RAT.ROT.evade) + -- F10/RAT//Group X/ + MENU_MISSION_COMMAND:New("Despawn group", self.Menu[self.SubMenuName].groups[self.SpawnIndex], self._Despawn, self, group) + MENU_MISSION_COMMAND:New("Place markers", self.Menu[self.SubMenuName].groups[self.SpawnIndex], self._PlaceMarkers, self, waypoints, self.SpawnIndex) + MENU_MISSION_COMMAND:New("Status report", self.Menu[self.SubMenuName].groups[self.SpawnIndex], self.Status, self, true, self.SpawnIndex) + end + + return self.SpawnIndex +end + + +--- Clear flight for landing. Sets tigger value to 1. +-- @param #RAT self +-- @param #string name Name of flight to be cleared for landing. +function RAT:ClearForLanding(name) + trigger.action.setUserFlag(name, 1) + local flagvalue=trigger.misc.getUserFlag(name) + self:T(RAT.id.."ATC: User flag value (landing) for "..name.." set to "..flagvalue) +end + +--- Respawn a group. +-- @param #RAT self +-- @param #number index Spawn index. +-- @param Core.Point#COORDINATE lastpos Last known position of the group. +-- @param #number delay Delay before respawn +function RAT:_Respawn(index, lastpos, delay) + + -- Get the spawn index from group + --local index=self:GetSpawnIndexFromGroup(group) + + -- Get departure and destination from previous journey. + local departure=self.ratcraft[index].departure + local destination=self.ratcraft[index].destination + local takeoff=self.ratcraft[index].takeoff + local landing=self.ratcraft[index].landing + local livery=self.ratcraft[index].livery + local lastwp=self.ratcraft[index].waypoints[#self.ratcraft[index].waypoints] + --local lastpos=group:GetCoordinate() + + local _departure=nil + local _destination=nil + local _takeoff=nil + local _landing=nil + local _livery=nil + local _lastwp=nil + local _lastpos=nil + + if self.continuejourney then + + -- We continue our journey from the old departure airport. + _departure=destination:GetName() + + -- Use the same livery for next aircraft. + _livery=livery + + -- Last known position of the aircraft, which should be the sparking spot location. + -- Note: we have to check that it was supposed to land and not respawned directly after landing or after takeoff. + -- TODO: Need to think if continuejourney with respawn_after_takeoff actually makes sense. + if landing==RAT.wp.landing and lastpos and not (self.respawn_at_landing or self.respawn_after_takeoff) then + -- Check that we have an airport or FARP but not a ship (which would be categroy 1). + if destination:GetCategory()==4 then + _lastpos=lastpos + end + end + + if self.destinationzone then + + -- Case: X --> Zone --> Zone --> Zone + _takeoff=RAT.wp.air + _landing=RAT.wp.air + + elseif self.returnzone then + + -- Case: X --> Zone --> X, X --> Zone --> X + -- We flew to a zone and back. Takeoff type does not change. + _takeoff=self.takeoff + + -- If we took of in air we also want to land "in air". + if self.takeoff==RAT.wp.air then + _landing=RAT.wp.air + else + _landing=RAT.wp.landing + end + + -- Departure stays the same. (The destination is the zone here.) + _departure=departure:GetName() + + else + + -- Default case. Takeoff and landing type does not change. + _takeoff=self.takeoff + _landing=self.landing + + end + + elseif self.commute then + + -- We commute between departure and destination. + + if self.starshape==true then + if destination:GetName()==self.homebase then + -- We are at our home base ==> destination is again randomly selected. + _departure=self.homebase + _destination=nil -- destination will be set anew + else + -- We are not a our home base ==> we fly back to our home base. + _departure=destination:GetName() + _destination=self.homebase + end + else + -- Simply switch departure and destination. + _departure=destination:GetName() + _destination=departure:GetName() + end + + -- Use the same livery for next aircraft. + _livery=livery + + -- Last known position of the aircraft, which should be the sparking spot location. + -- Note: we have to check that it was supposed to land and not respawned directly after landing or after takeoff. + -- TODO: Need to think if commute with respawn_after_takeoff actually makes sense. + if landing==RAT.wp.landing and lastpos and not (self.respawn_at_landing or self.respawn_after_takeoff) then + -- Check that we have landed on an airport or FARP but not a ship (which would be categroy 1). + if destination:GetCategory()==4 then + _lastpos=lastpos + end + end + + -- Handle takeoff type. + if self.destinationzone then + -- self.takeoff is either RAT.wp.air or RAT.wp.cold + -- self.landing is RAT.wp.Air + + if self.takeoff==RAT.wp.air then + + -- Case: Zone <--> Zone (both have takeoff air) + _takeoff=RAT.wp.air -- = self.takeoff (because we just checked) + _landing=RAT.wp.air -- = self.landing (because destinationzone) + + else + + -- Case: Airport <--> Zone + if takeoff==RAT.wp.air then + -- Last takeoff was air so we are at the airport now, takeoff is from ground. + _takeoff=self.takeoff -- must be either hot/cold/runway/hotcold + _landing=RAT.wp.air -- must be air = self.landing (because destinationzone) + else + -- Last takeoff was on ground so we are at a zone now ==> takeoff in air, landing at airport. + _takeoff=RAT.wp.air + _landing=RAT.wp.landing + end + + end + + elseif self.returnzone then + + -- We flew to a zone and back. No need to swap departure and destination. + _departure=departure:GetName() + _destination=destination:GetName() + + -- Takeoff and landing should also not change. + _takeoff=self.takeoff + _landing=self.landing + + end + + end + + -- Take the last waypoint as initial waypoint for next plane. + if _takeoff==RAT.wp.air and (self.continuejourney or self.commute) then + _lastwp=lastwp + end + + -- Debug + self:T2({departure=_departure, destination=_destination, takeoff=_takeoff, landing=_landing, livery=_livery, lastwp=_lastwp}) + + -- We should give it at least 3 sec since this seems to be the time until free parking spots after despawn are available again (Sirri Island test). + local respawndelay + if delay then + respawndelay=delay + elseif self.respawn_delay then + respawndelay=self.respawn_delay+3 -- despawn happens after self.respawndelay. We add another 3 sec for free parking. + else + respawndelay=3 + end + + -- Spawn new group. + local arg={} + arg.self=self + arg.departure=_departure + arg.destination=_destination + arg.takeoff=_takeoff + arg.landing=_landing + arg.livery=_livery + arg.lastwp=_lastwp + arg.lastpos=_lastpos + self:T(RAT.id..string.format("%s delayed respawn in %.1f seconds.", self.alias, respawndelay)) + SCHEDULER:New(nil, self._SpawnWithRouteTimer, {arg}, respawndelay) + +end + +--- Delayed spawn function called by scheduler. +-- @param #RAT self +-- @param #table arg Parameters: arg.self, arg.departure, arg.destination, arg.takeoff, arg.landing, arg.livery, arg.lastwp, arg.lastpos +function RAT._SpawnWithRouteTimer(arg) + RAT._SpawnWithRoute(arg.self, arg.departure, arg.destination, arg.takeoff, arg.landing, arg.livery, arg.lastwp, arg.lastpos) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set the route of the AI plane. Due to DCS landing bug, this has to be done before the unit is spawned. +-- @param #RAT self +-- @param #number takeoff Takeoff type. Could also be air start. +-- @param #number landing Landing type. Could also be a destination in air. +-- @param Wrapper.Airport#AIRBASE _departure (Optional) Departure airbase. +-- @param Wrapper.Airport#AIRBASE _destination (Optional) Destination airbase. +-- @param #table _waypoint Initial waypoint. +-- @return Wrapper.Airport#AIRBASE Departure airbase. +-- @return Wrapper.Airport#AIRBASE Destination airbase. +-- @return #table Table of flight plan waypoints. +-- @return #nil If no valid departure or destination airport could be found. +function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) + + -- Max cruise speed. + local VxCruiseMax + if self.Vcruisemax then + -- User input. + VxCruiseMax = math.min(self.Vcruisemax, self.aircraft.Vmax) + else + -- Max cruise speed 90% of Vmax or 900 km/h whichever is lower. + VxCruiseMax = math.min(self.aircraft.Vmax*0.90, 250) + end + + -- Min cruise speed 70% of max cruise or 600 km/h whichever is lower. + local VxCruiseMin = math.min(VxCruiseMax*0.70, 166) + + -- Cruise speed (randomized). Expectation value at midpoint between min and max. + local VxCruise = UTILS.RandomGaussian((VxCruiseMax-VxCruiseMin)/2+VxCruiseMin, (VxCruiseMax-VxCruiseMax)/4, VxCruiseMin, VxCruiseMax) + + -- Climb speed 90% ov Vmax but max 720 km/h. + local VxClimb = math.min(self.aircraft.Vmax*0.90, 200) + + -- Descent speed 60% of Vmax but max 500 km/h. + local VxDescent = math.min(self.aircraft.Vmax*0.60, 140) + + -- Holding speed is 90% of descent speed. + local VxHolding = VxDescent*0.9 + + -- Final leg is 90% of holding speed. + local VxFinal = VxHolding*0.9 + + -- Reasonably civil climb speed Vy=1500 ft/min = 7.6 m/s but max aircraft specific climb rate. + local VyClimb=math.min(self.Vclimb*RAT.unit.ft2meter/60, self.aircraft.Vymax) + + -- Climb angle in rad. + local AlphaClimb=math.asin(VyClimb/VxClimb) + + -- Descent angle in rad. + local AlphaDescent=math.rad(self.AlphaDescent) + + -- Expected cruise level (peak of Gaussian distribution) + local FLcruise_expect=self.FLcruise + + + -- DEPARTURE AIRPORT + -- Departure airport or zone. + local departure=nil + if _departure then + if self:_AirportExists(_departure) then + -- Check if new departure is an airport. + departure=AIRBASE:FindByName(_departure) + -- If we spawn in air, we convert departure to a zone. + if takeoff == RAT.wp.air then + departure=departure:GetZone() + end + elseif self:_ZoneExists(_departure) then + -- If it's not an airport, check whether it's a zone. + departure=ZONE:New(_departure) + else + local text=string.format("ERROR! Specified departure airport %s does not exist for %s.", _departure, self.alias) + self:E(RAT.id..text) + end + + else + departure=self:_PickDeparture(takeoff) + if self.commute and self.starshape==true and self.homebase==nil then + self.homebase=departure:GetName() + end + end + + -- Return nil if no departure could be found. + if not departure then + local text=string.format("ERROR! No valid departure airport could be found for %s.", self.alias) + self:E(RAT.id..text) + return nil + end + + -- Coordinates of departure point. + local Pdeparture + if takeoff==RAT.wp.air then + if _waypoint then + -- Use coordinates of previous flight (commute or journey). + Pdeparture=COORDINATE:New(_waypoint.x, _waypoint.alt, _waypoint.y) + else + -- For an air start, we take a random point within the spawn zone. + local vec2=departure:GetRandomVec2() + Pdeparture=COORDINATE:NewFromVec2(vec2) + end + else + Pdeparture=departure:GetCoordinate() + end + + -- Height ASL of departure point. + local H_departure + if takeoff==RAT.wp.air then + -- Absolute minimum AGL + local Hmin + if self.category==RAT.cat.plane then + Hmin=1000 + else + Hmin=50 + end + -- Departure altitude is 70% of default cruise with 30% variation and limited to 1000 m AGL (50 m for helos). + H_departure=self:_Randomize(FLcruise_expect*0.7, 0.3, Pdeparture.y+Hmin, FLcruise_expect) + if self.FLminuser then + H_departure=math.max(H_departure,self.FLminuser) + end + -- Use alt of last flight. + if _waypoint then + H_departure=_waypoint.alt + end + else + H_departure=Pdeparture.y + end + + -- Adjust min distance between departure and destination for user set min flight level. + local mindist=self.mindist + if self.FLminuser then + + -- We can conly consider the symmetric case, because no destination selected yet. + local hclimb=self.FLminuser-H_departure + local hdescent=self.FLminuser-H_departure + + -- Minimum distance for l + local Dclimb, Ddescent, Dtot=self:_MinDistance(AlphaClimb, AlphaDescent, hclimb, hdescent) + + if takeoff==RAT.wp.air and landing==RAT.wpair then + mindist=0 -- Takeoff and landing are in air. No mindist required. + elseif takeoff==RAT.wp.air then + mindist=Ddescent -- Takeoff in air. Need only space to descent. + elseif landing==RAT.wp.air then + mindist=Dclimb -- Landing "in air". Need only space to climb. + else + mindist=Dtot -- Takeoff and landing on ground. Need both space to climb and descent. + end + + -- Mindist is at least self.mindist. + mindist=math.max(self.mindist, mindist) + + local text=string.format("Adjusting min distance to %d km (for given min FL%03d)", mindist/1000, self.FLminuser/RAT.unit.FL2m) + self:T(RAT.id..text) + end + + -- DESTINATION AIRPORT + local destination=nil + if _destination then + + if self:_AirportExists(_destination) then + + destination=AIRBASE:FindByName(_destination) + if landing==RAT.wp.air or self.returnzone then + destination=destination:GetZone() + end + + elseif self:_ZoneExists(_destination) then + destination=ZONE:New(_destination) + else + local text=string.format("ERROR: Specified destination airport/zone %s does not exist for %s!", _destination, self.alias) + self:E(RAT.id.."ERROR: "..text) + end + + else + + -- This handles the case where we have a journey and the first flight is done, i.e. _departure is set. + -- If a user specified more than two destination airport explicitly, then we will stick to this. + -- Otherwise, the route is random from now on. + local random=self.random_destination + if self.continuejourney and _departure and #self.destination_ports<3 then + random=true + end + + -- In case of a returnzone the destination (i.e. return point) is always a zone. + local mylanding=landing + local acrange=self.aircraft.Reff + if self.returnzone then + mylanding=RAT.wp.air + acrange=self.aircraft.Reff/2 -- Aircraft needs to go to zone and back home. + end + + -- Pick a destination airport. + destination=self:_PickDestination(departure, Pdeparture, mindist, math.min(acrange, self.maxdist), random, mylanding) + end + + -- Return nil if no departure could be found. + if not destination then + local text=string.format("No valid destination airport could be found for %s!", self.alias) + MESSAGE:New(text, 60):ToAll() + self:E(RAT.id.."ERROR: "..text) + return nil + end + + -- Check that departure and destination are not the same. Should not happen due to mindist. + if destination:GetName()==departure:GetName() then + local text=string.format("%s: Destination and departure are identical. Airport/zone %s.", self.alias, destination:GetName()) + MESSAGE:New(text, 30):ToAll() + self:E(RAT.id.."ERROR: "..text) + end + + -- Get a random point inside zone return zone. + local Preturn + local destination_returnzone + if self.returnzone then + -- Get a random point inside zone return zone. + local vec2=destination:GetRandomVec2() + Preturn=COORDINATE:NewFromVec2(vec2) + -- Returnzone becomes destination. + destination_returnzone=destination + -- Set departure to destination. + destination=departure + end + + -- Get destination coordinate. Either in a zone or exactly at the airport. + local Pdestination + if landing==RAT.wp.air then + local vec2=destination:GetRandomVec2() + Pdestination=COORDINATE:NewFromVec2(vec2) + else + Pdestination=destination:GetCoordinate() + end + + -- Height ASL of destination airport/zone. + local H_destination=Pdestination.y + + -- DESCENT/HOLDING POINT + -- Get a random point between 5 and 10 km away from the destination. + local Rhmin=8000 + local Rhmax=20000 + if self.category==RAT.cat.heli then + -- For helos we set a distance between 500 to 1000 m. + Rhmin=500 + Rhmax=1000 + end + + -- Coordinates of the holding point. y is the land height at that point. + local Vholding=Pdestination:GetRandomVec2InRadius(Rhmax, Rhmin) + local Pholding=COORDINATE:NewFromVec2(Vholding) + + -- AGL height of holding point. + local H_holding=Pholding.y + + -- Holding point altitude. For planes between 1600 and 2400 m AGL. For helos 160 to 240 m AGL. + local h_holding + if self.category==RAT.cat.plane then + h_holding=1200 + else + h_holding=150 + end + h_holding=self:_Randomize(h_holding, 0.2) + + -- This is the actual height ASL of the holding point we want to fly to + local Hh_holding=H_holding+h_holding + + -- When we dont land, we set the holding altitude to the departure or cruise alt. + -- This is used in the calculations. + if landing==RAT.wp.air then + Hh_holding=H_departure + end + + -- Distance from holding point to final destination. + local d_holding=Pholding:Get2DDistance(Pdestination) + + -- GENERAL + local heading + local d_total + if self.returnzone then + + -- Heading from departure to destination in return zone. + heading=self:_Course(Pdeparture, Preturn) + + -- Total distance to return zone and back. + d_total=Pdeparture:Get2DDistance(Preturn) + Preturn:Get2DDistance(Pholding) + + else + -- Heading from departure to holding point of destination. + heading=self:_Course(Pdeparture, Pholding) + + -- Total distance between departure and holding point near destination. + d_total=Pdeparture:Get2DDistance(Pholding) + end + + -- Max height in case of air start, i.e. if we only would descent to holding point for the given distance. + if takeoff==RAT.wp.air then + local H_departure_max + if landing==RAT.wp.air then + H_departure_max = H_departure -- If we fly to a zone, there is no descent necessary. + else + H_departure_max = d_total * math.tan(AlphaDescent) + Hh_holding + end + H_departure=math.min(H_departure, H_departure_max) + end + + -------------------------------------------- + + -- Height difference between departure and destination. + local deltaH=math.abs(H_departure-Hh_holding) + + -- Slope between departure and destination. + local phi = math.atan(deltaH/d_total) + + -- Adjusted climb/descent angles. + local phi_climb + local phi_descent + if (H_departure > Hh_holding) then + phi_climb=AlphaClimb+phi + phi_descent=AlphaDescent-phi + else + phi_climb=AlphaClimb-phi + phi_descent=AlphaDescent+phi + end + + -- Total distance including slope. + local D_total + if self.returnzone then + D_total = math.sqrt(deltaH*deltaH+d_total/2*d_total/2) + else + D_total = math.sqrt(deltaH*deltaH+d_total*d_total) + end + + -- SSA triangle for sloped case. + local gamma=math.rad(180)-phi_climb-phi_descent + local a = D_total*math.sin(phi_climb)/math.sin(gamma) + local b = D_total*math.sin(phi_descent)/math.sin(gamma) + local hphi_max = b*math.sin(phi_climb) + local hphi_max2 = a*math.sin(phi_descent) + + -- Height of triangle. + local h_max1 = b*math.sin(AlphaClimb) + local h_max2 = a*math.sin(AlphaDescent) + + -- Max height relative to departure or destination. + local h_max + if (H_departure > Hh_holding) then + h_max=math.min(h_max1, h_max2) + else + h_max=math.max(h_max1, h_max2) + end + + -- Max flight level aircraft can reach for given angles and distance. + local FLmax = h_max+H_departure + + --CRUISE + -- Min cruise alt is just above holding point at destination or departure height, whatever is larger. + local FLmin=math.max(H_departure, Hh_holding) + + -- For helicopters we take cruise alt between 50 to 1000 meters above ground. Default cruise alt is ~150 m. + if self.category==RAT.cat.heli then + FLmin=math.max(H_departure, H_destination)+50 + FLmax=math.max(H_departure, H_destination)+1000 + end + + -- Ensure that FLmax not above its service ceiling. + FLmax=math.min(FLmax, self.aircraft.ceiling) + + -- Overrule setting if user specified min/max flight level explicitly. + if self.FLminuser then + FLmin=math.max(self.FLminuser, FLmin) -- Still take care that we dont fly too high. + end + if self.FLmaxuser then + FLmax=math.min(self.FLmaxuser, FLmax) -- Still take care that we dont fly too low. + end + + -- If the route is very short we set FLmin a bit lower than FLmax. + if FLmin>FLmax then + FLmin=FLmax + end + + -- Expected cruise altitude - peak of gaussian distribution. + if FLcruise_expectFLmax then + FLcruise_expect=FLmax + end + + -- Set cruise altitude. Selected from Gaussian distribution but limited to FLmin and FLmax. + local FLcruise=UTILS.RandomGaussian(FLcruise_expect, math.abs(FLmax-FLmin)/4, FLmin, FLmax) + + -- Overrule setting if user specified a flight level explicitly. + if self.FLuser then + FLcruise=self.FLuser + -- Still cruise alt should be with parameters! + FLcruise=math.max(FLcruise, FLmin) + FLcruise=math.min(FLcruise, FLmax) + end + + -- Climb and descent heights. + local h_climb = FLcruise - H_departure + local h_descent = FLcruise - Hh_holding + + -- Distances. + local d_climb = h_climb/math.tan(AlphaClimb) + local d_descent = h_descent/math.tan(AlphaDescent) + local d_cruise = d_total-d_climb-d_descent + + -- debug message + local text=string.format("\n******************************************************\n") + text=text..string.format("Template = %s\n", self.SpawnTemplatePrefix) + text=text..string.format("Alias = %s\n", self.alias) + text=text..string.format("Group name = %s\n\n", self:_AnticipatedGroupName()) + text=text..string.format("Speeds:\n") + text=text..string.format("VxCruiseMin = %6.1f m/s = %5.1f km/h\n", VxCruiseMin, VxCruiseMin*3.6) + text=text..string.format("VxCruiseMax = %6.1f m/s = %5.1f km/h\n", VxCruiseMax, VxCruiseMax*3.6) + text=text..string.format("VxCruise = %6.1f m/s = %5.1f km/h\n", VxCruise, VxCruise*3.6) + text=text..string.format("VxClimb = %6.1f m/s = %5.1f km/h\n", VxClimb, VxClimb*3.6) + text=text..string.format("VxDescent = %6.1f m/s = %5.1f km/h\n", VxDescent, VxDescent*3.6) + text=text..string.format("VxHolding = %6.1f m/s = %5.1f km/h\n", VxHolding, VxHolding*3.6) + text=text..string.format("VxFinal = %6.1f m/s = %5.1f km/h\n", VxFinal, VxFinal*3.6) + text=text..string.format("VyClimb = %6.1f m/s\n", VyClimb) + text=text..string.format("\nDistances:\n") + text=text..string.format("d_climb = %6.1f km\n", d_climb/1000) + text=text..string.format("d_cruise = %6.1f km\n", d_cruise/1000) + text=text..string.format("d_descent = %6.1f km\n", d_descent/1000) + text=text..string.format("d_holding = %6.1f km\n", d_holding/1000) + text=text..string.format("d_total = %6.1f km\n", d_total/1000) + text=text..string.format("\nHeights:\n") + text=text..string.format("H_departure = %6.1f m ASL\n", H_departure) + text=text..string.format("H_destination = %6.1f m ASL\n", H_destination) + text=text..string.format("H_holding = %6.1f m ASL\n", H_holding) + text=text..string.format("h_climb = %6.1f m\n", h_climb) + text=text..string.format("h_descent = %6.1f m\n", h_descent) + text=text..string.format("h_holding = %6.1f m\n", h_holding) + text=text..string.format("delta H = %6.1f m\n", deltaH) + text=text..string.format("FLmin = %6.1f m ASL = FL%03d\n", FLmin, FLmin/RAT.unit.FL2m) + text=text..string.format("FLcruise = %6.1f m ASL = FL%03d\n", FLcruise, FLcruise/RAT.unit.FL2m) + text=text..string.format("FLmax = %6.1f m ASL = FL%03d\n", FLmax, FLmax/RAT.unit.FL2m) + text=text..string.format("\nAngles:\n") + text=text..string.format("Alpha climb = %6.2f Deg\n", math.deg(AlphaClimb)) + text=text..string.format("Alpha descent = %6.2f Deg\n", math.deg(AlphaDescent)) + text=text..string.format("Phi (slope) = %6.2f Deg\n", math.deg(phi)) + text=text..string.format("Phi climb = %6.2f Deg\n", math.deg(phi_climb)) + text=text..string.format("Phi descent = %6.2f Deg\n", math.deg(phi_descent)) + if self.Debug then + -- Max heights and distances if we would travel at FLmax. + local h_climb_max = FLmax - H_departure + local h_descent_max = FLmax - Hh_holding + local d_climb_max = h_climb_max/math.tan(AlphaClimb) + local d_descent_max = h_descent_max/math.tan(AlphaDescent) + local d_cruise_max = d_total-d_climb_max-d_descent_max + text=text..string.format("Heading = %6.1f Deg\n", heading) + text=text..string.format("\nSSA triangle:\n") + text=text..string.format("D_total = %6.1f km\n", D_total/1000) + text=text..string.format("gamma = %6.1f Deg\n", math.deg(gamma)) + text=text..string.format("a = %6.1f m\n", a) + text=text..string.format("b = %6.1f m\n", b) + text=text..string.format("hphi_max = %6.1f m\n", hphi_max) + text=text..string.format("hphi_max2 = %6.1f m\n", hphi_max2) + text=text..string.format("h_max1 = %6.1f m\n", h_max1) + text=text..string.format("h_max2 = %6.1f m\n", h_max2) + text=text..string.format("h_max = %6.1f m\n", h_max) + text=text..string.format("\nMax heights and distances:\n") + text=text..string.format("d_climb_max = %6.1f km\n", d_climb_max/1000) + text=text..string.format("d_cruise_max = %6.1f km\n", d_cruise_max/1000) + text=text..string.format("d_descent_max = %6.1f km\n", d_descent_max/1000) + text=text..string.format("h_climb_max = %6.1f m\n", h_climb_max) + text=text..string.format("h_descent_max = %6.1f m\n", h_descent_max) + end + text=text..string.format("******************************************************\n") + self:T2(RAT.id..text) + + -- Ensure that cruise distance is positve. Can be slightly negative in special cases. And we don't want to turn back. + if d_cruise<0 then + d_cruise=100 + end + + -- Waypoints and coordinates + local wp={} + local c={} + local wpholding=nil + local wpfinal=nil + + -- Departure/Take-off + c[#c+1]=Pdeparture + wp[#wp+1]=self:_Waypoint(#wp+1, "Departure", takeoff, c[#wp+1], VxClimb, H_departure, departure) + self.waypointdescriptions[#wp]="Departure" + self.waypointstatus[#wp]=RAT.status.Departure + + -- Climb + if takeoff==RAT.wp.air then + + -- Air start. + if d_climb < 5000 or d_cruise < 5000 then + -- We omit the climb phase completely and add it to the cruise part. + d_cruise=d_cruise+d_climb + else + -- Only one waypoint at the end of climb = begin of cruise. + c[#c+1]=c[#c]:Translate(d_climb, heading) + + wp[#wp+1]=self:_Waypoint(#wp+1, "Begin of Cruise", RAT.wp.cruise, c[#wp+1], VxCruise, FLcruise) + self.waypointdescriptions[#wp]="Begin of Cruise" + self.waypointstatus[#wp]=RAT.status.Cruise + end + + else + + -- Ground start. + c[#c+1]=c[#c]:Translate(d_climb/2, heading) + c[#c+1]=c[#c]:Translate(d_climb/2, heading) + + wp[#wp+1]=self:_Waypoint(#wp+1, "Climb", RAT.wp.climb, c[#wp+1], VxClimb, H_departure+(FLcruise-H_departure)/2) + self.waypointdescriptions[#wp]="Climb" + self.waypointstatus[#wp]=RAT.status.Climb + + wp[#wp+1]=self:_Waypoint(#wp+1, "Begin of Cruise", RAT.wp.cruise, c[#wp+1], VxCruise, FLcruise) + self.waypointdescriptions[#wp]="Begin of Cruise" + self.waypointstatus[#wp]=RAT.status.Cruise + + end + + -- Cruise + + -- First add the little bit from begin of cruise to the return point. + if self.returnzone then + c[#c+1]=Preturn + wp[#wp+1]=self:_Waypoint(#wp+1, "Return Zone", RAT.wp.cruise, c[#wp+1], VxCruise, FLcruise) + self.waypointdescriptions[#wp]="Return Zone" + self.waypointstatus[#wp]=RAT.status.Uturn + end + + if landing==RAT.wp.air then + + -- Next waypoint is already the final destination. + c[#c+1]=Pdestination + wp[#wp+1]=self:_Waypoint(#wp+1, "Final Destination", RAT.wp.finalwp, c[#wp+1], VxCruise, FLcruise) + self.waypointdescriptions[#wp]="Final Destination" + self.waypointstatus[#wp]=RAT.status.Destination + + elseif self.returnzone then + + -- The little bit back to end of cruise. + c[#c+1]=c[#c]:Translate(d_cruise/2, heading-180) + wp[#wp+1]=self:_Waypoint(#wp+1, "End of Cruise", RAT.wp.cruise, c[#wp+1], VxCruise, FLcruise) + self.waypointdescriptions[#wp]="End of Cruise" + self.waypointstatus[#wp]=RAT.status.Descent + + else + + c[#c+1]=c[#c]:Translate(d_cruise, heading) + wp[#wp+1]=self:_Waypoint(#wp+1, "End of Cruise", RAT.wp.cruise, c[#wp+1], VxCruise, FLcruise) + self.waypointdescriptions[#wp]="End of Cruise" + self.waypointstatus[#wp]=RAT.status.Descent + + end + + -- Descent (only if we acually want to land) + if landing==RAT.wp.landing then + if self.returnzone then + c[#c+1]=c[#c]:Translate(d_descent/2, heading-180) + wp[#wp+1]=self:_Waypoint(#wp+1, "Descent", RAT.wp.descent, c[#wp+1], VxDescent, FLcruise-(FLcruise-(h_holding+H_holding))/2) + self.waypointdescriptions[#wp]="Descent" + self.waypointstatus[#wp]=RAT.status.DescentHolding + else + c[#c+1]=c[#c]:Translate(d_descent/2, heading) + wp[#wp+1]=self:_Waypoint(#wp+1, "Descent", RAT.wp.descent, c[#wp+1], VxDescent, FLcruise-(FLcruise-(h_holding+H_holding))/2) + self.waypointdescriptions[#wp]="Descent" + self.waypointstatus[#wp]=RAT.status.DescentHolding + end + end + + -- Holding and final destination. + if landing==RAT.wp.landing then + + -- Holding point + c[#c+1]=Pholding + wp[#wp+1]=self:_Waypoint(#wp+1, "Holding Point", RAT.wp.holding, c[#wp+1], VxHolding, H_holding+h_holding) + self.waypointdescriptions[#wp]="Holding Point" + self.waypointstatus[#wp]=RAT.status.Holding + wpholding=#wp + + -- Final destination. + c[#c+1]=Pdestination + wp[#wp+1]=self:_Waypoint(#wp+1, "Final Destination", landing, c[#wp+1], VxFinal, H_destination, destination) + self.waypointdescriptions[#wp]="Final Destination" + self.waypointstatus[#wp]=RAT.status.Destination + + end + + -- Final Waypoint + wpfinal=#wp + + -- Fill table with waypoints. + local waypoints={} + for _,p in ipairs(wp) do + table.insert(waypoints, p) + end + + -- Some info on the route. + self:_Routeinfo(waypoints, "Waypoint info in set_route:") + + -- Return departure, destination and waypoints. + if self.returnzone then + -- We return the actual zone here because returning the departure leads to problems with commute. + return departure, destination_returnzone, waypoints, wpholding, wpfinal + else + return departure, destination, waypoints, wpholding, wpfinal + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set the departure airport of the AI. If no airport name is given explicitly an airport from the coalition is chosen randomly. +-- If takeoff style is set to "air", we use zones around the airports or the zones specified by user input. +-- @param #RAT self +-- @param #number takeoff Takeoff type. +-- @return Wrapper.Airbase#AIRBASE Departure airport if spawning at airport. +-- @return Core.Zone#ZONE Departure zone if spawning in air. +function RAT:_PickDeparture(takeoff) + + -- Array of possible departure airports or zones. + local departures={} + + if self.random_departure then + + -- Airports of friendly coalitions. + for _,_airport in pairs(self.airports) do + + local airport=_airport --Wrapper.Airbase#AIRBASE + + local name=airport:GetName() + if not self:_Excluded(name) then + if takeoff==RAT.wp.air then + + table.insert(departures, airport:GetZone()) -- insert zone object. + + else + + -- Check if airbase has the right terminals. + local nspots=1 + if self.termtype~=nil then + nspots=airport:GetParkingSpotsNumber(self.termtype) + end + + if nspots>0 then + table.insert(departures, airport) -- insert airport object. + end + end + end + + end + + else + + -- Destination airports or zones specified by user. + for _,name in pairs(self.departure_ports) do + + local dep=nil + if self:_AirportExists(name) then + if takeoff==RAT.wp.air then + dep=AIRBASE:FindByName(name):GetZone() + else + dep=AIRBASE:FindByName(name) + -- Check if the airport has a valid parking spot + if self.termtype~=nil and dep~=nil then + local _dep=dep --Wrapper.Airbase#AIRBASE + local nspots=_dep:GetParkingSpotsNumber(self.termtype) + if nspots==0 then + dep=nil + end + end + end + elseif self:_ZoneExists(name) then + if takeoff==RAT.wp.air then + dep=ZONE:New(name) + else + self:E(RAT.id..string.format("ERROR! Takeoff is not in air. Cannot use %s as departure.", name)) + end + else + self:E(RAT.id..string.format("ERROR: No airport or zone found with name %s.", name)) + end + + -- Add to departures table. + if dep then + table.insert(departures, dep) + end + + end + + end + + -- Info message. + self:T(RAT.id..string.format("Number of possible departures for %s= %d", self.alias, #departures)) + + -- Select departure airport or zone. + local departure=departures[math.random(#departures)] + + local text + if departure and departure:GetName() then + if takeoff==RAT.wp.air then + text=string.format("%s: Chosen departure zone: %s", self.alias, departure:GetName()) + else + text=string.format("%s: Chosen departure airport: %s (ID %d)", self.alias, departure:GetName(), departure:GetID()) + end + --MESSAGE:New(text, 30):ToAllIf(self.Debug) + self:T(RAT.id..text) + else + self:E(RAT.id..string.format("ERROR! No departure airport or zone found for %s.", self.alias)) + departure=nil + end + + return departure +end + +--- Pick destination airport or zone depending on departure position. +-- @param #RAT self +-- @param Wrapper.Airbase#AIRBASE departure Departure airport or zone. +-- @param Core.Point#COORDINATE q Coordinate of the departure point. +-- @param #number minrange Minimum range to q in meters. +-- @param #number maxrange Maximum range to q in meters. +-- @param #boolean random Destination is randomly selected from friendly airport (true) or from destinations specified by user input (false). +-- @param #number landing Number indicating whether we land at a destination airport or fly to a zone object. +-- @return Wrapper.Airbase#AIRBASE destination Destination airport or zone. +function RAT:_PickDestination(departure, q, minrange, maxrange, random, landing) + + -- Min/max range to destination. + minrange=minrange or self.mindist + maxrange=maxrange or self.maxdist + + -- All possible destinations. + local destinations={} + + if random then + + -- Airports of friendly coalitions. + for _,_airport in pairs(self.airports) do + local airport=_airport --Wrapper.Airbase#AIRBASE + local name=airport:GetName() + if self:_IsFriendly(name) and not self:_Excluded(name) and name~=departure:GetName() then + + -- Distance from departure to possible destination + local distance=q:Get2DDistance(airport:GetCoordinate()) + + -- Check if distance form departure to destination is within min/max range. + if distance>=minrange and distance<=maxrange then + if landing==RAT.wp.air then + table.insert(destinations, airport:GetZone()) -- insert zone object. + else + -- Check if the requested terminal type is available. + local nspot=1 + if self.termtype then + nspot=airport:GetParkingSpotsNumber(self.termtype) + end + if nspot>0 then + table.insert(destinations, airport) -- insert airport object. + end + end + end + end + end + + else + + -- Destination airports or zones specified by user. + for _,name in pairs(self.destination_ports) do + + -- Make sure departure and destination are not identical. + if name ~= departure:GetName() then + + local dest=nil + if self:_AirportExists(name) then + if landing==RAT.wp.air then + dest=AIRBASE:FindByName(name):GetZone() + else + dest=AIRBASE:FindByName(name) + -- Check if the requested terminal type is available. + local nspot=1 + if self.termtype then + nspot=dest:GetParkingSpotsNumber(self.termtype) + end + if nspot==0 then + dest=nil + end + end + elseif self:_ZoneExists(name) then + if landing==RAT.wp.air then + dest=ZONE:New(name) + else + self:E(RAT.id..string.format("ERROR! Landing is not in air. Cannot use zone %s as destination!", name)) + end + else + self:E(RAT.id..string.format("ERROR! No airport or zone found with name %s", name)) + end + + if dest then + -- Distance from departure to possible destination + local distance=q:Get2DDistance(dest:GetCoordinate()) + + -- Add as possible destination if zone is within range. + if distance>=minrange and distance<=maxrange then + table.insert(destinations, dest) + else + local text=string.format("Destination %s is ouside range. Distance = %5.1f km, min = %5.1f km, max = %5.1f km.", name, distance, minrange, maxrange) + self:T(RAT.id..text) + end + end + + end + end + end + + -- Info message. + self:T(RAT.id..string.format("Number of possible destinations = %s.", #destinations)) + + if #destinations > 0 then + --- Compare distance of destination airports. + -- @param Core.Point#COORDINATE a Coordinate of point a. + -- @param Core.Point#COORDINATE b Coordinate of point b. + -- @return #list Table sorted by distance. + local function compare(a,b) + local qa=q:Get2DDistance(a:GetCoordinate()) + local qb=q:Get2DDistance(b:GetCoordinate()) + return qa < qb + end + table.sort(destinations, compare) + else + destinations=nil + end + + + -- Randomly select one possible destination. + local destination + if destinations and #destinations>0 then + + -- Random selection. + destination=destinations[math.random(#destinations)] -- Wrapper.Airbase#AIRBASE + + -- Debug message. + local text + if landing==RAT.wp.air then + text=string.format("%s: Chosen destination zone: %s.", self.alias, destination:GetName()) + else + text=string.format("%s Chosen destination airport: %s (ID %d).", self.alias, destination:GetName(), destination:GetID()) + end + self:T(RAT.id..text) + --MESSAGE:New(text, 30):ToAllIf(self.Debug) + + else + self:E(RAT.id.."ERROR! No destination airport or zone found.") + destination=nil + end + + -- Return the chosen destination. + return destination + +end + +--- Find airports within a zone. +-- @param #RAT self +-- @param Core.Zone#ZONE zone +-- @return #list Table with airport names that lie within the zone. +function RAT:_GetAirportsInZone(zone) + local airports={} + for _,airport in pairs(self.airports) do + local name=airport:GetName() + local coord=airport:GetCoordinate() + + if zone:IsPointVec3InZone(coord) then + table.insert(airports, name) + end + end + return airports +end + +--- Check if airport is excluded from possible departures and destinations. +-- @param #RAT self +-- @param #string port Name of airport, FARP or ship to check. +-- @return #boolean true if airport is excluded and false otherwise. +function RAT:_Excluded(port) + for _,name in pairs(self.excluded_ports) do + if name==port then + return true + end + end + return false +end + +--- Check if airport is friendly, i.e. belongs to the right coalition. +-- @param #RAT self +-- @param #string port Name of airport, FARP or ship to check. +-- @return #boolean true if airport is friendly and false otherwise. +function RAT:_IsFriendly(port) + for _,airport in pairs(self.airports) do + local name=airport:GetName() + if name==port then + return true + end + end + return false +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get all airports of the current map. +-- @param #RAT self +function RAT:_GetAirportsOfMap() + local _coalition + + for i=0,2 do -- cycle coalition.side 0=NEUTRAL, 1=RED, 2=BLUE + + -- set coalition + if i==0 then + _coalition=coalition.side.NEUTRAL + elseif i==1 then + _coalition=coalition.side.RED + elseif i==2 then + _coalition=coalition.side.BLUE + end + + -- get airbases of coalition + local ab=coalition.getAirbases(i) + + -- loop over airbases and put them in a table + for _,airbase in pairs(ab) do + + local _id=airbase:getID() + local _p=airbase:getPosition().p + local _name=airbase:getName() + local _myab=AIRBASE:FindByName(_name) + + if _myab then + + -- Add airport to table. + table.insert(self.airports_map, _myab) + + local text="MOOSE: Airport ID = ".._myab:GetID().." and Name = ".._myab:GetName()..", Category = ".._myab:GetCategory()..", TypeName = ".._myab:GetTypeName() + self:T(RAT.id..text) + + else + + self:E(RAT.id..string.format("WARNING: Airbase %s does not exsist as MOOSE object!", tostring(_name))) + + end + end + + end +end + +--- Get all "friendly" airports of the current map. Fills the self.airports{} table. +-- @param #RAT self +function RAT:_GetAirportsOfCoalition() + for _,coalition in pairs(self.ctable) do + for _,_airport in pairs(self.airports_map) do + local airport=_airport --Wrapper.Airbase#AIRBASE + local category=airport:GetAirbaseCategory() + if airport:GetCoalition()==coalition then + -- Planes cannot land on FARPs. + --local condition1=self.category==RAT.cat.plane and airport:GetTypeName()=="FARP" + local condition1=self.category==RAT.cat.plane and category==Airbase.Category.HELIPAD + -- Planes cannot land on ships. + --local condition2=self.category==RAT.cat.plane and airport:GetCategory()==1 + local condition2=self.category==RAT.cat.plane and category==Airbase.Category.SHIP + + -- Check that airport has the requested terminal types. + -- NOT good here because we would also not allow any airport zones! + --[[ + local nspots=1 + if self.termtype then + nspots=airport:GetParkingSpotsNumber(self.termtype) + end + local condition3 = nspots==0 + ]] + + if not (condition1 or condition2) then + table.insert(self.airports, airport) + end + end + end + end + + if #self.airports==0 then + local text=string.format("No possible departure/destination airports found for RAT %s.", tostring(self.alias)) + MESSAGE:New(text, 10):ToAll() + self:E(RAT.id..text) + end +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Report status of RAT groups. +-- @param #RAT self +-- @param #boolean message (Optional) Send message with report to all if true. +-- @param #number forID (Optional) Send message only for this ID. +function RAT:Status(message, forID) + + -- Optional arguments. + if message==nil then + message=false + end + if forID==nil then + forID=false + end + + -- Current time. + local Tnow=timer.getTime() + + -- Alive counter. + local nalive=0 + + -- Loop over all ratcraft. + for spawnindex,ratcraft in ipairs(self.ratcraft) do + + -- Get group. + local group=ratcraft.group --Wrapper.Group#GROUP + + if group and group:IsAlive() then + nalive=nalive+1 + + -- Gather some information. + local prefix=self:_GetPrefixFromGroup(group) + local life=self:_GetLife(group) + local fuel=group:GetFuel()*100.0 + local airborne=group:InAir() + local coords=group:GetCoordinate() + local alt=coords.y + --local vel=group:GetVelocityKMH() + local departure=ratcraft.departure:GetName() + local destination=ratcraft.destination:GetName() + local type=self.aircraft.type + local status=ratcraft.status + local active=ratcraft.active + local Nunits=ratcraft.nunits -- group:GetSize() + local N0units=group:GetInitialSize() + + -- Monitor time and distance on ground. + local Tg=0 + local Dg=0 + local dTlast=0 + local stationary=false --lets assume, we did move + if airborne then + -- Aircraft is airborne. + ratcraft["Tground"]=nil + ratcraft["Pground"]=nil + ratcraft["Uground"]=nil + ratcraft["Tlastcheck"]=nil + else + --Aircraft is on ground. + if ratcraft["Tground"] then + -- Aircraft was already on ground. Calculate total time on ground. + Tg=Tnow-ratcraft["Tground"] + + -- Distance on ground since last check. + Dg=coords:Get2DDistance(ratcraft["Pground"]) + + -- Time interval since last check. + dTlast=Tnow-ratcraft["Tlastcheck"] + + -- If more than Tinactive seconds passed since last check ==> check how much we moved meanwhile. + if dTlast > self.Tinactive then + + --[[ + if Dg<50 and active and status~=RAT.status.EventBirth then + stationary=true + end + ]] + + -- Loop over all units. + for _,_unit in pairs(group:GetUnits()) do + + if _unit and _unit:IsAlive() then + + -- Unit name, coord and distance since last check. + local unitname=_unit:GetName() + local unitcoord=_unit:GetCoordinate() + local Ug=unitcoord:Get2DDistance(ratcraft.Uground[unitname]) + + -- Debug info + self:T2(RAT.id..string.format("Unit %s travelled distance on ground %.1f m since %d seconds.", unitname, Ug, dTlast)) + + -- If aircraft did not move more than 50 m since last check, we call it stationary and despawn it. + -- Aircraft which are spawned uncontrolled or starting their engines are not counted. + if Ug<50 and active and status~=RAT.status.EventBirth then + stationary=true + end + + -- Update coords. + ratcraft["Uground"][unitname]=unitcoord + end + end + + -- Set the current time to know when the next check is necessary. + ratcraft["Tlastcheck"]=Tnow + ratcraft["Pground"]=coords + end + + else + -- First time we see that the aircraft is on ground. Initialize the times and position. + ratcraft["Tground"]=Tnow + ratcraft["Tlastcheck"]=Tnow + ratcraft["Pground"]=coords + ratcraft["Uground"]={} + for _,_unit in pairs(group:GetUnits()) do + local unitname=_unit:GetName() + ratcraft.Uground[unitname]=_unit:GetCoordinate() + end + end + end + + -- Monitor travelled distance since last check. + local Pn=coords + local Dtravel=Pn:Get2DDistance(ratcraft["Pnow"]) + ratcraft["Pnow"]=Pn + + -- Add up the travelled distance. + ratcraft["Distance"]=ratcraft["Distance"]+Dtravel + + -- Distance remaining to destination. + local Ddestination=Pn:Get2DDistance(ratcraft.destination:GetCoordinate()) + + -- Status report. + if (forID and spawnindex==forID) or (not forID) then + local text=string.format("ID %i of flight %s", spawnindex, prefix) + if N0units>1 then + text=text..string.format(" (%d/%d)\n", Nunits, N0units) + else + text=text.."\n" + end + if self.commute then + text=text..string.format("%s commuting between %s and %s\n", type, departure, destination) + elseif self.continuejourney then + text=text..string.format("%s travelling from %s to %s (and continueing form there)\n", type, departure, destination) + else + text=text..string.format("%s travelling from %s to %s\n", type, departure, destination) + end + text=text..string.format("Status: %s", status) + if airborne then + text=text.." [airborne]\n" + else + text=text.." [on ground]\n" + end + text=text..string.format("Fuel = %3.0f %%\n", fuel) + text=text..string.format("Life = %3.0f %%\n", life) + text=text..string.format("FL%03d = %i m ASL\n", alt/RAT.unit.FL2m, alt) + --text=text..string.format("Speed = %i km/h\n", vel) + text=text..string.format("Distance travelled = %6.1f km\n", ratcraft["Distance"]/1000) + text=text..string.format("Distance to destination = %6.1f km", Ddestination/1000) + if not airborne then + text=text..string.format("\nTime on ground = %6.0f seconds\n", Tg) + text=text..string.format("Position change = %8.1f m since %3.0f seconds.", Dg, dTlast) + end + self:T(RAT.id..text) + if message then + MESSAGE:New(text, 20):ToAll() + end + end + + -- Despawn groups if they are on ground and don't move or are damaged. + if not airborne then + + -- Despawn unit if it did not move more then 50 m in the last 180 seconds. + if stationary then + local text=string.format("Group %s is despawned after being %d seconds inaktive on ground.", self.alias, dTlast) + self:T(RAT.id..text) + self:_Despawn(group) + end + + -- Despawn group if life is < 10% and distance travelled < 100 m. + if life<10 and Dtravel<100 then + local text=string.format("Damaged group %s is despawned. Life = %3.0f", self.alias, life) + self:T(RAT.id..text) + self:_Despawn(group) + end + + end + + -- Despawn groups after they have reached their destination zones. + if ratcraft.despawnme then + + local text=string.format("Flight %s will be despawned NOW!", self.alias) + self:T(RAT.id..text) + + -- Respawn group + if (not self.norespawn) and (not self.respawn_after_takeoff) then + local idx=self:GetSpawnIndexFromGroup(group) + local coord=group:GetCoordinate() + self:_Respawn(idx, coord, 0) + end + + -- Despawn old group. + if self.despawnair then + self:_Despawn(group, 0) + end + + end + + else + -- Group does not exist. + local text=string.format("Group does not exist in loop ratcraft status.") + self:T2(RAT.id..text) + end + + end + + -- Alive groups. + local text=string.format("Alive groups of %s: %d, nalive=%d/%d", self.alias, self.alive, nalive, self.ngroups) + self:T(RAT.id..text) + MESSAGE:New(text, 20):ToAllIf(message and not forID) + +end + +--- Get (relative) life of first unit of a group. +-- @param #RAT self +-- @param Wrapper.Group#GROUP group Group of unit. +-- @return #number Life of unit in percent. +function RAT:_GetLife(group) + local life=0.0 + if group and group:IsAlive() then + local unit=group:GetUnit(1) + if unit then + life=unit:GetLife()/unit:GetLife0()*100 + else + self:T2(RAT.id.."ERROR! Unit does not exist in RAT_Getlife(). Returning zero.") + end + else + self:T2(RAT.id.."ERROR! Group does not exist in RAT_Getlife(). Returning zero.") + end + return life +end + +--- Set status of group. +-- @param #RAT self +-- @param Wrapper.Group#GROUP group Group. +-- @param #string status Status of group. +function RAT:_SetStatus(group, status) + + if group and group:IsAlive() then + + -- Get index from groupname. + local index=self:GetSpawnIndexFromGroup(group) + + if self.ratcraft[index] then + + -- Set new status. + self.ratcraft[index].status=status + + -- No status update message for "first waypoint", "holding" + local no1 = status==RAT.status.Departure + local no2 = status==RAT.status.EventBirthAir + local no3 = status==RAT.status.Holding + + local text=string.format("Flight %s: %s.", group:GetName(), status) + self:T(RAT.id..text) + + if not (no1 or no2 or no3) then + MESSAGE:New(text, 10):ToAllIf(self.reportstatus) + end + + end + + end +end + +--- Get status of group. +-- @param #RAT self +-- @param Wrapper.Group#GROUP group Group. +-- @return #string status Status of group. +function RAT:GetStatus(group) + + if group and group:IsAlive() then + + -- Get index from groupname. + local index=self:GetSpawnIndexFromGroup(group) + + if self.ratcraft[index] then + + -- Set new status. + return self.ratcraft[index].status + + end + + end + + return "nonexistant" +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Function is executed when a unit is spawned. +-- @param #RAT self +-- @param Core.Event#EVENTDATA EventData +function RAT:_OnBirth(EventData) + self:F3(EventData) + self:T3(RAT.id.."Captured event birth!") + + local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP + + if SpawnGroup then + + -- Get the template name of the group. This can be nil if this was not a spawned group. + local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup) + + if EventPrefix then + + -- Check that the template name actually belongs to this object. + if EventPrefix == self.alias then + + local text="Event: Group "..SpawnGroup:GetName().." was born." + self:T(RAT.id..text) + + -- Set status. + local status="unknown in birth" + if SpawnGroup:InAir() then + status=RAT.status.EventBirthAir + elseif self.uncontrolled then + status=RAT.status.Uncontrolled + else + status=RAT.status.EventBirth + end + self:_SetStatus(SpawnGroup, status) + + -- Get some info ablout this flight. + local i=self:GetSpawnIndexFromGroup(SpawnGroup) + local _departure=self.ratcraft[i].departure:GetName() + local _destination=self.ratcraft[i].destination:GetName() + local _nrespawn=self.ratcraft[i].nrespawn + local _takeoff=self.ratcraft[i].takeoff + local _landing=self.ratcraft[i].landing + local _livery=self.ratcraft[i].livery + + -- Some is only useful for an actual airbase (not a zone). + local _airbase=AIRBASE:FindByName(_departure) + + -- Check if aircraft group was accidentally spawned on the runway. + -- This can happen due to no parking slots available and other DCS bugs. + local onrunway=false + if _airbase then + -- Check that we did not want to spawn at a runway or in air. + if self.checkonrunway and _takeoff ~= RAT.wp.runway and _takeoff ~= RAT.wp.air then + onrunway=_airbase:CheckOnRunWay(SpawnGroup, self.onrunwayradius, false) + end + end + + -- Workaround if group was spawned on runway. + if onrunway then + + -- Error message. + local text=string.format("ERROR: RAT group of %s was spawned on runway. Group #%d will be despawned immediately!", self.alias, i) + MESSAGE:New(text,30):ToAllIf(self.Debug) + self:E(RAT.id..text) + if self.Debug then + SpawnGroup:FlareRed() + end + + -- Despawn the group. + self:_Despawn(SpawnGroup) + + -- Try to respawn the group if there is at least another airport or random airport selection is used. + if (self.Ndeparture_Airports>=2 or self.random_departure) and _nrespawn new state %s.", SpawnGroup:GetName(), currentstate, status) + self:T(RAT.id..text) + + -- Respawn group. + local idx=self:GetSpawnIndexFromGroup(SpawnGroup) + local coord=SpawnGroup:GetCoordinate() + self:_Respawn(idx, coord) + end + + -- Despawn group. + text="Event: Group "..SpawnGroup:GetName().." will be destroyed now." + self:T(RAT.id..text) + self:_Despawn(SpawnGroup) + + end + + end + end + + else + self:T2(RAT.id.."ERROR: Group does not exist in RAT:_OnEngineShutdown().") + end +end + +--- Function is executed when a unit is hit. +-- @param #RAT self +-- @param Core.Event#EVENTDATA EventData +function RAT:_OnHit(EventData) + self:F3(EventData) + self:T(RAT.id..string.format("Captured event Hit by %s! Initiator %s. Target %s", self.alias, tostring(EventData.IniUnitName), tostring(EventData.TgtUnitName))) + + local SpawnGroup = EventData.TgtGroup --Wrapper.Group#GROUP + + if SpawnGroup then + + -- Get the template name of the group. This can be nil if this was not a spawned group. + local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup) + + -- Check that the template name actually belongs to this object. + if EventPrefix and EventPrefix == self.alias then + -- Debug info. + self:T(RAT.id..string.format("Event: Group %s was hit. Unit %s.", SpawnGroup:GetName(), tostring(EventData.TgtUnitName))) + + local text=string.format("%s, unit %s was hit!", self.alias, EventData.TgtUnitName) + MESSAGE:New(text, 10):ToAllIf(self.reportstatus or self.Debug) + end + end +end + +--- Function is executed when a unit is dead or crashes. +-- @param #RAT self +-- @param Core.Event#EVENTDATA EventData +function RAT:_OnDeadOrCrash(EventData) + self:F3(EventData) + self:T3(RAT.id.."Captured event DeadOrCrash!") + + local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP + + if SpawnGroup then + + -- Get the template name of the group. This can be nil if this was not a spawned group. + local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup) + + if EventPrefix then + + -- Check that the template name actually belongs to this object. + if EventPrefix == self.alias then + + -- Decrease group alive counter. + self.alive=self.alive-1 + + -- Debug info. + local text=string.format("Event: Group %s crashed or died. Alive counter = %d.", SpawnGroup:GetName(), self.alive) + self:T(RAT.id..text) + + -- Split crash and dead events. + if EventData.id == world.event.S_EVENT_CRASH then + + -- Call crash event. This handles when a group crashed or + self:_OnCrash(EventData) + + elseif EventData.id == world.event.S_EVENT_DEAD then + + -- Call dead event. + self:_OnDead(EventData) + + end + end + end + end +end + +--- Function is executed when a unit is dead. +-- @param #RAT self +-- @param Core.Event#EVENTDATA EventData +function RAT:_OnDead(EventData) + self:F3(EventData) + self:T3(RAT.id.."Captured event Dead!") + + local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP + + if SpawnGroup then + + -- Get the template name of the group. This can be nil if this was not a spawned group. + local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup) + + if EventPrefix then + + -- Check that the template name actually belongs to this object. + if EventPrefix == self.alias then + + local text=string.format("Event: Group %s died. Unit %s.", SpawnGroup:GetName(), EventData.IniUnitName) + self:T(RAT.id..text) + + -- Set status. + local status=RAT.status.EventDead + self:_SetStatus(SpawnGroup, status) + + end + end + + else + self:T2(RAT.id.."ERROR: Group does not exist in RAT:_OnDead().") + end +end + +--- Function is executed when a unit crashes. +-- @param #RAT self +-- @param Core.Event#EVENTDATA EventData +function RAT:_OnCrash(EventData) + self:F3(EventData) + self:T3(RAT.id.."Captured event Crash!") + + local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP + + if SpawnGroup then + + -- Get the template name of the group. This can be nil if this was not a spawned group. + local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup) + + -- Check that the template name actually belongs to this object. + if EventPrefix and EventPrefix == self.alias then + + -- Update number of alive units in the group. + local _i=self:GetSpawnIndexFromGroup(SpawnGroup) + self.ratcraft[_i].nunits=self.ratcraft[_i].nunits-1 + local _n=self.ratcraft[_i].nunits + local _n0=SpawnGroup:GetInitialSize() + + -- Debug info. + local text=string.format("Event: Group %s crashed. Unit %s. Units still alive %d of %d.", SpawnGroup:GetName(), EventData.IniUnitName, _n, _n0) + self:T(RAT.id..text) + + -- Set status. + local status=RAT.status.EventCrash + self:_SetStatus(SpawnGroup, status) + + -- Respawn group if all units are dead. + if _n==0 and self.respawn_after_crash and not self.norespawn then + local text=string.format("No units left of group %s. Group will be respawned now.", SpawnGroup:GetName()) + self:T(RAT.id..text) + -- Respawn group. + local idx=self:GetSpawnIndexFromGroup(SpawnGroup) + local coord=SpawnGroup:GetCoordinate() + self:_Respawn(idx, coord) + end + + end + + else + if self.Debug then + self:E(RAT.id.."ERROR: Group does not exist in RAT:_OnCrash().") + end + end +end + +--- Despawn unit. Unit gets destoyed and group is set to nil. +-- Index of ratcraft array is taken from spawned group name. +-- @param #RAT self +-- @param Wrapper.Group#GROUP group Group to be despawned. +-- @param #number delay Delay in seconds before the despawn happens. +function RAT:_Despawn(group, delay) + + if group ~= nil then + + -- Get spawnindex of group. + local index=self:GetSpawnIndexFromGroup(group) + + if index ~= nil then + + self.ratcraft[index].group=nil + self.ratcraft[index]["status"]="Dead" + + --TODO: Maybe here could be some more arrays deleted? + --TODO: Somehow this causes issues. + --[[ + --self.ratcraft[index]["group"]=group + self.ratcraft[index]["destination"]=nil + self.ratcraft[index]["departure"]=nil + self.ratcraft[index]["waypoints"]=nil + self.ratcraft[index]["airborne"]=nil + self.ratcraft[index]["Tground"]=nil + self.ratcraft[index]["Pground"]=nil + self.ratcraft[index]["Tlastcheck"]=nil + self.ratcraft[index]["P0"]=nil + self.ratcraft[index]["Pnow"]=nil + self.ratcraft[index]["Distance"]=nil + self.ratcraft[index].takeoff=nil + self.ratcraft[index].landing=nil + self.ratcraft[index].wpholding=nil + self.ratcraft[index].wpfinal=nil + self.ratcraft[index].active=false + self.ratcraft[index]["status"]=nil + self.ratcraft[index].livery=nil + self.ratcraft[index].despawnme=nil + self.ratcraft[index].nrespawn=nil + ]] + -- Remove ratcraft table entry. + --table.remove(self.ratcraft, index) + + + -- We should give it at least 3 sec since this seems to be the time until free parking spots after despawn are available again (Sirri Island test). + local despawndelay=0 + if delay then + -- Explicitly requested delay time. + despawndelay=delay + elseif self.respawn_delay then + -- Despawn afer respawn_delay. Actual respawn happens in +3 seconds to allow for free parking. + despawndelay=self.respawn_delay + end + + -- This will destroy the DCS group and create a single DEAD event. + --if despawndelay>0.5 then + self:T(RAT.id..string.format("%s delayed despawn in %.1f seconds.", self.alias, despawndelay)) + SCHEDULER:New(nil, self._Destroy, {self, group}, despawndelay) + --else + --self:_Destroy(group) + --end + + -- Remove submenu for this group. + if self.f10menu and self.SubMenuName ~= nil then + self.Menu[self.SubMenuName]["groups"][index]:Remove() + end + + end + end +end + +--- Destroys the RAT DCS group and all of its DCS units. +-- Note that this raises a DEAD event at run-time. +-- So all event listeners will catch the DEAD event of this DCS group. +-- @param #RAT self +-- @param Wrapper.Group#GROUP group The RAT group to be destroyed. +function RAT:_Destroy(group) + self:F2(group) + + local DCSGroup = group:GetDCSObject() -- DCS#Group + + if DCSGroup and DCSGroup:isExist() then + + -- Cread one single Dead event and delete units from database. + local triggerdead=true + for _,DCSUnit in pairs(DCSGroup:getUnits()) do + + -- Dead event. + if DCSUnit then + if triggerdead then + self:_CreateEventDead(timer.getTime(), DCSUnit) + triggerdead=false + end + + -- Delete from data base. + _DATABASE:DeleteUnit(DCSUnit:getName()) + end + end + + -- Destroy DCS group. + DCSGroup:destroy() + DCSGroup = nil + end + + return nil +end + +--- Create a Dead event. +-- @param #RAT self +-- @param DCS#Time EventTime The time stamp of the event. +-- @param DCS#Object Initiator The initiating object of the event. +function RAT:_CreateEventDead(EventTime, Initiator) + self:F( { EventTime, Initiator } ) + + local Event = { + id = world.event.S_EVENT_DEAD, + time = EventTime, + initiator = Initiator, + } + + world.onEvent( Event ) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a waypoint that can be used with the Route command. +-- @param #RAT self +-- @param #number index Running index of waypoints. Starts with 1 which is normally departure/spawn waypoint. +-- @param #string description Descrition of Waypoint. +-- @param #number Type Type of waypoint. +-- @param Core.Point#COORDINATE Coord 3D coordinate of the waypoint. +-- @param #number Speed Speed in m/s. +-- @param #number Altitude Altitude in m. +-- @param Wrapper.Airbase#AIRBASE Airport Airport of object to spawn. +-- @return #table Waypoints for DCS task route or spawn template. +function RAT:_Waypoint(index, description, Type, Coord, Speed, Altitude, Airport) + + -- Altitude of input parameter or y-component of 3D-coordinate. + local _Altitude=Altitude or Coord.y + + -- Land height at given coordinate. + local Hland=Coord:GetLandHeight() + + -- convert type and action in DCS format + local _Type=nil + local _Action=nil + local _alttype="RADIO" + + if Type==RAT.wp.cold then + -- take-off with engine off + _Type="TakeOffParking" + _Action="From Parking Area" + _Altitude = 10 + _alttype="RADIO" + elseif Type==RAT.wp.hot then + -- take-off with engine on + _Type="TakeOffParkingHot" + _Action="From Parking Area Hot" + _Altitude = 10 + _alttype="RADIO" + elseif Type==RAT.wp.runway then + -- take-off from runway + _Type="TakeOff" + _Action="From Parking Area" + _Altitude = 10 + _alttype="RADIO" + elseif Type==RAT.wp.air then + -- air start + _Type="Turning Point" + _Action="Turning Point" + _alttype="BARO" + elseif Type==RAT.wp.climb then + _Type="Turning Point" + _Action="Turning Point" + _alttype="BARO" + elseif Type==RAT.wp.cruise then + _Type="Turning Point" + _Action="Turning Point" + _alttype="BARO" + elseif Type==RAT.wp.descent then + _Type="Turning Point" + _Action="Turning Point" + _alttype="BARO" + elseif Type==RAT.wp.holding then + _Type="Turning Point" + _Action="Turning Point" + --_Action="Fly Over Point" + _alttype="BARO" + elseif Type==RAT.wp.landing then + _Type="Land" + _Action="Landing" + _Altitude = 10 + _alttype="RADIO" + elseif Type==RAT.wp.finalwp then + _Type="Turning Point" + --_Action="Fly Over Point" + _Action="Turning Point" + _alttype="BARO" + else + self:E(RAT.id.."ERROR: Unknown waypoint type in RAT:Waypoint() function!") + _Type="Turning Point" + _Action="Turning Point" + _alttype="RADIO" + end + + -- some debug info about input parameters + local text=string.format("\n******************************************************\n") + text=text..string.format("Waypoint = %d\n", index) + text=text..string.format("Template = %s\n", self.SpawnTemplatePrefix) + text=text..string.format("Alias = %s\n", self.alias) + text=text..string.format("Type: %i - %s\n", Type, _Type) + text=text..string.format("Action: %s\n", _Action) + text=text..string.format("Coord: x = %6.1f km, y = %6.1f km, alt = %6.1f m\n", Coord.x/1000, Coord.z/1000, Coord.y) + text=text..string.format("Speed = %6.1f m/s = %6.1f km/h = %6.1f knots\n", Speed, Speed*3.6, Speed*1.94384) + text=text..string.format("Land = %6.1f m ASL\n", Hland) + text=text..string.format("Altitude = %6.1f m (%s)\n", _Altitude, _alttype) + if Airport then + if Type==RAT.wp.air then + text=text..string.format("Zone = %s\n", Airport:GetName()) + else + --text=text..string.format("Airport = %s with ID %i\n", Airport:GetName(), Airport:GetID()) + text=text..string.format("Airport = %s\n", Airport:GetName()) + end + else + text=text..string.format("No airport/zone specified\n") + end + text=text.."******************************************************\n" + self:T2(RAT.id..text) + + -- define waypoint + local RoutePoint = {} + -- coordinates and altitude + RoutePoint.x = Coord.x + RoutePoint.y = Coord.z + RoutePoint.alt = _Altitude + -- altitude type: BARO=ASL or RADIO=AGL + RoutePoint.alt_type = _alttype + -- type + RoutePoint.type = _Type + RoutePoint.action = _Action + -- speed in m/s + RoutePoint.speed = Speed + RoutePoint.speed_locked = true + -- ETA (not used) + RoutePoint.ETA=nil + RoutePoint.ETA_locked = false + -- waypoint description + RoutePoint.name=description + + if (Airport~=nil) and (Type~=RAT.wp.air) then + local AirbaseID = Airport:GetID() + local AirbaseCategory = Airport:GetAirbaseCategory() + if AirbaseCategory == Airbase.Category.SHIP then + RoutePoint.linkUnit = AirbaseID + RoutePoint.helipadId = AirbaseID + elseif AirbaseCategory == Airbase.Category.HELIPAD then + RoutePoint.linkUnit = AirbaseID + RoutePoint.helipadId = AirbaseID + elseif AirbaseCategory == Airbase.Category.AIRDROME then + RoutePoint.airdromeId = AirbaseID + else + self:T(RAT.id.."Unknown Airport category in _Waypoint()!") + end + end + -- properties + RoutePoint.properties = { + ["vnav"] = 1, + ["scale"] = 0, + ["angle"] = 0, + ["vangle"] = 0, + ["steer"] = 2, + } + -- tasks + local TaskCombo = {} + local TaskHolding = self:_TaskHolding({x=Coord.x, y=Coord.z}, Altitude, Speed, self:_Randomize(90,0.9)) + local TaskWaypoint = self:_TaskFunction("RAT._WaypointFunction", self, index) + + RoutePoint.task = {} + RoutePoint.task.id = "ComboTask" + RoutePoint.task.params = {} + + TaskCombo[#TaskCombo+1]=TaskWaypoint + if Type==RAT.wp.holding then + TaskCombo[#TaskCombo+1]=TaskHolding + end + + RoutePoint.task.params.tasks = TaskCombo + + -- Return waypoint. + return RoutePoint +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Provide information about the assigned flightplan. +-- @param #RAT self +-- @param #table waypoints Waypoints of the flight plan. +-- @param #string comment Some comment to identify the provided information. +-- @return #number total Total route length in meters. +function RAT:_Routeinfo(waypoints, comment) + local text=string.format("\n******************************************************\n") + text=text..string.format("Template = %s\n", self.SpawnTemplatePrefix) + if comment then + text=text..comment.."\n" + end + text=text..string.format("Number of waypoints = %i\n", #waypoints) + -- info on coordinate and altitude + for i=1,#waypoints do + local p=waypoints[i] + text=text..string.format("WP #%i: x = %6.1f km, y = %6.1f km, alt = %6.1f m %s\n", i-1, p.x/1000, p.y/1000, p.alt, self.waypointdescriptions[i]) + end + -- info on distance between waypoints + local total=0.0 + for i=1,#waypoints-1 do + local point1=waypoints[i] + local point2=waypoints[i+1] + local x1=point1.x + local y1=point1.y + local x2=point2.x + local y2=point2.y + local d=math.sqrt((x1-x2)^2 + (y1-y2)^2) + local heading=self:_Course(point1, point2) + total=total+d + text=text..string.format("Distance from WP %i-->%i = %6.1f km. Heading = %03d : %s - %s\n", i-1, i, d/1000, heading, self.waypointdescriptions[i], self.waypointdescriptions[i+1]) + end + text=text..string.format("Total distance = %6.1f km\n", total/1000) + text=text..string.format("******************************************************\n") + + -- Debug info. + self:T2(RAT.id..text) + + -- return total route length in meters + return total +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Orbit at a specified position at a specified alititude with a specified speed. +-- @param #RAT self +-- @param DCS#Vec2 P1 The point to hold the position. +-- @param #number Altitude The altitude ASL at which to hold the position. +-- @param #number Speed The speed flying when holding the position in m/s. +-- @param #number Duration Duration of holding pattern in seconds. +-- @return DCS#Task DCSTask +function RAT:_TaskHolding(P1, Altitude, Speed, Duration) + + --local LandHeight = land.getHeight(P1) + + --TODO: randomize P1 + -- Second point is 3 km north of P1 and 200 m for helos. + local dx=3000 + local dy=0 + if self.category==RAT.cat.heli then + dx=200 + dy=0 + end + + local P2={} + P2.x=P1.x+dx + P2.y=P1.y+dy + local Task = { + id = 'Orbit', + params = { + pattern = AI.Task.OrbitPattern.RACE_TRACK, + --pattern = AI.Task.OrbitPattern.CIRCLE, + point = P1, + point2 = P2, + speed = Speed, + altitude = Altitude + } + } + + local DCSTask={} + DCSTask.id="ControlledTask" + DCSTask.params={} + DCSTask.params.task=Task + + if self.ATCswitch then + -- Set stop condition for holding. Either flag=1 or after max. X min holding. + local userflagname=string.format("%s#%03d", self.alias, self.SpawnIndex+1) + local maxholdingduration=60*120 + DCSTask.params.stopCondition={userFlag=userflagname, userFlagValue=1, duration=maxholdingduration} + else + DCSTask.params.stopCondition={duration=Duration} + end + + return DCSTask +end + +--- Function which is called after passing every waypoint. Info on waypoint is given and special functions are executed. +-- @param Core.Group#GROUP group Group of aircraft. +-- @param #RAT rat RAT object. +-- @param #number wp Waypoint index. Running number of the waypoints. Determines the actions to be executed. +function RAT._WaypointFunction(group, rat, wp) + + -- Current time and Spawnindex. + local Tnow=timer.getTime() + local sdx=rat:GetSpawnIndexFromGroup(group) + + -- Departure and destination names. + local departure=rat.ratcraft[sdx].departure:GetName() + local destination=rat.ratcraft[sdx].destination:GetName() + local landing=rat.ratcraft[sdx].landing + local WPholding=rat.ratcraft[sdx].wpholding + local WPfinal=rat.ratcraft[sdx].wpfinal + + + -- For messages + local text + + -- Info on passing waypoint. + text=string.format("Flight %s passing waypoint #%d %s.", group:GetName(), wp, rat.waypointdescriptions[wp]) + BASE.T(rat, RAT.id..text) + + -- New status. + local status=rat.waypointstatus[wp] + rat:_SetStatus(group, status) + + if wp==WPholding then + + -- Aircraft arrived at holding point + text=string.format("Flight %s to %s ATC: Holding and awaiting landing clearance.", group:GetName(), destination) + MESSAGE:New(text, 10):ToAllIf(rat.reportstatus) + + -- Register aircraft at ATC. + if rat.ATCswitch then + if rat.f10menu then + MENU_MISSION_COMMAND:New("Clear for landing", rat.Menu[rat.SubMenuName].groups[sdx], rat.ClearForLanding, rat, group:GetName()) + end + rat._ATCRegisterFlight(rat, group:GetName(), Tnow) + end + end + + if wp==WPfinal then + text=string.format("Flight %s arrived at final destination %s.", group:GetName(), destination) + MESSAGE:New(text, 10):ToAllIf(rat.reportstatus) + BASE.T(rat, RAT.id..text) + + if landing==RAT.wp.air then + text=string.format("Activating despawn switch for flight %s! Group will be detroyed soon.", group:GetName()) + MESSAGE:New(text, 10):ToAllIf(rat.Debug) + BASE.T(rat, RAT.id..text) + -- Enable despawn switch. Next time the status function is called, the aircraft will be despawned. + rat.ratcraft[sdx].despawnme=true + end + end +end + +--- Task function. +-- @param #RAT self +-- @param #string FunctionString Name of the function to be called. +function RAT:_TaskFunction(FunctionString, ... ) + self:F2({FunctionString, arg}) + + local DCSTask + local ArgumentKey + + -- Templatename and anticipated name the group will get + local templatename=self.templategroup:GetName() + local groupname=self:_AnticipatedGroupName() + + local DCSScript = {} + DCSScript[#DCSScript+1] = "local MissionControllable = GROUP:FindByName(\""..groupname.."\") " + DCSScript[#DCSScript+1] = "local RATtemplateControllable = GROUP:FindByName(\""..templatename.."\") " + + if arg and arg.n > 0 then + ArgumentKey = '_' .. tostring(arg):match("table: (.*)") + self.templategroup:SetState(self.templategroup, ArgumentKey, arg) + DCSScript[#DCSScript+1] = "local Arguments = RATtemplateControllable:GetState(RATtemplateControllable, '" .. ArgumentKey .. "' ) " + DCSScript[#DCSScript+1] = FunctionString .. "( MissionControllable, unpack( Arguments ) )" + else + DCSScript[#DCSScript+1] = FunctionString .. "( MissionControllable )" + end + + DCSTask = self.templategroup:TaskWrappedAction(self.templategroup:CommandDoScript(table.concat(DCSScript))) + + return DCSTask +end + +--- Anticipated group name from alias and spawn index. +-- @param #RAT self +-- @param #number index Spawnindex of group if given or self.SpawnIndex+1 by default. +-- @return #string Name the group will get after it is spawned. +function RAT:_AnticipatedGroupName(index) + local index=index or self.SpawnIndex+1 + return string.format("%s#%03d", self.alias, index) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Randomly activates an uncontrolled aircraft. +-- @param #RAT self +function RAT:_ActivateUncontrolled() + self:F() + + -- Spawn indices of uncontrolled inactive aircraft. + local idx={} + local rat={} + + -- Number of active aircraft. + local nactive=0 + + -- Loop over RAT groups and count the active ones. + for spawnindex,ratcraft in pairs(self.ratcraft) do + + local group=ratcraft.group --Wrapper.Group#GROUP + + if group and group:IsAlive() then + + local text=string.format("Uncontrolled: Group = %s (spawnindex = %d), active = %s.", ratcraft.group:GetName(), spawnindex, tostring(ratcraft.active)) + self:T2(RAT.id..text) + + if ratcraft.active then + nactive=nactive+1 + else + table.insert(idx, spawnindex) + end + + end + end + + -- Debug message. + local text=string.format("Uncontrolled: Ninactive = %d, Nactive = %d (of max %d).", #idx, nactive, self.activate_max) + self:T(RAT.id..text) + + if #idx>0 and nactive Less effort. + self:T(RAT.id..string.format("Group %s is spawned on farp/ship/runway %s.", self.alias, departure:GetName())) + nfree=departure:GetFreeParkingSpotsNumber(termtype, true) + spots=departure:GetFreeParkingSpotsTable(termtype, true) + elseif parkingdata~=nil then + -- Parking data explicitly set by user as input parameter. + nfree=#parkingdata + spots=parkingdata + else + -- Helo is spawned. + if self.category==RAT.cat.heli then + if termtype==nil then + -- Try exclusive helo spots first. + self:T(RAT.id..string.format("Helo group %s is spawned at %s using terminal type %d.", self.alias, departure:GetName(), AIRBASE.TerminalType.HelicopterOnly)) + spots=departure:FindFreeParkingSpotForAircraft(TemplateGroup, AIRBASE.TerminalType.HelicopterOnly, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits) + nfree=#spots + if nfree=1 then + + -- All units get the same spot. DCS takes care of the rest. + for i=1,nunits do + table.insert(parkingspots, spots[1].Coordinate) + table.insert(parkingindex, spots[1].TerminalID) + end + -- This is actually used... + PointVec3=spots[1].Coordinate + + else + -- If there is absolutely not spot ==> air start! + _notenough=true + end + + elseif spawnonairport then + + if nfree>=nunits then + + for i=1,nunits do + table.insert(parkingspots, spots[i].Coordinate) + table.insert(parkingindex, spots[i].TerminalID) + end + + else + -- Not enough spots for the whole group ==> air start! + _notenough=true + end + end + + -- Not enough spots ==> Prepare airstart. + if _notenough then + + if self.respawn_inair and not self.SpawnUnControlled then + self:E(RAT.id..string.format("WARNING: Group %s has no parking spots at %s ==> air start!", self.SpawnTemplatePrefix, departure:GetName())) + + -- Not enough parking spots at the airport ==> Spawn in air. + spawnonground=false + spawnonship=false + spawnonfarp=false + spawnonrunway=false + + -- Set waypoint type/action to turning point. + waypoints[1].type = GROUPTEMPLATE.Takeoff[GROUP.Takeoff.Air][1] -- type = Turning Point + waypoints[1].action = GROUPTEMPLATE.Takeoff[GROUP.Takeoff.Air][2] -- action = Turning Point + + -- Adjust altitude to be 500-1000 m above the airbase. + PointVec3.x=PointVec3.x+math.random(-1500,1500) + PointVec3.z=PointVec3.z+math.random(-1500,1500) + if self.category==RAT.cat.heli then + PointVec3.y=PointVec3:GetLandHeight()+math.random(100,1000) + else + -- Randomize position so that multiple AC wont be spawned on top even in air. + PointVec3.y=PointVec3:GetLandHeight()+math.random(500,3000) + end + else + self:E(RAT.id..string.format("WARNING: Group %s has no parking spots at %s ==> No emergency air start or uncontrolled spawning ==> No spawn!", self.SpawnTemplatePrefix, departure:GetName())) + return nil + end + end + + else + + -- Air start requested initially! + + --PointVec3.y is already set from first waypoint here! + + end + + +--- new + + -- Translate the position of the Group Template to the Vec3. + for UnitID = 1, nunits do + + -- Template of the current unit. + local UnitTemplate = SpawnTemplate.units[UnitID] + + -- Tranlate position and preserve the relative position/formation of all aircraft. + local SX = UnitTemplate.x + local SY = UnitTemplate.y + local BX = SpawnTemplate.route.points[1].x + local BY = SpawnTemplate.route.points[1].y + local TX = PointVec3.x + (SX-BX) + local TY = PointVec3.z + (SY-BY) + + if spawnonground then + + -- Ships and FARPS seem to have a build in queue. + if spawnonship or spawnonfarp or spawnonrunway or automatic then + self:T(RAT.id..string.format("RAT group %s spawning at farp, ship or runway %s.", self.alias, departure:GetName())) + + -- Spawn on ship. We take only the position of the ship. + SpawnTemplate.units[UnitID].x = PointVec3.x --TX + SpawnTemplate.units[UnitID].y = PointVec3.z --TY + SpawnTemplate.units[UnitID].alt = PointVec3.y + else + self:T(RAT.id..string.format("RAT group %s spawning at airbase %s on parking spot id %d", self.alias, departure:GetName(), parkingindex[UnitID])) + + -- Get coordinates of parking spot. + SpawnTemplate.units[UnitID].x = parkingspots[UnitID].x + SpawnTemplate.units[UnitID].y = parkingspots[UnitID].z + SpawnTemplate.units[UnitID].alt = parkingspots[UnitID].y + end + + else + self:T(RAT.id..string.format("RAT group %s spawning in air at %s.", self.alias, departure:GetName())) + + -- Spawn in air as requested initially. Original template orientation is perserved, altitude is already correctly set. + SpawnTemplate.units[UnitID].x = TX + SpawnTemplate.units[UnitID].y = TY + SpawnTemplate.units[UnitID].alt = PointVec3.y + end + + -- Place marker at spawn position. + if self.Debug then + local unitspawn=COORDINATE:New(SpawnTemplate.units[UnitID].x, SpawnTemplate.units[UnitID].alt, SpawnTemplate.units[UnitID].y) + unitspawn:MarkToAll(string.format("RAT %s Spawnplace unit #%d", self.alias, UnitID)) + end + + -- Parking spot id. + UnitTemplate.parking = nil + UnitTemplate.parking_id = nil + if parkingindex[UnitID] and not automatic then + UnitTemplate.parking = parkingindex[UnitID] + end + + -- Debug info. + self:T2(RAT.id..string.format("RAT group %s unit number %d: Parking = %s",self.alias, UnitID, tostring(UnitTemplate.parking))) + self:T2(RAT.id..string.format("RAT group %s unit number %d: Parking ID = %s",self.alias, UnitID, tostring(UnitTemplate.parking_id))) + + + -- Set initial heading. + SpawnTemplate.units[UnitID].heading = heading + SpawnTemplate.units[UnitID].psi = -heading + + -- Set livery (will be the same for all units of the group). + if livery then + SpawnTemplate.units[UnitID].livery_id = livery + end + + -- Set type of aircraft. + if self.actype then + SpawnTemplate.units[UnitID]["type"] = self.actype + end + + -- Set AI skill. + SpawnTemplate.units[UnitID]["skill"] = self.skill + + -- Onboard number. + if self.onboardnum then + SpawnTemplate.units[UnitID]["onboard_num"] = string.format("%s%d%02d", self.onboardnum, (self.SpawnIndex-1)%10, (self.onboardnum0-1)+UnitID) + end + + -- Modify coaltion and country of template. + SpawnTemplate.CoalitionID=self.coalition + if self.country then + SpawnTemplate.CountryID=self.country + end + + end + + -- Copy waypoints into spawntemplate. By this we avoid the nasty DCS "landing bug" :) + for i,wp in ipairs(waypoints) do + SpawnTemplate.route.points[i]=wp + end + + -- Also modify x,y of the template. Not sure why. + SpawnTemplate.x = PointVec3.x + SpawnTemplate.y = PointVec3.z + + -- Enable/disable radio. Same as checking the COMM box in the ME + if self.radio then + SpawnTemplate.communication=self.radio + end + + -- Set radio frequency and modulation. + if self.frequency then + SpawnTemplate.frequency=self.frequency + end + if self.modulation then + SpawnTemplate.modulation=self.modulation + end + + -- Debug output. + self:T(SpawnTemplate) + end + end + + return true +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Initializes the ATC arrays and starts schedulers. +-- @param #RAT self +-- @param #table airports_map List of all airports of the map. +function RAT:_ATCInit(airports_map) + if not RAT.ATC.init then + local text + text="Starting RAT ATC.\nSimultanious = "..RAT.ATC.Nclearance.."\n".."Delay = "..RAT.ATC.delay + BASE:T(RAT.id..text) + RAT.ATC.init=true + for _,ap in pairs(airports_map) do + local name=ap:GetName() + RAT.ATC.airport[name]={} + RAT.ATC.airport[name].queue={} + RAT.ATC.airport[name].busy=false + RAT.ATC.airport[name].onfinal={} + RAT.ATC.airport[name].Nonfinal=0 + RAT.ATC.airport[name].traffic=0 + RAT.ATC.airport[name].Tlastclearance=nil + end + SCHEDULER:New(nil, RAT._ATCCheck, {self}, 5, 15) + SCHEDULER:New(nil, RAT._ATCStatus, {self}, 5, 60) + RAT.ATC.T0=timer.getTime() + end +end + +--- Adds andd initializes a new flight after it was spawned. +-- @param #RAT self +-- @param #string name Group name of the flight. +-- @param #string dest Name of the destination airport. +function RAT:_ATCAddFlight(name, dest) + BASE:T(string.format("%sATC %s: Adding flight %s with destination %s.", RAT.id, dest, name, dest)) + RAT.ATC.flight[name]={} + RAT.ATC.flight[name].destination=dest + RAT.ATC.flight[name].Tarrive=-1 + RAT.ATC.flight[name].holding=-1 + RAT.ATC.flight[name].Tonfinal=-1 +end + +--- Deletes a flight from ATC lists after it landed. +-- @param #RAT self +-- @param #table t Table. +-- @param #string entry Flight name which shall be deleted. +function RAT:_ATCDelFlight(t,entry) + for k,_ in pairs(t) do + if k==entry then + t[entry]=nil + end + end +end + +--- Registers a flight once it is near its holding point at the final destination. +-- @param #RAT self +-- @param #string name Group name of the flight. +-- @param #number time Time the fight first registered. +function RAT:_ATCRegisterFlight(name, time) + BASE:T(RAT.id.."Flight ".. name.." registered at ATC for landing clearance.") + RAT.ATC.flight[name].Tarrive=time + RAT.ATC.flight[name].holding=0 +end + + +--- ATC status report about flights. +-- @param #RAT self +function RAT:_ATCStatus() + + -- Current time. + local Tnow=timer.getTime() + + for name,_ in pairs(RAT.ATC.flight) do + + -- Holding time at destination. + local hold=RAT.ATC.flight[name].holding + local dest=RAT.ATC.flight[name].destination + + if hold >= 0 then + + -- Some string whether the runway is busy or not. + local busy="Runway state is unknown" + if RAT.ATC.airport[dest].Nonfinal>0 then + busy="Runway is occupied by "..RAT.ATC.airport[dest].Nonfinal + else + busy="Runway is currently clear" + end + + -- Aircraft is holding. + local text=string.format("ATC %s: Flight %s is holding for %i:%02d. %s.", dest, name, hold/60, hold%60, busy) + BASE:T(RAT.id..text) + + elseif hold==RAT.ATC.onfinal then + + -- Aircarft is on final approach for landing. + local Tfinal=Tnow-RAT.ATC.flight[name].Tonfinal + + local text=string.format("ATC %s: Flight %s is on final. Waiting %i:%02d for landing event.", dest, name, Tfinal/60, Tfinal%60) + BASE:T(RAT.id..text) + + elseif hold==RAT.ATC.unregistered then + + -- Aircraft has not arrived at holding point. + --self:T(string.format("ATC %s: Flight %s is not registered yet (hold %d).", dest, name, hold)) + + else + BASE:E(RAT.id.."ERROR: Unknown holding time in RAT:_ATCStatus().") + end + end + +end + +--- Main ATC function. Updates the landing queue of all airports and inceases holding time for all flights. +-- @param #RAT self +function RAT:_ATCCheck() + + -- Init queue of flights at all airports. + RAT:_ATCQueue() + + -- Current time. + local Tnow=timer.getTime() + + for name,_ in pairs(RAT.ATC.airport) do + + for qID,flight in ipairs(RAT.ATC.airport[name].queue) do + + -- Number of aircraft in queue. + local nqueue=#RAT.ATC.airport[name].queue + + -- Conditions to clear an aircraft for landing + local landing1 + if RAT.ATC.airport[name].Tlastclearance then + -- Landing if time is enough and less then two planes are on final. + landing1=(Tnow-RAT.ATC.airport[name].Tlastclearance > RAT.ATC.delay) and RAT.ATC.airport[name].Nonfinal < RAT.ATC.Nclearance + else + landing1=false + end + -- No other aircraft is on final. + local landing2=RAT.ATC.airport[name].Nonfinal==0 + + + if not landing1 and not landing2 then + + -- Update holding time. + RAT.ATC.flight[flight].holding=Tnow-RAT.ATC.flight[flight].Tarrive + + -- Debug message. + local text=string.format("ATC %s: Flight %s runway is busy. You are #%d of %d in landing queue. Your holding time is %i:%02d.", name, flight,qID, nqueue, RAT.ATC.flight[flight].holding/60, RAT.ATC.flight[flight].holding%60) + BASE:T(RAT.id..text) + + else + + local text=string.format("ATC %s: Flight %s was cleared for landing. Your holding time was %i:%02d.", name, flight, RAT.ATC.flight[flight].holding/60, RAT.ATC.flight[flight].holding%60) + BASE:T(RAT.id..text) + + -- Clear flight for landing. + RAT:_ATCClearForLanding(name, flight) + + end + + end + + end + + -- Update queue of flights at all airports. + RAT:_ATCQueue() + +end + +--- Giving landing clearance for aircraft by setting user flag. +-- @param #RAT self +-- @param #string airport Name of destination airport. +-- @param #string flight Group name of flight, which gets landing clearence. +function RAT:_ATCClearForLanding(airport, flight) + -- Flight is cleared for landing. + RAT.ATC.flight[flight].holding=RAT.ATC.onfinal + -- Airport runway is busy now. + RAT.ATC.airport[airport].busy=true + -- Flight which is landing. + RAT.ATC.airport[airport].onfinal[flight]=flight + -- Number of planes on final approach. + RAT.ATC.airport[airport].Nonfinal=RAT.ATC.airport[airport].Nonfinal+1 + -- Last time an aircraft got landing clearance. + RAT.ATC.airport[airport].Tlastclearance=timer.getTime() + -- Current time. + RAT.ATC.flight[flight].Tonfinal=timer.getTime() + -- Set user flag to 1 ==> stop condition for holding. + trigger.action.setUserFlag(flight, 1) + local flagvalue=trigger.misc.getUserFlag(flight) + + -- Debug message. + local text1=string.format("ATC %s: Flight %s cleared for landing (flag=%d).", airport, flight, flagvalue) + local text2=string.format("ATC %s: Flight %s you are cleared for landing.", airport, flight) + BASE:T( RAT.id..text1) + MESSAGE:New(text2, 10):ToAllIf(RAT.ATC.messages) +end + +--- Takes care of organisational stuff after a plane has landed. +-- @param #RAT self +-- @param #string name Group name of flight. +function RAT:_ATCFlightLanded(name) + + if RAT.ATC.flight[name] then + + -- Destination airport. + local dest=RAT.ATC.flight[name].destination + + -- Times for holding and final approach. + local Tnow=timer.getTime() + local Tfinal=Tnow-RAT.ATC.flight[name].Tonfinal + local Thold=RAT.ATC.flight[name].Tonfinal-RAT.ATC.flight[name].Tarrive + + -- Airport is not busy any more. + RAT.ATC.airport[dest].busy=false + + -- No aircraft on final any more. + RAT.ATC.airport[dest].onfinal[name]=nil + + -- Decrease number of aircraft on final. + RAT.ATC.airport[dest].Nonfinal=RAT.ATC.airport[dest].Nonfinal-1 + + -- Remove this flight from list of flights. + RAT:_ATCDelFlight(RAT.ATC.flight, name) + + -- Increase landing counter to monitor traffic. + RAT.ATC.airport[dest].traffic=RAT.ATC.airport[dest].traffic+1 + + -- Number of planes landing per hour. + local TrafficPerHour=RAT.ATC.airport[dest].traffic/(timer.getTime()-RAT.ATC.T0)*3600 + + -- Debug info + local text1=string.format("ATC %s: Flight %s landed. Tholding = %i:%02d, Tfinal = %i:%02d.", dest, name, Thold/60, Thold%60, Tfinal/60, Tfinal%60) + local text2=string.format("ATC %s: Number of flights still on final %d.", dest, RAT.ATC.airport[dest].Nonfinal) + local text3=string.format("ATC %s: Traffic report: Number of planes landed in total %d. Flights/hour = %3.2f.", dest, RAT.ATC.airport[dest].traffic, TrafficPerHour) + local text4=string.format("ATC %s: Flight %s landed. Welcome to %s.", dest, name, dest) + BASE:T(RAT.id..text1) + BASE:T(RAT.id..text2) + BASE:T(RAT.id..text3) + MESSAGE:New(text4, 10):ToAllIf(RAT.ATC.messages) + end + +end + +--- Creates a landing queue for all flights holding at airports. Aircraft with longest holding time gets first permission to land. +-- @param #RAT self +function RAT:_ATCQueue() + + for airport,_ in pairs(RAT.ATC.airport) do + + -- Local airport queue. + local _queue={} + + -- Loop over all flights. + for name,_ in pairs(RAT.ATC.flight) do + --fvh + local Tnow=timer.getTime() + + -- Update holding time (unless holing is set to onfinal=-100) + if RAT.ATC.flight[name].holding>=0 then + RAT.ATC.flight[name].holding=Tnow-RAT.ATC.flight[name].Tarrive + end + local hold=RAT.ATC.flight[name].holding + local dest=RAT.ATC.flight[name].destination + + -- Flight is holding at this airport. + if hold>=0 and airport==dest then + _queue[#_queue+1]={name,hold} + end + end + + -- Sort queue w.r.t holding time in ascending order. + local function compare(a,b) + return a[2] > b[2] + end + table.sort(_queue, compare) + + -- Transfer queue to airport queue. + RAT.ATC.airport[airport].queue={} + for k,v in ipairs(_queue) do + table.insert(RAT.ATC.airport[airport].queue, v[1]) + end + + end +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- RATMANAGER class +-- @type RATMANAGER +-- @field #string ClassName Name of the Class. +-- @field #boolean Debug If true, be more verbose on output in DCS.log file. +-- @field #table rat Array holding RAT objects etc. +-- @field #string name Name (alias) of RAT object. +-- @field #table alive Number of currently alive groups. +-- @field #table min Minimum number of RAT groups alive. +-- @field #number nrat Number of RAT objects. +-- @field #number ntot Total number of active RAT groups. +-- @field #number Tcheck Time interval in seconds between checking of alive groups. +-- @field #number dTspawn Time interval in seconds between spawns of groups. +-- @field Core.Scheduler#SCHEDULER manager Scheduler managing the RAT objects. +-- @field #number managerid Managing scheduler id. +-- @extends Core.Base#BASE + +---# RATMANAGER class, extends @{Core.Base#BASE} +-- The RATMANAGER class manages spawning of multiple RAT objects in a very simple way. It is created by the @{#RATMANAGER.New}() contructor. +-- RAT objects with different "tasks" can be defined as usual. However, they **must not** be spawned via the @{#RAT.Spawn}() function. +-- +-- Instead, these objects can be added to the manager via the @{#RATMANAGER.Add}(ratobject, min) function, where the first parameter "ratobject" is the @{#RAT} object, while the second parameter "min" defines the +-- minimum number of RAT aircraft of that object, which are alive at all time. +-- +-- The @{#RATMANAGER} must be started by the @{#RATMANAGER.Start}(startime) function, where the optional argument "startime" specifies the delay time in seconds after which the manager is started and the spawning beginns. +-- If desired, the @{#RATMANAGER} can be stopped by the @{#RATMANAGER.Stop}(stoptime) function. The parameter "stoptime" specifies the time delay in seconds after which the manager stops. +-- When this happens, no new aircraft will be spawned and the population will eventually decrease to zero. +-- +-- When you are using a time intervall like @{#RATMANAGER.dTspawn}(delay), @{#RATMANAGER} will ignore the amount set with @{#RATMANAGER.New}(). @{#RATMANAGER.dTspawn}(delay) will spawn infinite groups. +-- +-- ## Example +-- In this example, three different @{#RAT} objects are created (but not spawned manually). The @{#RATMANAGER} takes care that at least five aircraft of each type are alive and that the total number of aircraft +-- spawned is 25. The @{#RATMANAGER} is started after 30 seconds and stopped after two hours. +-- +-- local a10c=RAT:New("RAT_A10C", "A-10C managed") +-- a10c:SetDeparture({"Batumi"}) +-- +-- local f15c=RAT:New("RAT_F15C", "F15C managed") +-- f15c:SetDeparture({"Sochi-Adler"}) +-- f15c:DestinationZone() +-- f15c:SetDestination({"Zone C"}) +-- +-- local av8b=RAT:New("RAT_AV8B", "AV8B managed") +-- av8b:SetDeparture({"Zone C"}) +-- av8b:SetTakeoff("air") +-- av8b:DestinationZone() +-- av8b:SetDestination({"Zone A"}) +-- +-- local manager=RATMANAGER:New(25) +-- manager:Add(a10c, 5) +-- manager:Add(f15c, 5) +-- manager:Add(av8b, 5) +-- manager:Start(30) +-- manager:Stop(7200) +-- +-- @field #RATMANAGER +RATMANAGER={ + ClassName="RATMANAGER", + Debug=false, + rat={}, + name={}, + alive={}, + min={}, + nrat=0, + ntot=nil, + Tcheck=60, + dTspawn=1.0, + manager=nil, + managerid=nil, +} + +--- Some ID to identify who we are in output of the DCS.log file. +-- @field #string id +RATMANAGER.id="RATMANAGER | " + +--- Creates a new RATMANAGER object. +-- @param #RATMANAGER self +-- @param #number ntot Total number of RAT flights. +-- @return #RATMANAGER RATMANAGER object +function RATMANAGER:New(ntot) + + -- Inherit BASE. + local self=BASE:Inherit(self, BASE:New()) -- #RATMANAGER + + -- Total number of RAT groups. + self.ntot=ntot or 1 + + -- Debug info + self:E(RATMANAGER.id..string.format("Creating manager for %d groups.", ntot)) + + return self +end + + +--- Adds a RAT object to the RAT manager. Parameter min specifies the limit how many RAT groups are at least alive. +-- @param #RATMANAGER self +-- @param #RAT ratobject RAT object to be managed. +-- @param #number min Minimum number of groups for this RAT object. Default is 1. +-- @return #RATMANAGER RATMANAGER self object. +function RATMANAGER:Add(ratobject,min) + + --Automatic respawning is disabled. + ratobject.norespawn=true + ratobject.f10menu=false + + -- Increase RAT object counter. + self.nrat=self.nrat+1 + + self.rat[self.nrat]=ratobject + self.alive[self.nrat]=0 + self.name[self.nrat]=ratobject.alias + self.min[self.nrat]=min or 1 + + -- Debug info. + self:T(RATMANAGER.id..string.format("Adding ratobject %s with min flights = %d", self.name[self.nrat],self.min[self.nrat])) + + -- Call spawn to initialize RAT parameters. + ratobject:Spawn(0) + + return self +end + +--- Starts the RAT manager and spawns the initial random number RAT groups for each RAT object. +-- @param #RATMANAGER self +-- @param #number delay Time delay in seconds after which the RAT manager is started. Default is 5 seconds. +-- @return #RATMANAGER RATMANAGER self object. +function RATMANAGER:Start(delay) + + -- Time delay. + local delay=delay or 5 + + -- Info text. + local text=string.format(RATMANAGER.id.."RAT manager will be started in %d seconds.\n", delay) + text=text..string.format("Managed groups:\n") + for i=1,self.nrat do + text=text..string.format("- %s with min groups %d\n", self.name[i], self.min[i]) + end + text=text..string.format("Number of constantly alive groups %d", self.ntot) + self:E(text) + + -- Start scheduler. + SCHEDULER:New(nil, self._Start, {self}, delay) + + return self +end + +--- Instantly starts the RAT manager and spawns the initial random number RAT groups for each RAT object. +-- @param #RATMANAGER self +-- @return #RATMANAGER RATMANAGER self object. +function RATMANAGER:_Start() + + -- Ensure that ntot is at least sum of min RAT groups. + local n=0 + for i=1,self.nrat do + n=n+self.min[i] + end + self.ntot=math.max(self.ntot, n) + + -- Get randum number of new RAT groups. + local N=self:_RollDice(self.nrat, self.ntot, self.min, self.alive) + + -- Loop over all RAT objects and spawn groups. + local time=0.0 + for i=1,self.nrat do + for j=1,N[i] do + time=time+self.dTspawn + SCHEDULER:New(nil, RAT._SpawnWithRoute, {self.rat[i]}, time) + end + end + + -- Start activation scheduler for uncontrolled aircraft. + for i=1,self.nrat do + if self.rat[i].uncontrolled and self.rat[i].activate_uncontrolled then + -- Start activating stuff but not before the latest spawn has happend. + local Tactivate=math.max(time+1, self.rat[i].activate_delay) + SCHEDULER:New(self.rat[i], self.rat[i]._ActivateUncontrolled, {self.rat[i]}, Tactivate, self.rat[i].activate_delta, self.rat[i].activate_frand) + end + end + + -- Start the manager. But not earlier than the latest spawn has happened! + local TstartManager=math.max(time+1, self.Tcheck) + + -- Start manager scheduler. + self.manager, self.managerid = SCHEDULER:New(self, self._Manage, {self}, TstartManager, self.Tcheck) --Core.Scheduler#SCHEDULER + + -- Info + local text=string.format(RATMANAGER.id.."Starting RAT manager with scheduler ID %s in %d seconds. Repeat interval %d seconds.", self.managerid, TstartManager, self.Tcheck) + self:E(text) + + return self +end + +--- Stops the RAT manager. +-- @param #RATMANAGER self +-- @param #number delay Delay in seconds before the manager is stopped. Default is 1 second. +-- @return #RATMANAGER RATMANAGER self object. +function RATMANAGER:Stop(delay) + delay=delay or 1 + self:E(string.format(RATMANAGER.id.."Manager will be stopped in %d seconds.", delay)) + SCHEDULER:New(nil, self._Stop, {self}, delay) + return self +end + +--- Instantly stops the RAT manager by terminating its scheduler. +-- @param #RATMANAGER self +-- @return #RATMANAGER RATMANAGER self object. +function RATMANAGER:_Stop() + self:E(string.format(RATMANAGER.id.."Stopping manager with scheduler ID %s.", self.managerid)) + self.manager:Stop(self.managerid) + return self +end + +--- Sets the time interval between checks of alive RAT groups. Default is 60 seconds. +-- @param #RATMANAGER self +-- @param #number dt Time interval in seconds. +-- @return #RATMANAGER RATMANAGER self object. +function RATMANAGER:SetTcheck(dt) + self.Tcheck=dt or 60 + return self +end + +--- Sets the time interval between spawning of groups. +-- @param #RATMANAGER self +-- @param #number dt Time interval in seconds. Default is 1 second. +-- @return #RATMANAGER RATMANAGER self object. +function RATMANAGER:SetTspawn(dt) + self.dTspawn=dt or 1.0 + return self +end + + +--- Manager function. Calculating the number of current groups and respawning new groups if necessary. +-- @param #RATMANAGER self +function RATMANAGER:_Manage() + + -- Count total number of groups. + local ntot=self:_Count() + + -- Debug info. + local text=string.format("Number of alive groups %d. New groups to be spawned %d.", ntot, self.ntot-ntot) + self:T(RATMANAGER.id..text) + + -- Get number of necessary spawns. + local N=self:_RollDice(self.nrat, self.ntot, self.min, self.alive) + + -- Loop over all RAT objects and spawn new groups if necessary. + local time=0.0 + for i=1,self.nrat do + for j=1,N[i] do + time=time+self.dTspawn + SCHEDULER:New(nil, RAT._SpawnWithRoute, {self.rat[i]}, time) + end + end +end + +--- Counts the number of alive RAT objects. +-- @param #RATMANAGER self +function RATMANAGER:_Count() + + -- Init total counter. + local ntotal=0 + + -- Loop over all RAT objects. + for i=1,self.nrat do + local n=0 + + local ratobject=self.rat[i] --#RAT + + -- Loop over the RAT groups of this object. + for spawnindex,ratcraft in pairs(ratobject.ratcraft) do + local group=ratcraft.group --Wrapper.Group#GROUP + if group and group:IsAlive() then + n=n+1 + end + end + + -- Alive groups of this RAT object. + self.alive[i]=n + + -- Grand total. + ntotal=ntotal+n + + -- Debug output. + local text=string.format("Number of alive groups of %s = %d", self.name[i], n) + self:T(RATMANAGER.id..text) + end + + -- Return grand total. + return ntotal +end + +--- Rolls the dice for the number of necessary spawns. +-- @param #RATMANAGER self +-- @param #number nrat Number of RAT objects. +-- @param #number ntot Total number of RAT flights. +-- @param #table min Minimum number of groups for each RAT object. +-- @param #table alive Number of alive groups of each RAT object. +function RATMANAGER:_RollDice(nrat,ntot,min,alive) + + -- Calculate sum. + local function sum(A,index) + local summe=0 + for _,i in ipairs(index) do + summe=summe+A[i] + end + return summe + end + + -- Table of number of groups. + local N={} + local M={} + local P={} + for i=1,nrat do + N[#N+1]=0 + M[#M+1]=math.max(alive[i], min[i]) + P[#P+1]=math.max(min[i]-alive[i],0) + end + + -- Min/max group arrays. + local mini={} + local maxi={} + + -- Arrays. + local rattab={} + for i=1,nrat do + table.insert(rattab,i) + end + local done={} + + -- Number of new groups to be added. + local nnew=ntot + for i=1,nrat do + nnew=nnew-alive[i] + end + + for i=1,nrat-1 do + + -- Random entry from . + local r=math.random(#rattab) + -- Get value + local j=rattab[r] + + table.remove(rattab, r) + table.insert(done,j) + + -- Sum up the number of already distributed groups. + local sN=sum(N, done) + -- Sum up the minimum number of yet to be distributed groups. + local sP=sum(P, rattab) + + -- Max number that can be distributed for this object. + maxi[j]=nnew-sN-sP + + -- Min number that should be distributed for this object + mini[j]=P[j] + + -- Random number of new groups for this RAT object. + if maxi[j] >= mini[j] then + N[j]=math.random(mini[j], maxi[j]) + else + N[j]=0 + end + + -- Debug info + self:T3(string.format("RATMANAGER: i=%d, alive=%d, min=%d, mini=%d, maxi=%d, add=%d, sumN=%d, sumP=%d", j, alive[j], min[j], mini[j], maxi[j], N[j],sN, sP)) + + end + + -- Last RAT object, number of groups is determined from number of already distributed groups and nnew. + local j=rattab[1] + N[j]=nnew-sum(N, done) + mini[j]=nnew-sum(N, done) + maxi[j]=nnew-sum(N, done) + table.remove(rattab, 1) + table.insert(done,j) + + -- Debug info + local text=RATMANAGER.id.."\n" + for i=1,nrat do + text=text..string.format("%s: i=%d, alive=%d, min=%d, mini=%d, maxi=%d, add=%d\n", self.name[i], i, alive[i], min[i], mini[i], maxi[i], N[i]) + end + text=text..string.format("Total # of groups to add = %d", sum(N, done)) + self:T(text) + + -- Return number of groups to be spawned. + return N +end +--- **Functional** - Range Practice. +-- +-- === +-- +-- The RANGE class enables easy set up of bombing and strafing ranges within DCS World. +-- +-- Implementation is based on the [Simple Range Script](https://forums.eagle.ru/showthread.php?t=157991) by [Ciribob](https://forums.eagle.ru/member.php?u=112175), which itself was motivated +-- by a script by SNAFU [see here](https://forums.eagle.ru/showthread.php?t=109174). +-- +-- [476th - Air Weapons Range Objects mod](http://www.476vfightergroup.com/downloads.php?do=file&id=287) is highly recommended for this class. +-- +-- **Main Features:** +-- +-- * Impact points of bombs, rockets and missiles are recorded and distance to closest range target is measured and reported to the player. +-- * Number of hits on strafing passes are counted and reported. Also the percentage of hits w.r.t fired shots is evaluated. +-- * Results of all bombing and strafing runs are stored and top 10 results can be displayed. +-- * Range targets can be marked by smoke. +-- * Range can be illuminated by illumination bombs for night missions. +-- * Bomb, rocket and missile impact points can be marked by smoke. +-- * Direct hits on targets can trigger flares. +-- * Smoke and flare colors can be adjusted for each player via radio menu. +-- * Range information and weather report at the range can be reported via radio menu. +-- * Persistence: Bombing range results can be saved to disk and loaded the next time the mission is started. +-- * Range control voice overs (>40) for hit assessment. +-- +-- === +-- +-- ## Youtube Videos: + -- +-- * [MOOSE YouTube Channel](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg) +-- * [MOOSE - On the Range - Demonstration Video](https://www.youtube.com/watch?v=kIXcxNB9_3M) +-- +-- === +-- +-- ## Missions: +-- +-- * [MAR - On the Range - MOOSE - SC](https://www.digitalcombatsimulator.com/en/files/3317765/) by shagrat +-- +-- === +-- +-- ## Sound files: [MOOSE Sound Files](https://github.com/FlightControl-Master/MOOSE_SOUND/releases) +-- +-- === +-- +-- ### Author: **[funkyfranky](https://forums.eagle.ru/member.php?u=115026)** +-- +-- ### Contributions: [FlightControl](https://forums.eagle.ru/member.php?u=89536), [Ciribob](https://forums.eagle.ru/member.php?u=112175) +-- +-- === +-- @module Functional.Range +-- @image Range.JPG + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- RANGE class +-- @type RANGE +-- @field #string ClassName Name of the Class. +-- @field #boolean Debug If true, debug info is send as messages on the screen. +-- @field #boolean verbose Verbosity level. Higher means more output to DCS log file. +-- @field #string id String id of range for output in DCS log. +-- @field #string rangename Name of the range. +-- @field Core.Point#COORDINATE location Coordinate of the range location. +-- @field #number rangeradius Radius of range defining its total size for e.g. smoking bomb impact points and sending radio messages. Default 5 km. +-- @field Core.Zone#ZONE rangezone MOOSE zone object of the range. For example, no bomb impacts are smoked if bombs fall outside of the range zone. +-- @field #table strafeTargets Table of strafing targets. +-- @field #table bombingTargets Table of targets to bomb. +-- @field #number nbombtargets Number of bombing targets. +-- @field #number nstrafetargets Number of strafing targets. +-- @field #boolean messages Globally enable/disable all messages to players. +-- @field #table MenuAddedTo Table for monitoring which players already got an F10 menu. +-- @field #table planes Table for administration. +-- @field #table strafeStatus Table containing the current strafing target a player as assigned to. +-- @field #table strafePlayerResults Table containing the strafing results of each player. +-- @field #table bombPlayerResults Table containing the bombing results of each player. +-- @field #table PlayerSettings Individual player settings. +-- @field #number dtBombtrack Time step [sec] used for tracking released bomb/rocket positions. Default 0.005 seconds. +-- @field #number BombtrackThreshold Bombs/rockets/missiles are only tracked if player-range distance is smaller than this threshold [m]. Default 25000 m. +-- @field #number Tmsg Time [sec] messages to players are displayed. Default 30 sec. +-- @field #string examinergroupname Name of the examiner group which should get all messages. +-- @field #boolean examinerexclusive If true, only the examiner gets messages. If false, clients and examiner get messages. +-- @field #number strafemaxalt Maximum altitude above ground for registering for a strafe run. Default is 914 m = 3000 ft. +-- @field #number ndisplayresult Number of (player) results that a displayed. Default is 10. +-- @field Utilities.Utils#SMOKECOLOR BombSmokeColor Color id used for smoking bomb targets. +-- @field Utilities.Utils#SMOKECOLOR StrafeSmokeColor Color id used to smoke strafe targets. +-- @field Utilities.Utils#SMOKECOLOR StrafePitSmokeColor Color id used to smoke strafe pit approach boxes. +-- @field #number illuminationminalt Minimum altitude AGL in meters at which illumination bombs are fired. Default is 500 m. +-- @field #number illuminationmaxalt Maximum altitude AGL in meters at which illumination bombs are fired. Default is 1000 m. +-- @field #number scorebombdistance Distance from closest target up to which bomb hits are counted. Default 1000 m. +-- @field #number TdelaySmoke Time delay in seconds between impact of bomb and starting the smoke. Default 3 seconds. +-- @field #boolean eventmoose If true, events are handled by MOOSE. If false, events are handled directly by DCS eventhandler. Default true. +-- @field #boolean trackbombs If true (default), all bomb types are tracked and impact point to closest bombing target is evaluated. +-- @field #boolean trackrockets If true (default), all rocket types are tracked and impact point to closest bombing target is evaluated. +-- @field #boolean trackmissiles If true (default), all missile types are tracked and impact point to closest bombing target is evaluated. +-- @field #boolean defaultsmokebomb If true, initialize player settings to smoke bomb. +-- @field #boolean autosave If true, automatically save results every X seconds. +-- @field #number instructorfreq Frequency on which the range control transmitts. +-- @field Sound.RadioQueue#RADIOQUEUE instructor Instructor radio queue. +-- @field #number rangecontrolfreq Frequency on which the range control transmitts. +-- @field Sound.RadioQueue#RADIOQUEUE rangecontrol Range control radio queue. +-- @field #string rangecontrolrelayname Name of relay unit. +-- @field #string instructorrelayname Name of relay unit. +-- @field #string soundpath Path inside miz file where the sound files are located. Default is "Range Soundfiles/". +-- @extends Core.Fsm#FSM + +--- *Don't only practice your art, but force your way into its secrets; art deserves that, for it and knowledge can raise man to the Divine.* - Ludwig van Beethoven +-- +-- === +-- +-- ![Banner Image](..\Presentations\RANGE\RANGE_Main.png) +-- +-- # The Range Concept +-- +-- The RANGE class enables a mission designer to easily set up practice ranges in DCS. A new RANGE object can be created with the @{#RANGE.New}(*rangename*) contructor. +-- The parameter *rangename* defines the name of the range. It has to be unique since this is also the name displayed in the radio menu. +-- +-- Generally, a range consists of strafe pits and bombing targets. For strafe pits the number of hits for each pass is counted and tabulated. +-- For bombing targets, the distance from the impact point of the bomb, rocket or missile to the closest range target is measured and tabulated. +-- Each player can display his best results via a function in the radio menu or see the best best results from all players. +-- +-- When all targets have been defined in the script, the range is started by the @{#RANGE.Start}() command. +-- +-- **IMPORTANT** +-- +-- Due to a DCS bug, it is not possible to directly monitor when a player enters a plane. So in a mission with client slots, it is vital that +-- a player first enters as spectator or hits ESC twice and **after that** jumps into the slot of his aircraft! +-- If that is not done, the script is not started correctly. This can be checked by looking at the radio menues. If the mission was entered correctly, +-- there should be an "On the Range" menu items in the "F10. Other..." menu. +-- +-- # Strafe Pits +-- +-- Each strafe pit can consist of multiple targets. Often one finds two or three strafe targets next to each other. +-- +-- A strafe pit can be added to the range by the @{#RANGE.AddStrafePit}(*targetnames, boxlength, boxwidth, heading, inverseheading, goodpass, foulline*) function. +-- +-- * The first parameter *targetnames* defines the target or targets. This has to be given as a lua table which contains the names of @{Wrapper.Unit} or @{Static} objects defined in the mission editor. +-- * In order to perform a valid pass on the strafe pit, the pilot has to begin his run from the correct direction. Therefore, an "approach box" is defined in front +-- of the strafe targets. The parameters *boxlength* and *boxwidth* define the size of the box while the parameter *heading* defines its direction. +-- If the parameter *heading* is passed as **nil**, the heading is automatically taken from the heading of the first target unit as defined in the ME. +-- The parameter *inverseheading* turns the heading around by 180 degrees. This is sometimes useful, since the default heading of strafe target units point in the +-- wrong/opposite direction. +-- * The parameter *goodpass* defines the number of hits a pilot has to achieve during a run to be judged as a "good" pass. +-- * The last parameter *foulline* sets the distance from the pit targets to the foul line. Hit from closer than this line are not counted! +-- +-- Another function to add a strafe pit is @{#RANGE.AddStrafePitGroup}(*group, boxlength, boxwidth, heading, inverseheading, goodpass, foulline*). Here, +-- the first parameter *group* is a MOOSE @{Wrapper.Group} object and **all** units in this group define **one** strafe pit. +-- +-- Finally, a valid approach has to be performed below a certain maximum altitude. The default is 914 meters (3000 ft) AGL. This is a parameter valid for all +-- strafing pits of the range and can be adjusted by the @{#RANGE.SetMaxStrafeAlt}(maxalt) function. +-- +-- # Bombing targets +-- +-- One ore multiple bombing targets can be added to the range by the @{#RANGE.AddBombingTargets}(targetnames, goodhitrange, randommove) function. +-- +-- * The first parameter *targetnames* has to be a lua table, which contains the names of @{Wrapper.Unit} and/or @{Static} objects defined in the mission editor. +-- Note that the @{Range} logic **automatically** determines, if a name belongs to a @{Wrapper.Unit} or @{Static} object now. +-- * The (optional) parameter *goodhitrange* specifies the radius around the target. If a bomb or rocket falls at a distance smaller than this number, the hit is considered to be "good". +-- * If final (optional) parameter "*randommove*" can be enabled to create moving targets. If this parameter is set to true, the units of this bombing target will randomly move within the range zone. +-- Note that there might be quirks since DCS units can get stuck in buildings etc. So it might be safer to manually define a route for the units in the mission editor if moving targets are desired. +-- +-- ## Adding Groups +-- +-- Another possibility to add bombing targets is the @{#RANGE.AddBombingTargetGroup}(*group, goodhitrange, randommove*) function. Here the parameter *group* is a MOOSE @{Wrapper.Group} object +-- and **all** units in this group are defined as bombing targets. +-- +-- ## Specifying Coordinates +-- +-- It is also possible to specify coordinates rather than unit or static objects as bombing target locations. This has the advantage, that even when the unit/static object is dead, the specified +-- coordinate will still be a valid impact point. This can be done via the @{#RANGE.AddBombingTargetCoordinate}(*coord*, *name*, *goodhitrange*) function. +-- +-- # Fine Tuning +-- +-- Many range parameters have good default values. However, the mission designer can change these settings easily with the supplied user functions: +-- +-- * @{#RANGE.SetMaxStrafeAlt}() sets the max altitude for valid strafing runs. +-- * @{#RANGE.SetMessageTimeDuration}() sets the duration how long (most) messages are displayed. +-- * @{#RANGE.SetDisplayedMaxPlayerResults}() sets the number of results displayed. +-- * @{#RANGE.SetRangeRadius}() defines the total range area. +-- * @{#RANGE.SetBombTargetSmokeColor}() sets the color used to smoke bombing targets. +-- * @{#RANGE.SetStrafeTargetSmokeColor}() sets the color used to smoke strafe targets. +-- * @{#RANGE.SetStrafePitSmokeColor}() sets the color used to smoke strafe pit approach boxes. +-- * @{#RANGE.SetSmokeTimeDelay}() sets the time delay between smoking bomb/rocket impact points after impact. +-- * @{#RANGE.TrackBombsON}() or @{#RANGE.TrackBombsOFF}() can be used to enable/disable tracking and evaluating of all bomb types a player fires. +-- * @{#RANGE.TrackRocketsON}() or @{#RANGE.TrackRocketsOFF}() can be used to enable/disable tracking and evaluating of all rocket types a player fires. +-- * @{#RANGE.TrackMissilesON}() or @{#RANGE.TrackMissilesOFF}() can be used to enable/disable tracking and evaluating of all missile types a player fires. +-- +-- # Radio Menu +-- +-- Each range gets a radio menu with various submenus where each player can adjust his individual settings or request information about the range or his scores. +-- +-- The main range menu can be found at "F10. Other..." --> "F*X*. On the Range..." --> "F1. ...". +-- +-- The range menu contains the following submenues: +-- +-- ![Banner Image](..\Presentations\RANGE\Menu_Main.png) +-- +-- * "F1. Statistics...": Range results of all players and personal stats. +-- * "F2. Mark Targets": Mark range targets by smoke or flares. +-- * "F3. My Settings" Personal settings. +-- * "F4. Range Info": Information about the range, such as bearing and range. +-- +-- ## F1 Statistics +-- +-- ![Banner Image](..\Presentations\RANGE\Menu_Stats.png) +-- +-- ## F2 Mark Targets +-- +-- ![Banner Image](..\Presentations\RANGE\Menu_Stats.png) +-- +-- ## F3 My Settings +-- +-- ![Banner Image](..\Presentations\RANGE\Menu_MySettings.png) +-- +-- ## F4 Range Info +-- +-- ![Banner Image](..\Presentations\RANGE\Menu_RangeInfo.png) +-- +-- # Voice Overs +-- +-- Voice over sound files can be downloaded from the Moose Discord. Check the pinned messages in the *#func-range* channel. +-- +-- Instructor radio will inform players when they enter or exit the range zone and provide the radio frequency of the range control for hit assessment. +-- This can be enabled via the @{#RANGE.SetInstructorRadio}(*frequency*) functions, where *frequency* is the AM frequency in MHz. +-- +-- The range control can be enabled via the @{#RANGE.SetRangeControl}(*frequency*) functions, where *frequency* is the AM frequency in MHz. +-- +-- By default, the sound files are placed in the "Range Soundfiles/" folder inside the mission (.miz) file. Another folder can be specified via the @{#RANGE.SetSoundfilesPath}(*path*) function. +-- +-- # Persistence +-- +-- To automatically save bombing results to disk, use the @{#RANGE.SetAutosave}() function. Bombing results will be saved as csv file in your "Saved Games\DCS.openbeta\Logs" directory. +-- Each range has a separate file, which is named "RANGE-<*RangeName*>_BombingResults.csv". +-- +-- The next time you start the mission, these results are also automatically loaded. +-- +-- Strafing results are currently **not** saved. +-- +-- # Examples +-- +-- ## Goldwater Range +-- +-- This example shows hot to set up the [Barry M. Goldwater range](https://en.wikipedia.org/wiki/Barry_M._Goldwater_Air_Force_Range). +-- It consists of two strafe pits each has two targets plus three bombing targets. +-- +-- -- Strafe pits. Each pit can consist of multiple targets. Here we have two pits and each of the pits has two targets. +-- -- These are names of the corresponding units defined in the ME. +-- local strafepit_left={"GWR Strafe Pit Left 1", "GWR Strafe Pit Left 2"} +-- local strafepit_right={"GWR Strafe Pit Right 1", "GWR Strafe Pit Right 2"} +-- +-- -- Table of bombing target names. Again these are the names of the corresponding units as defined in the ME. +-- local bombtargets={"GWR Bomb Target Circle Left", "GWR Bomb Target Circle Right", "GWR Bomb Target Hard"} +-- +-- -- Create a range object. +-- GoldwaterRange=RANGE:New("Goldwater Range") +-- +-- -- Distance between strafe target and foul line. You have to specify the names of the unit or static objects. +-- -- Note that this could also be done manually by simply measuring the distance between the target and the foul line in the ME. +-- GoldwaterRange:GetFoullineDistance("GWR Strafe Pit Left 1", "GWR Foul Line Left") +-- +-- -- Add strafe pits. Each pit (left and right) consists of two targets. +-- GoldwaterRange:AddStrafePit(strafepit_left, 3000, 300, nil, true, 20, fouldist) +-- GoldwaterRange:AddStrafePit(strafepit_right, nil, nil, nil, true, nil, fouldist) +-- +-- -- Add bombing targets. A good hit is if the bomb falls less then 50 m from the target. +-- GoldwaterRange:AddBombingTargets(bombtargets, 50) +-- +-- -- Start range. +-- GoldwaterRange:Start() +-- +-- The [476th - Air Weapons Range Objects mod](http://www.476vfightergroup.com/downloads.php?do=file&id=287) is (implicitly) used in this example. +-- +-- # Debugging +-- +-- In case you have problems, it is always a good idea to have a look at your DCS log file. You find it in your "Saved Games" folder, so for example in +-- C:\Users\\Saved Games\DCS\Logs\dcs.log +-- All output concerning the RANGE class should have the string "RANGE" in the corresponding line. +-- +-- The verbosity of the output can be increased by adding the following lines to your script: +-- +-- BASE:TraceOnOff(true) +-- BASE:TraceLevel(1) +-- BASE:TraceClass("RANGE") +-- +-- To get even more output you can increase the trace level to 2 or even 3, c.f. @{BASE} for more details. +-- +-- The function @{#RANGE.DebugON}() can be used to send messages on screen. It also smokes all defined strafe and bombing targets, the strafe pit approach boxes and the range zone. +-- +-- Note that it can happen that the RANGE radio menu is not shown. Check that the range object is defined as a **global** variable rather than a local one. +-- The could avoid the lua garbage collection to accidentally/falsely deallocate the RANGE objects. +-- +-- +-- +-- @field #RANGE +RANGE={ + ClassName = "RANGE", + Debug = false, + verbose = 0, + id = nil, + rangename = nil, + location = nil, + messages = true, + rangeradius = 5000, + rangezone = nil, + strafeTargets = {}, + bombingTargets = {}, + nbombtargets = 0, + nstrafetargets = 0, + MenuAddedTo = {}, + planes = {}, + strafeStatus = {}, + strafePlayerResults = {}, + bombPlayerResults = {}, + PlayerSettings = {}, + dtBombtrack = 0.005, + BombtrackThreshold = 25000, + Tmsg = 30, + examinergroupname = nil, + examinerexclusive = nil, + strafemaxalt = 914, + ndisplayresult = 10, + BombSmokeColor = SMOKECOLOR.Red, + StrafeSmokeColor = SMOKECOLOR.Green, + StrafePitSmokeColor = SMOKECOLOR.White, + illuminationminalt = 500, + illuminationmaxalt = 1000, + scorebombdistance = 1000, + TdelaySmoke = 3.0, + eventmoose = true, + trackbombs = true, + trackrockets = true, + trackmissiles = true, + defaultsmokebomb = true, + autosave = false, + instructorfreq = nil, + instructor = nil, + rangecontrolfreq = nil, + rangecontrol = nil, + soundpath = "Range Soundfiles/" +} + +--- Default range parameters. +-- @list Defaults +RANGE.Defaults={ + goodhitrange=25, + strafemaxalt=914, + dtBombtrack=0.005, + Tmsg=30, + ndisplayresult=10, + rangeradius=5000, + TdelaySmoke=3.0, + boxlength=3000, + boxwidth=300, + goodpass=20, + goodhitrange=25, + foulline=610, +} + +--- Target type, i.e. unit, static, or coordinate. +-- @type RANGE.TargetType +-- @field #string UNIT Target is a unit. +-- @field #string STATIC Target is a static. +-- @field #string COORD Target is a coordinate. +RANGE.TargetType={ + UNIT="Unit", + STATIC="Static", + COORD="Coordinate", +} + +--- Player settings. +-- @type RANGE.PlayerData +-- @field #boolean smokebombimpact Smoke bomb impact points. +-- @field #boolean flaredirecthits Flare when player directly hits a target. +-- @field #number smokecolor Color of smoke. +-- @field #number flarecolor Color of flares. +-- @field #boolean messages Display info messages. +-- @field Wrapper.Client#CLIENT client Client object of player. +-- @field #string unitname Name of player aircraft unit. +-- @field #string playername Name of player. +-- @field #string airframe Aircraft type name. +-- @field #boolean inzone If true, player is inside the range zone. + +--- Bomb target data. +-- @type RANGE.BombTarget +-- @field #string name Name of unit. +-- @field Wrapper.Unit#UNIT target Target unit. +-- @field Core.Point#COORDINATE coordinate Coordinate of the target. +-- @field #number goodhitrange Range in meters for a good hit. +-- @field #boolean move If true, unit move randomly. +-- @field #number speed Speed of unit. +-- @field #RANGE.TargetType type Type of target. + +--- Strafe target data. +-- @type RANGE.StrafeTarget +-- @field #string name Name of the unit. +-- @field Core.Zone#ZONE_POLYGON polygon Polygon zone. +-- @field Core.Point#COORDINATE coordinate Center coordinate of the pit. +-- @field #number goodPass Number of hits for a good pass. +-- @field #table targets Table of target units. +-- @field #number foulline Foul line +-- @field #number smokepoints Number of smoke points. +-- @field #number heading Heading of pit. + +--- Bomb target result. +-- @type RANGE.BombResult +-- @field #string name Name of closest target. +-- @field #number distance Distance in meters. +-- @field #number radial Radial in degrees. +-- @field #string weapon Name of the weapon. +-- @field #string quality Hit quality. +-- @field #string player Player name. +-- @field #string airframe Aircraft type of player. +-- @field #number time Time via timer.getAbsTime() in seconds of impact. +-- @field #string date OS date. + +--- Sound file data. +-- @type RANGE.Soundfile +-- @field #string filename Name of the file +-- @field #number duration Duration in seconds. + +--- Sound files. +-- @type RANGE.Sound +-- @field #RANGE.Soundfile RC0 +-- @field #RANGE.Soundfile RC1 +-- @field #RANGE.Soundfile RC2 +-- @field #RANGE.Soundfile RC3 +-- @field #RANGE.Soundfile RC4 +-- @field #RANGE.Soundfile RC5 +-- @field #RANGE.Soundfile RC6 +-- @field #RANGE.Soundfile RC7 +-- @field #RANGE.Soundfile RC8 +-- @field #RANGE.Soundfile RC9 +-- @field #RANGE.Soundfile RCAccuracy +-- @field #RANGE.Soundfile RCDegrees +-- @field #RANGE.Soundfile RCExcellentHit +-- @field #RANGE.Soundfile RCExcellentPass +-- @field #RANGE.Soundfile RCFeet +-- @field #RANGE.Soundfile RCFor +-- @field #RANGE.Soundfile RCGoodHit +-- @field #RANGE.Soundfile RCGoodPass +-- @field #RANGE.Soundfile RCHitsOnTarget +-- @field #RANGE.Soundfile RCImpact +-- @field #RANGE.Soundfile RCIneffectiveHit +-- @field #RANGE.Soundfile RCIneffectivePass +-- @field #RANGE.Soundfile RCInvalidHit +-- @field #RANGE.Soundfile RCLeftStrafePitTooQuickly +-- @field #RANGE.Soundfile RCPercent +-- @field #RANGE.Soundfile RCPoorHit +-- @field #RANGE.Soundfile RCPoorPass +-- @field #RANGE.Soundfile RCRollingInOnStrafeTarget +-- @field #RANGE.Soundfile RCTotalRoundsFired +-- @field #RANGE.Soundfile RCWeaponImpactedTooFar +-- @field #RANGE.Soundfile IR0 +-- @field #RANGE.Soundfile IR1 +-- @field #RANGE.Soundfile IR2 +-- @field #RANGE.Soundfile IR3 +-- @field #RANGE.Soundfile IR4 +-- @field #RANGE.Soundfile IR5 +-- @field #RANGE.Soundfile IR6 +-- @field #RANGE.Soundfile IR7 +-- @field #RANGE.Soundfile IR8 +-- @field #RANGE.Soundfile IR9 +-- @field #RANGE.Soundfile IRDecimal +-- @field #RANGE.Soundfile IRMegaHertz +-- @field #RANGE.Soundfile IREnterRange +-- @field #RANGE.Soundfile IRExitRange +RANGE.Sound = { + RC0={filename="RC-0.ogg", duration=0.60}, + RC1={filename="RC-1.ogg", duration=0.47}, + RC2={filename="RC-2.ogg", duration=0.43}, + RC3={filename="RC-3.ogg", duration=0.50}, + RC4={filename="RC-4.ogg", duration=0.58}, + RC5={filename="RC-5.ogg", duration=0.54}, + RC6={filename="RC-6.ogg", duration=0.61}, + RC7={filename="RC-7.ogg", duration=0.53}, + RC8={filename="RC-8.ogg", duration=0.34}, + RC9={filename="RC-9.ogg", duration=0.54}, + RCAccuracy={filename="RC-Accuracy.ogg", duration=0.67}, + RCDegrees={filename="RC-Degrees.ogg", duration=0.59}, + RCExcellentHit={filename="RC-ExcellentHit.ogg", duration=0.76}, + RCExcellentPass={filename="RC-ExcellentPass.ogg", duration=0.89}, + RCFeet={filename="RC-Feet.ogg", duration=0.49}, + RCFor={filename="RC-For.ogg", duration=0.64}, + RCGoodHit={filename="RC-GoodHit.ogg", duration=0.52}, + RCGoodPass={filename="RC-GoodPass.ogg", duration=0.62}, + RCHitsOnTarget={filename="RC-HitsOnTarget.ogg", duration=0.88}, + RCImpact={filename="RC-Impact.ogg", duration=0.61}, + RCIneffectiveHit={filename="RC-IneffectiveHit.ogg", duration=0.86}, + RCIneffectivePass={filename="RC-IneffectivePass.ogg", duration=0.99}, + RCInvalidHit={filename="RC-InvalidHit.ogg", duration=2.97}, + RCLeftStrafePitTooQuickly={filename="RC-LeftStrafePitTooQuickly.ogg", duration=3.09}, + RCPercent={filename="RC-Percent.ogg", duration=0.56}, + RCPoorHit={filename="RC-PoorHit.ogg", duration=0.54}, + RCPoorPass={filename="RC-PoorPass.ogg", duration=0.68}, + RCRollingInOnStrafeTarget={filename="RC-RollingInOnStrafeTarget.ogg", duration=1.38}, + RCTotalRoundsFired={filename="RC-TotalRoundsFired.ogg", duration=1.22}, + RCWeaponImpactedTooFar={filename="RC-WeaponImpactedTooFar.ogg", duration=3.73}, + IR0={filename="IR-0.ogg", duration=0.55}, + IR1={filename="IR-1.ogg", duration=0.41}, + IR2={filename="IR-2.ogg", duration=0.37}, + IR3={filename="IR-3.ogg", duration=0.41}, + IR4={filename="IR-4.ogg", duration=0.37}, + IR5={filename="IR-5.ogg", duration=0.43}, + IR6={filename="IR-6.ogg", duration=0.55}, + IR7={filename="IR-7.ogg", duration=0.43}, + IR8={filename="IR-8.ogg", duration=0.38}, + IR9={filename="IR-9.ogg", duration=0.55}, + IRDecimal={filename="IR-Decimal.ogg", duration=0.54}, + IRMegaHertz={filename="IR-MegaHertz.ogg", duration=0.87}, + IREnterRange={filename="IR-EnterRange.ogg", duration=4.83}, + IRExitRange={filename="IR-ExitRange.ogg", duration=3.10}, +} + +--- Global list of all defined range names. +-- @field #table Names +RANGE.Names={} + +--- Main radio menu on group level. +-- @field #table MenuF10 Root menu table on group level. +RANGE.MenuF10={} + +--- Main radio menu on mission level. +-- @field #table MenuF10Root Root menu on mission level. +RANGE.MenuF10Root=nil + +--- Range script version. +-- @field #string version +RANGE.version="2.3.0" + +--TODO list: +--TODO: Verbosity level for messages. +--TODO: Add option for default settings such as smoke off. +--TODO: Add custom weapons, which can be specified by the user. +--TODO: Check if units are still alive. +--DONE: Add statics for strafe pits. +--DONE: Add missiles. +--DONE: Convert env.info() to self:T() +--DONE: Add user functions. +--DONE: Rename private functions, i.e. start with _functionname. +--DONE: number of displayed results variable. +--DONE: Add tire option for strafe pits. ==> No really feasible since tires are very small and cannot be seen. +--DONE: Check that menu texts are short enough to be correctly displayed in VR. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- RANGE contructor. Creates a new RANGE object. +-- @param #RANGE self +-- @param #string rangename Name of the range. Has to be unique. Will we used to create F10 menu items etc. +-- @return #RANGE RANGE object. +function RANGE:New(rangename) + BASE:F({rangename=rangename}) + + -- Inherit BASE. + local self=BASE:Inherit(self, FSM:New()) -- #RANGE + + -- Get range name. + --TODO: make sure that the range name is not given twice. This would lead to problems in the F10 radio menu. + self.rangename=rangename or "Practice Range" + + -- Log id. + self.id=string.format("RANGE %s | ", self.rangename) + + -- Debug info. + local text=string.format("Script version %s - creating new RANGE object %s.", RANGE.version, self.rangename) + self:I(self.id..text) + + -- Defaults + self:SetDefaultPlayerSmokeBomb() + + -- Start State. + self:SetStartState("Stopped") + + --- + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start RANGE script. + self:AddTransition("*", "Status", "*") -- Status of RANGE script. + self:AddTransition("*", "Impact", "*") -- Impact of bomb/rocket/missile. + self:AddTransition("*", "EnterRange", "*") -- Player enters the range. + self:AddTransition("*", "ExitRange", "*") -- Player leaves the range. + self:AddTransition("*", "Save", "*") -- Save player results. + self:AddTransition("*", "Load", "*") -- Load player results. + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the RANGE. Initializes parameters and starts event handlers. + -- @function [parent=#RANGE] Start + -- @param #RANGE self + + --- Triggers the FSM event "Start" after a delay. Starts the RANGE. Initializes parameters and starts event handlers. + -- @function [parent=#RANGE] __Start + -- @param #RANGE self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the RANGE and all its event handlers. + -- @param #RANGE self + + --- Triggers the FSM event "Stop" after a delay. Stops the RANGE and all its event handlers. + -- @function [parent=#RANGE] __Stop + -- @param #RANGE self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Status". + -- @function [parent=#RANGE] Status + -- @param #RANGE self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#RANGE] __Status + -- @param #RANGE self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Impact". + -- @function [parent=#RANGE] Impact + -- @param #RANGE self + -- @param #RANGE.BombResult result Data of bombing run. + -- @param #RANGE.PlayerData player Data of player settings etc. + + --- Triggers the FSM delayed event "Impact". + -- @function [parent=#RANGE] __Impact + -- @param #RANGE self + -- @param #number delay Delay in seconds before the function is called. + -- @param #RANGE.BombResult result Data of the bombing run. + -- @param #RANGE.PlayerData player Data of player settings etc. + + --- On after "Impact" event user function. Called when a bomb/rocket/missile impacted. + -- @function [parent=#RANGE] OnAfterImpact + -- @param #RANGE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #RANGE.BombResult result Data of the bombing run. + -- @param #RANGE.PlayerData player Data of player settings etc. + + --- Triggers the FSM event "EnterRange". + -- @function [parent=#RANGE] EnterRange + -- @param #RANGE self + -- @param #RANGE.PlayerData player Data of player settings etc. + + --- Triggers the FSM delayed event "EnterRange". + -- @function [parent=#RANGE] __EnterRange + -- @param #RANGE self + -- @param #number delay Delay in seconds before the function is called. + -- @param #RANGE.PlayerData player Data of player settings etc. + + --- On after "EnterRange" event user function. Called when a player enters the range zone. + -- @function [parent=#RANGE] OnAfterEnterRange + -- @param #RANGE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #RANGE.PlayerData player Data of player settings etc. + + --- Triggers the FSM event "ExitRange". + -- @function [parent=#RANGE] ExitRange + -- @param #RANGE self + -- @param #RANGE.PlayerData player Data of player settings etc. + + --- Triggers the FSM delayed event "ExitRange". + -- @function [parent=#RANGE] __ExitRange + -- @param #RANGE self + -- @param #number delay Delay in seconds before the function is called. + -- @param #RANGE.PlayerData player Data of player settings etc. + + --- On after "ExitRange" event user function. Called when a player leaves the range zone. + -- @function [parent=#RANGE] OnAfterExitRange + -- @param #RANGE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #RANGE.PlayerData player Data of player settings etc. + + -- Return object. + return self +end + +--- Initializes number of targets and location of the range. Starts the event handlers. +-- @param #RANGE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RANGE:onafterStart() + + -- Location/coordinate of range. + local _location=nil + + -- Count bomb targets. + local _count=0 + for _,_target in pairs(self.bombingTargets) do + _count=_count+1 + + -- Get range location. + if _location==nil then + _location=self:_GetBombTargetCoordinate(_target) + end + end + self.nbombtargets=_count + + -- Count strafing targets. + _count=0 + for _,_target in pairs(self.strafeTargets) do + _count=_count+1 + + for _,_unit in pairs(_target.targets) do + if _location==nil then + _location=_unit:GetCoordinate() + end + end + end + self.nstrafetargets=_count + + -- Location of the range. We simply take the first unit/target we find if it was not explicitly specified by the user. + if self.location==nil then + self.location=_location + end + + if self.location==nil then + local text=string.format("ERROR! No range location found. Number of strafe targets = %d. Number of bomb targets = %d.", self.nstrafetargets, self.nbombtargets) + self:E(self.id..text) + return + end + + -- Define a MOOSE zone of the range. + if self.rangezone==nil then + self.rangezone=ZONE_RADIUS:New(self.rangename, {x=self.location.x, y=self.location.z}, self.rangeradius) + end + + -- Starting range. + local text=string.format("Starting RANGE %s. Number of strafe targets = %d. Number of bomb targets = %d.", self.rangename, self.nstrafetargets, self.nbombtargets) + self:I(self.id..text) + + -- Event handling. + if self.eventmoose then + -- Events are handled my MOOSE. + self:T(self.id.."Events are handled by MOOSE.") + self:HandleEvent(EVENTS.Birth) + self:HandleEvent(EVENTS.Hit) + self:HandleEvent(EVENTS.Shot) + else + -- Events are handled directly by DCS. + self:T(self.id.."Events are handled directly by DCS.") + world.addEventHandler(self) + end + + -- Make bomb target move randomly within the range zone. + for _,_target in pairs(self.bombingTargets) do + + -- Check if it is a static object. + --local _static=self:_CheckStatic(_target.target:GetName()) + local _static=_target.type==RANGE.TargetType.STATIC + + if _target.move and _static==false and _target.speed>1 then + local unit=_target.target --Wrapper.Unit#UNIT + _target.target:PatrolZones({self.rangezone}, _target.speed*0.75, "Off road") + end + + end + + -- Init range control. + if self.rangecontrolfreq then + + -- Radio queue. + self.rangecontrol=RADIOQUEUE:New(self.rangecontrolfreq, nil, self.rangename) + self.rangecontrol.schedonce=true + + -- Init numbers. + self.rangecontrol:SetDigit(0, RANGE.Sound.RC0.filename, RANGE.Sound.RC0.duration, self.soundpath) + self.rangecontrol:SetDigit(1, RANGE.Sound.RC1.filename, RANGE.Sound.RC1.duration, self.soundpath) + self.rangecontrol:SetDigit(2, RANGE.Sound.RC2.filename, RANGE.Sound.RC2.duration, self.soundpath) + self.rangecontrol:SetDigit(3, RANGE.Sound.RC3.filename, RANGE.Sound.RC3.duration, self.soundpath) + self.rangecontrol:SetDigit(4, RANGE.Sound.RC4.filename, RANGE.Sound.RC4.duration, self.soundpath) + self.rangecontrol:SetDigit(5, RANGE.Sound.RC5.filename, RANGE.Sound.RC5.duration, self.soundpath) + self.rangecontrol:SetDigit(6, RANGE.Sound.RC6.filename, RANGE.Sound.RC6.duration, self.soundpath) + self.rangecontrol:SetDigit(7, RANGE.Sound.RC7.filename, RANGE.Sound.RC7.duration, self.soundpath) + self.rangecontrol:SetDigit(8, RANGE.Sound.RC8.filename, RANGE.Sound.RC8.duration, self.soundpath) + self.rangecontrol:SetDigit(9, RANGE.Sound.RC9.filename, RANGE.Sound.RC9.duration, self.soundpath) + + -- Set location where the messages are transmitted from. + self.rangecontrol:SetSenderCoordinate(self.location) + self.rangecontrol:SetSenderUnitName(self.rangecontrolrelayname) + + -- Start range control radio queue. + self.rangecontrol:Start(1, 0.1) + + -- Init range control. + if self.instructorfreq then + + -- Radio queue. + self.instructor=RADIOQUEUE:New(self.instructorfreq, nil, self.rangename) + self.instructor.schedonce=true + + -- Init numbers. + self.instructor:SetDigit(0, RANGE.Sound.IR0.filename, RANGE.Sound.IR0.duration, self.soundpath) + self.instructor:SetDigit(1, RANGE.Sound.IR1.filename, RANGE.Sound.IR1.duration, self.soundpath) + self.instructor:SetDigit(2, RANGE.Sound.IR2.filename, RANGE.Sound.IR2.duration, self.soundpath) + self.instructor:SetDigit(3, RANGE.Sound.IR3.filename, RANGE.Sound.IR3.duration, self.soundpath) + self.instructor:SetDigit(4, RANGE.Sound.IR4.filename, RANGE.Sound.IR4.duration, self.soundpath) + self.instructor:SetDigit(5, RANGE.Sound.IR5.filename, RANGE.Sound.IR5.duration, self.soundpath) + self.instructor:SetDigit(6, RANGE.Sound.IR6.filename, RANGE.Sound.IR6.duration, self.soundpath) + self.instructor:SetDigit(7, RANGE.Sound.IR7.filename, RANGE.Sound.IR7.duration, self.soundpath) + self.instructor:SetDigit(8, RANGE.Sound.IR8.filename, RANGE.Sound.IR8.duration, self.soundpath) + self.instructor:SetDigit(9, RANGE.Sound.IR9.filename, RANGE.Sound.IR9.duration, self.soundpath) + + -- Set location where the messages are transmitted from. + self.instructor:SetSenderCoordinate(self.location) + self.instructor:SetSenderUnitName(self.instructorrelayname) + + -- Start instructor radio queue. + self.instructor:Start(1, 0.1) + + end + + end + + -- Load prev results. + if self.autosave then + self:Load() + end + + -- Debug mode: smoke all targets and range zone. + if self.Debug then + self:_MarkTargetsOnMap() + self:_SmokeBombTargets() + self:_SmokeStrafeTargets() + self:_SmokeStrafeTargetBoxes() + self.rangezone:SmokeZone(SMOKECOLOR.White) + end + + self:__Status(-60) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set maximal strafing altitude. Player entering a strafe pit above that altitude are not registered for a valid pass. +-- @param #RANGE self +-- @param #number maxalt Maximum altitude AGL in meters. Default is 914 m= 3000 ft. +-- @return #RANGE self +function RANGE:SetMaxStrafeAlt(maxalt) + self.strafemaxalt=maxalt or RANGE.Defaults.strafemaxalt + return self +end + +--- Set time interval for tracking bombs. A smaller time step increases accuracy but needs more CPU time. +-- @param #RANGE self +-- @param #number dt Time interval in seconds. Default is 0.005 s. +-- @return #RANGE self +function RANGE:SetBombtrackTimestep(dt) + self.dtBombtrack=dt or RANGE.Defaults.dtBombtrack + return self +end + +--- Set time how long (most) messages are displayed. +-- @param #RANGE self +-- @param #number time Time in seconds. Default is 30 s. +-- @return #RANGE self +function RANGE:SetMessageTimeDuration(time) + self.Tmsg=time or RANGE.Defaults.Tmsg + return self +end + +--- Automatically save player results to disc. +-- @param #RANGE self +-- @return #RANGE self +function RANGE:SetAutosaveOn() + self.autosave=true + return self +end + +--- Switch off auto save player results. +-- @param #RANGE self +-- @return #RANGE self +function RANGE:SetAutosaveOff() + self.autosave=false + return self +end + +--- Set messages to examiner. The examiner will receive messages from all clients. +-- @param #RANGE self +-- @param #string examinergroupname Name of the group of the examiner. +-- @param #boolean exclusively If true, messages are send exclusively to the examiner, i.e. not to the clients. +-- @return #RANGE self +function RANGE:SetMessageToExaminer(examinergroupname, exclusively) + self.examinergroupname=examinergroupname + self.examinerexclusive=exclusively + return self +end + +--- Set max number of player results that are displayed. +-- @param #RANGE self +-- @param #number nmax Number of results. Default is 10. +-- @return #RANGE self +function RANGE:SetDisplayedMaxPlayerResults(nmax) + self.ndisplayresult=nmax or RANGE.Defaults.ndisplayresult + return self +end + +--- Set range radius. Defines the area in which e.g. bomb impacts are smoked. +-- @param #RANGE self +-- @param #number radius Radius in km. Default 5 km. +-- @return #RANGE self +function RANGE:SetRangeRadius(radius) + self.rangeradius=radius*1000 or RANGE.Defaults.rangeradius + return self +end + +--- Set player setting whether bomb impact points are smoked or not. +-- @param #RANGE self +-- @param #boolean switch If true nor nil default is to smoke impact points of bombs. +-- @return #RANGE self +function RANGE:SetDefaultPlayerSmokeBomb(switch) + if switch==true or switch==nil then + self.defaultsmokebomb=true + else + self.defaultsmokebomb=false + end + return self +end + +--- Set bomb track threshold distance. Bombs/rockets/missiles are only tracked if player-range distance is less than this distance. Default 25 km. +-- @param #RANGE self +-- @param #number distance Threshold distance in km. Default 25 km. +-- @return #RANGE self +function RANGE:SetBombtrackThreshold(distance) + self.BombtrackThreshold=(distance or 25)*1000 + return self +end + +--- Set range location. If this is not done, one (random) unit position of the range is used to determine the location of the range. +-- The range location determines the position at which the weather data is evaluated. +-- @param #RANGE self +-- @param Core.Point#COORDINATE coordinate Coordinate of the range. +-- @return #RANGE self +function RANGE:SetRangeLocation(coordinate) + self.location=coordinate + return self +end + +--- Set range zone. For example, no bomb impact points are smoked if a bomb falls outside of this zone. +-- If a zone is not explicitly specified, the range zone is determined by its location and radius. +-- @param #RANGE self +-- @param Core.Zone#ZONE zone MOOSE zone defining the range perimeters. +-- @return #RANGE self +function RANGE:SetRangeZone(zone) + self.rangezone=zone + return self +end + +--- Set smoke color for marking bomb targets. By default bomb targets are marked by red smoke. +-- @param #RANGE self +-- @param Utilities.Utils#SMOKECOLOR colorid Color id. Default SMOKECOLOR.Red. +-- @return #RANGE self +function RANGE:SetBombTargetSmokeColor(colorid) + self.BombSmokeColor=colorid or SMOKECOLOR.Red + return self +end + +--- Set score bomb distance. +-- @param #RANGE self +-- @param #number distance Distance in meters. Default 1000 m. +-- @return #RANGE self +function RANGE:SetScoreBombDistance(distance) + self.scorebombdistance=distance or 1000 + return self +end + +--- Set smoke color for marking strafe targets. By default strafe targets are marked by green smoke. +-- @param #RANGE self +-- @param Utilities.Utils#SMOKECOLOR colorid Color id. Default SMOKECOLOR.Green. +-- @return #RANGE self +function RANGE:SetStrafeTargetSmokeColor(colorid) + self.StrafeSmokeColor=colorid or SMOKECOLOR.Green + return self +end + +--- Set smoke color for marking strafe pit approach boxes. By default strafe pit boxes are marked by white smoke. +-- @param #RANGE self +-- @param Utilities.Utils#SMOKECOLOR colorid Color id. Default SMOKECOLOR.White. +-- @return #RANGE self +function RANGE:SetStrafePitSmokeColor(colorid) + self.StrafePitSmokeColor=colorid or SMOKECOLOR.White + return self +end + +--- Set time delay between bomb impact and starting to smoke the impact point. +-- @param #RANGE self +-- @param #number delay Time delay in seconds. Default is 3 seconds. +-- @return #RANGE self +function RANGE:SetSmokeTimeDelay(delay) + self.TdelaySmoke=delay or RANGE.Defaults.TdelaySmoke + return self +end + +--- Enable debug modus. +-- @param #RANGE self +-- @return #RANGE self +function RANGE:DebugON() + self.Debug=true + return self +end + +--- Disable debug modus. +-- @param #RANGE self +-- @return #RANGE self +function RANGE:DebugOFF() + self.Debug=false + return self +end + +--- Disable ALL messages to players. +-- @param #RANGE self +-- @return #RANGE self +function RANGE:SetMessagesOFF() + self.messages=false + return self +end + +--- Enable messages to players. This is the default +-- @param #RANGE self +-- @return #RANGE self +function RANGE:SetMessagesON() + self.messages=true + return self +end + + +--- Enables tracking of all bomb types. Note that this is the default setting. +-- @param #RANGE self +-- @return #RANGE self +function RANGE:TrackBombsON() + self.trackbombs=true + return self +end + +--- Disables tracking of all bomb types. +-- @param #RANGE self +-- @return #RANGE self +function RANGE:TrackBombsOFF() + self.trackbombs=false + return self +end + +--- Enables tracking of all rocket types. Note that this is the default setting. +-- @param #RANGE self +-- @return #RANGE self +function RANGE:TrackRocketsON() + self.trackrockets=true + return self +end + +--- Disables tracking of all rocket types. +-- @param #RANGE self +-- @return #RANGE self +function RANGE:TrackRocketsOFF() + self.trackrockets=false + return self +end + +--- Enables tracking of all missile types. Note that this is the default setting. +-- @param #RANGE self +-- @return #RANGE self +function RANGE:TrackMissilesON() + self.trackmissiles=true + return self +end + +--- Disables tracking of all missile types. +-- @param #RANGE self +-- @return #RANGE self +function RANGE:TrackMissilesOFF() + self.trackmissiles=false + return self +end + + +--- Enable range control and set frequency. +-- @param #RANGE self +-- @param #number frequency Frequency in MHz. Default 256 MHz. +-- @param #string relayunitname Name of the unit used for transmission. +-- @return #RANGE self +function RANGE:SetRangeControl(frequency, relayunitname) + self.rangecontrolfreq=frequency or 256 + self.rangecontrolrelayname=relayunitname + return self +end + +--- Enable instructor radio and set frequency. +-- @param #RANGE self +-- @param #number frequency Frequency in MHz. Default 305 MHz. +-- @param #string relayunitname Name of the unit used for transmission. +-- @return #RANGE self +function RANGE:SetInstructorRadio(frequency, relayunitname) + self.instructorfreq=frequency or 305 + self.instructorrelayname=relayunitname + return self +end + +--- Set sound files folder within miz file. +-- @param #RANGE self +-- @param #string path Path for sound files. Default "ATIS Soundfiles/". Mind the slash "/" at the end! +-- @return #RANGE self +function RANGE:SetSoundfilesPath(path) + self.soundpath=tostring(path or "Range Soundfiles/") + self:I(self.id..string.format("Setting sound files path to %s", self.soundpath)) + return self +end + +--- Add new strafe pit. For a strafe pit, hits from guns are counted. One pit can consist of several units. +-- Note, an approach is only valid, if the player enters via a zone in front of the pit, which defined by boxlength and boxheading. +-- Furthermore, the player must not be too high and fly in the direction of the pit to make a valid target apporoach. +-- @param #RANGE self +-- @param #table targetnames Table of unit or static names defining the strafe targets. The first target in the list determines the approach zone (heading and box). +-- @param #number boxlength (Optional) Length of the approach box in meters. Default is 3000 m. +-- @param #number boxwidth (Optional) Width of the approach box in meters. Default is 300 m. +-- @param #number heading (Optional) Approach heading in Degrees. Default is heading of the unit as defined in the mission editor. +-- @param #boolean inverseheading (Optional) Take inverse heading (heading --> heading - 180 Degrees). Default is false. +-- @param #number goodpass (Optional) Number of hits for a "good" strafing pass. Default is 20. +-- @param #number foulline (Optional) Foul line distance. Hits from closer than this distance are not counted. Default 610 m = 2000 ft. Set to 0 for no foul line. +-- @return #RANGE self +function RANGE:AddStrafePit(targetnames, boxlength, boxwidth, heading, inverseheading, goodpass, foulline) + self:F({targetnames=targetnames, boxlength=boxlength, boxwidth=boxwidth, heading=heading, inverseheading=inverseheading, goodpass=goodpass, foulline=foulline}) + + -- Create table if necessary. + if type(targetnames) ~= "table" then + targetnames={targetnames} + end + + -- Make targets + local _targets={} + local center=nil --Wrapper.Unit#UNIT + local ntargets=0 + + for _i,_name in ipairs(targetnames) do + + -- Check if we have a static or unit object. + local _isstatic=self:_CheckStatic(_name) + + local unit=nil + if _isstatic==true then + + -- Add static object. + self:T(self.id..string.format("Adding STATIC object %s as strafe target #%d.", _name, _i)) + unit=STATIC:FindByName(_name, false) + + elseif _isstatic==false then + + -- Add unit object. + self:T(self.id..string.format("Adding UNIT object %s as strafe target #%d.", _name, _i)) + unit=UNIT:FindByName(_name) + + else + + -- Neither unit nor static object with this name could be found. + local text=string.format("ERROR! Could not find ANY strafe target object with name %s.", _name) + self:E(self.id..text) + + end + + -- Add object to targets. + if unit then + table.insert(_targets, unit) + -- Define center as the first unit we find + if center==nil then + center=unit + end + ntargets=ntargets+1 + end + + end + + -- Check if at least one target could be found. + if ntargets==0 then + local text=string.format("ERROR! No strafe target could be found when calling RANGE:AddStrafePit() for range %s", self.rangename) + self:E(self.id..text) + return + end + + -- Approach box dimensions. + local l=boxlength or RANGE.Defaults.boxlength + local w=(boxwidth or RANGE.Defaults.boxwidth)/2 + + -- Heading: either manually entered or automatically taken from unit heading. + local heading=heading or center:GetHeading() + + -- Invert the heading since some units point in the "wrong" direction. In particular the strafe pit from 476th range objects. + if inverseheading ~= nil then + if inverseheading then + heading=heading-180 + end + end + if heading<0 then + heading=heading+360 + end + if heading>360 then + heading=heading-360 + end + + -- Number of hits called a "good" pass. + goodpass=goodpass or RANGE.Defaults.goodpass + + -- Foule line distance. + foulline=foulline or RANGE.Defaults.foulline + + -- Coordinate of the range. + local Ccenter=center:GetCoordinate() + + -- Name of the target defined as its unit name. + local _name=center:GetName() + + -- Points defining the approach area. + local p={} + p[#p+1]=Ccenter:Translate( w, heading+90) + p[#p+1]= p[#p]:Translate( l, heading) + p[#p+1]= p[#p]:Translate(2*w, heading-90) + p[#p+1]= p[#p]:Translate( -l, heading) + + local pv2={} + for i,p in ipairs(p) do + pv2[i]={x=p.x, y=p.z} + end + + -- Create polygon zone. + local _polygon=ZONE_POLYGON_BASE:New(_name, pv2) + + -- Create tires + --_polygon:BoundZone() + + local st={} --#RANGE.StrafeTarget + st.name=_name + st.polygon=_polygon + st.coordinate=Ccenter + st.goodPass=goodpass + st.targets=_targets + st.foulline=foulline + st.smokepoints=p + st.heading=heading + + -- Add zone to table. + table.insert(self.strafeTargets, st) + + -- Debug info + local text=string.format("Adding new strafe target %s with %d targets: heading = %03d, box_L = %.1f, box_W = %.1f, goodpass = %d, foul line = %.1f", _name, ntargets, heading, l, w, goodpass, foulline) + self:T(self.id..text) + + return self +end + + +--- Add all units of a group as one new strafe target pit. +-- For a strafe pit, hits from guns are counted. One pit can consist of several units. +-- Note, an approach is only valid, if the player enters via a zone in front of the pit, which defined by boxlength and boxheading. +-- Furthermore, the player must not be too high and fly in the direction of the pit to make a valid target apporoach. +-- @param #RANGE self +-- @param Wrapper.Group#GROUP group MOOSE group of unit names defining the strafe target pit. The first unit in the group determines the approach zone (heading and box). +-- @param #number boxlength (Optional) Length of the approach box in meters. Default is 3000 m. +-- @param #number boxwidth (Optional) Width of the approach box in meters. Default is 300 m. +-- @param #number heading (Optional) Approach heading in Degrees. Default is heading of the unit as defined in the mission editor. +-- @param #boolean inverseheading (Optional) Take inverse heading (heading --> heading - 180 Degrees). Default is false. +-- @param #number goodpass (Optional) Number of hits for a "good" strafing pass. Default is 20. +-- @param #number foulline (Optional) Foul line distance. Hits from closer than this distance are not counted. Default 610 m = 2000 ft. Set to 0 for no foul line. +-- @return #RANGE self +function RANGE:AddStrafePitGroup(group, boxlength, boxwidth, heading, inverseheading, goodpass, foulline) + self:F({group=group, boxlength=boxlength, boxwidth=boxwidth, heading=heading, inverseheading=inverseheading, goodpass=goodpass, foulline=foulline}) + + if group and group:IsAlive() then + + -- Get units of group. + local _units=group:GetUnits() + + -- Make table of unit names. + local _names={} + for _,_unit in ipairs(_units) do + + local _unit=_unit --Wrapper.Unit#UNIT + + if _unit and _unit:IsAlive() then + local _name=_unit:GetName() + table.insert(_names,_name) + end + + end + + -- Add strafe pit. + self:AddStrafePit(_names, boxlength, boxwidth, heading, inverseheading, goodpass, foulline) + end + + return self +end + +--- Add bombing target(s) to range. +-- @param #RANGE self +-- @param #table targetnames Table containing names of unit or static objects serving as bomb targets. +-- @param #number goodhitrange (Optional) Max distance from target unit (in meters) which is considered as a good hit. Default is 25 m. +-- @param #boolean randommove If true, unit will move randomly within the range. Default is false. +-- @return #RANGE self +function RANGE:AddBombingTargets(targetnames, goodhitrange, randommove) + self:F({targetnames=targetnames, goodhitrange=goodhitrange, randommove=randommove}) + + -- Create a table if necessary. + if type(targetnames) ~= "table" then + targetnames={targetnames} + end + + -- Default range is 25 m. + goodhitrange=goodhitrange or RANGE.Defaults.goodhitrange + + for _,name in pairs(targetnames) do + + -- Check if we have a static or unit object. + local _isstatic=self:_CheckStatic(name) + + if _isstatic==true then + local _static=STATIC:FindByName(name) + self:T2(self.id..string.format("Adding static bombing target %s with hit range %d.", name, goodhitrange, false)) + self:AddBombingTargetUnit(_static, goodhitrange) + elseif _isstatic==false then + local _unit=UNIT:FindByName(name) + self:T2(self.id..string.format("Adding unit bombing target %s with hit range %d.", name, goodhitrange, randommove)) + self:AddBombingTargetUnit(_unit, goodhitrange) + else + self:E(self.id..string.format("ERROR! Could not find bombing target %s.", name)) + end + + end + + return self +end + +--- Add a unit or static object as bombing target. +-- @param #RANGE self +-- @param Wrapper.Positionable#POSITIONABLE unit Positionable (unit or static) of the strafe target. +-- @param #number goodhitrange Max distance from unit which is considered as a good hit. +-- @param #boolean randommove If true, unit will move randomly within the range. Default is false. +-- @return #RANGE self +function RANGE:AddBombingTargetUnit(unit, goodhitrange, randommove) + self:F({unit=unit, goodhitrange=goodhitrange, randommove=randommove}) + + -- Get name of positionable. + local name=unit:GetName() + + -- Check if we have a static or unit object. + local _isstatic=self:_CheckStatic(name) + + -- Default range is 25 m. + goodhitrange=goodhitrange or RANGE.Defaults.goodhitrange + + -- Set randommove to false if it was not specified. + if randommove==nil or _isstatic==true then + randommove=false + end + + -- Debug or error output. + if _isstatic==true then + self:I(self.id..string.format("Adding STATIC bombing target %s with good hit range %d. Random move = %s.", name, goodhitrange, tostring(randommove))) + elseif _isstatic==false then + self:I(self.id..string.format("Adding UNIT bombing target %s with good hit range %d. Random move = %s.", name, goodhitrange, tostring(randommove))) + else + self:E(self.id..string.format("ERROR! No bombing target with name %s could be found. Carefully check all UNIT and STATIC names defined in the mission editor!", name)) + end + + -- Get max speed of unit in km/h. + local speed=0 + if _isstatic==false then + speed=self:_GetSpeed(unit) + end + + local target={} --#RANGE.BombTarget + target.name=name + target.target=unit + target.goodhitrange=goodhitrange + target.move=randommove + target.speed=speed + target.coordinate=unit:GetCoordinate() + if _isstatic then + target.type=RANGE.TargetType.STATIC + else + target.type=RANGE.TargetType.UNIT + end + + -- Insert target to table. + table.insert(self.bombingTargets, target) + + return self +end + + +--- Add a coordinate of a bombing target. This +-- @param #RANGE self +-- @param Core.Point#COORDINATE coord The coordinate. +-- @param #string name Name of target. +-- @param #number goodhitrange Max distance from unit which is considered as a good hit. +-- @return #RANGE self +function RANGE:AddBombingTargetCoordinate(coord, name, goodhitrange) + + local target={} --#RANGE.BombTarget + target.name=name or "Bomb Target" + target.target=nil + target.goodhitrange=goodhitrange or RANGE.Defaults.goodhitrange + target.move=false + target.speed=0 + target.coordinate=coord + target.type=RANGE.TargetType.COORD + + -- Insert target to table. + table.insert(self.bombingTargets, target) + + return self +end + +--- Add all units of a group as bombing targets. +-- @param #RANGE self +-- @param Wrapper.Group#GROUP group Group of bombing targets. +-- @param #number goodhitrange Max distance from unit which is considered as a good hit. +-- @param #boolean randommove If true, unit will move randomly within the range. Default is false. +-- @return #RANGE self +function RANGE:AddBombingTargetGroup(group, goodhitrange, randommove) + self:F({group=group, goodhitrange=goodhitrange, randommove=randommove}) + + if group then + + local _units=group:GetUnits() + + for _,_unit in pairs(_units) do + if _unit and _unit:IsAlive() then + self:AddBombingTargetUnit(_unit, goodhitrange, randommove) + end + end + end + + return self +end + +--- Measures the foule line distance between two unit or static objects. +-- @param #RANGE self +-- @param #string namepit Name of the strafe pit target object. +-- @param #string namefoulline Name of the fould line distance marker object. +-- @return #number Foul line distance in meters. +function RANGE:GetFoullineDistance(namepit, namefoulline) + self:F({namepit=namepit, namefoulline=namefoulline}) + + -- Check if we have units or statics. + local _staticpit=self:_CheckStatic(namepit) + local _staticfoul=self:_CheckStatic(namefoulline) + + -- Get the unit or static pit object. + local pit=nil + if _staticpit==true then + pit=STATIC:FindByName(namepit, false) + elseif _staticpit==false then + pit=UNIT:FindByName(namepit) + else + self:E(self.id..string.format("ERROR! Pit object %s could not be found in GetFoullineDistance function. Check the name in the ME.", namepit)) + end + + -- Get the unit or static foul line object. + local foul=nil + if _staticfoul==true then + foul=STATIC:FindByName(namefoulline, false) + elseif _staticfoul==false then + foul=UNIT:FindByName(namefoulline) + else + self:E(self.id..string.format("ERROR! Foul line object %s could not be found in GetFoullineDistance function. Check the name in the ME.", namefoulline)) + end + + -- Get the distance between the two objects. + local fouldist=0 + if pit~=nil and foul~=nil then + fouldist=pit:GetCoordinate():Get2DDistance(foul:GetCoordinate()) + else + self:E(self.id..string.format("ERROR! Foul line distance could not be determined. Check pit object name %s and foul line object name %s in the ME.", namepit, namefoulline)) + end + + self:T(self.id..string.format("Foul line distance = %.1f m.", fouldist)) + return fouldist +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Event Handling +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- General event handler. +-- @param #RANGE self +-- @param #table Event DCS event table. +function RANGE:onEvent(Event) + self:F3(Event) + + if Event == nil or Event.initiator == nil then + self:T3("Skipping onEvent. Event or Event.initiator unknown.") + return true + end + if Unit.getByName(Event.initiator:getName()) == nil then + self:T3("Skipping onEvent. Initiator unit name unknown.") + return true + end + + local DCSiniunit = Event.initiator + local DCStgtunit = Event.target + local DCSweapon = Event.weapon + + local EventData={} + local _playerunit=nil + local _playername=nil + + if Event.initiator then + EventData.IniUnitName = Event.initiator:getName() + EventData.IniDCSGroup = Event.initiator:getGroup() + EventData.IniGroupName = Event.initiator:getGroup():getName() + -- Get player unit and name. This returns nil,nil if the event was not fired by a player unit. And these are the only events we are interested in. + _playerunit, _playername = self:_GetPlayerUnitAndName(EventData.IniUnitName) + end + + if Event.target then + EventData.TgtUnitName = Event.target:getName() + EventData.TgtUnit = UNIT:FindByName(EventData.TgtUnitName) + end + + if Event.weapon then + EventData.Weapon = Event.weapon + EventData.weapon = Event.weapon + EventData.WeaponTypeName = Event.weapon:getTypeName() + end + + -- Event info. + self:T3(self.id..string.format("EVENT: Event in onEvent with ID = %s", tostring(Event.id))) + self:T3(self.id..string.format("EVENT: Ini unit = %s" , tostring(EventData.IniUnitName))) + self:T3(self.id..string.format("EVENT: Ini group = %s" , tostring(EventData.IniGroupName))) + self:T3(self.id..string.format("EVENT: Ini player = %s" , tostring(_playername))) + self:T3(self.id..string.format("EVENT: Tgt unit = %s" , tostring(EventData.TgtUnitName))) + self:T3(self.id..string.format("EVENT: Wpn type = %s" , tostring(EventData.WeaponTypeName))) + + -- Call event Birth function. + if Event.id==world.event.S_EVENT_BIRTH and _playername then + self:OnEventBirth(EventData) + end + + -- Call event Shot function. + if Event.id==world.event.S_EVENT_SHOT and _playername and Event.weapon then + self:OnEventShot(EventData) + end + + -- Call event Hit function. + if Event.id==world.event.S_EVENT_HIT and _playername and DCStgtunit then + self:OnEventHit(EventData) + end + +end + + +--- Range event handler for event birth. +-- @param #RANGE self +-- @param Core.Event#EVENTDATA EventData +function RANGE:OnEventBirth(EventData) + self:F({eventbirth = EventData}) + + local _unitName=EventData.IniUnitName + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + + self:T3(self.id.."BIRTH: unit = "..tostring(EventData.IniUnitName)) + self:T3(self.id.."BIRTH: group = "..tostring(EventData.IniGroupName)) + self:T3(self.id.."BIRTH: player = "..tostring(_playername)) + + if _unit and _playername then + + local _uid=_unit:GetID() + local _group=_unit:GetGroup() + local _gid=_group:GetID() + local _callsign=_unit:GetCallsign() + + -- Debug output. + local text=string.format("Player %s, callsign %s entered unit %s (UID %d) of group %s (GID %d)", _playername, _callsign, _unitName, _uid, _group:GetName(), _gid) + self:T(self.id..text) + + -- Reset current strafe status. + self.strafeStatus[_uid] = nil + + -- Add Menu commands after a delay of 0.1 seconds. + --SCHEDULER:New(nil, self._AddF10Commands, {self,_unitName}, 0.1) + self:ScheduleOnce(0.1, self._AddF10Commands, self, _unitName) + + -- By default, some bomb impact points and do not flare each hit on target. + self.PlayerSettings[_playername]={} --#RANGE.PlayerData + self.PlayerSettings[_playername].smokebombimpact=self.defaultsmokebomb + self.PlayerSettings[_playername].flaredirecthits=false + self.PlayerSettings[_playername].smokecolor=SMOKECOLOR.Blue + self.PlayerSettings[_playername].flarecolor=FLARECOLOR.Red + self.PlayerSettings[_playername].delaysmoke=true + self.PlayerSettings[_playername].messages=true + self.PlayerSettings[_playername].client=CLIENT:FindByName(_unitName, nil, true) + self.PlayerSettings[_playername].unitname=_unitName + self.PlayerSettings[_playername].playername=_playername + self.PlayerSettings[_playername].airframe=EventData.IniUnit:GetTypeName() + self.PlayerSettings[_playername].inzone=false + + -- Start check in zone timer. + if self.planes[_uid] ~= true then + --SCHEDULER:New(nil, self._CheckInZone, {self, EventData.IniUnitName}, 1, 1) + self.timerCheckZone=TIMER:New(self._CheckInZone, self, EventData.IniUnitName):Start(1, 1) + self.planes[_uid] = true + end + + end +end + +--- Range event handler for event hit. +-- @param #RANGE self +-- @param Core.Event#EVENTDATA EventData +function RANGE:OnEventHit(EventData) + self:F({eventhit = EventData}) + + -- Debug info. + self:T3(self.id.."HIT: Ini unit = "..tostring(EventData.IniUnitName)) + self:T3(self.id.."HIT: Ini group = "..tostring(EventData.IniGroupName)) + self:T3(self.id.."HIT: Tgt target = "..tostring(EventData.TgtUnitName)) + + -- Player info + local _unitName = EventData.IniUnitName + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + if _unit==nil or _playername==nil then + return + end + + -- Unit ID + local _unitID = _unit:GetID() + + -- Target + local target = EventData.TgtUnit + local targetname = EventData.TgtUnitName + + -- Current strafe target of player. + local _currentTarget = self.strafeStatus[_unitID] + + -- Player has rolled in on a strafing target. + if _currentTarget and target:IsAlive() then + + local playerPos = _unit:GetCoordinate() + local targetPos = target:GetCoordinate() + + -- Loop over valid targets for this run. + for _,_target in pairs(_currentTarget.zone.targets) do + + -- Check the the target is the same that was actually hit. + if _target and _target:IsAlive() and _target:GetName() == targetname then + + -- Get distance between player and target. + local dist=playerPos:Get2DDistance(targetPos) + + if dist > _currentTarget.zone.foulline then + -- Increase hit counter of this run. + _currentTarget.hits = _currentTarget.hits + 1 + + -- Flare target. + if _unit and _playername and self.PlayerSettings[_playername].flaredirecthits then + targetPos:Flare(self.PlayerSettings[_playername].flarecolor) + end + else + -- Too close to the target. + if _currentTarget.pastfoulline==false and _unit and _playername then + local _d=_currentTarget.zone.foulline + local text=string.format("%s, Invalid hit!\nYou already passed foul line distance of %d m for target %s.", self:_myname(_unitName), _d, targetname) + self:_DisplayMessageToGroup(_unit, text) + self:T2(self.id..text) + _currentTarget.pastfoulline=true + end + end + + end + end + end + + -- Bombing Targets + for _,_bombtarget in pairs(self.bombingTargets) do + + local _target=_bombtarget.target --Wrapper.Positionable#POSITIONABLE + + -- Check if one of the bomb targets was hit. + if _target and _target:IsAlive() and _bombtarget.name == targetname then + + if _unit and _playername then + + -- Flare target. + if self.PlayerSettings[_playername].flaredirecthits then + + -- Position of target. + local targetPos = _target:GetCoordinate() + + targetPos:Flare(self.PlayerSettings[_playername].flarecolor) + end + + end + end + end +end + +--- Range event handler for event shot (when a unit releases a rocket or bomb (but not a fast firing gun). +-- @param #RANGE self +-- @param Core.Event#EVENTDATA EventData +function RANGE:OnEventShot(EventData) + self:F({eventshot = EventData}) + + -- Nil checks. + if EventData.Weapon==nil then + return + end + if EventData.IniDCSUnit==nil then + return + end + + -- Weapon data. + local _weapon = EventData.Weapon:getTypeName() -- should be the same as Event.WeaponTypeName + local _weaponStrArray = UTILS.Split(_weapon,"%.") + local _weaponName = _weaponStrArray[#_weaponStrArray] + + -- Weapon descriptor. + local desc=EventData.Weapon:getDesc() + + -- Weapon category: 0=SHELL, 1=MISSILE, 2=ROCKET, 3=BOMB (Weapon.Category.X) + local weaponcategory=desc.category + + -- Debug info. + self:T(self.id.."EVENT SHOT: Range "..self.rangename) + self:T(self.id.."EVENT SHOT: Ini unit = "..EventData.IniUnitName) + self:T(self.id.."EVENT SHOT: Ini group = "..EventData.IniGroupName) + self:T(self.id.."EVENT SHOT: Weapon type = ".._weapon) + self:T(self.id.."EVENT SHOT: Weapon name = ".._weaponName) + self:T(self.id.."EVENT SHOT: Weapon cate = "..weaponcategory) + + -- Tracking conditions for bombs, rockets and missiles. + local _bombs = weaponcategory==Weapon.Category.BOMB --string.match(_weapon, "weapons.bombs") + local _rockets = weaponcategory==Weapon.Category.ROCKET --string.match(_weapon, "weapons.nurs") + local _missiles = weaponcategory==Weapon.Category.MISSILE --string.match(_weapon, "weapons.missiles") or _viggen + + -- Check if any condition applies here. + local _track = (_bombs and self.trackbombs) or (_rockets and self.trackrockets) or (_missiles and self.trackmissiles) + + -- Get unit name. + local _unitName = EventData.IniUnitName + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Set this to larger value than the threshold. + local dPR=self.BombtrackThreshold*2 + + -- Distance player to range. + if _unit and _playername then + dPR=_unit:GetCoordinate():Get2DDistance(self.location) + self:T(self.id..string.format("Range %s, player %s, player-range distance = %d km.", self.rangename, _playername, dPR/1000)) + end + + -- Only track if distance player to range is < 25 km. Also check that a player shot. No need to track AI weapons. + if _track and dPR<=self.BombtrackThreshold and _unit and _playername then + + -- Player data. + local playerData=self.PlayerSettings[_playername] --#RANGE.PlayerData + + -- Tracking info and init of last bomb position. + self:T(self.id..string.format("RANGE %s: Tracking %s - %s.", self.rangename, _weapon, EventData.weapon:getName())) + + -- Init bomb position. + local _lastBombPos = {x=0,y=0,z=0} --DCS#Vec3 + + -- Function monitoring the position of a bomb until impact. + local function trackBomb(_ordnance) + + -- When the pcall returns a failure the weapon has hit. + local _status,_bombPos = pcall( + function() + return _ordnance:getPoint() + end) + + self:T2(self.id..string.format("Range %s: Bomb still in air: %s", self.rangename, tostring(_status))) + if _status then + + ---------------------------- + -- Weapon is still in air -- + ---------------------------- + + -- Remember this position. + _lastBombPos = {x = _bombPos.x, y = _bombPos.y, z= _bombPos.z } + + -- Check again in ~0.005 seconds ==> 200 checks per second. + return timer.getTime() + self.dtBombtrack + + else + + ----------------------------- + -- Bomb did hit the ground -- + ----------------------------- + + -- Get closet target to last position. + local _closetTarget=nil --#RANGE.BombTarget + local _distance=nil + local _closeCoord=nil + local _hitquality="POOR" + + -- Get callsign. + local _callsign=self:_myname(_unitName) + + -- Coordinate of impact point. + local impactcoord=COORDINATE:NewFromVec3(_lastBombPos) + + -- Check if impact happened in range zone. + local insidezone=self.rangezone:IsCoordinateInZone(impactcoord) + + -- Impact point of bomb. + if self.Debug then + impactcoord:MarkToAll("Bomb impact point") + end + + -- Smoke impact point of bomb. + if playerData.smokebombimpact and insidezone then + if playerData.delaysmoke then + timer.scheduleFunction(self._DelayedSmoke, {coord=impactcoord, color=playerData.smokecolor}, timer.getTime() + self.TdelaySmoke) + else + impactcoord:Smoke(playerData.smokecolor) + end + end + + -- Loop over defined bombing targets. + for _,_bombtarget in pairs(self.bombingTargets) do + + -- Get target coordinate. + local targetcoord=self:_GetBombTargetCoordinate(_bombtarget) + + if targetcoord then + + -- Distance between bomb and target. + local _temp = impactcoord:Get2DDistance(targetcoord) + + -- Find closest target to last known position of the bomb. + if _distance == nil or _temp < _distance then + _distance = _temp + _closetTarget = _bombtarget + _closeCoord=targetcoord + if _distance <= 0.5*_bombtarget.goodhitrange then + _hitquality = "EXCELLENT" + elseif _distance <= _bombtarget.goodhitrange then + _hitquality = "GOOD" + elseif _distance <= 2*_bombtarget.goodhitrange then + _hitquality = "INEFFECTIVE" + else + _hitquality = "POOR" + end + + end + end + end + + -- Count if bomb fell less than ~1 km away from the target. + if _distance and _distance <= self.scorebombdistance then + -- Init bomb player results. + if not self.bombPlayerResults[_playername] then + self.bombPlayerResults[_playername]={} + end + + -- Local results. + local _results=self.bombPlayerResults[_playername] + + local result={} --#RANGE.BombResult + result.name=_closetTarget.name or "unknown" + result.distance=_distance + result.radial=_closeCoord:HeadingTo(impactcoord) + result.weapon=_weaponName or "unknown" + result.quality=_hitquality + result.player=playerData.playername + result.time=timer.getAbsTime() + result.airframe=playerData.airframe + + -- Add to table. + table.insert(_results, result) + + -- Call impact. + self:Impact(result, playerData) + + elseif insidezone then + + -- Send message. + local _message=string.format("%s, weapon impacted too far from nearest range target (>%.1f km). No score!", _callsign, self.scorebombdistance/1000) + self:_DisplayMessageToGroup(_unit, _message, nil, false) + + if self.rangecontrol then + self.rangecontrol:NewTransmission(RANGE.Sound.RCWeaponImpactedTooFar.filename, RANGE.Sound.RCWeaponImpactedTooFar.duration, self.soundpath, nil, nil, _message, self.subduration) + end + + else + self:T(self.id.."Weapon impacted outside range zone.") + end + + --Terminate the timer + self:T(self.id..string.format("Range %s, player %s: Terminating bomb track timer.", self.rangename, _playername)) + return nil + + end -- _status check + + end -- end function trackBomb + + -- Weapon is not yet "alife" just yet. Start timer in one second. + self:T(self.id..string.format("Range %s, player %s: Tracking of weapon starts in 0.1 seconds.", self.rangename, _playername)) + timer.scheduleFunction(trackBomb, EventData.weapon, timer.getTime()+0.1) + + end --if _track (string.match) and player-range distance < threshold. + +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Functions +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check spawn queue and spawn aircraft if necessary. +-- @param #RANGE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RANGE:onafterStatus(From, Event, To) + + if self.verbose>0 then + + local fsmstate=self:GetState() + + local text=string.format("Range status: %s", fsmstate) + + if self.instructor then + local alive="N/A" + if self.instructorrelayname then + local relay=UNIT:FindByName(self.instructorrelayname) + if relay then + alive=tostring(relay:IsAlive()) + end + end + text=text..string.format(", Instructor %.3f MHz (Relay=%s alive=%s)", self.instructorfreq, tostring(self.instructorrelayname), alive) + end + + if self.rangecontrol then + local alive="N/A" + if self.rangecontrolrelayname then + local relay=UNIT:FindByName(self.rangecontrolrelayname) + if relay then + alive=tostring(relay:IsAlive()) + end + end + text=text..string.format(", Control %.3f MHz (Relay=%s alive=%s)", self.rangecontrolfreq, tostring(self.rangecontrolrelayname), alive) + end + + + -- Check range status. + self:I(self.id..text) + + end + + -- Check player status. + self:_CheckPlayers() + + -- Check back in ~10 seconds. + self:__Status(-10) +end + +--- Function called after player enters the range zone. +-- @param #RANGE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #RANGE.PlayerData player Player data. +function RANGE:onafterEnterRange(From, Event, To, player) + + if self.instructor and self.rangecontrol then + + -- Range control radio frequency split. + local RF=UTILS.Split(string.format("%.3f", self.rangecontrolfreq), ".") + + -- Radio message that player entered the range + self.instructor:NewTransmission(RANGE.Sound.IREnterRange.filename, RANGE.Sound.IREnterRange.duration, self.soundpath) + self.instructor:Number2Transmission(RF[1]) + if tonumber(RF[2])>0 then + self.instructor:NewTransmission(RANGE.Sound.IRDecimal.filename, RANGE.Sound.IRDecimal.duration, self.soundpath) + self.instructor:Number2Transmission(RF[2]) + end + self.instructor:NewTransmission(RANGE.Sound.IRMegaHertz.filename, RANGE.Sound.IRMegaHertz.duration, self.soundpath) + end + +end + +--- Function called after player leaves the range zone. +-- @param #RANGE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #RANGE.PlayerData player Player data. +function RANGE:onafterExitRange(From, Event, To, player) + + if self.instructor then + self.instructor:NewTransmission(RANGE.Sound.IRExitRange.filename, RANGE.Sound.IRExitRange.duration, self.soundpath) + end + +end + + +--- Function called after bomb impact on range. +-- @param #RANGE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #RANGE.BombResult result Result of bomb impact. +-- @param #RANGE.PlayerData player Player data table. +function RANGE:onafterImpact(From, Event, To, result, player) + + -- Only display target name if there is more than one bomb target. + local targetname=nil + if #self.bombingTargets>1 then + local targetname=result.name + end + + -- Send message to player. + local text=string.format("%s, impact %03d° for %d ft", player.playername, result.radial, UTILS.MetersToFeet(result.distance)) + if targetname then + text=text..string.format(" from bulls of target %s.") + else + text=text.."." + end + text=text..string.format(" %s hit.", result.quality) + + if self.rangecontrol then + self.rangecontrol:NewTransmission(RANGE.Sound.RCImpact.filename, RANGE.Sound.RCImpact.duration, self.soundpath, nil, nil, text, self.subduration) + self.rangecontrol:Number2Transmission(string.format("%03d", result.radial), nil, 0.1) + self.rangecontrol:NewTransmission(RANGE.Sound.RCDegrees.filename, RANGE.Sound.RCDegrees.duration, self.soundpath) + self.rangecontrol:NewTransmission(RANGE.Sound.RCFor.filename, RANGE.Sound.RCFor.duration, self.soundpath) + self.rangecontrol:Number2Transmission(string.format("%d", UTILS.MetersToFeet(result.distance))) + self.rangecontrol:NewTransmission(RANGE.Sound.RCFeet.filename, RANGE.Sound.RCFeet.duration, self.soundpath) + if result.quality=="POOR" then + self.rangecontrol:NewTransmission(RANGE.Sound.RCPoorHit.filename, RANGE.Sound.RCPoorHit.duration, self.soundpath, nil, 0.5) + elseif result.quality=="INEFFECTIVE" then + self.rangecontrol:NewTransmission(RANGE.Sound.RCIneffectiveHit.filename, RANGE.Sound.RCIneffectiveHit.duration, self.soundpath, nil, 0.5) + elseif result.quality=="GOOD" then + self.rangecontrol:NewTransmission(RANGE.Sound.RCGoodHit.filename, RANGE.Sound.RCGoodHit.duration, self.soundpath, nil, 0.5) + elseif result.quality=="EXCELLENT" then + self.rangecontrol:NewTransmission(RANGE.Sound.RCExcellentHit.filename, RANGE.Sound.RCExcellentHit.duration, self.soundpath, nil, 0.5) + end + + end + + -- Unit. + local unit=UNIT:FindByName(player.unitname) + + -- Send message. + self:_DisplayMessageToGroup(unit, text, nil, true) + self:T(self.id..text) + + -- Save results. + if self.autosave then + self:Save() + end + +end + +--- Function called before save event. Checks that io and lfs are desanitized. +-- @param #RANGE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RANGE:onbeforeSave(From, Event, To) + if io and lfs then + return true + else + self:E(self.id..string.format("WARNING: io and/or lfs not desanitized. Cannot save player results.")) + return false + end +end + +--- Function called after save. +-- @param #RANGE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RANGE:onafterSave(From, Event, To) + + local function _savefile(filename, data) + local f=io.open(filename, "wb") + if f then + f:write(data) + f:close() + self:I(self.id..string.format("Saving player results to file %s", tostring(filename))) + else + self:E(self.id..string.format("ERROR: Could not save results to file %s", tostring(filename))) + end + end + + -- Path. + local path=lfs.writedir()..[[Logs\]] + + -- Set file name. + local filename=path..string.format("RANGE-%s_BombingResults.csv", self.rangename) + + -- Header line. + local scores="Name,Pass,Target,Distance,Radial,Quality,Weapon,Airframe,Mission Time" + + -- Loop over all players. + for playername,results in pairs(self.bombPlayerResults) do + + -- Loop over player grades table. + for i,_result in pairs(results) do + local result=_result --#RANGE.BombResult + local distance=result.distance + local weapon=result.weapon + local target=result.name + local radial=result.radial + local quality=result.quality + local time=UTILS.SecondsToClock(result.time) + local airframe=result.airframe + local date="n/a" + if os then + date=os.date() + end + scores=scores..string.format("\n%s,%d,%s,%.2f,%03d,%s,%s,%s,%s,%s", playername, i, target, distance, radial, quality, weapon, airframe, time, date) + end + end + + _savefile(filename, scores) +end + +--- Function called before save event. Checks that io and lfs are desanitized. +-- @param #RANGE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RANGE:onbeforeLoad(From, Event, To) + if io and lfs then + return true + else + self:E(self.id..string.format("WARNING: io and/or lfs not desanitized. Cannot load player results.")) + return false + end +end + +--- On after "Load" event. Loads results of all players from file. +-- @param #RANGE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RANGE:onafterLoad(From, Event, To) + + --- Function that load data from a file. + local function _loadfile(filename) + local f=io.open(filename, "rb") + if f then + --self:I(self.id..string.format("Loading player results from file %s", tostring(filename))) + local data=f:read("*all") + f:close() + return data + else + self:E(self.id..string.format("WARNING: Could not load player results from file %s. File might not exist just yet.", tostring(filename))) + return nil + end + end + + -- Path in DCS log file. + local path=lfs.writedir()..[[Logs\]] + + -- Set file name. + local filename=path..string.format("RANGE-%s_BombingResults.csv", self.rangename) + + -- Info message. + local text=string.format("Loading player bomb results from file %s", filename) + self:I(self.id..text) + + -- Load asset data from file. + local data=_loadfile(filename) + + if data then + + -- Split by line break. + local results=UTILS.Split(data,"\n") + + -- Remove first header line. + table.remove(results, 1) + + -- Init player scores table. + self.bombPlayerResults={} + + -- Loop over all lines. + for _,_result in pairs(results) do + + -- Parameters are separated by commata. + local resultdata=UTILS.Split(_result, ",") + + -- Grade table + local result={} --#RANGE.BombResult + + -- Player name. + local playername=resultdata[1] + result.player=playername + + -- Results data. + result.name=tostring(resultdata[3]) + result.distance=tonumber(resultdata[4]) + result.radial=tonumber(resultdata[5]) + result.quality=tostring(resultdata[6]) + result.weapon=tostring(resultdata[7]) + result.airframe=tostring(resultdata[8]) + result.time=UTILS.ClockToSeconds(resultdata[9] or "00:00:00") + result.date=resultdata[10] or "n/a" + + -- Create player array if necessary. + self.bombPlayerResults[playername]=self.bombPlayerResults[playername] or {} + + -- Add result to table. + table.insert(self.bombPlayerResults[playername], result) + end + end +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Display Messages +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Start smoking a coordinate with a delay. +-- @param #table _args Argements passed. +function RANGE._DelayedSmoke(_args) + trigger.action.smoke(_args.coord:GetVec3(), _args.color) +end + +--- Display top 10 stafing results of a specific player. +-- @param #RANGE self +-- @param #string _unitName Name of the player unit. +function RANGE:_DisplayMyStrafePitResults(_unitName) + self:F(_unitName) + + -- Get player unit and name + local _unit,_playername = self:_GetPlayerUnitAndName(_unitName) + + if _unit and _playername then + + -- Message header. + local _message = string.format("My Top %d Strafe Pit Results:\n", self.ndisplayresult) + + -- Get player results. + local _results = self.strafePlayerResults[_playername] + + -- Create message. + if _results == nil then + -- No score yet. + _message = string.format("%s: No Score yet.", _playername) + else + + -- Sort results table wrt number of hits. + local _sort = function( a,b ) return a.hits > b.hits end + table.sort(_results,_sort) + + -- Prepare message of best results. + local _bestMsg = "" + local _count = 1 + + -- Loop over results + for _,_result in pairs(_results) do + + -- Message text. + _message = _message..string.format("\n[%d] Hits %d - %s - %s", _count, _result.hits, _result.zone.name, _result.text) + + -- Best result. + if _bestMsg == "" then + _bestMsg = string.format("Hits %d - %s - %s", _result.hits, _result.zone.name, _result.text) + end + + -- 10 runs + if _count == self.ndisplayresult then + break + end + + -- Increase counter + _count = _count+1 + end + + -- Message text. + _message = _message .."\n\nBEST: ".._bestMsg + end + + -- Send message to group. + self:_DisplayMessageToGroup(_unit, _message, nil, true, true) + end +end + +--- Display top 10 strafing results of all players. +-- @param #RANGE self +-- @param #string _unitName Name fo the player unit. +function RANGE:_DisplayStrafePitResults(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + + -- Results table. + local _playerResults = {} + + -- Message text. + local _message = string.format("Strafe Pit Results - Top %d Players:\n", self.ndisplayresult) + + -- Loop over player results. + for _playerName,_results in pairs(self.strafePlayerResults) do + + -- Get the best result of the player. + local _best = nil + for _,_result in pairs(_results) do + if _best == nil or _result.hits > _best.hits then + _best = _result + end + end + + -- Add best result to table. + if _best ~= nil then + local text=string.format("%s: Hits %i - %s - %s", _playerName, _best.hits, _best.zone.name, _best.text) + table.insert(_playerResults,{msg = text, hits = _best.hits}) + end + + end + + --Sort list! + local _sort = function( a,b ) return a.hits > b.hits end + table.sort(_playerResults,_sort) + + -- Add top 10 results. + for _i = 1, math.min(#_playerResults, self.ndisplayresult) do + _message = _message..string.format("\n[%d] %s", _i, _playerResults[_i].msg) + end + + -- In case there are no scores yet. + if #_playerResults<1 then + _message = _message.."No player scored yet." + end + + -- Send message. + self:_DisplayMessageToGroup(_unit, _message, nil, true, true) + end +end + +--- Display top 10 bombing run results of specific player. +-- @param #RANGE self +-- @param #string _unitName Name of the player unit. +function RANGE:_DisplayMyBombingResults(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + if _unit and _playername then + + -- Init message. + local _message = string.format("My Top %d Bombing Results:\n", self.ndisplayresult) + + -- Results from player. + local _results = self.bombPlayerResults[_playername] + + -- No score so far. + if _results == nil then + _message = _playername..": No Score yet." + else + + -- Sort results wrt to distance. + local _sort = function( a,b ) return a.distance < b.distance end + table.sort(_results,_sort) + + -- Loop over results. + local _bestMsg = "" + for i,_result in pairs(_results) do + local result=_result --#RANGE.BombResult + + -- Message with name, weapon and distance. + _message = _message.."\n"..string.format("[%d] %d m %03d° - %s - %s - %s hit", i, result.distance, result.radial, result.name, result.weapon, result.quality) + + -- Store best/first result. + if _bestMsg == "" then + _bestMsg = string.format("%d m %03d° - %s - %s - %s hit", result.distance, result.radial, result.name, result.weapon, result.quality) + end + + -- Best 10 runs only. + if i==self.ndisplayresult then + break + end + + end + + -- Message. + _message = _message .."\n\nBEST: ".._bestMsg + end + + -- Send message. + self:_DisplayMessageToGroup(_unit, _message, nil, true, true) + end +end + +--- Display best bombing results of top 10 players. +-- @param #RANGE self +-- @param #string _unitName Name of player unit. +function RANGE:_DisplayBombingResults(_unitName) + self:F(_unitName) + + -- Results table. + local _playerResults = {} + + -- Get player unit and name. + local _unit, _player = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit with a player. + if _unit and _player then + + -- Message header. + local _message = string.format("Bombing Results - Top %d Players:\n", self.ndisplayresult) + + -- Loop over players. + for _playerName,_results in pairs(self.bombPlayerResults) do + + -- Find best result of player. + local _best = nil + for _,_result in pairs(_results) do + if _best == nil or _result.distance < _best.distance then + _best = _result + end + end + + -- Put best result of player into table. + if _best ~= nil then + local bestres=string.format("%s: %d m - %s - %s - %s hit", _playerName, _best.distance, _best.name, _best.weapon, _best.quality) + table.insert(_playerResults, {msg = bestres, distance = _best.distance}) + end + + end + + -- Sort list of player results. + local _sort = function( a,b ) return a.distance < b.distance end + table.sort(_playerResults,_sort) + + -- Loop over player results. + for _i = 1, math.min(#_playerResults, self.ndisplayresult) do + _message = _message..string.format("\n[%d] %s", _i, _playerResults[_i].msg) + end + + -- In case there are no scores yet. + if #_playerResults<1 then + _message = _message.."No player scored yet." + end + + -- Send message. + self:_DisplayMessageToGroup(_unit, _message, nil, true, true) + end +end + +--- Report information like bearing and range from player unit to range. +-- @param #RANGE self +-- @param #string _unitname Name of the player unit. +function RANGE:_DisplayRangeInfo(_unitname) + self:F(_unitname) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Message text. + local text="" + + -- Current coordinates. + local coord=unit:GetCoordinate() + + if self.location then + + local settings=_DATABASE:GetPlayerSettings(playername) or _SETTINGS --Core.Settings#SETTINGS + + -- Direction vector from current position (coord) to target (position). + local position=self.location --Core.Point#COORDINATE + local bulls=position:ToStringBULLS(unit:GetCoalition(), settings) + local lldms=position:ToStringLLDMS(settings) + local llddm=position:ToStringLLDDM(settings) + local rangealt=position:GetLandHeight() + local vec3=coord:GetDirectionVec3(position) + local angle=coord:GetAngleDegrees(vec3) + local range=coord:Get2DDistance(position) + + -- Bearing string. + local Bs=string.format('%03d°', angle) + + local texthit + if self.PlayerSettings[playername].flaredirecthits then + texthit=string.format("Flare direct hits: ON (flare color %s)\n", self:_flarecolor2text(self.PlayerSettings[playername].flarecolor)) + else + texthit=string.format("Flare direct hits: OFF\n") + end + local textbomb + if self.PlayerSettings[playername].smokebombimpact then + textbomb=string.format("Smoke bomb impact points: ON (smoke color %s)\n", self:_smokecolor2text(self.PlayerSettings[playername].smokecolor)) + else + textbomb=string.format("Smoke bomb impact points: OFF\n") + end + local textdelay + if self.PlayerSettings[playername].delaysmoke then + textdelay=string.format("Smoke bomb delay: ON (delay %.1f seconds)", self.TdelaySmoke) + else + textdelay=string.format("Smoke bomb delay: OFF") + end + + -- Player unit settings. + local trange=string.format("%.1f km", range/1000) + local trangealt=string.format("%d m", rangealt) + local tstrafemaxalt=string.format("%d m", self.strafemaxalt) + if settings:IsImperial() then + trange=string.format("%.1f NM", UTILS.MetersToNM(range)) + trangealt=string.format("%d feet", UTILS.MetersToFeet(rangealt)) + tstrafemaxalt=string.format("%d feet", UTILS.MetersToFeet(self.strafemaxalt)) + end + + -- Message. + text=text..string.format("Information on %s:\n", self.rangename) + text=text..string.format("-------------------------------------------------------\n") + text=text..string.format("Bearing %s, Range %s\n", Bs, trange) + text=text..string.format("%s\n", bulls) + text=text..string.format("%s\n", lldms) + text=text..string.format("%s\n", llddm) + text=text..string.format("Altitude ASL: %s\n", trangealt) + text=text..string.format("Max strafing alt AGL: %s\n", tstrafemaxalt) + text=text..string.format("# of strafe targets: %d\n", self.nstrafetargets) + text=text..string.format("# of bomb targets: %d\n", self.nbombtargets) + text=text..texthit + text=text..textbomb + text=text..textdelay + + -- Send message to player group. + self:_DisplayMessageToGroup(unit, text, nil, true, true) + + -- Debug output. + self:T2(self.id..text) + end + end +end + +--- Display bombing target locations to player. +-- @param #RANGE self +-- @param #string _unitname Name of the player unit. +function RANGE:_DisplayBombTargets(_unitname) + self:F(_unitname) + + -- Get player unit and player name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if _unit and _playername then + + -- Player settings. + local _settings=_DATABASE:GetPlayerSettings(_playername) or _SETTINGS --Core.Settings#SETTINGS + + -- Message text. + local _text="Bomb Target Locations:" + + for _,_bombtarget in pairs(self.bombingTargets) do + local bombtarget=_bombtarget --#RANGE.BombTarget + + -- Coordinate of bombtarget. + local coord=self:_GetBombTargetCoordinate(bombtarget) + + if coord then + + -- Get elevation + local elevation=coord:GetLandHeight() + local eltxt=string.format("%d m", elevation) + if not _settings:IsMetric() then + elevation=UTILS.MetersToFeet(elevation) + eltxt=string.format("%d ft", elevation) + end + + local ca2g=coord:ToStringA2G(_unit,_settings) + _text=_text..string.format("\n- %s:\n%s @ %s", bombtarget.name or "unknown", ca2g, eltxt) + end + end + + self:_DisplayMessageToGroup(_unit,_text, 60, true, true) + end +end + +--- Display pit location and heading to player. +-- @param #RANGE self +-- @param #string _unitname Name of the player unit. +function RANGE:_DisplayStrafePits(_unitname) + self:F(_unitname) + + -- Get player unit and player name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if _unit and _playername then + + -- Player settings. + local _settings=_DATABASE:GetPlayerSettings(_playername) or _SETTINGS --Core.Settings#SETTINGS + + -- Message text. + local _text="Strafe Target Locations:" + + for _,_strafepit in pairs(self.strafeTargets) do + local _target=_strafepit --Wrapper.Positionable#POSITIONABLE + + -- Pit parameters. + local coord=_strafepit.coordinate --Core.Point#COORDINATE + local heading=_strafepit.heading + + -- Turn heading around ==> approach heading. + if heading>180 then + heading=heading-180 + else + heading=heading+180 + end + + local mycoord=coord:ToStringA2G(_unit, _settings) + _text=_text..string.format("\n- %s: heading %03d°\n%s",_strafepit.name, heading, mycoord) + end + + self:_DisplayMessageToGroup(_unit,_text, nil, true, true) + end +end + + +--- Report weather conditions at range. Temperature, QFE pressure and wind data. +-- @param #RANGE self +-- @param #string _unitname Name of the player unit. +function RANGE:_DisplayRangeWeather(_unitname) + self:F(_unitname) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Message text. + local text="" + + -- Current coordinates. + local coord=unit:GetCoordinate() + + if self.location then + + -- Get atmospheric data at range location. + local position=self.location --Core.Point#COORDINATE + local T=position:GetTemperature() + local P=position:GetPressure() + local Wd,Ws=position:GetWind() + + -- Get Beaufort wind scale. + local Bn,Bd=UTILS.BeaufortScale(Ws) + + local WD=string.format('%03d°', Wd) + local Ts=string.format("%d°C",T) + + local hPa2inHg=0.0295299830714 + local hPa2mmHg=0.7500615613030 + + local settings=_DATABASE:GetPlayerSettings(playername) or _SETTINGS --Core.Settings#SETTINGS + local tT=string.format("%d°C",T) + local tW=string.format("%.1f m/s", Ws) + local tP=string.format("%.1f mmHg", P*hPa2mmHg) + if settings:IsImperial() then + --tT=string.format("%d°F", UTILS.CelciusToFarenheit(T)) + tW=string.format("%.1f knots", UTILS.MpsToKnots(Ws)) + tP=string.format("%.2f inHg", P*hPa2inHg) + end + + + -- Message text. + text=text..string.format("Weather Report at %s:\n", self.rangename) + text=text..string.format("--------------------------------------------------\n") + text=text..string.format("Temperature %s\n", tT) + text=text..string.format("Wind from %s at %s (%s)\n", WD, tW, Bd) + text=text..string.format("QFE %.1f hPa = %s", P, tP) + else + text=string.format("No range location defined for range %s.", self.rangename) + end + + -- Send message to player group. + self:_DisplayMessageToGroup(unit, text, nil, true, true) + + -- Debug output. + self:T2(self.id..text) + else + self:T(self.id..string.format("ERROR! Could not find player unit in RangeInfo! Name = %s", _unitname)) + end +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Timer Functions + +--- Check status of players. +-- @param #RANGE self +-- @param #string _unitName Name of player unit. +function RANGE:_CheckPlayers() + + for playername,_playersettings in pairs(self.PlayerSettings) do + local playersettings=_playersettings --#RANGE.PlayerData + + local unitname=playersettings.unitname + local unit=UNIT:FindByName(unitname) + + if unit and unit:IsAlive() then + + if unit:IsInZone(self.rangezone) then + + ------------------------------ + -- Player INSIDE Range Zone -- + ------------------------------ + + if not playersettings.inzone then + playersettings.inzone=true + self:EnterRange(playersettings) + end + + else + + ------------------------------- + -- Player OUTSIDE Range Zone -- + ------------------------------- + + if playersettings.inzone==true then + playersettings.inzone=false + self:ExitRange(playersettings) + end + + end + end + end + +end + +--- Check if player is inside a strafing zone. If he is, we start looking for hits. If he was and left the zone again, the result is stored. +-- @param #RANGE self +-- @param #string _unitName Name of player unit. +function RANGE:_CheckInZone(_unitName) + self:F2(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + if _unit and _playername then + + --- Function to check if unit is in zone and facing in the right direction and is below the max alt. + local function checkme(targetheading, _zone) + local zone=_zone --Core.Zone#ZONE + + -- Heading check. + local unitheading = _unit:GetHeading() + local pitheading = targetheading-180 + local deltaheading = unitheading-pitheading + local towardspit = math.abs(deltaheading)<=90 or math.abs(deltaheading-360)<=90 + + if towardspit then + + local vec3=_unit:GetVec3() + local vec2={x=vec3.x, y=vec3.z} --DCS#Vec2 + local landheight=land.getHeight(vec2) + local unitalt=vec3.y-landheight + + if unitalt<=self.strafemaxalt then + local unitinzone=zone:IsVec2InZone(vec2) + return unitinzone + end + end + + return false + end + + -- Current position of player unit. + local _unitID = _unit:GetID() + + -- Currently strafing? (strafeStatus is nil if not) + local _currentStrafeRun = self.strafeStatus[_unitID] + + if _currentStrafeRun then -- player has already registered for a strafing run. + + -- Get the current approach zone and check if player is inside. + local zone=_currentStrafeRun.zone.polygon --Core.Zone#ZONE_POLYGON_BASE + + -- Check if unit in zone and facing the right direction. + local unitinzone=checkme(_currentStrafeRun.zone.heading, zone) + + -- Check if player is in strafe zone and below max alt. + if unitinzone then + + -- Still in zone, keep counting hits. Increase counter. + _currentStrafeRun.time = _currentStrafeRun.time+1 + + else + + -- Increase counter + _currentStrafeRun.time = _currentStrafeRun.time+1 + + if _currentStrafeRun.time <= 3 then + + -- Reset current run. + self.strafeStatus[_unitID] = nil + + -- Message text. + local _msg = string.format("%s left strafing zone %s too quickly. No Score.", _playername, _currentStrafeRun.zone.name) + + -- Send message. + self:_DisplayMessageToGroup(_unit, _msg, nil, true) + + if self.rangecontrol then + self.rangecontrol:NewTransmission(RANGE.Sound.RCLeftStrafePitTooQuickly.filename, RANGE.Sound.RCLeftStrafePitTooQuickly.duration, self.soundpath) + end + + else + + -- Get current ammo. + local _ammo=self:_GetAmmo(_unitName) + + -- Result. + local _result = self.strafeStatus[_unitID] + local _sound = nil --#RANGE.Soundfile + + -- Judge this pass. Text is displayed on summary. + if _result.hits >= _result.zone.goodPass*2 then + _result.text = "EXCELLENT PASS" + _sound=RANGE.Sound.RCExcellentPass + elseif _result.hits >= _result.zone.goodPass then + _result.text = "GOOD PASS" + _sound=RANGE.Sound.RCGoodPass + elseif _result.hits >= _result.zone.goodPass/2 then + _result.text = "INEFFECTIVE PASS" + _sound=RANGE.Sound.RCIneffectivePass + else + _result.text = "POOR PASS" + _sound=RANGE.Sound.RCPoorPass + end + + -- Calculate accuracy of run. Number of hits wrt number of rounds fired. + local shots=_result.ammo-_ammo + local accur=0 + if shots>0 then + accur=_result.hits/shots*100 + if accur > 100 then accur = 100 end + end + + -- Message text. + local _text=string.format("%s, hits on target %s: %d", self:_myname(_unitName), _result.zone.name, _result.hits) + if shots and accur then + _text=_text..string.format("\nTotal rounds fired %d. Accuracy %.1f %%.", shots, accur) + end + _text=_text..string.format("\n%s", _result.text) + + -- Send message. + self:_DisplayMessageToGroup(_unit, _text) + + -- Voice over. + if self.rangecontrol then + self.rangecontrol:NewTransmission(RANGE.Sound.RCHitsOnTarget.filename, RANGE.Sound.RCHitsOnTarget.duration, self.soundpath) + self.rangecontrol:Number2Transmission(string.format("%d", _result.hits)) + if shots and accur then + self.rangecontrol:NewTransmission(RANGE.Sound.RCTotalRoundsFired.filename, RANGE.Sound.RCTotalRoundsFired.duration, self.soundpath, nil, 0.2) + self.rangecontrol:Number2Transmission(string.format("%d", shots), nil, 0.2) + self.rangecontrol:NewTransmission(RANGE.Sound.RCAccuracy.filename, RANGE.Sound.RCAccuracy.duration, self.soundpath, nil, 0.2) + self.rangecontrol:Number2Transmission(string.format("%d", UTILS.Round(accur, 0))) + self.rangecontrol:NewTransmission(RANGE.Sound.RCPercent.filename, RANGE.Sound.RCPercent.duration, self.soundpath) + end + self.rangecontrol:NewTransmission(_sound.filename, _sound.duration, self.soundpath, nil, 0.5) + end + + -- Set strafe status to nil. + self.strafeStatus[_unitID] = nil + + -- Save stats so the player can retrieve them. + local _stats = self.strafePlayerResults[_playername] or {} + table.insert(_stats, _result) + self.strafePlayerResults[_playername] = _stats + end + + end + + else + + -- Check to see if we're in any of the strafing zones (first time). + for _,_targetZone in pairs(self.strafeTargets) do + + -- Get the current approach zone and check if player is inside. + local zone=_targetZone.polygon --Core.Zone#ZONE_POLYGON_BASE + + -- Check if unit in zone and facing the right direction. + local unitinzone=checkme(_targetZone.heading, zone) + + -- Player is inside zone. + if unitinzone then + + -- Get ammo at the beginning of the run. + local _ammo=self:_GetAmmo(_unitName) + + -- Init strafe status for this player. + self.strafeStatus[_unitID] = {hits = 0, zone = _targetZone, time = 1, ammo=_ammo, pastfoulline=false} + + -- Rolling in! + local _msg=string.format("%s, rolling in on strafe pit %s.", self:_myname(_unitName), _targetZone.name) + + if self.rangecontrol then + self.rangecontrol:NewTransmission(RANGE.Sound.RCRollingInOnStrafeTarget.filename, RANGE.Sound.RCRollingInOnStrafeTarget.duration, self.soundpath) + end + + -- Send message. + self:_DisplayMessageToGroup(_unit, _msg, 10, true) + + -- We found our player. Skip remaining checks. + break + + end -- unit in zone check + + end -- loop over zones + end + end + +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Menu Functions +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add menu commands for player. +-- @param #RANGE self +-- @param #string _unitName Name of player unit. +function RANGE:_AddF10Commands(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check for player unit. + if _unit and playername then + + -- Get group and ID. + local group=_unit:GetGroup() + local _gid=group:GetID() + + if group and _gid then + + if not self.MenuAddedTo[_gid] then + + -- Enable switch so we don't do this twice. + self.MenuAddedTo[_gid] = true + + -- Range root menu path. + local _rangePath=nil + + if RANGE.MenuF10Root then + + ------------------- + -- MISSION LEVEL -- + ------------------- + + _rangePath = missionCommands.addSubMenuForGroup(_gid, self.rangename, RANGE.MenuF10Root) + + else + + ----------------- + -- GROUP LEVEL -- + ----------------- + + -- Main F10 menu: F10/On the Range// + if RANGE.MenuF10[_gid] == nil then + RANGE.MenuF10[_gid]=missionCommands.addSubMenuForGroup(_gid, "On the Range") + end + _rangePath = missionCommands.addSubMenuForGroup(_gid, self.rangename, RANGE.MenuF10[_gid]) + + end + + + local _statsPath = missionCommands.addSubMenuForGroup(_gid, "Statistics", _rangePath) + local _markPath = missionCommands.addSubMenuForGroup(_gid, "Mark Targets", _rangePath) + local _settingsPath = missionCommands.addSubMenuForGroup(_gid, "My Settings", _rangePath) + local _infoPath = missionCommands.addSubMenuForGroup(_gid, "Range Info", _rangePath) + -- F10/On the Range//My Settings/ + local _mysmokePath = missionCommands.addSubMenuForGroup(_gid, "Smoke Color", _settingsPath) + local _myflarePath = missionCommands.addSubMenuForGroup(_gid, "Flare Color", _settingsPath) + + -- F10/On the Range//Mark Targets/ + missionCommands.addCommandForGroup(_gid, "Mark On Map", _markPath, self._MarkTargetsOnMap, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Illuminate Range", _markPath, self._IlluminateBombTargets, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Smoke Strafe Pits", _markPath, self._SmokeStrafeTargetBoxes, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Smoke Strafe Tgts", _markPath, self._SmokeStrafeTargets, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Smoke Bomb Tgts", _markPath, self._SmokeBombTargets, self, _unitName) + -- F10/On the Range//Stats/ + missionCommands.addCommandForGroup(_gid, "All Strafe Results", _statsPath, self._DisplayStrafePitResults, self, _unitName) + missionCommands.addCommandForGroup(_gid, "All Bombing Results", _statsPath, self._DisplayBombingResults, self, _unitName) + missionCommands.addCommandForGroup(_gid, "My Strafe Results", _statsPath, self._DisplayMyStrafePitResults, self, _unitName) + missionCommands.addCommandForGroup(_gid, "My Bomb Results", _statsPath, self._DisplayMyBombingResults, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Reset All Stats", _statsPath, self._ResetRangeStats, self, _unitName) + -- F10/On the Range//My Settings/Smoke Color/ + missionCommands.addCommandForGroup(_gid, "Blue Smoke", _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.Blue) + missionCommands.addCommandForGroup(_gid, "Green Smoke", _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.Green) + missionCommands.addCommandForGroup(_gid, "Orange Smoke", _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.Orange) + missionCommands.addCommandForGroup(_gid, "Red Smoke", _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.Red) + missionCommands.addCommandForGroup(_gid, "White Smoke", _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.White) + -- F10/On the Range//My Settings/Flare Color/ + missionCommands.addCommandForGroup(_gid, "Green Flares", _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.Green) + missionCommands.addCommandForGroup(_gid, "Red Flares", _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.Red) + missionCommands.addCommandForGroup(_gid, "White Flares", _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.White) + missionCommands.addCommandForGroup(_gid, "Yellow Flares", _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.Yellow) + -- F10/On the Range//My Settings/ + missionCommands.addCommandForGroup(_gid, "Smoke Delay On/Off", _settingsPath, self._SmokeBombDelayOnOff, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Smoke Impact On/Off", _settingsPath, self._SmokeBombImpactOnOff, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Flare Hits On/Off", _settingsPath, self._FlareDirectHitsOnOff, self, _unitName) + missionCommands.addCommandForGroup(_gid, "All Messages On/Off", _settingsPath, self._MessagesToPlayerOnOff, self, _unitName) + + -- F10/On the Range//Range Information + missionCommands.addCommandForGroup(_gid, "General Info", _infoPath, self._DisplayRangeInfo, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Weather Report", _infoPath, self._DisplayRangeWeather, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Bombing Targets", _infoPath, self._DisplayBombTargets, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Strafe Pits", _infoPath, self._DisplayStrafePits, self, _unitName) + end + else + self:E(self.id.."Could not find group or group ID in AddF10Menu() function. Unit name: ".._unitName) + end + else + self:E(self.id.."Player unit does not exist in AddF10Menu() function. Unit name: ".._unitName) + end + +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Helper Functions +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get the number of shells a unit currently has. +-- @param #RANGE self +-- @param #RANGE.BombTarget target Bomb target data. +-- @return Core.Point#COORDINATE Target coordinate. +function RANGE:_GetBombTargetCoordinate(target) + + local coord=nil --Core.Point#COORDINATE + + if target.type==RANGE.TargetType.UNIT then + + if not target.move then + -- Target should not move. + coord=target.coordinate + else + -- Moving target. Check if alive and get current position + if target.target and target.target:IsAlive() then + coord=target.target:GetCoordinate() + end + end + + elseif target.type==RANGE.TargetType.STATIC then + + -- Static targets dont move. + coord=target.coordinate + + elseif target.type==RANGE.TargetType.COORD then + + -- Coordinates dont move. + coord=target.coordinate + + else + self:E(self.id.."ERROR: Unknown target type.") + end + + return coord +end + + +--- Get the number of shells a unit currently has. +-- @param #RANGE self +-- @param #string unitname Name of the player unit. +-- @return Number of shells left +function RANGE:_GetAmmo(unitname) + self:F2(unitname) + + -- Init counter. + local ammo=0 + + local unit, playername = self:_GetPlayerUnitAndName(unitname) + + if unit and playername then + + local has_ammo=false + + local ammotable=unit:GetAmmo() + self:T2({ammotable=ammotable}) + + if ammotable ~= nil then + + local weapons=#ammotable + self:T2(self.id..string.format("Number of weapons %d.", weapons)) + + for w=1,weapons do + + local Nammo=ammotable[w]["count"] + local Tammo=ammotable[w]["desc"]["typeName"] + + -- We are specifically looking for shells here. + if string.match(Tammo, "shell") then + + -- Add up all shells + ammo=ammo+Nammo + + local text=string.format("Player %s has %d rounds ammo of type %s", playername, Nammo, Tammo) + self:T(self.id..text) + else + local text=string.format("Player %s has %d ammo of type %s", playername, Nammo, Tammo) + self:T(self.id..text) + end + end + end + end + + return ammo +end + +--- Mark targets on F10 map. +-- @param #RANGE self +-- @param #string _unitName Name of the player unit. +function RANGE:_MarkTargetsOnMap(_unitName) + self:F(_unitName) + + -- Get group. + local group=nil --Wrapper.Group#GROUP + if _unitName then + group=UNIT:FindByName(_unitName):GetGroup() + end + + -- Mark bomb targets. + for _,_bombtarget in pairs(self.bombingTargets) do + local bombtarget=_bombtarget --#RANGE.BombTarget + local coord=self:_GetBombTargetCoordinate(_bombtarget) + if group then + coord:MarkToGroup(string.format("Bomb target %s:\n%s\n%s", bombtarget.name, coord:ToStringLLDMS(), coord:ToStringBULLS(group:GetCoalition())), group) + else + coord:MarkToAll(string.format("Bomb target %s", bombtarget.name)) + end + end + + -- Mark strafe targets. + for _,_strafepit in pairs(self.strafeTargets) do + for _,_target in pairs(_strafepit.targets) do + local _target=_target --Wrapper.Positionable#POSITIONABLE + if _target and _target:IsAlive() then + local coord=_target:GetCoordinate() --Core.Point#COORDINATE + if group then + --coord:MarkToGroup("Strafe target ".._target:GetName(), group) + coord:MarkToGroup(string.format("Strafe target %s:\n%s\n%s", _target:GetName(), coord:ToStringLLDMS(), coord:ToStringBULLS(group:GetCoalition())), group) + else + coord:MarkToAll("Strafe target ".._target:GetName()) + end + end + end + end + + if _unitName then + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + local text=string.format("%s, %s, range targets are now marked on F10 map.", self.rangename, _playername) + self:_DisplayMessageToGroup(_unit, text, 5) + end + +end + +--- Illuminate targets. Fires illumination bombs at one random bomb and one random strafe target at a random altitude between 400 and 800 m. +-- @param #RANGE self +-- @param #string _unitName (Optional) Name of the player unit. +function RANGE:_IlluminateBombTargets(_unitName) + self:F(_unitName) + + -- All bombing target coordinates. + local bomb={} + + for _,_bombtarget in pairs(self.bombingTargets) do + local _target=_bombtarget.target --Wrapper.Positionable#POSITIONABLE + local coord=self:_GetBombTargetCoordinate(_bombtarget) + if coord then + table.insert(bomb, coord) + end + end + + if #bomb>0 then + local coord=bomb[math.random(#bomb)] --Core.Point#COORDINATE + local c=COORDINATE:New(coord.x,coord.y+math.random(self.illuminationminalt,self.illuminationmaxalt),coord.z) + c:IlluminationBomb() + end + + -- All strafe target coordinates. + local strafe={} + + for _,_strafepit in pairs(self.strafeTargets) do + for _,_target in pairs(_strafepit.targets) do + local _target=_target --Wrapper.Positionable#POSITIONABLE + if _target and _target:IsAlive() then + local coord=_target:GetCoordinate() --Core.Point#COORDINATE + table.insert(strafe, coord) + end + end + end + + -- Pick a random strafe target. + if #strafe>0 then + local coord=strafe[math.random(#strafe)] --Core.Point#COORDINATE + local c=COORDINATE:New(coord.x,coord.y+math.random(self.illuminationminalt,self.illuminationmaxalt),coord.z) + c:IlluminationBomb() + end + + if _unitName then + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + local text=string.format("%s, %s, range targets are illuminated.", self.rangename, _playername) + self:_DisplayMessageToGroup(_unit, text, 5) + end +end + +--- Reset player statistics. +-- @param #RANGE self +-- @param #string _unitName Name of the player unit. +function RANGE:_ResetRangeStats(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + if _unit and _playername then + self.strafePlayerResults[_playername] = nil + self.bombPlayerResults[_playername] = nil + local text=string.format("%s, %s, your range stats were cleared.", self.rangename, _playername) + self:DisplayMessageToGroup(_unit, text, 5, false, true) + end +end + +--- Display message to group. +-- @param #RANGE self +-- @param Wrapper.Unit#UNIT _unit Player unit. +-- @param #string _text Message text. +-- @param #number _time Duration how long the message is displayed. +-- @param #boolean _clear Clear up old messages. +-- @param #boolean display If true, display message regardless of player setting "Messages Off". +function RANGE:_DisplayMessageToGroup(_unit, _text, _time, _clear, display) + self:F({unit=_unit, text=_text, time=_time, clear=_clear}) + + -- Defaults + _time=_time or self.Tmsg + if _clear==nil or _clear==false then + _clear=false + else + _clear=true + end + + -- Messages globally disabled. + if self.messages==false then + return + end + + -- Check if unit is alive. + if _unit and _unit:IsAlive() then + + -- Group ID. + local _gid=_unit:GetGroup():GetID() + + -- Get playername and player settings + local _, playername=self:_GetPlayerUnitAndName(_unit:GetName()) + local playermessage=self.PlayerSettings[playername].messages + + -- Send message to player if messages enabled and not only for the examiner. + if _gid and (playermessage==true or display) and (not self.examinerexclusive) then + trigger.action.outTextForGroup(_gid, _text, _time, _clear) + end + + -- Send message to examiner. + if self.examinergroupname~=nil then + local _examinerid=GROUP:FindByName(self.examinergroupname):GetID() + if _examinerid then + trigger.action.outTextForGroup(_examinerid, _text, _time, _clear) + end + end + end + +end + +--- Toggle status of smoking bomb impact points. +-- @param #RANGE self +-- @param #string unitname Name of the player unit. +function RANGE:_SmokeBombImpactOnOff(unitname) + self:F(unitname) + + local unit, playername = self:_GetPlayerUnitAndName(unitname) + if unit and playername then + local text + if self.PlayerSettings[playername].smokebombimpact==true then + self.PlayerSettings[playername].smokebombimpact=false + text=string.format("%s, %s, smoking impact points of bombs is now OFF.", self.rangename, playername) + else + self.PlayerSettigs[playername].smokebombimpact=true + text=string.format("%s, %s, smoking impact points of bombs is now ON.", self.rangename, playername) + end + self:_DisplayMessageToGroup(unit, text, 5, false, true) + end + +end + +--- Toggle status of time delay for smoking bomb impact points +-- @param #RANGE self +-- @param #string unitname Name of the player unit. +function RANGE:_SmokeBombDelayOnOff(unitname) + self:F(unitname) + + local unit, playername = self:_GetPlayerUnitAndName(unitname) + if unit and playername then + local text + if self.PlayerSettings[playername].delaysmoke==true then + self.PlayerSettings[playername].delaysmoke=false + text=string.format("%s, %s, delayed smoke of bombs is now OFF.", self.rangename, playername) + else + self.PlayerSettigs[playername].delaysmoke=true + text=string.format("%s, %s, delayed smoke of bombs is now ON.", self.rangename, playername) + end + self:_DisplayMessageToGroup(unit, text, 5, false, true) + end + +end + +--- Toggle display messages to player. +-- @param #RANGE self +-- @param #string unitname Name of the player unit. +function RANGE:_MessagesToPlayerOnOff(unitname) + self:F(unitname) + + local unit, playername = self:_GetPlayerUnitAndName(unitname) + if unit and playername then + local text + if self.PlayerSettings[playername].messages==true then + text=string.format("%s, %s, display of ALL messages is now OFF.", self.rangename, playername) + else + text=string.format("%s, %s, display of ALL messages is now ON.", self.rangename, playername) + end + self:_DisplayMessageToGroup(unit, text, 5, false, true) + self.PlayerSettings[playername].messages=not self.PlayerSettings[playername].messages + end + +end + +--- Toggle status of flaring direct hits of range targets. +-- @param #RANGE self +-- @param #string unitname Name of the player unit. +function RANGE:_FlareDirectHitsOnOff(unitname) + self:F(unitname) + + local unit, playername = self:_GetPlayerUnitAndName(unitname) + if unit and playername then + local text + if self.PlayerSettings[playername].flaredirecthits==true then + self.PlayerSettings[playername].flaredirecthits=false + text=string.format("%s, %s, flaring direct hits is now OFF.", self.rangename, playername) + else + self.PlayerSettings[playername].flaredirecthits=true + text=string.format("%s, %s, flaring direct hits is now ON.", self.rangename, playername) + end + self:_DisplayMessageToGroup(unit, text, 5, false, true) + end + +end + +--- Mark bombing targets with smoke. +-- @param #RANGE self +-- @param #string unitname Name of the player unit. +function RANGE:_SmokeBombTargets(unitname) + self:F(unitname) + + for _,_bombtarget in pairs(self.bombingTargets) do + local _target=_bombtarget.target --Wrapper.Positionable#POSITIONABLE + local coord=self:_GetBombTargetCoordinate(_bombtarget) + if coord then + coord:Smoke(self.BombSmokeColor) + end + end + + if unitname then + local unit, playername = self:_GetPlayerUnitAndName(unitname) + local text=string.format("%s, %s, bombing targets are now marked with %s smoke.", self.rangename, playername, self:_smokecolor2text(self.BombSmokeColor)) + self:_DisplayMessageToGroup(unit, text, 5) + end + +end + +--- Mark strafing targets with smoke. +-- @param #RANGE self +-- @param #string unitname Name of the player unit. +function RANGE:_SmokeStrafeTargets(unitname) + self:F(unitname) + + for _,_target in pairs(self.strafeTargets) do + _target.coordinate:Smoke(self.StrafeSmokeColor) + end + + if unitname then + local unit, playername = self:_GetPlayerUnitAndName(unitname) + local text=string.format("%s, %s, strafing tragets are now marked with %s smoke.", self.rangename, playername, self:_smokecolor2text(self.StrafeSmokeColor)) + self:_DisplayMessageToGroup(unit, text, 5) + end + +end + +--- Mark approach boxes of strafe targets with smoke. +-- @param #RANGE self +-- @param #string unitname Name of the player unit. +function RANGE:_SmokeStrafeTargetBoxes(unitname) + self:F(unitname) + + for _,_target in pairs(self.strafeTargets) do + local zone=_target.polygon --Core.Zone#ZONE + zone:SmokeZone(self.StrafePitSmokeColor, 4) + for _,_point in pairs(_target.smokepoints) do + _point:SmokeOrange() --Corners are smoked orange. + end + end + + if unitname then + local unit, playername = self:_GetPlayerUnitAndName(unitname) + local text=string.format("%s, %s, strafing pit approach boxes are now marked with %s smoke.", self.rangename, playername, self:_smokecolor2text(self.StrafePitSmokeColor)) + self:_DisplayMessageToGroup(unit, text, 5) + end + +end + +--- Sets the smoke color used to smoke players bomb impact points. +-- @param #RANGE self +-- @param #string _unitName Name of the player unit. +-- @param Utilities.Utils#SMOKECOLOR color ID of the smoke color. +function RANGE:_playersmokecolor(_unitName, color) + self:F({unitname=_unitName, color=color}) + + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + if _unit and _playername then + self.PlayerSettings[_playername].smokecolor=color + local text=string.format("%s, %s, your bomb impacts are now smoked in %s.", self.rangename, _playername, self:_smokecolor2text(color)) + self:_DisplayMessageToGroup(_unit, text, 5) + end + +end + +--- Sets the flare color used when player makes a direct hit on target. +-- @param #RANGE self +-- @param #string _unitName Name of the player unit. +-- @param Utilities.Utils#FLARECOLOR color ID of flare color. +function RANGE:_playerflarecolor(_unitName, color) + self:F({unitname=_unitName, color=color}) + + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + if _unit and _playername then + self.PlayerSettings[_playername].flarecolor=color + local text=string.format("%s, %s, your direct hits are now flared in %s.", self.rangename, _playername, self:_flarecolor2text(color)) + self:_DisplayMessageToGroup(_unit, text, 5) + end + +end + +--- Converts a smoke color id to text. E.g. SMOKECOLOR.Blue --> "blue". +-- @param #RANGE self +-- @param Utilities.Utils#SMOKECOLOR color Color Id. +-- @return #string Color text. +function RANGE:_smokecolor2text(color) + self:F(color) + + local txt="" + if color==SMOKECOLOR.Blue then + txt="blue" + elseif color==SMOKECOLOR.Green then + txt="green" + elseif color==SMOKECOLOR.Orange then + txt="orange" + elseif color==SMOKECOLOR.Red then + txt="red" + elseif color==SMOKECOLOR.White then + txt="white" + else + txt=string.format("unknown color (%s)", tostring(color)) + end + + return txt +end + +--- Sets the flare color used to flare players direct target hits. +-- @param #RANGE self +-- @param Utilities.Utils#FLARECOLOR color Color Id. +-- @return #string Color text. +function RANGE:_flarecolor2text(color) + self:F(color) + + local txt="" + if color==FLARECOLOR.Green then + txt="green" + elseif color==FLARECOLOR.Red then + txt="red" + elseif color==FLARECOLOR.White then + txt="white" + elseif color==FLARECOLOR.Yellow then + txt="yellow" + else + txt=string.format("unknown color (%s)", tostring(color)) + end + + return txt +end + +--- Checks if a static object with a certain name exists. It also added it to the MOOSE data base, if it is not already in there. +-- @param #RANGE self +-- @param #string name Name of the potential static object. +-- @return #boolean Returns true if a static with this name exists. Retruns false if a unit with this name exists. Returns nil if neither unit or static exist. +function RANGE:_CheckStatic(name) + self:F2(name) + + -- Get DCS static object. + local _DCSstatic=StaticObject.getByName(name) + + if _DCSstatic and _DCSstatic:isExist() then + + --Static does exist at least in DCS. Check if it also in the MOOSE DB. + local _MOOSEstatic=STATIC:FindByName(name, false) + + -- If static is not yet in MOOSE DB, we add it. Can happen for cargo statics! + if not _MOOSEstatic then + self:T(self.id..string.format("Adding DCS static to MOOSE database. Name = %s.", name)) + _DATABASE:AddStatic(name) + end + + return true + else + self:T3(self.id..string.format("No static object with name %s exists.", name)) + end + + -- Check if a unit has this name. + if UNIT:FindByName(name) then + return false + else + self:T3(self.id..string.format("No unit object with name %s exists.", name)) + end + + -- If not unit or static exist, we return nil. + return nil +end + +--- Get max speed of controllable. +-- @param #RANGE self +-- @param Wrapper.Controllable#CONTROLLABLE controllable +-- @return Maximum speed in km/h. +function RANGE:_GetSpeed(controllable) + self:F2(controllable) + + -- Get DCS descriptors + local desc=controllable:GetDesc() + + -- Get speed + local speed=0 + if desc then + speed=desc.speedMax*3.6 + self:T({speed=speed}) + end + + return speed +end + +--- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned. +-- @param #RANGE self +-- @param #string _unitName Name of the player unit. +-- @return Wrapper.Unit#UNIT Unit of player. +-- @return #string Name of the player. +-- @return nil If player does not exist. +function RANGE:_GetPlayerUnitAndName(_unitName) + self:F2(_unitName) + + if _unitName ~= nil then + + -- Get DCS unit from its name. + local DCSunit=Unit.getByName(_unitName) + + if DCSunit then + + local playername=DCSunit:getPlayerName() + local unit=UNIT:Find(DCSunit) + + self:T2({DCSunit=DCSunit, unit=unit, playername=playername}) + if DCSunit and unit and playername then + return unit, playername + end + + end + + end + + -- Return nil if we could not find a player. + return nil,nil +end + +--- Returns a string which consits of this callsign and the player name. +-- @param #RANGE self +-- @param #string unitname Name of the player unit. +function RANGE:_myname(unitname) + self:F2(unitname) + + local unit=UNIT:FindByName(unitname) + local pname=unit:GetPlayerName() + local csign=unit:GetCallsign() + + --return string.format("%s (%s)", csign, pname) + return string.format("%s", pname) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- **Functional (WIP)** -- Base class that models processes to achieve goals involving a Zone. +-- +-- === +-- +-- ZONE_GOAL models processes that have a Goal with a defined achievement involving a Zone. +-- Derived classes implement the ways how the achievements can be realized. +-- +-- === +-- +-- ### Author: **FlightControl** +-- ### Contributions: **funkyfranky** +-- +-- === +-- +-- @module Functional.ZoneGoal +-- @image MOOSE.JPG + +do -- Zone + + --- @type ZONE_GOAL + -- @field #string ClassName Name of the class. + -- @field Core.Goal#GOAL Goal The goal object. + -- @field #number SmokeTime Time stamp in seconds when the last smoke of the zone was triggered. + -- @field Core.Scheduler#SCHEDULER SmokeScheduler Scheduler responsible for smoking the zone. + -- @field #number SmokeColor Color of the smoke. + -- @field #boolean SmokeZone If true, smoke zone. + -- @extends Core.Zone#ZONE_RADIUS + + + --- Models processes that have a Goal with a defined achievement involving a Zone. + -- Derived classes implement the ways how the achievements can be realized. + -- + -- ## 1. ZONE_GOAL constructor + -- + -- * @{#ZONE_GOAL.New}(): Creates a new ZONE_GOAL object. + -- + -- ## 2. ZONE_GOAL is a finite state machine (FSM). + -- + -- ### 2.1 ZONE_GOAL States + -- + -- * None: Initial State + -- + -- ### 2.2 ZONE_GOAL Events + -- + -- * DestroyedUnit: A @{Wrapper.Unit} is destroyed in the Zone. The event will only get triggered if the method @{#ZONE_GOAL.MonitorDestroyedUnits}() is used. + -- + -- @field #ZONE_GOAL + ZONE_GOAL = { + ClassName = "ZONE_GOAL", + Goal = nil, + SmokeTime = nil, + SmokeScheduler = nil, + SmokeColor = nil, + SmokeZone = nil, + } + + --- ZONE_GOAL Constructor. + -- @param #ZONE_GOAL self + -- @param Core.Zone#ZONE_RADIUS Zone A @{Zone} object with the goal to be achieved. + -- @return #ZONE_GOAL + function ZONE_GOAL:New( Zone ) + + local self = BASE:Inherit( self, ZONE_RADIUS:New( Zone:GetName(), Zone:GetVec2(), Zone:GetRadius() ) ) -- #ZONE_GOAL + self:F( { Zone = Zone } ) + + -- Goal object. + self.Goal = GOAL:New() + + self.SmokeTime = nil + + -- Set smoke ON. + self:SetSmokeZone(true) + + self:AddTransition( "*", "DestroyedUnit", "*" ) + + --- DestroyedUnit event. + -- @function [parent=#ZONE_GOAL] DestroyedUnit + -- @param #ZONE_GOAL self + + --- DestroyedUnit delayed event + -- @function [parent=#ZONE_GOAL] __DestroyedUnit + -- @param #ZONE_GOAL self + -- @param #number delay Delay in seconds. + + --- DestroyedUnit Handler OnAfter for ZONE_GOAL + -- @function [parent=#ZONE_GOAL] OnAfterDestroyedUnit + -- @param #ZONE_GOAL self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Wrapper.Unit#UNIT DestroyedUnit The destroyed unit. + -- @param #string PlayerName The name of the player. + + return self + end + + --- Get the Zone. + -- @param #ZONE_GOAL self + -- @return #ZONE_GOAL + function ZONE_GOAL:GetZone() + return self + end + + + --- Get the name of the Zone. + -- @param #ZONE_GOAL self + -- @return #string + function ZONE_GOAL:GetZoneName() + return self:GetName() + end + + + --- Activate smoking of zone with the color or the current owner. + -- @param #ZONE_GOAL self + -- @param #boolean switch If *true* or *nil* activate smoke. If *false* or *nil*, no smoke. + -- @return #ZONE_GOAL + function ZONE_GOAL:SetSmokeZone(switch) + self.SmokeZone=switch + --[[ + if switch==nil or switch==true then + self.SmokeZone=true + else + self.SmokeZone=false + end + ]] + return self + end + + --- Set the smoke color. + -- @param #ZONE_GOAL self + -- @param DCS#SMOKECOLOR.Color SmokeColor + function ZONE_GOAL:Smoke( SmokeColor ) + self:F( { SmokeColor = SmokeColor} ) + + self.SmokeColor = SmokeColor + end + + + --- Flare the zone boundary. + -- @param #ZONE_GOAL self + -- @param DCS#SMOKECOLOR.Color FlareColor + function ZONE_GOAL:Flare( FlareColor ) + self:FlareZone( FlareColor, 30) + end + + + --- When started, check the Smoke and the Zone status. + -- @param #ZONE_GOAL self + function ZONE_GOAL:onafterGuard() + self:F("Guard") + + -- Start smoke + if self.SmokeZone and not self.SmokeScheduler then + self.SmokeScheduler = self:ScheduleRepeat(1, 1, 0.1, nil, self.StatusSmoke, self) + end + end + + + --- Check status Smoke. + -- @param #ZONE_GOAL self + function ZONE_GOAL:StatusSmoke() + self:F({self.SmokeTime, self.SmokeColor}) + + if self.SmokeZone then + + -- Current time. + local CurrentTime = timer.getTime() + + -- Restart smoke every 5 min. + if self.SmokeTime == nil or self.SmokeTime + 300 <= CurrentTime then + if self.SmokeColor then + self:GetCoordinate():Smoke( self.SmokeColor ) + self.SmokeTime = CurrentTime + end + end + + end + + end + + + --- @param #ZONE_GOAL self + -- @param Core.Event#EVENTDATA EventData Event data table. + function ZONE_GOAL:__Destroyed( EventData ) + self:F( { "EventDead", EventData } ) + + self:F( { EventData.IniUnit } ) + + if EventData.IniDCSUnit then + + local Vec3 = EventData.IniDCSUnit:getPosition().p + self:F( { Vec3 = Vec3 } ) + + if Vec3 and self:IsVec3InZone(Vec3) then + + local PlayerHits = _DATABASE.HITS[EventData.IniUnitName] + + if PlayerHits then + + for PlayerName, PlayerHit in pairs( PlayerHits.Players or {} ) do + self.Goal:AddPlayerContribution( PlayerName ) + self:DestroyedUnit( EventData.IniUnitName, PlayerName ) + end + + end + + end + end + + end + + + --- Activate the event UnitDestroyed to be fired when a unit is destroyed in the zone. + -- @param #ZONE_GOAL self + function ZONE_GOAL:MonitorDestroyedUnits() + + self:HandleEvent( EVENTS.Dead, self.__Destroyed ) + self:HandleEvent( EVENTS.Crash, self.__Destroyed ) + + end + +end +--- **Functional (WIP)** -- Base class that models processes to achieve goals involving a Zone for a Coalition. +-- +-- === +-- +-- ZONE_GOAL_COALITION models processes that have a Goal with a defined achievement involving a Zone for a Coalition. +-- Derived classes implement the ways how the achievements can be realized. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module Functional.ZoneGoalCoalition +-- @image MOOSE.JPG + +do -- ZoneGoal + + --- @type ZONE_GOAL_COALITION + -- @field #string ClassName Name of the Class. + -- @field #number Coalition The current coalition ID of the zone owner. + -- @field #number PreviousCoalition The previous owner of the zone. + -- @field #table UnitCategories Table of unit categories that are able to capture and hold the zone. Default is only GROUND units. + -- @field #table ObjectCategories Table of object categories that are able to hold a zone. Default is UNITS and STATICS. + -- @extends Functional.ZoneGoal#ZONE_GOAL + + + --- ZONE_GOAL_COALITION models processes that have a Goal with a defined achievement involving a Zone for a Coalition. + -- Derived classes implement the ways how the achievements can be realized. + -- + -- ## 1. ZONE_GOAL_COALITION constructor + -- + -- * @{#ZONE_GOAL_COALITION.New}(): Creates a new ZONE_GOAL_COALITION object. + -- + -- ## 2. ZONE_GOAL_COALITION is a finite state machine (FSM). + -- + -- ### 2.1 ZONE_GOAL_COALITION States + -- + -- ### 2.2 ZONE_GOAL_COALITION Events + -- + -- ### 2.3 ZONE_GOAL_COALITION State Machine + -- + -- @field #ZONE_GOAL_COALITION + ZONE_GOAL_COALITION = { + ClassName = "ZONE_GOAL_COALITION", + Coalition = nil, + PreviousCoaliton = nil, + UnitCategories = nil, + ObjectCategories = nil, + } + + --- @field #table ZONE_GOAL_COALITION.States + ZONE_GOAL_COALITION.States = {} + + --- ZONE_GOAL_COALITION Constructor. + -- @param #ZONE_GOAL_COALITION self + -- @param Core.Zone#ZONE Zone A @{Zone} object with the goal to be achieved. + -- @param DCSCoalition.DCSCoalition#coalition Coalition The initial coalition owning the zone. Default coalition.side.NEUTRAL. + -- @param #table UnitCategories Table of unit categories. See [DCS Class Unit](https://wiki.hoggitworld.com/view/DCS_Class_Unit). Default {Unit.Category.GROUND_UNIT}. + -- @return #ZONE_GOAL_COALITION + function ZONE_GOAL_COALITION:New( Zone, Coalition, UnitCategories ) + + if not Zone then + BASE:E("ERROR: No Zone specified in ZONE_GOAL_COALITON!") + return nil + end + + -- Inherit ZONE_GOAL. + local self = BASE:Inherit( self, ZONE_GOAL:New( Zone ) ) -- #ZONE_GOAL_COALITION + self:F( { Zone = Zone, Coalition = Coalition } ) + + -- Set initial owner. + self:SetCoalition( Coalition or coalition.side.NEUTRAL) + + -- Set default unit and object categories for the zone scan. + self:SetUnitCategories(UnitCategories) + self:SetObjectCategories() + + return self + end + + + --- Set the owning coalition of the zone. + -- @param #ZONE_GOAL_COALITION self + -- @param DCSCoalition.DCSCoalition#coalition Coalition The coalition ID, e.g. *coalition.side.RED*. + -- @return #ZONE_GOAL_COALITION + function ZONE_GOAL_COALITION:SetCoalition( Coalition ) + self.PreviousCoalition=self.Coalition or Coalition + self.Coalition = Coalition + return self + end + + --- Set the owning coalition of the zone. + -- @param #ZONE_GOAL_COALITION self + -- @param #table UnitCategories Table of unit categories. See [DCS Class Unit](https://wiki.hoggitworld.com/view/DCS_Class_Unit). Default {Unit.Category.GROUND_UNIT}. + -- @return #ZONE_GOAL_COALITION + function ZONE_GOAL_COALITION:SetUnitCategories( UnitCategories ) + + if UnitCategories and type(UnitCategories)~="table" then + UnitCategories={UnitCategories} + end + + self.UnitCategories=UnitCategories or {Unit.Category.GROUND_UNIT} + + return self + end + + --- Set the owning coalition of the zone. + -- @param #ZONE_GOAL_COALITION self + -- @param #table ObjectCategories Table of unit categories. See [DCS Class Object](https://wiki.hoggitworld.com/view/DCS_Class_Object). Default {Object.Category.UNIT, Object.Category.STATIC}, i.e. all UNITS and STATICS. + -- @return #ZONE_GOAL_COALITION + function ZONE_GOAL_COALITION:SetObjectCategories( ObjectCategories ) + + if ObjectCategories and type(ObjectCategories)~="table" then + ObjectCategories={ObjectCategories} + end + + self.ObjectCategories=ObjectCategories or {Object.Category.UNIT, Object.Category.STATIC} + + return self + end + + --- Get the owning coalition of the zone. + -- @param #ZONE_GOAL_COALITION self + -- @return DCSCoalition.DCSCoalition#coalition Coalition. + function ZONE_GOAL_COALITION:GetCoalition() + return self.Coalition + end + + --- Get the previous coaliton, i.e. the one owning the zone before the current one. + -- @param #ZONE_GOAL_COALITION self + -- @return DCSCoalition.DCSCoalition#coalition Coalition. + function ZONE_GOAL_COALITION:GetPreviousCoalition() + return self.PreviousCoalition + end + + + --- Get the owning coalition name of the zone. + -- @param #ZONE_GOAL_COALITION self + -- @return #string Coalition name. + function ZONE_GOAL_COALITION:GetCoalitionName() + return UTILS.GetCoalitionName(self.Coalition) + end + + + --- Check status Coalition ownership. + -- @param #ZONE_GOAL_COALITION self + -- @return #ZONE_GOAL_COALITION + function ZONE_GOAL_COALITION:StatusZone() + + -- Get current state. + local State = self:GetState() + + -- Debug text. + local text=string.format("Zone state=%s, Owner=%s, Scanning...", State, self:GetCoalitionName()) + self:F(text) + + -- Scan zone. + self:Scan( self.ObjectCategories, self.UnitCategories ) + + return self + end + +end + +--- **Functional** -- Models the process to zone guarding and capturing. +-- +-- === +-- +-- ## Features: +-- +-- * Models the possible state transitions between the Guarded, Attacked, Empty and Captured states. +-- * A zone has an owning coalition, that means that at a specific point in time, a zone can be owned by the red or blue coalition. +-- * Provide event handlers to tailor the actions when a zone changes coalition or state. +-- +-- === +-- +-- ## Missions: +-- +-- [CAZ - Capture Zones](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/CAZ%20-%20Capture%20Zones) +-- +-- === +-- +-- # Player Experience +-- +-- ![States](..\Presentations\ZONE_CAPTURE_COALITION\Dia3.JPG) +-- +-- The above models the possible state transitions between the **Guarded**, **Attacked**, **Empty** and **Captured** states. +-- A zone has an __owning coalition__, that means that at a specific point in time, a zone can be owned by the red or blue coalition. +-- +-- The Zone can be in the state **Guarded** by the __owning coalition__, which is the coalition that initially occupies the zone with units of its coalition. +-- Once units of an other coalition are entering the Zone, the state will change to **Attacked**. As long as these units remain in the zone, the state keeps set to Attacked. +-- When all units are destroyed in the Zone, the state will change to **Empty**, which expresses that the Zone is empty, and can be captured. +-- When units of the other coalition are in the Zone, and no other units of the owning coalition is in the Zone, the Zone is captured, and its state will change to **Captured**. +-- +-- The zone needs to be monitored regularly for the presence of units to interprete the correct state transition required. +-- This monitoring process MUST be started using the @{#ZONE_CAPTURE_COALITION.Start}() method. +-- Otherwise no monitoring will be active and the zone will stay in the current state forever. +-- +-- === +-- +-- ## [YouTube Playlist](https://www.youtube.com/watch?v=0m6K6Yxa-os&list=PL7ZUrU4zZUl0qqJsfa8DPvZWDY-OyDumE) +-- +-- === +-- +-- ### Author: **FlightControl** +-- ### Contributions: **Millertime** - Concept, **funkyfranky** +-- +-- === +-- +-- @module Functional.ZoneCaptureCoalition +-- @image Capture_Zones.JPG + +do -- ZONE_CAPTURE_COALITION + + --- @type ZONE_CAPTURE_COALITION + -- @field #string ClassName Name of the class. + -- @field #number MarkBlue ID of blue F10 mark. + -- @field #number MarkRed ID of red F10 mark. + -- @field #number StartInterval Time in seconds after the status monitor is started. + -- @field #number RepeatInterval Time in seconds after which the zone status is updated. + -- @field #boolean HitsOn If true, hit events are monitored and trigger the "Attack" event when a defending unit is hit. + -- @field #number HitTimeLast Time stamp in seconds when the last unit inside the zone was hit. + -- @field #number HitTimeAttackOver Time interval in seconds before the zone goes from "Attacked" to "Guarded" state after the last hit. + -- @field #boolean MarkOn If true, create marks of zone status on F10 map. + -- @extends Functional.ZoneGoalCoalition#ZONE_GOAL_COALITION + + + --- Models the process to capture a Zone for a Coalition, which is guarded by another Coalition. + -- This is a powerful concept that allows to create very dynamic missions based on the different state transitions of various zones. + -- + -- === + -- + -- In order to use ZONE_CAPTURE_COALITION, you need to: + -- + -- * Create a @{Zone} object from one of the ZONE_ classes. + -- Note that ZONE_POLYGON_ classes are not yet functional. + -- The only functional ZONE_ classses are those derived from a ZONE_RADIUS. + -- * Set the state of the zone. Most of the time, Guarded would be the initial state. + -- * Start the zone capturing **monitoring process**. + -- This will check the presence of friendly and/or enemy units within the zone and will transition the state of the zone when the tactical situation changed. + -- The frequency of the monitoring must not be real-time, a 30 second interval to execute the checks is sufficient. + -- + -- ![New](..\Presentations\ZONE_CAPTURE_COALITION\Dia5.JPG) + -- + -- ### Important: + -- + -- You must start the monitoring process within your code, or there won't be any state transition checks executed. + -- See further the start/stop monitoring process. + -- + -- ### Important: + -- + -- Ensure that the object containing the ZONE_CAPTURE_COALITION object is persistent. + -- Otherwise the garbage collector of lua will remove the object and the monitoring process will stop. + -- This will result in your object to be destroyed (removed) from internal memory and there won't be any zone state transitions anymore detected! + -- So use the `local` keyword in lua with thought! Most of the time, you can declare your object gobally. + -- + -- + -- + -- # Example: + -- + -- -- Define a new ZONE object, which is based on the trigger zone `CaptureZone`, which is defined within the mission editor. + -- CaptureZone = ZONE:New( "CaptureZone" ) + -- + -- -- Here we create a new ZONE_CAPTURE_COALITION object, using the :New constructor. + -- ZoneCaptureCoalition = ZONE_CAPTURE_COALITION:New( CaptureZone, coalition.side.RED ) + -- + -- -- Set the zone to Guarding state. + -- ZoneCaptureCoalition:__Guard( 1 ) + -- + -- -- Start the zone monitoring process in 30 seconds and check every 30 seconds. + -- ZoneCaptureCoalition:Start( 30, 30 ) + -- + -- + -- # Constructor: + -- + -- Use the @{#ZONE_CAPTURE_COALITION.New}() constructor to create a new ZONE_CAPTURE_COALITION object. + -- + -- # ZONE_CAPTURE_COALITION is a finite state machine (FSM). + -- + -- ![States](..\Presentations\ZONE_CAPTURE_COALITION\Dia4.JPG) + -- + -- ## ZONE_CAPTURE_COALITION States + -- + -- * **Captured**: The Zone has been captured by an other coalition. + -- * **Attacked**: The Zone is currently intruded by an other coalition. There are units of the owning coalition and an other coalition in the Zone. + -- * **Guarded**: The Zone is guarded by the owning coalition. There is no other unit of an other coalition in the Zone. + -- * **Empty**: The Zone is empty. There is not valid unit in the Zone. + -- + -- ## 2.2 ZONE_CAPTURE_COALITION Events + -- + -- * **Capture**: The Zone has been captured by an other coalition. + -- * **Attack**: The Zone is currently intruded by an other coalition. There are units of the owning coalition and an other coalition in the Zone. + -- * **Guard**: The Zone is guarded by the owning coalition. There is no other unit of an other coalition in the Zone. + -- * **Empty**: The Zone is empty. There is not valid unit in the Zone. + -- + -- # "Script It" + -- + -- ZONE_CAPTURE_COALITION allows to take action on the various state transitions and add your custom code and logic. + -- + -- ## Take action using state- and event handlers. + -- + -- ![States](..\Presentations\ZONE_CAPTURE_COALITION\Dia6.JPG) + -- + -- The most important to understand is how states and events can be tailored. + -- Carefully study the diagram and the explanations. + -- + -- **State Handlers** capture the moment: + -- + -- - On Leave from the old state. Return false to cancel the transition. + -- - On Enter to the new state. + -- + -- **Event Handlers** capture the moment: + -- + -- - On Before the event is triggered. Return false to cancel the transition. + -- - On After the event is triggered. + -- + -- ![States](..\Presentations\ZONE_CAPTURE_COALITION\Dia7.JPG) + -- + -- Each handler can receive optionally 3 parameters: + -- + -- - **From**: A string containing the From State. + -- - **Event**: A string containing the Event. + -- - **To**: A string containing the To State. + -- + -- The mission designer can use these values to alter the logic. + -- For example: + -- + -- --- @param Functional.ZoneCaptureCoalition#ZONE_CAPTURE_COALITION self + -- function ZoneCaptureCoalition:OnEnterGuarded( From, Event, To ) + -- if From ~= "Empty" then + -- -- Display a message + -- end + -- end + -- + -- This code checks that when the __Guarded__ state has been reached, that if the **From** state was __Empty__, then display a message. + -- + -- ## Example Event Handler. + -- + -- --- @param Functional.ZoneCaptureCoalition#ZONE_CAPTURE_COALITION self + -- function ZoneCaptureCoalition:OnEnterGuarded( From, Event, To ) + -- if From ~= To then + -- local Coalition = self:GetCoalition() + -- self:E( { Coalition = Coalition } ) + -- if Coalition == coalition.side.BLUE then + -- ZoneCaptureCoalition:Smoke( SMOKECOLOR.Blue ) + -- US_CC:MessageTypeToCoalition( string.format( "%s is under protection of the USA", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information ) + -- RU_CC:MessageTypeToCoalition( string.format( "%s is under protection of the USA", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information ) + -- else + -- ZoneCaptureCoalition:Smoke( SMOKECOLOR.Red ) + -- RU_CC:MessageTypeToCoalition( string.format( "%s is under protection of Russia", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information ) + -- US_CC:MessageTypeToCoalition( string.format( "%s is under protection of Russia", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information ) + -- end + -- end + -- end + -- + -- ## Stop and Start the zone monitoring process. + -- + -- At regular intervals, the state of the zone needs to be monitored. + -- The zone needs to be scanned for the presence of units within the zone boundaries. + -- Depending on the owning coalition of the zone and the presence of units (of the owning and/or other coalition(s)), the zone will transition to another state. + -- + -- However, ... this scanning process is rather CPU intensive. Imagine you have 10 of these capture zone objects setup within your mission. + -- That would mean that your mission would check 10 capture zones simultaneously, each checking for the presence of units. + -- It would be highly **CPU inefficient**, as some of these zones are not required to be monitored (yet). + -- + -- Therefore, the mission designer is given 2 methods that allow to take control of the CPU utilization efficiency: + -- + -- * @{#ZONE_CAPTURE_COALITION.Start}(): This starts the monitoring process. + -- * @{#ZONE_CAPTURE_COALITION.Stop}(): This stops the monitoring process. + -- + -- ### IMPORTANT + -- + -- **Each capture zone object must have the monitoring process started specifically. The monitoring process is NOT started by default!** + -- + -- + -- # Full Example + -- + -- The following annotated code shows a real example of how ZONE_CAPTURE_COALITION can be applied. + -- + -- The concept is simple. + -- + -- The USA (US), blue coalition, needs to capture the Russian (RU), red coalition, zone, which is near groom lake. + -- + -- A capture zone has been setup that guards the presence of the troops. + -- Troops are guarded by red forces. Blue is required to destroy the red forces and capture the zones. + -- + -- At first, we setup the Command Centers + -- + -- do + -- + -- RU_CC = COMMANDCENTER:New( GROUP:FindByName( "REDHQ" ), "Russia HQ" ) + -- US_CC = COMMANDCENTER:New( GROUP:FindByName( "BLUEHQ" ), "USA HQ" ) + -- + -- end + -- + -- Next, we define the mission, and add some scoring to it. + -- + -- do -- Missions + -- + -- US_Mission_EchoBay = MISSION:New( US_CC, "Echo Bay", "Primary", + -- "Welcome trainee. The airport Groom Lake in Echo Bay needs to be captured.\n" .. + -- "There are five random capture zones located at the airbase.\n" .. + -- "Move to one of the capture zones, destroy the fuel tanks in the capture zone, " .. + -- "and occupy each capture zone with a platoon.\n " .. + -- "Your orders are to hold position until all capture zones are taken.\n" .. + -- "Use the map (F10) for a clear indication of the location of each capture zone.\n" .. + -- "Note that heavy resistance can be expected at the airbase!\n" .. + -- "Mission 'Echo Bay' is complete when all five capture zones are taken, and held for at least 5 minutes!" + -- , coalition.side.RED ) + -- + -- US_Mission_EchoBay:Start() + -- + -- end + -- + -- + -- Now the real work starts. + -- We define a **CaptureZone** object, which is a ZONE object. + -- Within the mission, a trigger zone is created with the name __CaptureZone__, with the defined radius within the mission editor. + -- + -- CaptureZone = ZONE:New( "CaptureZone" ) + -- + -- Next, we define the **ZoneCaptureCoalition** object, as explained above. + -- + -- ZoneCaptureCoalition = ZONE_CAPTURE_COALITION:New( CaptureZone, coalition.side.RED ) + -- + -- Of course, we want to let the **ZoneCaptureCoalition** object do something when the state transitions. + -- Do accomodate this, it is very simple, as explained above. + -- We use **Event Handlers** to tailor the logic. + -- + -- Here we place an Event Handler at the Guarded event. So when the **Guarded** event is triggered, then this method is called! + -- With the variables **From**, **Event**, **To**. Each of these variables containing a string. + -- + -- We check if the previous state wasn't Guarded also. + -- If not, we retrieve the owning Coalition of the **ZoneCaptureCoalition**, using `self:GetCoalition()`. + -- So **Coalition** will contain the current owning coalition of the zone. + -- + -- Depending on the zone ownership, different messages are sent. + -- Note the methods `ZoneCaptureCoalition:GetZoneName()`. + -- + -- --- @param Functional.ZoneCaptureCoalition#ZONE_CAPTURE_COALITION self + -- function ZoneCaptureCoalition:OnEnterGuarded( From, Event, To ) + -- if From ~= To then + -- local Coalition = self:GetCoalition() + -- self:E( { Coalition = Coalition } ) + -- if Coalition == coalition.side.BLUE then + -- ZoneCaptureCoalition:Smoke( SMOKECOLOR.Blue ) + -- US_CC:MessageTypeToCoalition( string.format( "%s is under protection of the USA", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information ) + -- RU_CC:MessageTypeToCoalition( string.format( "%s is under protection of the USA", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information ) + -- else + -- ZoneCaptureCoalition:Smoke( SMOKECOLOR.Red ) + -- RU_CC:MessageTypeToCoalition( string.format( "%s is under protection of Russia", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information ) + -- US_CC:MessageTypeToCoalition( string.format( "%s is under protection of Russia", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information ) + -- end + -- end + -- end + -- + -- As you can see, not a rocket science. + -- Next is the Event Handler when the **Empty** state transition is triggered. + -- Now we smoke the ZoneCaptureCoalition with a green color, using `self:Smoke( SMOKECOLOR.Green )`. + -- + -- --- @param Functional.Protect#ZONE_CAPTURE_COALITION self + -- function ZoneCaptureCoalition:OnEnterEmpty() + -- self:Smoke( SMOKECOLOR.Green ) + -- US_CC:MessageTypeToCoalition( string.format( "%s is unprotected, and can be captured!", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information ) + -- RU_CC:MessageTypeToCoalition( string.format( "%s is unprotected, and can be captured!", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information ) + -- end + -- + -- The next Event Handlers speak for itself. + -- When the zone is Attacked, we smoke the zone white and send some messages to each coalition. + -- + -- --- @param Functional.Protect#ZONE_CAPTURE_COALITION self + -- function ZoneCaptureCoalition:OnEnterAttacked() + -- ZoneCaptureCoalition:Smoke( SMOKECOLOR.White ) + -- local Coalition = self:GetCoalition() + -- self:E({Coalition = Coalition}) + -- if Coalition == coalition.side.BLUE then + -- US_CC:MessageTypeToCoalition( string.format( "%s is under attack by Russia", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information ) + -- RU_CC:MessageTypeToCoalition( string.format( "We are attacking %s", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information ) + -- else + -- RU_CC:MessageTypeToCoalition( string.format( "%s is under attack by the USA", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information ) + -- US_CC:MessageTypeToCoalition( string.format( "We are attacking %s", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information ) + -- end + -- end + -- + -- When the zone is Captured, we send some victory or loss messages to the correct coalition. + -- And we add some score. + -- + -- --- @param Functional.Protect#ZONE_CAPTURE_COALITION self + -- function ZoneCaptureCoalition:OnEnterCaptured() + -- local Coalition = self:GetCoalition() + -- self:E({Coalition = Coalition}) + -- if Coalition == coalition.side.BLUE then + -- RU_CC:MessageTypeToCoalition( string.format( "%s is captured by the USA, we lost it!", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information ) + -- US_CC:MessageTypeToCoalition( string.format( "We captured %s, Excellent job!", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information ) + -- else + -- US_CC:MessageTypeToCoalition( string.format( "%s is captured by Russia, we lost it!", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information ) + -- RU_CC:MessageTypeToCoalition( string.format( "We captured %s, Excellent job!", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information ) + -- end + -- + -- self:__Guard( 30 ) + -- end + -- + -- And this call is the most important of all! + -- In the context of the mission, we need to start the zone capture monitoring process. + -- Or nothing will be monitored and the zone won't change states. + -- We start the monitoring after 5 seconds, and will repeat every 30 seconds a check. + -- + -- ZoneCaptureCoalition:Start( 5, 30 ) + -- + -- + -- @field #ZONE_CAPTURE_COALITION + ZONE_CAPTURE_COALITION = { + ClassName = "ZONE_CAPTURE_COALITION", + MarkBlue = nil, + MarkRed = nil, + StartInterval = nil, + RepeatInterval = nil, + HitsOn = nil, + HitTimeLast = nil, + HitTimeAttackOver = nil, + MarkOn = nil, + } + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor and Start/Stop Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + --- ZONE_CAPTURE_COALITION Constructor. + -- @param #ZONE_CAPTURE_COALITION self + -- @param Core.Zone#ZONE Zone A @{Zone} object with the goal to be achieved. + -- @param DCSCoalition.DCSCoalition#coalition Coalition The initial coalition owning the zone. + -- @param #table UnitCategories Table of unit categories. See [DCS Class Unit](https://wiki.hoggitworld.com/view/DCS_Class_Unit). Default {Unit.Category.GROUND_UNIT}. + -- @param #table ObjectCategories Table of unit categories. See [DCS Class Object](https://wiki.hoggitworld.com/view/DCS_Class_Object). Default {Object.Category.UNIT, Object.Category.STATIC}, i.e. all UNITS and STATICS. + -- @return #ZONE_CAPTURE_COALITION + -- @usage + -- + -- AttackZone = ZONE:New( "AttackZone" ) + -- + -- ZoneCaptureCoalition = ZONE_CAPTURE_COALITION:New( AttackZone, coalition.side.RED, {UNITS ) -- Create a new ZONE_CAPTURE_COALITION object of zone AttackZone with ownership RED coalition. + -- ZoneCaptureCoalition:__Guard( 1 ) -- Start the Guarding of the AttackZone. + -- + function ZONE_CAPTURE_COALITION:New( Zone, Coalition, UnitCategories, ObjectCategories ) + + local self = BASE:Inherit( self, ZONE_GOAL_COALITION:New( Zone, Coalition, UnitCategories ) ) -- #ZONE_CAPTURE_COALITION + self:F( { Zone = Zone, Coalition = Coalition, UnitCategories = UnitCategories, ObjectCategories = ObjectCategories } ) + + self:SetObjectCategories(ObjectCategories) + + -- Default is no smoke. + self:SetSmokeZone(false) + -- Default is F10 marks ON. + self:SetMarkZone(true) + -- Start in state "Empty". + self:SetStartState("Empty") + + do + + --- Captured State Handler OnLeave for ZONE_CAPTURE_COALITION + -- @function [parent=#ZONE_CAPTURE_COALITION] OnLeaveCaptured + -- @param #ZONE_CAPTURE_COALITION self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Captured State Handler OnEnter for ZONE_CAPTURE_COALITION + -- @function [parent=#ZONE_CAPTURE_COALITION] OnEnterCaptured + -- @param #ZONE_CAPTURE_COALITION self + -- @param #string From + -- @param #string Event + -- @param #string To + + end + + + do + + --- Attacked State Handler OnLeave for ZONE_CAPTURE_COALITION + -- @function [parent=#ZONE_CAPTURE_COALITION] OnLeaveAttacked + -- @param #ZONE_CAPTURE_COALITION self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Attacked State Handler OnEnter for ZONE_CAPTURE_COALITION + -- @function [parent=#ZONE_CAPTURE_COALITION] OnEnterAttacked + -- @param #ZONE_CAPTURE_COALITION self + -- @param #string From + -- @param #string Event + -- @param #string To + + end + + do + + --- Guarded State Handler OnLeave for ZONE_CAPTURE_COALITION + -- @function [parent=#ZONE_CAPTURE_COALITION] OnLeaveGuarded + -- @param #ZONE_CAPTURE_COALITION self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Guarded State Handler OnEnter for ZONE_CAPTURE_COALITION + -- @function [parent=#ZONE_CAPTURE_COALITION] OnEnterGuarded + -- @param #ZONE_CAPTURE_COALITION self + -- @param #string From + -- @param #string Event + -- @param #string To + + end + + + do + + --- Empty State Handler OnLeave for ZONE_CAPTURE_COALITION + -- @function [parent=#ZONE_CAPTURE_COALITION] OnLeaveEmpty + -- @param #ZONE_CAPTURE_COALITION self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Empty State Handler OnEnter for ZONE_CAPTURE_COALITION + -- @function [parent=#ZONE_CAPTURE_COALITION] OnEnterEmpty + -- @param #ZONE_CAPTURE_COALITION self + -- @param #string From + -- @param #string Event + -- @param #string To + + end + + self:AddTransition( "*", "Guard", "Guarded" ) + + --- Guard Handler OnBefore for ZONE_CAPTURE_COALITION + -- @function [parent=#ZONE_CAPTURE_COALITION] OnBeforeGuard + -- @param #ZONE_CAPTURE_COALITION self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Guard Handler OnAfter for ZONE_CAPTURE_COALITION + -- @function [parent=#ZONE_CAPTURE_COALITION] OnAfterGuard + -- @param #ZONE_CAPTURE_COALITION self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Guard Trigger for ZONE_CAPTURE_COALITION + -- @function [parent=#ZONE_CAPTURE_COALITION] Guard + -- @param #ZONE_CAPTURE_COALITION self + + --- Guard Asynchronous Trigger for ZONE_CAPTURE_COALITION + -- @function [parent=#ZONE_CAPTURE_COALITION] __Guard + -- @param #ZONE_CAPTURE_COALITION self + -- @param #number Delay + + self:AddTransition( "*", "Empty", "Empty" ) + + --- Empty Handler OnBefore for ZONE_CAPTURE_COALITION + -- @function [parent=#ZONE_CAPTURE_COALITION] OnBeforeEmpty + -- @param #ZONE_CAPTURE_COALITION self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Empty Handler OnAfter for ZONE_CAPTURE_COALITION + -- @function [parent=#ZONE_CAPTURE_COALITION] OnAfterEmpty + -- @param #ZONE_CAPTURE_COALITION self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Empty Trigger for ZONE_CAPTURE_COALITION + -- @function [parent=#ZONE_CAPTURE_COALITION] Empty + -- @param #ZONE_CAPTURE_COALITION self + + --- Empty Asynchronous Trigger for ZONE_CAPTURE_COALITION + -- @function [parent=#ZONE_CAPTURE_COALITION] __Empty + -- @param #ZONE_CAPTURE_COALITION self + -- @param #number Delay + + + self:AddTransition( { "Guarded", "Empty" }, "Attack", "Attacked" ) + + --- Attack Handler OnBefore for ZONE_CAPTURE_COALITION + -- @function [parent=#ZONE_CAPTURE_COALITION] OnBeforeAttack + -- @param #ZONE_CAPTURE_COALITION self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Attack Handler OnAfter for ZONE_CAPTURE_COALITION + -- @function [parent=#ZONE_CAPTURE_COALITION] OnAfterAttack + -- @param #ZONE_CAPTURE_COALITION self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Attack Trigger for ZONE_CAPTURE_COALITION + -- @function [parent=#ZONE_CAPTURE_COALITION] Attack + -- @param #ZONE_CAPTURE_COALITION self + + --- Attack Asynchronous Trigger for ZONE_CAPTURE_COALITION + -- @function [parent=#ZONE_CAPTURE_COALITION] __Attack + -- @param #ZONE_CAPTURE_COALITION self + -- @param #number Delay + + self:AddTransition( { "Guarded", "Attacked", "Empty" }, "Capture", "Captured" ) + + --- Capture Handler OnBefore for ZONE_CAPTURE_COALITION + -- @function [parent=#ZONE_CAPTURE_COALITION] OnBeforeCapture + -- @param #ZONE_CAPTURE_COALITION self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Capture Handler OnAfter for ZONE_CAPTURE_COALITION + -- @function [parent=#ZONE_CAPTURE_COALITION] OnAfterCapture + -- @param #ZONE_CAPTURE_COALITION self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Capture Trigger for ZONE_CAPTURE_COALITION + -- @function [parent=#ZONE_CAPTURE_COALITION] Capture + -- @param #ZONE_CAPTURE_COALITION self + + --- Capture Asynchronous Trigger for ZONE_CAPTURE_COALITION + -- @function [parent=#ZONE_CAPTURE_COALITION] __Capture + -- @param #ZONE_CAPTURE_COALITION self + -- @param #number Delay + + -- ZoneGoal objects are added to the _DATABASE.ZONES_GOAL and SET_ZONE_GOAL sets. + _EVENTDISPATCHER:CreateEventNewZoneGoal(self) + + return self + end + + + --- Starts the zone capturing monitoring process. + -- This process can be CPU intensive, ensure that you specify reasonable time intervals for the monitoring process. + -- Note that the monitoring process is NOT started automatically during the `:New()` constructor. + -- It is advised that the zone monitoring process is only started when the monitoring is of relevance in context of the current mission goals. + -- When the zone is of no relevance, it is advised NOT to start the monitoring process, or to stop the monitoring process to save CPU resources. + -- Therefore, the mission designer will need to use the `:Start()` method within his script to start the monitoring process specifically. + -- @param #ZONE_CAPTURE_COALITION self + -- @param #number StartInterval (optional) Specifies the start time interval in seconds when the zone state will be checked for the first time. + -- @param #number RepeatInterval (optional) Specifies the repeat time interval in seconds when the zone state will be checked repeatedly. + -- @return #ZONE_CAPTURE_COALITION self + -- @usage + -- + -- -- Setup the zone. + -- CaptureZone = ZONE:New( "CaptureZone" ) + -- ZoneCaptureCoalition = ZONE_CAPTURE_COALITION:New( CaptureZone, coalition.side.RED ) + -- + -- -- This starts the monitoring process within 15 seconds, repeating every 15 seconds. + -- ZoneCaptureCoalition:Start() + -- + -- -- This starts the monitoring process immediately, but repeats every 30 seconds. + -- ZoneCaptureCoalition:Start( 0, 30 ) + -- + function ZONE_CAPTURE_COALITION:Start( StartInterval, RepeatInterval ) + + self.StartInterval = StartInterval or 1 + self.RepeatInterval = RepeatInterval or 15 + + if self.ScheduleStatusZone then + self:ScheduleStop( self.ScheduleStatusZone ) + end + + -- Start Status scheduler. + self.ScheduleStatusZone = self:ScheduleRepeat( self.StartInterval, self.RepeatInterval, 0.1, nil, self.StatusZone, self ) + + -- We check if a unit within the zone is hit. If it is, then we must move the zone to attack state. + self:HandleEvent(EVENTS.Hit, self.OnEventHit) + + -- Create mark on F10 map. + self:Mark() + + return self + end + + + --- Stops the zone capturing monitoring process. + -- When the zone capturing monitor process is stopped, there won't be any changes anymore in the state and the owning coalition of the zone. + -- This method becomes really useful when the zone is of no relevance anymore within a long lasting mission. + -- In this case, it is advised to stop the monitoring process, not to consume unnecessary the CPU intensive scanning of units presence within the zone. + -- @param #ZONE_CAPTURE_COALITION self + -- @usage + -- -- Setup the zone. + -- CaptureZone = ZONE:New( "CaptureZone" ) + -- ZoneCaptureCoalition = ZONE_CAPTURE_COALITION:New( CaptureZone, coalition.side.RED ) + -- + -- -- This starts the monitoring process within 15 seconds, repeating every 15 seconds. + -- ZoneCaptureCoalition:Start() + -- + -- -- When the zone capturing is of no relevance anymore, stop the monitoring! + -- ZoneCaptureCoalition:Stop() + -- + -- @usage + -- -- For example, one could stop the monitoring when the zone was captured! + -- --- @param Functional.Protect#ZONE_CAPTURE_COALITION self + -- function ZoneCaptureCoalition:OnEnterCaptured() + -- local Coalition = self:GetCoalition() + -- self:E({Coalition = Coalition}) + -- if Coalition == coalition.side.BLUE then + -- RU_CC:MessageTypeToCoalition( string.format( "%s is captured by the USA, we lost it!", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information ) + -- US_CC:MessageTypeToCoalition( string.format( "We captured %s, Excellent job!", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information ) + -- else + -- US_CC:MessageTypeToCoalition( string.format( "%s is captured by Russia, we lost it!", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information ) + -- RU_CC:MessageTypeToCoalition( string.format( "We captured %s, Excellent job!", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information ) + -- end + -- + -- self:AddScore( "Captured", "Zone captured: Extra points granted.", 200 ) + -- + -- self:Stop() + -- end + -- + function ZONE_CAPTURE_COALITION:Stop() + + if self.ScheduleStatusZone then + self:ScheduleStop(self.ScheduleStatusZone) + end + + if self.SmokeScheduler then + self:ScheduleStop(self.SmokeScheduler) + end + + self:UnHandleEvent(EVENTS.Hit) + + end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User API Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + --- Set whether hit events of defending units are monitored and trigger "Attack" events. + -- @param #ZONE_CAPTURE_COALITION self + -- @param #boolean Switch If *true*, hit events are monitored. If *false* or *nil*, hit events are not monitored. + -- @param #number TimeAttackOver (Optional) Time in seconds after an attack is over after the last hit and the zone state goes to "Guarded". Default is 300 sec = 5 min. + -- @return #ZONE_CAPTURE_COALITION self + function ZONE_CAPTURE_COALITION:SetMonitorHits(Switch, TimeAttackOver) + self.HitsOn=Switch + self.HitTimeAttackOver=TimeAttackOver or 5*60 + return self + end + + --- Set whether marks on the F10 map are shown, which display the current zone status. + -- @param #ZONE_CAPTURE_COALITION self + -- @param #boolean Switch If *true* or *nil*, marks are shown. If *false*, marks are not displayed. + -- @return #ZONE_CAPTURE_COALITION self + function ZONE_CAPTURE_COALITION:SetMarkZone(Switch) + if Switch==nil or Switch==true then + self.MarkOn=true + else + self.MarkOn=false + end + return self + end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- DCS Event Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + + --- Monitor hit events. + -- @param #ZONE_CAPTURE_COALITION self + -- @param Core.Event#EVENTDATA EventData The event data. + function ZONE_CAPTURE_COALITION:OnEventHit( EventData ) + + if self.HitsOn then + + local UnitHit = EventData.TgtUnit + + if UnitHit.ClassName ~= "SCENERY" then + -- Check if unit is inside the capture zone and that it is of the defending coalition. + if UnitHit and UnitHit:IsInZone(self) and UnitHit:GetCoalition()==self.Coalition then + + -- Update last hit time. + self.HitTimeLast=timer.getTime() + + -- Only trigger attacked event if not already in state "Attacked". + if self:GetState()~="Attacked" then + self:F2("Hit ==> Attack") + self:Attack() + end + + end + end + end + + end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Event Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + --- On after "Guard" event. + -- @param #ZONE_CAPTURE_COALITION self + function ZONE_CAPTURE_COALITION:onafterGuard() + self:F2("After Guard") + + if self.SmokeZone and not self.SmokeScheduler then + self.SmokeScheduler = self:ScheduleRepeat( self.StartInterval, self.RepeatInterval, 0.1, nil, self.StatusSmoke, self ) + end + + end + + --- On enter "Guarded" state. + -- @param #ZONE_CAPTURE_COALITION self + function ZONE_CAPTURE_COALITION:onenterGuarded() + self:F2("Enter Guarded") + self:Mark() + end + + --- On enter "Captured" state. + -- @param #ZONE_CAPTURE_COALITION self + function ZONE_CAPTURE_COALITION:onenterCaptured() + self:F2("Enter Captured") + + -- Get new coalition. + local NewCoalition = self:GetScannedCoalition() + self:F( { NewCoalition = NewCoalition } ) + + -- Set new owner of zone. + self:SetCoalition(NewCoalition) + + -- Update mark. + self:Mark() + + -- Goal achieved. + self.Goal:Achieved() + end + + --- On enter "Empty" state. + -- @param #ZONE_CAPTURE_COALITION self + function ZONE_CAPTURE_COALITION:onenterEmpty() + self:F2("Enter Empty") + self:Mark() + end + + --- On enter "Attacked" state. + -- @param #ZONE_CAPTURE_COALITION self + function ZONE_CAPTURE_COALITION:onenterAttacked() + self:F2("Enter Attacked") + self:Mark() + end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Status Check Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + --- Check if zone is "Empty". + -- @param #ZONE_CAPTURE_COALITION self + -- @return #boolean self:IsNoneInZone() + function ZONE_CAPTURE_COALITION:IsEmpty() + + local IsEmpty = self:IsNoneInZone() + self:F( { IsEmpty = IsEmpty } ) + + return IsEmpty + end + + --- Check if zone is "Guarded", i.e. only one (the defending) coaliton is present inside the zone. + -- @param #ZONE_CAPTURE_COALITION self + -- @return #boolean self:IsAllInZoneOfCoalition( self.Coalition ) + function ZONE_CAPTURE_COALITION:IsGuarded() + + local IsGuarded = self:IsAllInZoneOfCoalition( self.Coalition ) + self:F( { IsGuarded = IsGuarded } ) + + return IsGuarded + end + + --- Check if zone is "Captured", i.e. another coalition took control over the zone and is the only one present. + -- @param #ZONE_CAPTURE_COALITION self + -- @return #boolean self:IsAllInZoneOfOtherCoalition( self.Coalition ) + function ZONE_CAPTURE_COALITION:IsCaptured() + + local IsCaptured = self:IsAllInZoneOfOtherCoalition( self.Coalition ) + self:F( { IsCaptured = IsCaptured } ) + + return IsCaptured + end + + --- Check if zone is "Attacked", i.e. another coaliton entered the zone. + -- @param #ZONE_CAPTURE_COALITION self + -- @return #boolean self:IsSomeInZoneOfCoalition( self.Coalition ) + function ZONE_CAPTURE_COALITION:IsAttacked() + + local IsAttacked = self:IsSomeInZoneOfCoalition( self.Coalition ) + self:F( { IsAttacked = IsAttacked } ) + + return IsAttacked + end + + + --- Check status Coalition ownership. + -- @param #ZONE_CAPTURE_COALITION self + function ZONE_CAPTURE_COALITION:StatusZone() + + -- Get FSM state. + local State = self:GetState() + + -- Scan zone in parent class ZONE_GOAL_COALITION + self:GetParent( self, ZONE_CAPTURE_COALITION ).StatusZone( self ) + + local Tnow=timer.getTime() + + -- Check if zone is guarded. + if State ~= "Guarded" and self:IsGuarded() then + + -- Check that there was a sufficient amount of time after the last hit before going back to "Guarded". + if self.HitTimeLast==nil or Tnow>=self.HitTimeLast+self.HitTimeAttackOver then + self:Guard() + self.HitTimeLast=nil + end + end + + -- Check if zone is empty. + if State ~= "Empty" and self:IsEmpty() then + self:Empty() + end + + -- Check if zone is attacked. + if State ~= "Attacked" and self:IsAttacked() then + self:Attack() + end + + -- Check if zone is captured. + if State ~= "Captured" and self:IsCaptured() then + self:Capture() + end + + -- Count stuff in zone. + local unitset=self:GetScannedSetUnit() + local nRed=0 + local nBlue=0 + for _,object in pairs(unitset:GetSet()) do + local coal=object:GetCoalition() + if object:IsAlive() then + if coal==coalition.side.RED then + nRed=nRed+1 + elseif coal==coalition.side.BLUE then + nBlue=nBlue+1 + end + end + end + + -- Status text. + if false then + local text=string.format("CAPTURE ZONE %s: Owner=%s (Previous=%s): #blue=%d, #red=%d, Status %s", self:GetZoneName(), self:GetCoalitionName(), UTILS.GetCoalitionName(self:GetPreviousCoalition()), nBlue, nRed, State) + local NewState = self:GetState() + if NewState~=State then + text=text..string.format(" --> %s", NewState) + end + self:I(text) + end + + end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + + --- Update Mark on F10 map. + -- @param #ZONE_CAPTURE_COALITION self + function ZONE_CAPTURE_COALITION:Mark() + + if self.MarkOn then + + local Coord = self:GetCoordinate() + local ZoneName = self:GetZoneName() + local State = self:GetState() + + -- Remove marks. + if self.MarkRed then + Coord:RemoveMark(self.MarkRed) + end + if self.MarkBlue then + Coord:RemoveMark(self.MarkBlue) + end + + -- Create new marks for each coaliton. + if self.Coalition == coalition.side.BLUE then + self.MarkBlue = Coord:MarkToCoalitionBlue( "Coalition: Blue\nGuard Zone: " .. ZoneName .. "\nStatus: " .. State ) + self.MarkRed = Coord:MarkToCoalitionRed( "Coalition: Blue\nCapture Zone: " .. ZoneName .. "\nStatus: " .. State ) + elseif self.Coalition == coalition.side.RED then + self.MarkRed = Coord:MarkToCoalitionRed( "Coalition: Red\nGuard Zone: " .. ZoneName .. "\nStatus: " .. State ) + self.MarkBlue = Coord:MarkToCoalitionBlue( "Coalition: Red\nCapture Zone: " .. ZoneName .. "\nStatus: " .. State ) + else + self.MarkRed = Coord:MarkToCoalitionRed( "Coalition: Neutral\nCapture Zone: " .. ZoneName .. "\nStatus: " .. State ) + self.MarkBlue = Coord:MarkToCoalitionBlue( "Coalition: Neutral\nCapture Zone: " .. ZoneName .. "\nStatus: " .. State ) + end + + end + + end + +end +--- **Functional** - Control artillery units. +-- +-- === +-- +-- The ARTY class can be used to easily assign and manage targets for artillery units using an advanced queueing system. +-- +-- ## Features: +-- +-- * Multiple targets can be assigned. No restriction on number of targets. +-- * Targets can be given a priority. Engagement of targets is executed a according to their priority. +-- * Engagements can be scheduled, i.e. will be executed at a certain time of the day. +-- * Multiple relocations of the group can be assigned and scheduled via queueing system. +-- * Special weapon types can be selected for each attack, e.g. cruise missiles for Naval units. +-- * Automatic rearming once the artillery is out of ammo (optional). +-- * Automatic relocation after each firing engagement to prevent counter strikes (optional). +-- * Automatic relocation movements to get the battery within firing range (optional). +-- * Simulation of tactical nuclear shells as well as illumination and smoke shells. +-- * New targets can be added during the mission, e.g. when they are detected by recon units. +-- * Targets and relocations can be assigned by placing markers on the F10 map. +-- * Finite state machine implementation. Mission designer can interact when certain events occur. +-- +-- ==== +-- +-- ## [MOOSE YouTube Channel](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg) +-- +-- === +-- +-- ### Author: **[funkyfranky](https://forums.eagle.ru/member.php?u=115026)** +-- +-- ### Contributions: [FlightControl](https://forums.eagle.ru/member.php?u=89536) +-- +-- ==== +-- @module Functional.Arty +-- @image Artillery.JPG + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- ARTY class +-- @type ARTY +-- @field #string ClassName Name of the class. +-- @field #string lid Log id for DCS.log file. +-- @field #boolean Debug Write Debug messages to DCS log file and send Debug messages to all players. +-- @field #table targets All targets assigned. +-- @field #table moves All moves assigned. +-- @field #ARTY.Target currentTarget Holds the current target, if there is one assigned. +-- @field #table currentMove Holds the current commanded move, if there is one assigned. +-- @field #number Nammo0 Initial amount total ammunition (shells+rockets+missiles) of the whole group. +-- @field #number Nshells0 Initial amount of shells of the whole group. +-- @field #number Nrockets0 Initial amount of rockets of the whole group. +-- @field #number Nmissiles0 Initial amount of missiles of the whole group. +-- @field #number Nukes0 Initial amount of tactical nukes of the whole group. Default is 0. +-- @field #number Nillu0 Initial amount of illumination shells of the whole group. Default is 0. +-- @field #number Nsmoke0 Initial amount of smoke shells of the whole group. Default is 0. +-- @field #number StatusInterval Update interval in seconds between status updates. Default 10 seconds. +-- @field #number WaitForShotTime Max time in seconds to wait until fist shot event occurs after target is assigned. If time is passed without shot, the target is deleted. Default is 300 seconds. +-- @field #table DCSdesc DCS descriptors of the ARTY group. +-- @field #string Type Type of the ARTY group. +-- @field #string DisplayName Extended type name of the ARTY group. +-- @field #number IniGroupStrength Inital number of units in the ARTY group. +-- @field #boolean IsArtillery If true, ARTY group has attribute "Artillery". This is automatically derived from the DCS descriptor table. +-- @field #boolean ismobile If true, ARTY group can move. +-- @field #boolean iscargo If true, ARTY group is defined as possible cargo. If it is immobile, targets out of range are not deleted from the queue. +-- @field Cargo.CargoGroup#CARGO_GROUP cargogroup Cargo group object if ARTY group is a cargo that will be transported to another place. +-- @field #string groupname Name of the ARTY group as defined in the mission editor. +-- @field #string alias Name of the ARTY group. +-- @field #table clusters Table of names of clusters the group belongs to. Can be used to address all groups within the cluster simultaniously. +-- @field #number SpeedMax Maximum speed of ARTY group in km/h. This is determined from the DCS descriptor table. +-- @field #number Speed Default speed in km/h the ARTY group moves at. Maximum speed possible is 80% of maximum speed the group can do. +-- @field #number RearmingDistance Safe distance in meters between ARTY group and rearming group or place at which rearming is possible. Default 100 m. +-- @field Wrapper.Group#GROUP RearmingGroup Unit designated to rearm the ARTY group. +-- @field #number RearmingGroupSpeed Speed in km/h the rearming unit moves at. Default is 50% of the max speed possible of the group. +-- @field #boolean RearmingGroupOnRoad If true, rearming group will move to ARTY group or rearming place using mainly roads. Default false. +-- @field Core.Point#COORDINATE RearmingGroupCoord Initial coordinates of the rearming unit. After rearming complete, the unit will return to this position. +-- @field Core.Point#COORDINATE RearmingPlaceCoord Coordinates of the rearming place. If the place is more than 100 m away from the ARTY group, the group will go there. +-- @field #boolean RearmingArtyOnRoad If true, ARTY group will move to rearming place using mainly roads. Default false. +-- @field Core.Point#COORDINATE InitialCoord Initial coordinates of the ARTY group. +-- @field #boolean report Arty group sends messages about their current state or target to its coaliton. +-- @field #table ammoshells Table holding names of the shell types which are included when counting the ammo. Default is {"weapons.shells"} which include most shells. +-- @field #table ammorockets Table holding names of the rocket types which are included when counting the ammo. Default is {"weapons.nurs"} which includes most unguided rockets. +-- @field #table ammomissiles Table holding names of the missile types which are included when counting the ammo. Default is {"weapons.missiles"} which includes some guided missiles. +-- @field #number Nshots Number of shots fired on current target. +-- @field #number minrange Minimum firing range in kilometers. Targets closer than this distance are not engaged. Default 0.1 km. +-- @field #number maxrange Maximum firing range in kilometers. Targets further away than this distance are not engaged. Default 10000 km. +-- @field #number nukewarhead Explosion strength of tactical nuclear warhead in kg TNT. Default 75000. +-- @field #number Nukes Number of nuclear shells, the group has available. Note that if normal shells are empty, firing nukes is also not possible any more. +-- @field #number Nillu Number of illumination shells the group has available. Note that if normal shells are empty, firing illumination shells is also not possible any more. +-- @field #number illuPower Power of illumination warhead in mega candela. Default 1 mcd. +-- @field #number illuMinalt Minimum altitude in meters the illumination warhead will detonate. +-- @field #number illuMaxalt Maximum altitude in meters the illumination warhead will detonate. +-- @field #number Nsmoke Number of smoke shells the group has available. Note that if normal shells are empty, firing smoke shells is also not possible any more. +-- @field Utilities.Utils#SMOKECOLOR Smoke color of smoke shells. Default SMOKECOLOR.red. +-- @field #number nukerange Demolition range of tactical nuclear explostions. +-- @field #boolean nukefire Ignite additional fires and smoke for nuclear explosions Default true. +-- @field #number nukefires Number of nuclear fires and subexplosions. +-- @field #boolean relocateafterfire Group will relocate after each firing task. Default false. +-- @field #number relocateRmin Minimum distance in meters the group will look for places to relocate. +-- @field #number relocateRmax Maximum distance in meters the group will look for places to relocate. +-- @field #boolean markallow If true, Players are allowed to assign targets and moves for ARTY group by placing markers on the F10 map. Default is false. +-- @field #number markkey Authorization key. Only player who know this key can assign targets and moves via markers on the F10 map. Default no authorization required. +-- @field #boolean markreadonly Marks for targets are readonly and cannot be removed by players. Default is false. +-- @field #boolean autorelocate ARTY group will automatically move to within the max/min firing range. +-- @field #number autorelocatemaxdist Max distance [m] the ARTY group will travel to get within firing range. Default 50000 m = 50 km. +-- @field #boolean autorelocateonroad ARTY group will use mainly road to automatically get within firing range. Default is false. +-- @field #number coalition The coalition of the arty group. +-- @field #boolean respawnafterdeath Respawn arty group after all units are dead. +-- @field #number respawndelay Respawn delay in seconds. +-- @extends Core.Fsm#FSM_CONTROLLABLE + +--- Enables mission designers easily to assign targets for artillery units. Since the implementation is based on a Finite State Model (FSM), the mission designer can +-- interact with the process at certain events or states. +-- +-- A new ARTY object can be created with the @{#ARTY.New}(*group*) contructor. +-- The parameter *group* has to be a MOOSE Group object and defines ARTY group. +-- +-- The ARTY FSM process can be started by the @{#ARTY.Start}() command. +-- +-- ## The ARTY Process +-- +-- ![Process](..\Presentations\ARTY\ARTY_Process.png) +-- +-- ### Blue Branch +-- After the FMS process is started the ARTY group will be in the state **CombatReady**. Once a target is assigned the **OpenFire** event will be triggered and the group starts +-- firing. At this point the group in in the state **Firing**. +-- When the defined number of shots has been fired on the current target the event **CeaseFire** is triggered. The group will stop firing and go back to the state **CombatReady**. +-- If another target is defined (or multiple engagements of the same target), the cycle starts anew. +-- +-- ### Violet Branch +-- When the ARTY group runs out of ammunition, the event **Winchester** is triggered and the group enters the state **OutOfAmmo**. +-- In this state, the group is unable to engage further targets. +-- +-- ### Red Branch +-- With the @{#ARTY.SetRearmingGroup}(*group*) command, a special group can be defined to rearm the ARTY group. If this unit has been assigned and the group has entered the state +-- **OutOfAmmo** the event **Rearm** is triggered followed by a transition to the state **Rearming**. +-- If the rearming group is less than 100 meters away from the ARTY group, the rearming process starts. If the rearming group is more than 100 meters away from the ARTY unit, the +-- rearming group is routed to a point 20 to 100 m from the ARTY group. +-- +-- Once the rearming is complete, the **Rearmed** event is triggered and the group enters the state **CombatReady**. At this point targeted can be engaged again. +-- +-- ### Green Branch +-- The ARTY group can be ordered to change its position via the @{#ARTY.AssignMoveCoord}() function as described below. When the group receives the command to move +-- the event **Move** is triggered and the state changes to **Moving**. When the unit arrives to its destination the event **Arrived** is triggered and the group +-- becomes **CombatReady** again. +-- +-- Note, that the ARTY group will not open fire while it is in state **Moving**. This property differentiates artillery from tanks. +-- +-- ### Yellow Branch +-- When a new target is assigned via the @{#ARTY.AssignTargetCoord}() function (see below), the **NewTarget** event is triggered. +-- +-- ## Assigning Targets +-- Assigning targets is a central point of the ARTY class. Multiple targets can be assigned simultanioulsly and are put into a queue. +-- Of course, targets can be added at any time during the mission. For example, once they are detected by a reconnaissance unit. +-- +-- In order to add a target, the function @{#ARTY.AssignTargetCoord}(*coord*, *prio*, *radius*, *nshells*, *maxengage*, *time*, *weapontype*, *name*) has to be used. +-- Only the first parameter *coord* is mandatory while all remaining parameters are all optional. +-- +-- ### Parameters: +-- +-- * *coord*: Coordinates of the target, given as @{Core.Point#COORDINATE} object. +-- * *prio*: Priority of the target. This a number between 1 (high prio) and 100 (low prio). Targets with higher priority are engaged before targets with lower priority. +-- * *radius*: Radius in meters which defines the area the ARTY group will attempt to be hitting. Default is 100 meters. +-- * *nshells*: Number of shots (shells, rockets, missiles) fired by the group at each engagement of a target. Default is 5. +-- * *maxengage*: Number of times a target is engaged. +-- * *time*: Time of day the engagement is schedule in the format "hh:mm:ss" for hh=hours, mm=minutes, ss=seconds. +-- For example "10:15:35". In the case the attack will be executed at a quarter past ten in the morning at the day the mission started. +-- If the engagement should start on the following day the format can be specified as "10:15:35+1", where the +1 denots the following day. +-- This is useful for longer running missions or if the mission starts at 23:00 hours and the attack should be scheduled at 01:00 hours on the following day. +-- Of course, later days are also possible by appending "+2", "+3", etc. +-- **Note** that the time has to be given as a string. So the enclosing quotation marks "" are important. +-- * *weapontype*: Specified the weapon type that should be used for this attack if the ARTY group has multiple weapons to engage the target. +-- For example, this is useful for naval units which carry a bigger arsenal (cannons and missiles). Default is Auto, i.e. DCS logic selects the appropriate weapon type. +-- *name*: A special name can be defined for this target. Default name are the coordinates of the target in LL DMS format. If a name is already given for another target +-- or the same target should be attacked two or more times with different parameters a suffix "#01", "#02", "#03" is automatically appended to the specified name. +-- +-- ## Target Queue +-- In case multiple targets have been defined, it is important to understand how the target queue works. +-- +-- Here, the essential parameters are the priority *prio*, the number of engagements *maxengage* and the scheduled *time* as described above. +-- +-- For example, we have assigned two targets one with *prio*=10 and the other with *prio*=50 and both targets should be engaged three times (*maxengage*=3). +-- Let's first consider the case that none of the targets is scheduled to be executed at a certain time (*time*=nil). +-- The ARTY group will first engage the target with higher priority (*prio*=10). After the engagement is finished, the target with lower priority is attacked. +-- This is because the target with lower prio has been attacked one time less. After the attack on the lower priority task is finished and both targets +-- have been engaged equally often, the target with the higher priority is engaged again. This coninues until a target has engaged three times. +-- Once the maximum number of engagements is reached, the target is deleted from the queue. +-- +-- In other words, the queue is first sorted with respect to the number of engagements and targets with the same number of engagements are sorted with +-- respect to their priority. +-- +-- ### Timed Engagements +-- +-- As mentioned above, targets can be engaged at a specific time of the day via the *time* parameter. +-- +-- If the *time* parameter is specified for a target, the first engagement of that target will happen at that time of the day and not before. +-- This also applies when multiple engagements are requested via the *maxengage* parameter. The first attack will not happen before the specifed time. +-- When that timed attack is finished, the *time* parameter is deleted and the remaining engagements are carried out in the same manner as for untimed targets (described above). +-- +-- Of course, it can happen that a scheduled task should be executed at a time, when another target is already under attack. +-- If the priority of the target is higher than the priority of the current target, then the current attack is cancelled and the engagement of the target with the higher +-- priority is started. +-- +-- By contrast, if the current target has a higher priority than the target scheduled at that time, the current attack is finished before the scheduled attack is started. +-- +-- ## Determining the Amount of Ammo +-- +-- In order to determin when a unit is out of ammo and possible initiate the rearming process it is necessary to know which types of weapons have to be counted. +-- For most artillery unit types, this is simple because they only have one type of weapon and hence ammunition. +-- +-- However, there are more complex scenarios. For example, naval units carry a big arsenal of different ammunition types ranging from various cannon shell types +-- over surface-to-air missiles to cruise missiles. Obviously, not all of these ammo types can be employed for artillery tasks. +-- +-- Unfortunately, there is no easy way to count only those ammo types useable as artillery. Therefore, to keep the implementation general the user +-- can specify the names of the ammo types by the following functions: +-- +-- * @{#ARTY.SetShellTypes}(*tableofnames*): Defines the ammo types for unguided cannons, e.g. *tableofnames*={"weapons.shells"}, i.e. **all** types of shells are counted. +-- * @{#ARTY.SetRocketTypes}(*tableofnames*): Defines the ammo types of unguided rockets, e.g. *tableofnames*={"weapons.nurs"}, i.e. **all** types of rockets are counted. +-- * @{#ARTY.SetMissileTypes}(*tableofnames*): Defines the ammo types of guided missiles, e.g. is *tableofnames*={"weapons.missiles"}, i.e. **all** types of missiles are counted. +-- +-- **Note** that the default parameters "weapons.shells", "weapons.nurs", "weapons.missiles" **should in priciple** capture all the corresponding ammo types. +-- However, the logic searches for the string "weapon.missies" in the ammo type. Especially for missiles, this string is often not contained in the ammo type descriptor. +-- +-- One way to determin which types of ammo the unit carries, one can use the debug mode of the arty class via @{#ARTY.SetDebugON}(). +-- In debug mode, the all ammo types of the group are printed to the monitor as message and can be found in the DCS.log file. +-- +-- ## Employing Selected Weapons +-- +-- If an ARTY group carries multiple weapons, which can be used for artillery task, a certain weapon type can be selected to attack the target. +-- This is done via the *weapontype* parameter of the @{#ARTY.AssignTargetCoord}(..., *weapontype*, ...) function. +-- +-- The enumerator @{#ARTY.WeaponType} has been defined to select a certain weapon type. Supported values are: +-- +-- * @{#ARTY.WeaponType}.Auto: Automatic weapon selection by the DCS logic. This is the default setting. +-- * @{#ARTY.WeaponType}.Cannon: Only cannons are used during the attack. Corresponding ammo type are shells and can be defined by @{#ARTY.SetShellTypes}. +-- * @{#ARTY.WeaponType}.Rockets: Only unguided are used during the attack. Corresponding ammo type are rockets/nurs and can be defined by @{#ARTY.SetRocketTypes}. +-- * @{#ARTY.WeaponType}.CruiseMissile: Only cruise missiles are used during the attack. Corresponding ammo type are missiles and can be defined by @{#ARTY.SetMissileTypes}. +-- * @{#ARTY.WeaponType}.TacticalNukes: Use tactical nuclear shells. This works only with units that have shells and is described below. +-- * @{#ARTY.WeaponType}.IlluminationShells: Use illumination shells. This works only with units that have shells and is described below. +-- * @{#ARTY.WeaponType}.SmokeShells: Use smoke shells. This works only with units that have shells and is described below. +-- +-- ## Assigning Relocation Movements +-- The ARTY group can be commanded to move. This is done by the @{#ARTY.AssignMoveCoord}(*coord*, *time*, *speed*, *onroad*, *cancel*, *name*) function. +-- With this multiple timed moves of the group can be scheduled easily. By default, these moves will only be executed if the group is state **CombatReady**. +-- +-- ### Parameters +-- +-- * *coord*: Coordinates where the group should move to given as @{Core.Point#COORDINATE} object. +-- * *time*: The time when the move should be executed. This has to be given as a string in the format "hh:mm:ss" (hh=hours, mm=minutes, ss=seconds). +-- * *speed*: Speed of the group in km/h. +-- * *onroad*: If this parameter is set to true, the group uses mainly roads to get to the commanded coordinates. +-- * *cancel*: If set to true, any current engagement of targets is cancelled at the time the move should be executed. +-- * *name*: Can be used to set a user defined name of the move. By default the name is created from the LL DMS coordinates. +-- +-- ## Automatic Rearming +-- +-- If an ARTY group runs out of ammunition, it can be rearmed automatically. +-- +-- ### Rearming Group +-- The first way to activate the automatic rearming is to define a rearming group with the function @{#ARTY.SetRearmingGroup}(*group*). For the blue side, this +-- could be a M181 transport truck and for the red side an Ural-375 truck. +-- +-- Once the ARTY group is out of ammo and the **Rearm** event is triggered, the defined rearming truck will drive to the ARTY group. +-- So the rearming truck does not have to be placed nearby the artillery group. When the rearming is complete, the rearming truck will drive back to its original position. +-- +-- ### Rearming Place +-- The second alternative is to define a rearming place, e.g. a FRAP, airport or any other warehouse. This is done with the function @{#ARTY.SetRearmingPlace}(*coord*). +-- The parameter *coord* specifies the coordinate of the rearming place which should not be further away then 100 meters from the warehouse. +-- +-- When the **Rearm** event is triggered, the ARTY group will move to the rearming place. Of course, the group must be mobil. So for a mortar this rearming procedure would not work. +-- +-- After the rearming is complete, the ARTY group will move back to its original position and resume normal operations. +-- +-- ### Rearming Group **and** Rearming Place +-- If both a rearming group *and* a rearming place are specified like described above, both the ARTY group and the rearming truck will move to the rearming place and meet there. +-- +-- After the rearming is complete, both groups will move back to their original positions. +-- +-- ## Simulated Weapons +-- +-- In addtion to the standard weapons a group has available some special weapon types that are not possible to use in the native DCS environment are simulated. +-- +-- ### Tactical Nukes +-- +-- ARTY groups that can fire shells can also be used to fire tactical nukes. This is achieved by setting the weapon type to **ARTY.WeaponType.TacticalNukes** in the +-- @{#ARTY.AssignTargetCoord}() function. +-- +-- By default, they group does not have any nukes available. To give the group the ability the function @{#ARTY.SetTacNukeShells}(*n*) can be used. +-- This supplies the group with *n* nuclear shells, where *n* is restricted to the number of conventional shells the group can carry. +-- Note that the group must always have convenctional shells left in order to fire a nuclear shell. +-- +-- The default explostion strength is 0.075 kilo tons TNT. The can be changed with the @{#ARTY.SetTacNukeWarhead}(*strength*), where *strength* is given in kilo tons TNT. +-- +-- ### Illumination Shells +-- +-- ARTY groups that possess shells can fire shells with illumination bombs. First, the group needs to be equipped with this weapon. This is done by the +-- function @{ARTY.SetIlluminationShells}(*n*, *power*), where *n* is the number of shells the group has available and *power* the illumination power in mega candela (mcd). +-- +-- In order to execute an engagement with illumination shells one has to use the weapon type *ARTY.WeaponType.IlluminationShells* in the +-- @{#ARTY.AssignTargetCoord}() function. +-- +-- In the simulation, the explosive shell that is fired is destroyed once it gets close to the target point but before it can actually impact. +-- At this position an illumination bomb is triggered at a random altitude between 500 and 1000 meters. This interval can be set by the function +-- @{ARTY.SetIlluminationMinMaxAlt}(*minalt*, *maxalt*). +-- +-- ### Smoke Shells +-- +-- In a similar way to illumination shells, ARTY groups can also employ smoke shells. The numer of smoke shells the group has available is set by the function +-- @{#ARTY.SetSmokeShells}(*n*, *color*), where *n* is the number of shells and *color* defines the smoke color. Default is SMOKECOLOR.Red. +-- +-- The weapon type to be used in the @{#ARTY.AssignTargetCoord}() function is *ARTY.WeaponType.SmokeShells*. +-- +-- The explosive shell the group fired is destroyed shortly before its impact on the ground and smoke of the speficied color is triggered at that position. +-- +-- +-- ## Assignments via Markers on F10 Map +-- +-- Targets and relocations can be assigned by players via placing a mark on the F10 map. The marker text must contain certain keywords. +-- +-- This feature can be turned on with the @{#ARTY.SetMarkAssignmentsOn}(*key*, *readonly*). The parameter *key* is optional. When set, it can be used as PIN, i.e. only +-- players who know the correct key are able to assign and cancel targets or relocations. Default behavior is that all players belonging to the same coalition as the +-- ARTY group are able to assign targets and moves without a key. +-- +-- ### Target Assignments +-- A new target can be assigned by writing **arty engage** in the marker text. +-- This is followed by a **comma separated list** of (optional) keywords and parameters. +-- First, it is important to address the ARTY group or groups that should engage. This can be done in numrous ways. The keywords are *battery*, *alias*, *cluster*. +-- It is also possible to address all ARTY groups by the keyword *everyone* or *allbatteries*. These two can be used synonymously. +-- **Note that**, if no battery is assigned nothing will happen. +-- +-- * *everyone* or *allbatteries* The target is assigned to all batteries. +-- * *battery* Name of the ARTY group that the target is assigned to. Note that **the name is case sensitive** and has to be given in quotation marks. Default is all ARTY groups of the right coalition. +-- * *alias* Alias of the ARTY group that the target is assigned to. The alias is **case sensitive** and needs to be in quotation marks. +-- * *cluster* The cluster of ARTY groups that is addessed. Clusters can be defined by the function @{#ARTY.AddToCluster}(*clusters*). Names are **case sensitive** and need to be in quotation marks. +-- * *key* A number to authorize the target assignment. Only specifing the correct number will trigger an engagement. +-- * *time* Time for which which the engagement is schedules, e.g. 08:42. Default is as soon as possible. +-- * *prio* Priority of the engagement as number between 1 (high prio) and 100 (low prio). Default is 50, i.e. medium priority. +-- * *shots* Number of shots (shells, rockets or missiles) fired at each engagement. Default is 5. +-- * *maxengage* Number of times the target is engaged. Default is 1. +-- * *radius* Scattering radius of the fired shots in meters. Default is 100 m. +-- * *weapon* Type of weapon to be used. Valid parameters are *cannon*, *rocket*, *missile*, *nuke*. Default is automatic selection. +-- * *lldms* Specify the coordinates in Lat/Long degrees, minutes and seconds format. The actual location of the marker is unimportant here. The group will engage the coordinates given in the lldms keyword. +-- Format is DD:MM:SS[N,S] DD:MM:SS[W,E]. See example below. This can be useful when coordinates in this format are obtained from elsewhere. +-- * *readonly* The marker is readonly and cannot be deleted by users. Hence, assignment cannot be cancelled by removing the marker. +-- +-- Here are examples of valid marker texts: +-- arty engage, battery "Blue Paladin Alpha" +-- arty engage, everyone +-- arty engage, allbatteries +-- arty engage, alias "Bob", weapon missiles +-- arty engage, cluster "All Mortas" +-- arty engage, cluster "Northern Batteries" "Southern Batteries" +-- arty engage, cluster "Northern Batteries", cluster "Southern Batteries" +-- arty engage, cluster "Horwitzers", shots 20, prio 10, time 08:15, weapon cannons +-- arty engage, battery "Blue Paladin 1" "Blue MRLS 1", shots 10, time 10:15 +-- arty engage, battery "Blue MRLS 1", key 666 +-- arty engage, battery "Paladin Alpha", weapon nukes, shots 1, time 20:15 +-- arty engage, battery "Horwitzer 1", lldms 41:51:00N 41:47:58E +-- +-- Note that the keywords and parameters are *case insensitve*. Only exception are the battery, alias and cluster names. +-- These must be exactly the same as the names of the goups defined in the mission editor or the aliases and cluster names defined in the script. +-- +-- ### Relocation Assignments +-- +-- Markers can also be used to relocate the group with the keyphrase **arty move**. This is done in a similar way as assigning targets. Here, the (optional) keywords and parameters are: +-- +-- * *time* Time for which which the relocation/move is schedules, e.g. 08:42. Default is as soon as possible. +-- * *speed* The speed in km/h the group will drive at. Default is 70% of its max possible speed. +-- * *on road* Group will use mainly roads. Default is off, i.e. it will go in a straight line from its current position to the assigned coordinate. +-- * *canceltarget* Group will cancel all running firing engagements and immidiately start to move. Default is that group will wait until is current assignment is over. +-- * *battery* Name of the ARTY group that the relocation is assigned to. +-- * *alias* Alias of the ARTY group that the target is assigned to. The alias is **case sensitive** and needs to be in quotation marks. +-- * *cluster* The cluster of ARTY groups that is addessed. Clusters can be defined by the function @{#ARTY.AddToCluster}(*clusters*). Names are **case sensitive** and need to be in quotation marks. +-- * *key* A number to authorize the target assignment. Only specifing the correct number will trigger an engagement. +-- * *lldms* Specify the coordinates in Lat/Long degrees, minutes and seconds format. The actual location of the marker is unimportant. The group will move to the coordinates given in the lldms keyword. +-- Format is DD:MM:SS[N,S] DD:MM:SS[W,E]. See example below. +-- * *readonly* Marker cannot be deleted by users any more. Hence, assignment cannot be cancelled by removing the marker. +-- +-- Here are some examples: +-- arty move, battery "Blue Paladin" +-- arty move, battery "Blue MRLS", canceltarget, speed 10, on road +-- arty move, cluster "mobile", lldms 41:51:00N 41:47:58E +-- arty move, alias "Bob", weapon missiles +-- arty move, cluster "All Howitzer" +-- arty move, cluster "Northern Batteries" "Southern Batteries" +-- arty move, cluster "Northern Batteries", cluster "Southern Batteries" +-- arty move, everyone +-- +-- ### Requests +-- +-- Marks can also be to send requests to the ARTY group. This is done by the keyword **arty request**, which can have the keywords +-- +-- * *target* All assigned targets are reported. +-- * *move* All assigned relocation moves are reported. +-- * *ammo* Current ammunition status is reported. +-- +-- For example +-- arty request, everyone, ammo +-- arty request, battery "Paladin Bravo", targets +-- arty request, cluster "All Mortars", move +-- +-- The actual location of the marker is irrelevant for these requests. +-- +-- ### Cancel +-- +-- Current actions can be cancelled by the keyword **arty cancel**. Actions that can be cancelled are current engagements, relocations and rearming assignments. +-- +-- For example +-- arty cancel, target, battery "Paladin Bravo" +-- arty cancel, everyone, move +-- arty cancel, rearming, battery "MRLS Charly" +-- +-- ### Settings +-- +-- A few options can be set by marks. The corresponding keyword is **arty set**. This can be used to define the rearming place and group for a battery. +-- +-- To set the reamring place of a group at the marker position type +-- arty set, battery "Paladin Alpha", rearming place +-- +-- Setting the rearming group is independent of the position of the mark. Just create one anywhere on the map and type +-- arty set, battery "Mortar Bravo", rearming group "Ammo Truck M818" +-- Note that the name of the rearming group has to be given in quotation marks and spellt exactly as the group name defined in the mission editor. +-- +-- ## Transporting +-- +-- ARTY groups can be transported to another location as @{Cargo.Cargo} by means of classes such as @{AI.AI_Cargo_APC}, @{AI.AI_Cargo_Dispatcher_APC}, +-- @{AI.AI_Cargo_Helicopter}, @{AI.AI_Cargo_Dispatcher_Helicopter} or @{AI.AI_Cargo_Airplane}. +-- +-- In order to do this, one needs to define an ARTY object via the @{#ARTY.NewFromCargoGroup}(*cargogroup*, *alias*) function. +-- The first argument *cargogroup* has to be a @{Cargo.CargoGroup#CARGO_GROUP} object. The second argument *alias* is a string which can be freely chosen by the user. +-- +-- ## Fine Tuning +-- +-- The mission designer has a few options to tailor the ARTY object according to his needs. +-- +-- * @{#ARTY.SetAutoRelocateToFiringRange}(*maxdist*, *onroad*) lets the ARTY group automatically move to within firing range if a current target is outside the min/max firing range. The +-- optional parameter *maxdist* is the maximum distance im km the group will move. If the distance is greater no relocation is performed. Default is 50 km. +-- * @{#ARTY.SetAutoRelocateAfterEngagement}(*rmax*, *rmin*) will cause the ARTY group to change its position after each firing assignment. +-- Optional parameters *rmax*, *rmin* define the max/min distance for relocation of the group. Default distance is randomly between 300 and 800 m. +-- * @{#ARTY.AddToCluster}(*clusters*) Can be used to add the ARTY group to one or more clusters. All groups in a cluster can be addressed simultaniously with one marker command. +-- * @{#ARTY.SetSpeed}(*speed*) sets the speed in km/h the group moves at if not explicitly stated otherwise. +-- * @{#ARTY.RemoveAllTargets}() removes all targets from the target queue. +-- * @{#ARTY.RemoveTarget}(*name*) deletes the target with *name* from the target queue. +-- * @{#ARTY.SetMaxFiringRange}(*range*) defines the maximum firing range. Targets further away than this distance are not engaged. +-- * @{#ARTY.SetMinFiringRange}(*range*) defines the minimum firing range. Targets closer than this distance are not engaged. +-- * @{#ARTY.SetRearmingGroup}(*group*) sets the group responsible for rearming of the ARTY group once it is out of ammo. +-- * @{#ARTY.SetReportON}() and @{#ARTY.SetReportOFF}() can be used to enable/disable status reports of the ARTY group send to all coalition members. +-- * @{#ARTY.SetWaitForShotTime}(*waittime*) sets the time after which a target is deleted from the queue if no shooting event occured after the target engagement started. +-- Default is 300 seconds. Note that this can for example happen, when the assigned target is out of range. +-- * @{#ARTY.SetDebugON}() and @{#ARTY.SetDebugOFF}() can be used to enable/disable the debug mode. +-- +-- ## Examples +-- +-- ### Assigning Multiple Targets +-- This basic example illustrates how to assign multiple targets and defining a rearming group. +-- -- Creat a new ARTY object from a Paladin group. +-- paladin=ARTY:New(GROUP:FindByName("Blue Paladin")) +-- +-- -- Define a rearming group. This is a Transport M818 truck. +-- paladin:SetRearmingGroup(GROUP:FindByName("Blue Ammo Truck")) +-- +-- -- Set the max firing range. A Paladin unit has a range of 20 km. +-- paladin:SetMaxFiringRange(20) +-- +-- -- Low priorty (90) target, will be engage last. Target is engaged two times. At each engagement five shots are fired. +-- paladin:AssignTargetCoord(GROUP:FindByName("Red Targets 3"):GetCoordinate(), 90, nil, 5, 2) +-- -- Medium priorty (nil=50) target, will be engage second. Target is engaged two times. At each engagement ten shots are fired. +-- paladin:AssignTargetCoord(GROUP:FindByName("Red Targets 1"):GetCoordinate(), nil, nil, 10, 2) +-- -- High priorty (10) target, will be engage first. Target is engaged three times. At each engagement twenty shots are fired. +-- paladin:AssignTargetCoord(GROUP:FindByName("Red Targets 2"):GetCoordinate(), 10, nil, 20, 3) +-- +-- -- Start ARTY process. +-- paladin:Start() +-- **Note** +-- +-- * If a parameter should be set to its default value, it has to be set to *nil* if other non-default parameters follow. Parameters at the end can simply be skiped. +-- * In this example, the target coordinates are taken from groups placed in the mission edit using the COORDINATE:GetCoordinate() function. +-- +-- ### Scheduled Engagements +-- -- Mission starts at 8 o'clock. +-- -- Assign two scheduled targets. +-- +-- -- Create ARTY object from Paladin group. +-- paladin=ARTY:New(GROUP:FindByName("Blue Paladin")) +-- +-- -- Assign target coordinates. Priority=50 (medium), radius=100 m, use 5 shells per engagement, engage 1 time at two past 8 o'clock. +-- paladin:AssignTargetCoord(GROUP:FindByName("Red Targets 1"):GetCoordinate(), 50, 100, 5, 1, "08:02:00", ARTY.WeaponType.Auto, "Target 1") +-- +-- -- Assign target coordinates. Priority=10 (high), radius=300 m, use 10 shells per engagement, engage 1 time at seven past 8 o'clock. +-- paladin:AssignTargetCoord(GROUP:FindByName("Red Targets 2"):GetCoordinate(), 10, 300, 10, 1, "08:07:00", ARTY.WeaponType.Auto, "Target 2") +-- +-- -- Start ARTY process. +-- paladin:Start() +-- +-- ### Specific Weapons +-- This example demonstrates how to use specific weapons during an engagement. +-- -- Define the Normandy as ARTY object. +-- normandy=ARTY:New(GROUP:FindByName("Normandy")) +-- +-- -- Add target: prio=50, radius=300 m, number of missiles=20, number of engagements=1, start time=08:05 hours, only use cruise missiles for this attack. +-- normandy:AssignTargetCoord(GROUP:FindByName("Red Targets 1"):GetCoordinate(), 20, 300, 50, 1, "08:01:00", ARTY.WeaponType.CruiseMissile) +-- +-- -- Add target: prio=50, radius=300 m, number of shells=100, number of engagements=1, start time=08:15 hours, only use cannons during this attack. +-- normandy:AssignTargetCoord(GROUP:FindByName("Red Targets 1"):GetCoordinate(), 50, 300, 100, 1, "08:15:00", ARTY.WeaponType.Cannon) +-- +-- -- Define shells that are counted to check whether the ship is out of ammo. +-- -- Note that this is necessary because the Normandy has a lot of other shell type weapons which cannot be used to engage ground targets in an artillery style manner. +-- normandy:SetShellTypes({"MK45_127"}) +-- +-- -- Define missile types that are counted. +-- normandy:SetMissileTypes({"BGM"}) +-- +-- -- Start ARTY process. +-- normandy:Start() +-- +-- ### Transportation as Cargo +-- This example demonstates how an ARTY group can be transported to another location as cargo. +-- -- Define a group as CARGO_GROUP +-- CargoGroupMortars=CARGO_GROUP:New(GROUP:FindByName("Mortars"), "Mortars", "Mortar Platoon Alpha", 100 , 10) +-- +-- -- Define the mortar CARGO GROUP as ARTY object +-- mortars=ARTY:NewFromCargoGroup(CargoGroupMortars, "Mortar Platoon Alpha") +-- +-- -- Start ARTY process +-- mortars:Start() +-- +-- -- Setup AI cargo dispatcher for e.g. helos +-- SetHeloCarriers = SET_GROUP:New():FilterPrefixes("CH-47D"):FilterStart() +-- SetCargoMortars = SET_CARGO:New():FilterTypes("Mortars"):FilterStart() +-- SetZoneDepoly = SET_ZONE:New():FilterPrefixes("Deploy"):FilterStart() +-- CargoHelo=AI_CARGO_DISPATCHER_HELICOPTER:New(SetHeloCarriers, SetCargoMortars, SetZoneDepoly) +-- CargoHelo:Start() +-- The ARTY group will be transported and resume its normal operation after it has been deployed. New targets can be assigned at any time also during the transportation process. +-- +-- @field #ARTY +ARTY={ + ClassName="ARTY", + lid=nil, + Debug=false, + targets={}, + moves={}, + currentTarget=nil, + currentMove=nil, + Nammo0=0, + Nshells0=0, + Nrockets0=0, + Nmissiles0=0, + Nukes0=0, + Nillu0=0, + Nsmoke0=0, + StatusInterval=10, + WaitForShotTime=300, + DCSdesc=nil, + Type=nil, + DisplayName=nil, + groupname=nil, + alias=nil, + clusters={}, + ismobile=true, + iscargo=false, + cargogroup=nil, + IniGroupStrength=0, + IsArtillery=nil, + RearmingDistance=100, + RearmingGroup=nil, + RearmingGroupSpeed=nil, + RearmingGroupOnRoad=false, + RearmingGroupCoord=nil, + RearmingPlaceCoord=nil, + RearmingArtyOnRoad=false, + InitialCoord=nil, + report=true, + ammoshells={}, + ammorockets={}, + ammomissiles={}, + Nshots=0, + minrange=300, + maxrange=1000000, + nukewarhead=75000, + Nukes=nil, + nukefire=false, + nukefires=nil, + nukerange=nil, + Nillu=nil, + illuPower=1000000, + illuMinalt=500, + illuMaxalt=1000, + Nsmoke=nil, + smokeColor=SMOKECOLOR.Red, + relocateafterfire=false, + relocateRmin=300, + relocateRmax=800, + markallow=false, + markkey=nil, + markreadonly=false, + autorelocate=false, + autorelocatemaxdist=50000, + autorelocateonroad=false, + coalition=nil, + respawnafterdeath=false, + respawndelay=nil +} + +--- Weapong type ID. See [here](http://wiki.hoggit.us/view/DCS_enum_weapon_flag). +-- @type ARTY.WeaponType +-- @field #number Auto Automatic selection of weapon type. +-- @field #number Cannon Cannons using conventional shells. +-- @field #number Rockets Unguided rockets. +-- @field #number CruiseMissile Cruise missiles. +-- @field #number TacticalNukes Tactical nuclear shells (simulated). +-- @field #number IlluminationShells Illumination shells (simulated). +-- @field #number SmokeShells Smoke shells (simulated). +ARTY.WeaponType={ + Auto=1073741822, + Cannon=805306368, + Rockets=30720, + CruiseMissile=2097152, + TacticalNukes=666, + IlluminationShells=667, + SmokeShells=668, +} + +--- Database of common artillery unit properties. +-- @type ARTY.db +ARTY.db={ + ["2B11 mortar"] = { -- type "2B11 mortar" + minrange = 500, -- correct? + maxrange = 7000, -- 7 km + reloadtime = 30, -- 30 sec + }, + ["SPH 2S1 Gvozdika"] = { -- type "SAU Gvozdika" + minrange = 300, -- correct? + maxrange = 15000, -- 15 km + reloadtime = nil, -- unknown + }, + ["SPH 2S19 Msta"] = { --type "SAU Msta", alias "2S19 Msta" + minrange = 300, -- correct? + maxrange = 23500, -- 23.5 km + reloadtime = nil, -- unknown + }, + ["SPH 2S3 Akatsia"] = { -- type "SAU Akatsia", alias "2S3 Akatsia" + minrange = 300, -- correct? + maxrange = 17000, -- 17 km + reloadtime = nil, -- unknown + }, + ["SPH 2S9 Nona"] = { --type "SAU 2-C9" + minrange = 500, -- correct? + maxrange = 7000, -- 7 km + reloadtime = nil, -- unknown + }, + ["SPH M109 Paladin"] = { -- type "M-109", alias "M109" + minrange = 300, -- correct? + maxrange = 22000, -- 22 km + reloadtime = nil, -- unknown + }, + ["SpGH Dana"] = { -- type "SpGH_Dana" + minrange = 300, -- correct? + maxrange = 18700, -- 18.7 km + reloadtime = nil, -- unknown + }, + ["MLRS BM-21 Grad"] = { --type "Grad-URAL", alias "MLRS BM-21 Grad" + minrange = 5000, -- 5 km + maxrange = 19000, -- 19 km + reloadtime = 420, -- 7 min + }, + ["MLRS 9K57 Uragan BM-27"] = { -- type "Uragan_BM-27" + minrange = 11500, -- 11.5 km + maxrange = 35800, -- 35.8 km + reloadtime = 840, -- 14 min + }, + ["MLRS 9A52 Smerch"] = { -- type "Smerch" + minrange = 20000, -- 20 km + maxrange = 70000, -- 70 km + reloadtime = 2160, -- 36 min + }, + ["MLRS M270"] = { --type "MRLS", alias "M270 MRLS" + minrange = 10000, -- 10 km + maxrange = 32000, -- 32 km + reloadtime = 540, -- 9 min + }, +} + +--- Target. +-- @type ARTY.Target +-- @field #string name Name of target. +-- @field Core.Point#COORDINATE coord Target coordinates. +-- @field #number radius Shelling radius in meters. +-- @field #number nshells Number of shells (or other weapon types) fired upon target. +-- @field #number engaged Number of times this target was engaged. +-- @field #boolean underfire If true, target is currently under fire. +-- @field #number prio Priority of target. +-- @field #number maxengage Max number of times, the target will be engaged. +-- @field #number time Abs. mission time in seconds, when the target is scheduled to be attacked. +-- @field #number weapontype Type of weapon used for engagement. See #ARTY.WeaponType. +-- @field #number Tassigned Abs. mission time when target was assigned. +-- @field #boolean attackgroup If true, use task attack group rather than fire at point for engagement. + +--- Arty script version. +-- @field #string version +ARTY.version="1.2.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO list: +-- TODO: Add hit event and make the arty group relocate. +-- TODO: Handle rearming for ships. How? +-- DONE: Delete targets from queue user function. +-- DONE: Delete entire target queue user function. +-- DONE: Add weapon types. Done but needs improvements. +-- DONE: Add user defined rearm weapon types. +-- DONE: Check if target is in range. Maybe this requires a data base with the ranges of all arty units. +-- DONE: Make ARTY move to rearming position. +-- DONE: Check that right rearming vehicle is specified. Blue M818, Red Ural-375. Are there more? +-- DONE: Check if ARTY group is still alive. +-- DONE: Handle dead events. +-- DONE: Abort firing task if no shooting event occured with 5(?) minutes. Something went wrong then. Min/max range for example. +-- DONE: Improve assigned time for engagement. Next day? +-- DONE: Improve documentation. +-- DONE: Add pseudo user transitions. OnAfter... +-- DONE: Make reaming unit a group. +-- DONE: Write documenation. +-- DONE: Add command move to make arty group move. +-- DONE: remove schedulers for status event. +-- DONE: Improve handling of special weapons. When winchester if using selected weapons? +-- DONE: Make coordinate after rearming general, i.e. also work after the group has moved to anonther location. +-- DONE: Add set commands via markers. E.g. set rearming place. +-- DONE: Test stationary types like mortas ==> rearming etc. +-- DONE: Add illumination and smoke. + +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Creates a new ARTY object from a MOOSE group object. +-- @param #ARTY self +-- @param Wrapper.Group#GROUP group The GROUP object for which artillery tasks should be assigned. +-- @param alias (Optional) Alias name the group will be calling itself when sending messages. Default is the group name. +-- @return #ARTY ARTY object or nil if group does not exist or is not a ground or naval group. +function ARTY:New(group, alias) + + -- Inherits from FSM_CONTROLLABLE + local self=BASE:Inherit(self, FSM_CONTROLLABLE:New()) -- #ARTY + + -- If group name was given. + if type(group)=="string" then + self.groupname=group + group=GROUP:FindByName(group) + if not group then + self:E(string.format("ERROR: Requested ARTY group %s does not exist! (Has to be a MOOSE group.)", self.groupname)) + return nil + end + end + + -- Check that group is present. + if group then + self:T(string.format("ARTY script version %s. Added group %s.", ARTY.version, group:GetName())) + else + self:E("ERROR: Requested ARTY group does not exist! (Has to be a MOOSE group.)") + return nil + end + + -- Check that we actually have a GROUND group. + if not (group:IsGround() or group:IsShip()) then + self:E(string.format("ERROR: ARTY group %s has to be a GROUND or SHIP group!", group:GetName())) + return nil + end + + -- Set the controllable for the FSM. + self:SetControllable(group) + + -- Set the group name + self.groupname=group:GetName() + + -- Get coalition. + self.coalition=group:GetCoalition() + + -- Set an alias name. + if alias~=nil then + self.alias=tostring(alias) + else + self.alias=self.groupname + end + + -- Log id. + self.lid=string.format("ARTY %s | ", self.alias) + + -- Set the initial coordinates of the ARTY group. + self.InitialCoord=group:GetCoordinate() + + -- Get DCS descriptors of group. + local DCSgroup=Group.getByName(group:GetName()) + local DCSunit=DCSgroup:getUnit(1) + self.DCSdesc=DCSunit:getDesc() + + -- DCS descriptors. + self:T3(self.lid.."DCS descriptors for group "..group:GetName()) + for id,desc in pairs(self.DCSdesc) do + self:T3({id=id, desc=desc}) + end + + -- Maximum speed in km/h. + self.SpeedMax=group:GetSpeedMax() + + -- Group is mobile or not (e.g. mortars). + if self.SpeedMax>1 then + self.ismobile=true + else + self.ismobile=false + end + + -- Set speed to 0.7 of maximum. + self.Speed=self.SpeedMax * 0.7 + + -- Displayed name (similar to type name below) + self.DisplayName=self.DCSdesc.displayName + + -- Is this infantry or not. + self.IsArtillery=DCSunit:hasAttribute("Artillery") + + -- Type of group. + self.Type=group:GetTypeName() + + -- Initial group strength. + self.IniGroupStrength=#group:GetUnits() + + --------------- + -- Transitions: + --------------- + + -- Entry. + self:AddTransition("*", "Start", "CombatReady") + + -- Blue branch. + self:AddTransition("CombatReady", "OpenFire", "Firing") + self:AddTransition("Firing", "CeaseFire", "CombatReady") + + -- Violett branch. + self:AddTransition("CombatReady", "Winchester", "OutOfAmmo") + + -- Red branch. + self:AddTransition({"CombatReady", "OutOfAmmo"}, "Rearm", "Rearming") + self:AddTransition("Rearming", "Rearmed", "Rearmed") + + -- Green branch. + self:AddTransition("*", "Move", "Moving") + self:AddTransition("Moving", "Arrived", "Arrived") + + -- Yellow branch. + self:AddTransition("*", "NewTarget", "*") + + -- Not in diagram. + self:AddTransition("*", "CombatReady", "CombatReady") + self:AddTransition("*", "Status", "*") + self:AddTransition("*", "NewMove", "*") + self:AddTransition("*", "Dead", "*") + self:AddTransition("*", "Respawn", "CombatReady") + + -- Transport as cargo (not in diagram). + self:AddTransition("*", "Loaded", "InTransit") + self:AddTransition("InTransit", "UnLoaded", "CombatReady") + + -- Unknown transitons. To be checked if adding these causes problems. + self:AddTransition("Rearming", "Arrived", "Rearming") + self:AddTransition("Rearming", "Move", "Rearming") + + + --- User function for OnAfter "NewTarget" event. + -- @function [parent=#ARTY] OnAfterNewTarget + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #table target Array holding the target info. + + --- User function for OnAfter "OpenFire" event. + -- @function [parent=#ARTY] OnAfterOpenFire + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #table target Array holding the target info. + + --- User function for OnAfter "CeaseFire" event. + -- @function [parent=#ARTY] OnAfterCeaseFire + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #table target Array holding the target info. + + --- User function for OnAfer "NewMove" event. + -- @function [parent=#ARTY] OnAfterNewMove + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #table move Array holding the move info. + + --- User function for OnAfer "Move" event. + -- @function [parent=#ARTY] OnAfterMove + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #table move Array holding the move info. + + --- User function for OnAfer "Arrived" event. + -- @function [parent=#ARTY] OnAfterArrvied + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- User function for OnAfter "Winchester" event. + -- @function [parent=#ARTY] OnAfterWinchester + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- User function for OnAfter "Rearm" event. + -- @function [parent=#ARTY] OnAfterRearm + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- User function for OnAfter "Rearmed" event. + -- @function [parent=#ARTY] OnAfterRearmed + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- User function for OnAfter "Start" event. + -- @function [parent=#ARTY] OnAfterStart + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- User function for OnAfter "Status" event. + -- @function [parent=#ARTY] OnAfterStatus + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- User function for OnAfter "Dead" event. + -- @function [parent=#ARTY] OnAfterDead + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string Unitname Name of the dead unit. + + --- User function for OnAfter "Respawn" event. + -- @function [parent=#ARTY] OnAfterRespawn + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- User function for OnEnter "CombatReady" state. + -- @function [parent=#ARTY] OnEnterCombatReady + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- User function for OnEnter "Firing" state. + -- @function [parent=#ARTY] OnEnterFiring + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- User function for OnEnter "OutOfAmmo" state. + -- @function [parent=#ARTY] OnEnterOutOfAmmo + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- User function for OnEnter "Rearming" state. + -- @function [parent=#ARTY] OnEnterRearming + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- User function for OnEnter "Rearmed" state. + -- @function [parent=#ARTY] OnEnterRearmed + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- User function for OnEnter "Moving" state. + -- @function [parent=#ARTY] OnEnterMoving + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Function to start the ARTY FSM process. + -- @function [parent=#ARTY] Start + -- @param #ARTY self + + --- Function to start the ARTY FSM process after a delay. + -- @function [parent=#ARTY] __Start + -- @param #ARTY self + -- @param #number Delay before start in seconds. + + --- Function to update the status of the ARTY group and tigger FSM events. Triggers the FSM event "Status". + -- @function [parent=#ARTY] Status + -- @param #ARTY self + + --- Function to update the status of the ARTY group and tigger FSM events after a delay. Triggers the FSM event "Status". + -- @function [parent=#ARTY] __Status + -- @param #ARTY self + -- @param #number Delay in seconds. + + --- Function called when a unit of the ARTY group died. Triggers the FSM event "Dead". + -- @function [parent=#ARTY] Dead + -- @param #ARTY self + -- @param #string unitname Name of the unit that died. + + --- Function called when a unit of the ARTY group died after a delay. Triggers the FSM event "Dead". + -- @function [parent=#ARTY] __Dead + -- @param #ARTY self + -- @param #number Delay in seconds. + -- @param #string unitname Name of the unit that died. + + --- Add a new target for the ARTY group. Triggers the FSM event "NewTarget". + -- @function [parent=#ARTY] NewTarget + -- @param #ARTY self + -- @param #table target Array holding the target data. + + --- Add a new target for the ARTY group with a delay. Triggers the FSM event "NewTarget". + -- @function [parent=#ARTY] __NewTarget + -- @param #ARTY self + -- @param #number delay Delay in seconds. + -- @param #table target Array holding the target data. + + --- Add a new relocation move for the ARTY group. Triggers the FSM event "NewMove". + -- @function [parent=#ARTY] NewMove + -- @param #ARTY self + -- @param #table move Array holding the relocation move data. + + --- Add a new relocation for the ARTY group after a delay. Triggers the FSM event "NewMove". + -- @function [parent=#ARTY] __NewMove + -- @param #ARTY self + -- @param #number delay Delay in seconds. + -- @param #table move Array holding the relocation move data. + + --- Order ARTY group to open fire on a target. Triggers the FSM event "OpenFire". + -- @function [parent=#ARTY] OpenFire + -- @param #ARTY self + -- @param #table target Array holding the target data. + + --- Order ARTY group to open fire on a target with a delay. Triggers the FSM event "Move". + -- @function [parent=#ARTY] __OpenFire + -- @param #ARTY self + -- @param #number delay Delay in seconds. + -- @param #table target Array holding the target data. + + --- Order ARTY group to cease firing on a target. Triggers the FSM event "CeaseFire". + -- @function [parent=#ARTY] CeaseFire + -- @param #ARTY self + -- @param #table target Array holding the target data. + + --- Order ARTY group to cease firing on a target after a delay. Triggers the FSM event "CeaseFire". + -- @function [parent=#ARTY] __CeaseFire + -- @param #ARTY self + -- @param #number delay Delay in seconds. + -- @param #table target Array holding the target data. + + --- Order ARTY group to move to another location. Triggers the FSM event "Move". + -- @function [parent=#ARTY] Move + -- @param #ARTY self + -- @param #table move Array holding the relocation move data. + + --- Order ARTY group to move to another location after a delay. Triggers the FSM event "Move". + -- @function [parent=#ARTY] __Move + -- @param #ARTY self + -- @param #number delay Delay in seconds. + -- @param #table move Array holding the relocation move data. + + --- Tell ARTY group it has arrived at its destination. Triggers the FSM event "Arrived". + -- @function [parent=#ARTY] Arrived + -- @param #ARTY self + + --- Tell ARTY group it has arrived at its destination after a delay. Triggers the FSM event "Arrived". + -- @function [parent=#ARTY] __Arrived + -- @param #ARTY self + -- @param #number delay Delay in seconds. + + --- Tell ARTY group it is combat ready. Triggers the FSM event "CombatReady". + -- @function [parent=#ARTY] CombatReady + -- @param #ARTY self + + --- Tell ARTY group it is combat ready after a delay. Triggers the FSM event "CombatReady". + -- @function [parent=#ARTY] __CombatReady + -- @param #ARTY self + -- @param #number delay Delay in seconds. + + --- Tell ARTY group it is out of ammo. Triggers the FSM event "Winchester". + -- @function [parent=#ARTY] Winchester + -- @param #ARTY self + + --- Tell ARTY group it is out of ammo after a delay. Triggers the FSM event "Winchester". + -- @function [parent=#ARTY] __Winchester + -- @param #ARTY self + -- @param #number delay Delay in seconds. + + --- Respawn ARTY group. + -- @function [parent=#ARTY] Respawn + -- @param #ARTY self + + --- Respawn ARTY group after a delay. + -- @function [parent=#ARTY] __Respawn + -- @param #ARTY self + -- @param #number delay Delay in seconds. + + return self +end + +--- Creates a new ARTY object from a MOOSE CARGO_GROUP object. +-- @param #ARTY self +-- @param Cargo.CargoGroup#CARGO_GROUP cargogroup The CARGO GROUP object for which artillery tasks should be assigned. +-- @param alias (Optional) Alias name the group will be calling itself when sending messages. Default is the group name. +-- @return #ARTY ARTY object or nil if group does not exist or is not a ground or naval group. +function ARTY:NewFromCargoGroup(cargogroup, alias) + + if cargogroup then + BASE:T(string.format("ARTY script version %s. Added CARGO group %s.", ARTY.version, cargogroup:GetName())) + else + BASE:E("ERROR: Requested ARTY CARGO GROUP does not exist! (Has to be a MOOSE CARGO(!) group.)") + return nil + end + + -- Get group belonging to the cargo group. + local group=cargogroup:GetObject() + + -- Create ARTY object. + local arty=ARTY:New(group,alias) + + -- Set iscargo flag. + arty.iscargo=true + + -- Set cargo group object. + arty.cargogroup=cargogroup + + return arty +end + + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Assign target coordinates to the ARTY group. Only the first parameter, i.e. the coordinate of the target is mandatory. The remaining parameters are optional and can be used to fine tune the engagement. +-- @param #ARTY self +-- @param Core.Point#COORDINATE coord Coordinates of the target. +-- @param #number prio (Optional) Priority of target. Number between 1 (high) and 100 (low). Default 50. +-- @param #number radius (Optional) Radius. Default is 100 m. +-- @param #number nshells (Optional) How many shells (or rockets) are fired on target per engagement. Default 5. +-- @param #number maxengage (Optional) How many times a target is engaged. Default 1. +-- @param #string time (Optional) Day time at which the target should be engaged. Passed as a string in format "08:13:45". Current task will be canceled. +-- @param #number weapontype (Optional) Type of weapon to be used to attack this target. Default ARTY.WeaponType.Auto, i.e. the DCS logic automatically determins the appropriate weapon. +-- @param #string name (Optional) Name of the target. Default is LL DMS coordinate of the target. If the name was already given, the numbering "#01", "#02",... is appended automatically. +-- @param #boolean unique (Optional) Target is unique. If the target name is already known, the target is rejected. Default false. +-- @return #string Name of the target. Can be used for further reference, e.g. deleting the target from the list. +-- @usage paladin=ARTY:New(GROUP:FindByName("Blue Paladin")) +-- paladin:AssignTargetCoord(GROUP:FindByName("Red Targets 1"):GetCoordinate(), 10, 300, 10, 1, "08:02:00", ARTY.WeaponType.Auto, "Target 1") +-- paladin:Start() +function ARTY:AssignTargetCoord(coord, prio, radius, nshells, maxengage, time, weapontype, name, unique) + self:F({coord=coord, prio=prio, radius=radius, nshells=nshells, maxengage=maxengage, time=time, weapontype=weapontype, name=name, unique=unique}) + + -- Set default values. + nshells=nshells or 5 + radius=radius or 100 + maxengage=maxengage or 1 + prio=prio or 50 + prio=math.max( 1, prio) + prio=math.min(100, prio) + if unique==nil then + unique=false + end + weapontype=weapontype or ARTY.WeaponType.Auto + + -- Check if we have a coordinate object. + local text=nil + if coord:IsInstanceOf("GROUP") then + text="WARNING: ARTY:AssignTargetCoordinate(coord, ...) needs a COORDINATE object as first parameter - you gave a GROUP. Converting to COORDINATE..." + coord=coord:GetCoordinate() + elseif coord:IsInstanceOf("UNIT") then + text="WARNING: ARTY:AssignTargetCoordinate(coord, ...) needs a COORDINATE object as first parameter - you gave a UNIT. Converting to COORDINATE..." + coord=coord:GetCoordinate() + elseif coord:IsInstanceOf("POSITIONABLE") then + text="WARNING: ARTY:AssignTargetCoordinate(coord, ...) needs a COORDINATE object as first parameter - you gave a POSITIONABLE. Converting to COORDINATE..." + coord=coord:GetCoordinate() + elseif coord:IsInstanceOf("COORDINATE") then + -- Nothing to do here. + else + text="ERROR: ARTY:AssignTargetCoordinate(coord, ...) needs a COORDINATE object as first parameter!" + MESSAGE:New(text, 30):ToAll() + self:E(self.lid..text) + return nil + end + if text~=nil then + self:E(self.lid..text) + end + + -- Name of the target. + local _name=name or coord:ToStringLLDMS() + local _unique=true + + -- Check if the name has already been used for another target. If so, the function returns a new unique name. + _name,_unique=self:_CheckName(self.targets, _name, not unique) + + -- Target name should be unique and is not. + if unique==true and _unique==false then + self:T(self.lid..string.format("%s: target %s should have a unique name but name was already given. Rejecting target!", self.groupname, _name)) + return nil + end + + -- Time in seconds. + local _time + if type(time)=="string" then + _time=self:_ClockToSeconds(time) + elseif type(time)=="number" then + _time=timer.getAbsTime()+time + else + _time=timer.getAbsTime() + end + + -- Prepare target array. + local _target={name=_name, coord=coord, radius=radius, nshells=nshells, engaged=0, underfire=false, prio=prio, maxengage=maxengage, time=_time, weapontype=weapontype} + + -- Add to table. + table.insert(self.targets, _target) + + -- Trigger new target event. + self:__NewTarget(1, _target) + + return _name +end + +--- Assign a target group to the ARTY group. Note that this will use the Attack Group Task rather than the Fire At Point Task. +-- @param #ARTY self +-- @param Wrapper.Group#GROUP group Target group. +-- @param #number prio (Optional) Priority of target. Number between 1 (high) and 100 (low). Default 50. +-- @param #number radius (Optional) Radius. Default is 100 m. +-- @param #number nshells (Optional) How many shells (or rockets) are fired on target per engagement. Default 5. +-- @param #number maxengage (Optional) How many times a target is engaged. Default 1. +-- @param #string time (Optional) Day time at which the target should be engaged. Passed as a string in format "08:13:45". Current task will be canceled. +-- @param #number weapontype (Optional) Type of weapon to be used to attack this target. Default ARTY.WeaponType.Auto, i.e. the DCS logic automatically determins the appropriate weapon. +-- @param #string name (Optional) Name of the target. Default is LL DMS coordinate of the target. If the name was already given, the numbering "#01", "#02",... is appended automatically. +-- @param #boolean unique (Optional) Target is unique. If the target name is already known, the target is rejected. Default false. +-- @return #string Name of the target. Can be used for further reference, e.g. deleting the target from the list. +-- @usage paladin=ARTY:New(GROUP:FindByName("Blue Paladin")) +-- paladin:AssignTargetCoord(GROUP:FindByName("Red Targets 1"):GetCoordinate(), 10, 300, 10, 1, "08:02:00", ARTY.WeaponType.Auto, "Target 1") +-- paladin:Start() +function ARTY:AssignAttackGroup(group, prio, radius, nshells, maxengage, time, weapontype, name, unique) + + -- Set default values. + nshells=nshells or 5 + radius=radius or 100 + maxengage=maxengage or 1 + prio=prio or 50 + prio=math.max( 1, prio) + prio=math.min(100, prio) + if unique==nil then + unique=false + end + weapontype=weapontype or ARTY.WeaponType.Auto + + -- TODO Check if we have a group object. + if type(group)=="string" then + group=GROUP:FindByName(group) + end + + if group and group:IsAlive() then + + local coord=group:GetCoordinate() + + -- Name of the target. + local _name=group:GetName() + local _unique=true + + -- Check if the name has already been used for another target. If so, the function returns a new unique name. + _name,_unique=self:_CheckName(self.targets, _name, not unique) + + -- Target name should be unique and is not. + if unique==true and _unique==false then + self:T(self.lid..string.format("%s: target %s should have a unique name but name was already given. Rejecting target!", self.groupname, _name)) + return nil + end + + -- Time in seconds. + local _time + if type(time)=="string" then + _time=self:_ClockToSeconds(time) + elseif type(time)=="number" then + _time=timer.getAbsTime()+time + else + _time=timer.getAbsTime() + end + + -- Prepare target array. + local target={} --#ARTY.Target + target.attackgroup=true + target.name=_name + target.coord=coord + target.radius=radius + target.nshells=nshells + target.engaged=0 + target.underfire=false + target.prio=prio + target.time=_time + target.maxengage=maxengage + target.weapontype=weapontype + + -- Add to table. + table.insert(self.targets, target) + + -- Trigger new target event. + self:__NewTarget(1, target) + + return _name + else + self:E("ERROR: Group does not exist!") + end + + return nil +end + + + +--- Assign coordinate to where the ARTY group should move. +-- @param #ARTY self +-- @param Core.Point#COORDINATE coord Coordinates of the new position. +-- @param #string time (Optional) Day time at which the group should start moving. Passed as a string in format "08:13:45". Default is now. +-- @param #number speed (Optinal) Speed in km/h the group should move at. Default 70% of max posible speed of group. +-- @param #boolean onroad (Optional) If true, group will mainly use roads. Default off, i.e. go directly towards the specified coordinate. +-- @param #boolean cancel (Optional) If true, cancel any running attack when move should begin. Default is false. +-- @param #string name (Optional) Name of the coordinate. Default is LL DMS string of the coordinate. If the name was already given, the numbering "#01", "#02",... is appended automatically. +-- @param #boolean unique (Optional) Move is unique. If the move name is already known, the move is rejected. Default false. +-- @return #string Name of the move. Can be used for further reference, e.g. deleting the move from the list. +function ARTY:AssignMoveCoord(coord, time, speed, onroad, cancel, name, unique) + self:F({coord=coord, time=time, speed=speed, onroad=onroad, cancel=cancel, name=name, unique=unique}) + + -- Reject move if the group is immobile. + if not self.ismobile then + self:T(self.lid..string.format("%s: group is immobile. Rejecting move request!", self.groupname)) + return nil + end + + -- Default + if unique==nil then + unique=false + end + + -- Name of the target. + local _name=name or coord:ToStringLLDMS() + local _unique=true + + -- Check if the name has already been used for another target. If so, the function returns a new unique name. + _name,_unique=self:_CheckName(self.moves, _name, not unique) + + -- Move name should be unique and is not. + if unique==true and _unique==false then + self:T(self.lid..string.format("%s: move %s should have a unique name but name was already given. Rejecting move!", self.groupname, _name)) + return nil + end + + -- Set speed. + if speed then + -- Make sure, given speed is less than max physiaclly possible speed of group. + speed=math.min(speed, self.SpeedMax) + elseif self.Speed then + speed=self.Speed + else + speed=self.SpeedMax*0.7 + end + + -- Default is off road. + if onroad==nil then + onroad=false + end + + -- Default is not to cancel a running attack. + if cancel==nil then + cancel=false + end + + -- Time in seconds. + local _time + if type(time)=="string" then + _time=self:_ClockToSeconds(time) + elseif type(time)=="number" then + _time=timer.getAbsTime()+time + else + _time=timer.getAbsTime() + end + + -- Prepare move array. + local _move={name=_name, coord=coord, time=_time, speed=speed, onroad=onroad, cancel=cancel} + + -- Add to table. + table.insert(self.moves, _move) + + return _name +end + +--- Set alias, i.e. the name the group will use when sending messages. +-- @param #ARTY self +-- @param #string alias The alias for the group. +-- @return self +function ARTY:SetAlias(alias) + self:F({alias=alias}) + self.alias=tostring(alias) + return self +end + +--- Add ARTY group to one or more clusters. Enables addressing all ARTY groups within a cluster simultaniously via marker assignments. +-- @param #ARTY self +-- @param #table clusters Table of cluster names the group should belong to. +-- @return self +function ARTY:AddToCluster(clusters) + self:F({clusters=clusters}) + + -- Convert input to table. + local names + if type(clusters)=="table" then + names=clusters + elseif type(clusters)=="string" then + names={clusters} + else + -- error message + self:E(self.lid.."ERROR: Input parameter must be a string or a table in ARTY:AddToCluster()!") + return + end + + -- Add names to cluster array. + for _,cluster in pairs(names) do + table.insert(self.clusters, cluster) + end + + return self +end + +--- Set minimum firing range. Targets closer than this distance are not engaged. +-- @param #ARTY self +-- @param #number range Min range in kilometers. Default is 0.1 km. +-- @return self +function ARTY:SetMinFiringRange(range) + self:F({range=range}) + self.minrange=range*1000 or 100 + return self +end + +--- Set maximum firing range. Targets further away than this distance are not engaged. +-- @param #ARTY self +-- @param #number range Max range in kilometers. Default is 1000 km. +-- @return self +function ARTY:SetMaxFiringRange(range) + self:F({range=range}) + self.maxrange=range*1000 or 1000*1000 + return self +end + +--- Set time interval between status updates. During the status check, new events are triggered. +-- @param #ARTY self +-- @param #number interval Time interval in seconds. Default 10 seconds. +-- @return self +function ARTY:SetStatusInterval(interval) + self:F({interval=interval}) + self.StatusInterval=interval or 10 + return self +end + +--- Set time how it is waited a unit the first shot event happens. If no shot is fired after this time, the task to fire is aborted and the target removed. +-- @param #ARTY self +-- @param #number waittime Time in seconds. Default 300 seconds. +-- @return self +function ARTY:SetWaitForShotTime(waittime) + self:F({waittime=waittime}) + self.WaitForShotTime=waittime or 300 + return self +end + +--- Define the safe distance between ARTY group and rearming unit or rearming place at which rearming process is possible. +-- @param #ARTY self +-- @param #number distance Safe distance in meters. Default is 100 m. +-- @return self +function ARTY:SetRearmingDistance(distance) + self:F({distance=distance}) + self.RearmingDistance=distance or 100 + return self +end + +--- Assign a group, which is responsible for rearming the ARTY group. If the group is too far away from the ARTY group it will be guided towards the ARTY group. +-- @param #ARTY self +-- @param Wrapper.Group#GROUP group Group that is supposed to rearm the ARTY group. For the blue coalition, this is often a unarmed M818 transport whilst for red an unarmed Ural-375 transport can be used. +-- @return self +function ARTY:SetRearmingGroup(group) + self:F({group=group}) + self.RearmingGroup=group + return self +end + +--- Set the speed the rearming group moves at towards the ARTY group or the rearming place. +-- @param #ARTY self +-- @param #number speed Speed in km/h. +-- @return self +function ARTY:SetRearmingGroupSpeed(speed) + self:F({speed=speed}) + self.RearmingGroupSpeed=speed + return self +end + +--- Define if rearming group uses mainly roads to drive to the ARTY group or rearming place. +-- @param #ARTY self +-- @param #boolean onroad If true, rearming group uses mainly roads. If false, it drives directly to the ARTY group or rearming place. +-- @return self +function ARTY:SetRearmingGroupOnRoad(onroad) + self:F({onroad=onroad}) + if onroad==nil then + onroad=true + end + self.RearmingGroupOnRoad=onroad + return self +end + +--- Define if ARTY group uses mainly roads to drive to the rearming place. +-- @param #ARTY self +-- @param #boolean onroad If true, ARTY group uses mainly roads. If false, it drives directly to the rearming place. +-- @return self +function ARTY:SetRearmingArtyOnRoad(onroad) + self:F({onroad=onroad}) + if onroad==nil then + onroad=true + end + self.RearmingArtyOnRoad=onroad + return self +end + +--- Defines the rearming place of the ARTY group. If the place is too far away from the ARTY group it will be routed to the place. +-- @param #ARTY self +-- @param Core.Point#COORDINATE coord Coordinates of the rearming place. +-- @return self +function ARTY:SetRearmingPlace(coord) + self:F({coord=coord}) + self.RearmingPlaceCoord=coord + return self +end + +--- Set automatic relocation of ARTY group if a target is assigned which is out of range. The unit will drive automatically towards or away from the target to be in max/min firing range. +-- @param #ARTY self +-- @param #number maxdistance (Optional) The maximum distance in km the group will travel to get within firing range. Default is 50 km. No automatic relocation is performed if targets are assigned which are further away. +-- @param #boolean onroad (Optional) If true, ARTY group uses roads whenever possible. Default false, i.e. group will move in a straight line to the assigned coordinate. +-- @return self +function ARTY:SetAutoRelocateToFiringRange(maxdistance, onroad) + self:F({distance=maxdistance, onroad=onroad}) + self.autorelocate=true + self.autorelocatemaxdist=maxdistance or 50 + self.autorelocatemaxdist=self.autorelocatemaxdist*1000 + if onroad==nil then + onroad=false + end + self.autorelocateonroad=onroad + return self +end + +--- Set relocate after firing. Group will find a new location after each engagement. Default is off +-- @param #ARTY self +-- @param #number rmax (Optional) Max distance in meters, the group will move to relocate. Default is 800 m. +-- @param #number rmin (Optional) Min distance in meters, the group will move to relocate. Default is 300 m. +-- @return self +function ARTY:SetAutoRelocateAfterEngagement(rmax, rmin) + self.relocateafterfire=true + self.relocateRmax=rmax or 800 + self.relocateRmin=rmin or 300 + + -- Ensure that Rmin<=Rmax + self.relocateRmin=math.min(self.relocateRmin, self.relocateRmax) + + return self +end + +--- Report messages of ARTY group turned on. This is the default. +-- @param #ARTY self +-- @return self +function ARTY:SetReportON() + self.report=true + return self +end + +--- Report messages of ARTY group turned off. Default is on. +-- @param #ARTY self +-- @return self +function ARTY:SetReportOFF() + self.report=false + return self +end + +--- Respawn group once all units are dead. +-- @param #ARTY self +-- @param #number delay (Optional) Delay before respawn in seconds. +-- @return self +function ARTY:SetRespawnOnDeath(delay) + self.respawnafterdeath=true + self.respawndelay=delay + return self +end + +--- Turn debug mode on. Information is printed to screen. +-- @param #ARTY self +-- @return self +function ARTY:SetDebugON() + self.Debug=true + return self +end + +--- Turn debug mode off. This is the default setting. +-- @param #ARTY self +-- @return self +function ARTY:SetDebugOFF() + self.Debug=false + return self +end + +--- Set default speed the group is moving at if not specified otherwise. +-- @param #ARTY self +-- @param #number speed Speed in km/h. +-- @return self +function ARTY:SetSpeed(speed) + self.Speed=speed + return self +end + +--- Delete a target from target list. If the target is currently engaged, it is cancelled. +-- @param #ARTY self +-- @param #string name Name of the target. +function ARTY:RemoveTarget(name) + self:F2(name) + + -- Get target ID from namd + local id=self:_GetTargetIndexByName(name) + + if id then + + -- Remove target from table. + self:T(self.lid..string.format("Group %s: Removing target %s (id=%d).", self.groupname, name, id)) + table.remove(self.targets, id) + + -- Delete marker belonging to this engagement. + if self.markallow then + local batteryname,markTargetID, markMoveID=self:_GetMarkIDfromName(name) + if batteryname==self.groupname and markTargetID~=nil then + COORDINATE:RemoveMark(markTargetID) + end + end + + end + self:T(self.lid..string.format("Group %s: Number of targets = %d.", self.groupname, #self.targets)) +end + +--- Delete a move from move list. +-- @param #ARTY self +-- @param #string name Name of the target. +function ARTY:RemoveMove(name) + self:F2(name) + + -- Get move ID from name. + local id=self:_GetMoveIndexByName(name) + + if id then + + -- Remove move from table. + self:T(self.lid..string.format("Group %s: Removing move %s (id=%d).", self.groupname, name, id)) + table.remove(self.moves, id) + + -- Delete marker belonging to this relocation move. + if self.markallow then + local batteryname,markTargetID,markMoveID=self:_GetMarkIDfromName(name) + if batteryname==self.groupname and markMoveID~=nil then + COORDINATE:RemoveMark(markMoveID) + end + end + + end + self:T(self.lid..string.format("Group %s: Number of moves = %d.", self.groupname, #self.moves)) +end + +--- Delete ALL targets from current target list. +-- @param #ARTY self +function ARTY:RemoveAllTargets() + self:F2() + for _,target in pairs(self.targets) do + self:RemoveTarget(target.name) + end +end + +--- Define shell types that are counted to determine the ammo amount the ARTY group has. +-- @param #ARTY self +-- @param #table tableofnames Table of shell type names. +-- @return self +function ARTY:SetShellTypes(tableofnames) + self:F2(tableofnames) + self.ammoshells={} + for _,_type in pairs(tableofnames) do + table.insert(self.ammoshells, _type) + end + return self +end + +--- Define rocket types that are counted to determine the ammo amount the ARTY group has. +-- @param #ARTY self +-- @param #table tableofnames Table of rocket type names. +-- @return self +function ARTY:SetRocketTypes(tableofnames) + self:F2(tableofnames) + self.ammorockets={} + for _,_type in pairs(tableofnames) do + table.insert(self.ammorockets, _type) + end + return self +end + +--- Define missile types that are counted to determine the ammo amount the ARTY group has. +-- @param #ARTY self +-- @param #table tableofnames Table of rocket type names. +-- @return self +function ARTY:SetMissileTypes(tableofnames) + self:F2(tableofnames) + self.ammomissiles={} + for _,_type in pairs(tableofnames) do + table.insert(self.ammomissiles, _type) + end + return self +end + +--- Set number of tactical nuclear warheads available to the group. +-- Note that it can be max the number of normal shells. Also if all normal shells are empty, firing nuclear shells is also not possible any more until group gets rearmed. +-- @param #ARTY self +-- @param #number n Number of warheads for the whole group. +-- @return self +function ARTY:SetTacNukeShells(n) + self.Nukes=n + return self +end + +--- Set nuclear warhead explosion strength. +-- @param #ARTY self +-- @param #number strength Explosion strength in kilo tons TNT. Default is 0.075 kt. +-- @return self +function ARTY:SetTacNukeWarhead(strength) + self.nukewarhead=strength or 0.075 + self.nukewarhead=self.nukewarhead*1000*1000 -- convert to kg TNT. + return self +end + +--- Set number of illumination shells available to the group. +-- Note that it can be max the number of normal shells. Also if all normal shells are empty, firing illumination shells is also not possible any more until group gets rearmed. +-- @param #ARTY self +-- @param #number n Number of illumination shells for the whole group. +-- @param #number power (Optional) Power of illumination warhead in mega candela. Default 1.0 mcd. +-- @return self +function ARTY:SetIlluminationShells(n, power) + self.Nillu=n + self.illuPower=power or 1.0 + self.illuPower=self.illuPower * 1000000 + return self +end + +--- Set minimum and maximum detotation altitude for illumination shells. A value between min/max is selected randomly. +-- The illumination bomb will burn for 300 seconds (5 minutes). Assuming a descent rate of ~3 m/s the "optimal" altitude would be 900 m. +-- @param #ARTY self +-- @param #number minalt (Optional) Minium altitude in meters. Default 500 m. +-- @param #number maxalt (Optional) Maximum altitude in meters. Default 1000 m. +-- @return self +function ARTY:SetIlluminationMinMaxAlt(minalt, maxalt) + self.illuMinalt=minalt or 500 + self.illuMaxalt=maxalt or 1000 + + if self.illuMinalt>self.illuMaxalt then + self.illuMinalt=self.illuMaxalt + end + return self +end + +--- Set number of smoke shells available to the group. +-- Note that it can be max the number of normal shells. Also if all normal shells are empty, firing smoke shells is also not possible any more until group gets rearmed. +-- @param #ARTY self +-- @param #number n Number of smoke shells for the whole group. +-- @param Utilities.Utils#SMOKECOLOR color (Optional) Color of the smoke. Default SMOKECOLOR.Red. +-- @return self +function ARTY:SetSmokeShells(n, color) + self.Nsmoke=n + self.smokeColor=color or SMOKECOLOR.Red + return self +end + +--- Set nuclear fires and extra demolition explosions. +-- @param #ARTY self +-- @param #number nfires (Optional) Number of big smoke and fire objects created in the demolition zone. +-- @param #number demolitionrange (Optional) Demolition range in meters. +-- @return self +function ARTY:SetTacNukeFires(nfires, range) + self.nukefire=true + self.nukefires=nfires + self.nukerange=range + return self +end + +--- Enable assigning targets and moves by placing markers on the F10 map. +-- @param #ARTY self +-- @param #number key (Optional) Authorization key. Only players knowing this key can assign targets. Default is no authorization required. +-- @param #boolean readonly (Optional) Marks are readonly and cannot be removed by players. This also means that targets cannot be cancelled by removing the mark. Default false. +-- @return self +function ARTY:SetMarkAssignmentsOn(key, readonly) + self.markkey=key + self.markallow=true + if readonly==nil then + self.markreadonly=false + end + return self +end + +--- Disable assigning targets by placing markers on the F10 map. +-- @param #ARTY self +-- @return self +function ARTY:SetMarkTargetsOff() + self.markallow=false + self.markkey=nil + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Start Event +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- After "Start" event. Initialized ROE and alarm state. Starts the event handler. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARTY:onafterStart(Controllable, From, Event, To) + self:_EventFromTo("onafterStart", Event, From, To) + + -- Debug output. + local text=string.format("Started ARTY version %s for group %s.", ARTY.version, Controllable:GetName()) + self:I(self.lid..text) + MESSAGE:New(text, 5):ToAllIf(self.Debug) + + -- Get Ammo. + self.Nammo0, self.Nshells0, self.Nrockets0, self.Nmissiles0=self:GetAmmo(self.Debug) + + -- Init nuclear explosion parameters if they were not set by user. + if self.nukerange==nil then + self.nukerange=1500/75000*self.nukewarhead -- linear dependence + end + if self.nukefires==nil then + self.nukefires=20/1000/1000*self.nukerange*self.nukerange + end + + -- Init nuclear shells. + if self.Nukes~=nil then + self.Nukes0=math.min(self.Nukes, self.Nshells0) + else + self.Nukes=0 + self.Nukes0=0 + end + + -- Init illumination shells. + if self.Nillu~=nil then + self.Nillu0=math.min(self.Nillu, self.Nshells0) + else + self.Nillu=0 + self.Nillu0=0 + end + + -- Init smoke shells. + if self.Nsmoke~=nil then + self.Nsmoke0=math.min(self.Nsmoke, self.Nshells0) + else + self.Nsmoke=0 + self.Nsmoke0=0 + end + + -- Check if we have and arty type that is in the DB. + local _dbproperties=self:_CheckDB(self.DisplayName) + self:T({dbproperties=_dbproperties}) + if _dbproperties~=nil then + for property,value in pairs(_dbproperties) do + self:T({property=property, value=value}) + self[property]=value + end + end + + -- Some mobility consitency checks if group cannot move. + if not self.ismobile then + self.RearmingPlaceCoord=nil + self.relocateafterfire=false + self.autorelocate=false + --self.RearmingGroupSpeed=20 + end + + -- Check that default speed is below max speed. + self.Speed=math.min(self.Speed, self.SpeedMax) + + -- Set Rearming group speed if not specified by user + if self.RearmingGroup then + + -- Get max speed of rearming group. + local speedmax=self.RearmingGroup:GetSpeedMax() + self:T(self.lid..string.format("%s, rearming group %s max speed = %.1f km/h.", self.groupname, self.RearmingGroup:GetName(), speedmax)) + + if self.RearmingGroupSpeed==nil then + -- Set rearming group speed to 50% of max possible speed. + self.RearmingGroupSpeed=speedmax*0.5 + else + -- Ensure that speed is <= max speed. + self.RearmingGroupSpeed=math.min(self.RearmingGroupSpeed, self.RearmingGroup:GetSpeedMax()) + end + else + -- Just to have a reasonable number for output format below. + self.RearmingGroupSpeed=23 + end + + local text=string.format("\n******************************************************\n") + text=text..string.format("Arty group = %s\n", self.groupname) + text=text..string.format("Arty alias = %s\n", self.alias) + text=text..string.format("Artillery attribute = %s\n", tostring(self.IsArtillery)) + text=text..string.format("Type = %s\n", self.Type) + text=text..string.format("Display Name = %s\n", self.DisplayName) + text=text..string.format("Number of units = %d\n", self.IniGroupStrength) + text=text..string.format("Speed max = %d km/h\n", self.SpeedMax) + text=text..string.format("Speed default = %d km/h\n", self.Speed) + text=text..string.format("Is mobile = %s\n", tostring(self.ismobile)) + text=text..string.format("Is cargo = %s\n", tostring(self.iscargo)) + text=text..string.format("Min range = %.1f km\n", self.minrange/1000) + text=text..string.format("Max range = %.1f km\n", self.maxrange/1000) + text=text..string.format("Total ammo count = %d\n", self.Nammo0) + text=text..string.format("Number of shells = %d\n", self.Nshells0) + text=text..string.format("Number of rockets = %d\n", self.Nrockets0) + text=text..string.format("Number of missiles = %d\n", self.Nmissiles0) + text=text..string.format("Number of nukes = %d\n", self.Nukes0) + text=text..string.format("Nuclear warhead = %d tons TNT\n", self.nukewarhead/1000) + text=text..string.format("Nuclear demolition = %d m\n", self.nukerange) + text=text..string.format("Nuclear fires = %d (active=%s)\n", self.nukefires, tostring(self.nukefire)) + text=text..string.format("Number of illum. = %d\n", self.Nillu0) + text=text..string.format("Illuminaton Power = %.3f mcd\n", self.illuPower/1000000) + text=text..string.format("Illuminaton Minalt = %d m\n", self.illuMinalt) + text=text..string.format("Illuminaton Maxalt = %d m\n", self.illuMaxalt) + text=text..string.format("Number of smoke = %d\n", self.Nsmoke0) + text=text..string.format("Smoke color = %d\n", self.smokeColor) + if self.RearmingGroup or self.RearmingPlaceCoord then + text=text..string.format("Rearming safe dist. = %d m\n", self.RearmingDistance) + end + if self.RearmingGroup then + text=text..string.format("Rearming group = %s\n", self.RearmingGroup:GetName()) + text=text..string.format("Rearming group speed= %d km/h\n", self.RearmingGroupSpeed) + text=text..string.format("Rearming group roads= %s\n", tostring(self.RearmingGroupOnRoad)) + end + if self.RearmingPlaceCoord then + local dist=self.InitialCoord:Get2DDistance(self.RearmingPlaceCoord) + text=text..string.format("Rearming coord dist = %d m\n", dist) + text=text..string.format("Rearming ARTY roads = %s\n", tostring(self.RearmingArtyOnRoad)) + end + text=text..string.format("Relocate after fire = %s\n", tostring(self.relocateafterfire)) + text=text..string.format("Relocate min dist. = %d m\n", self.relocateRmin) + text=text..string.format("Relocate max dist. = %d m\n", self.relocateRmax) + text=text..string.format("Auto move in range = %s\n", tostring(self.autorelocate)) + text=text..string.format("Auto move dist. max = %.1f km\n", self.autorelocatemaxdist/1000) + text=text..string.format("Auto move on road = %s\n", tostring(self.autorelocateonroad)) + text=text..string.format("Marker assignments = %s\n", tostring(self.markallow)) + text=text..string.format("Marker auth. key = %s\n", tostring(self.markkey)) + text=text..string.format("Marker readonly = %s\n", tostring(self.markreadonly)) + text=text..string.format("Clusters:\n") + for _,cluster in pairs(self.clusters) do + text=text..string.format("- %s\n", tostring(cluster)) + end + text=text..string.format("******************************************************\n") + text=text..string.format("Targets:\n") + for _, target in pairs(self.targets) do + text=text..string.format("- %s\n", self:_TargetInfo(target)) + local possible=self:_CheckWeaponTypePossible(target) + if not possible then + self:E(self.lid..string.format("WARNING: Selected weapon type %s is not possible", self:_WeaponTypeName(target.weapontype))) + end + if self.Debug then + local zone=ZONE_RADIUS:New(target.name, target.coord:GetVec2(), target.radius) + zone:BoundZone(180, coalition.side.NEUTRAL) + end + end + text=text..string.format("Moves:\n") + for i=1,#self.moves do + text=text..string.format("- %s\n", self:_MoveInfo(self.moves[i])) + end + text=text..string.format("******************************************************\n") + text=text..string.format("Shell types:\n") + for _,_type in pairs(self.ammoshells) do + text=text..string.format("- %s\n", _type) + end + text=text..string.format("Rocket types:\n") + for _,_type in pairs(self.ammorockets) do + text=text..string.format("- %s\n", _type) + end + text=text..string.format("Missile types:\n") + for _,_type in pairs(self.ammomissiles) do + text=text..string.format("- %s\n", _type) + end + text=text..string.format("******************************************************") + if self.Debug then + self:I(self.lid..text) + else + self:T(self.lid..text) + end + + -- Set default ROE to weapon hold. + self.Controllable:OptionROEHoldFire() + + -- Add event handler. + self:HandleEvent(EVENTS.Shot) --, self._OnEventShot) + self:HandleEvent(EVENTS.Dead) --, self._OnEventDead) + --self:HandleEvent(EVENTS.MarkAdded, self._OnEventMarkAdded) + + -- Add DCS event handler - necessary for S_EVENT_MARK_* events. So we only start it, if this was requested. + if self.markallow then + world.addEventHandler(self) + end + + -- Start checking status. + self:__Status(self.StatusInterval) +end + +--- Check the DB for properties of the specified artillery unit type. +-- @param #ARTY self +-- @return #table Properties of the requested artillery type. Returns nil if no matching DB entry could be found. +function ARTY:_CheckDB(displayname) + for _type,_properties in pairs(ARTY.db) do + self:T({type=_type, properties=_properties}) + if _type==displayname then + self:T({type=_type, properties=_properties}) + return _properties + end + end + return nil +end + +--- After "Start" event. Initialized ROE and alarm state. Starts the event handler. +-- @param #ARTY self +-- @param #boolean display (Optional) If true, send message to coalition. Default false. +function ARTY:_StatusReport(display) + + -- Set default. + if display==nil then + display=false + end + + -- Get Ammo. + local Nammo, Nshells, Nrockets, Nmissiles=self:GetAmmo() + local Nnukes=self.Nukes + local Nillu=self.Nillu + local Nsmoke=self.Nsmoke + + local Tnow=timer.getTime() + local Clock=self:_SecondsToClock(timer.getAbsTime()) + + local text=string.format("\n******************* STATUS ***************************\n") + text=text..string.format("ARTY group = %s\n", self.groupname) + text=text..string.format("Clock = %s\n", Clock) + text=text..string.format("FSM state = %s\n", self:GetState()) + text=text..string.format("Total ammo count = %d\n", Nammo) + text=text..string.format("Number of shells = %d\n", Nshells) + text=text..string.format("Number of rockets = %d\n", Nrockets) + text=text..string.format("Number of missiles = %d\n", Nmissiles) + text=text..string.format("Number of nukes = %d\n", Nnukes) + text=text..string.format("Number of illum. = %d\n", Nillu) + text=text..string.format("Number of smoke = %d\n", Nsmoke) + if self.currentTarget then + text=text..string.format("Current Target = %s\n", tostring(self.currentTarget.name)) + text=text..string.format("Curr. Tgt assigned = %d\n", Tnow-self.currentTarget.Tassigned) + else + text=text..string.format("Current Target = %s\n", "none") + end + text=text..string.format("Nshots curr. Target = %d\n", self.Nshots) + text=text..string.format("Targets:\n") + for i=1,#self.targets do + text=text..string.format("- %s\n", self:_TargetInfo(self.targets[i])) + end + if self.currentMove then + text=text..string.format("Current Move = %s\n", tostring(self.currentMove.name)) + else + text=text..string.format("Current Move = %s\n", "none") + end + text=text..string.format("Moves:\n") + for i=1,#self.moves do + text=text..string.format("- %s\n", self:_MoveInfo(self.moves[i])) + end + text=text..string.format("******************************************************") + env.info(self.lid..text) + MESSAGE:New(text, 20):Clear():ToCoalitionIf(self.coalition, display) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Event Handling +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Eventhandler for shot event. +-- @param #ARTY self +-- @param Core.Event#EVENTDATA EventData +function ARTY:OnEventShot(EventData) + self:F(EventData) + + -- Weapon data. + local _weapon = EventData.Weapon:getTypeName() -- should be the same as Event.WeaponTypeName + local _weaponStrArray = self:_split(_weapon,"%.") + local _weaponName = _weaponStrArray[#_weaponStrArray] + + -- Debug info. + self:T3(self.lid.."EVENT SHOT: Ini unit = "..EventData.IniUnitName) + self:T3(self.lid.."EVENT SHOT: Ini group = "..EventData.IniGroupName) + self:T3(self.lid.."EVENT SHOT: Weapon type = ".._weapon) + self:T3(self.lid.."EVENT SHOT: Weapon name = ".._weaponName) + + local group = EventData.IniGroup --Wrapper.Group#GROUP + + if group and group:IsAlive() then + + if EventData.IniGroupName == self.groupname then + + if self.currentTarget then + + -- Increase number of shots fired by this group on this target. + self.Nshots=self.Nshots+1 + + -- Debug output. + local text=string.format("%s, fired shot %d of %d with weapon %s on target %s.", self.alias, self.Nshots, self.currentTarget.nshells, _weaponName, self.currentTarget.name) + self:T(self.lid..text) + MESSAGE:New(text, 5):Clear():ToAllIf(self.report or self.Debug) + + -- Last known position of the weapon fired. + local _lastpos={x=0, y=0, z=0} + + --- Track the position of the weapon if it is supposed to model a tac nuke, illumination or smoke shell. + -- @param #table _weapon + local function _TrackWeapon(_data) + + -- When the pcall status returns false the weapon has hit. + local _weaponalive,_currpos = pcall( + function() + return _data.weapon:getPoint() + end) + + -- Debug + self:T3(self.lid..string.format("ARTY %s: Weapon still in air: %s", self.groupname, tostring(_weaponalive))) + + -- Destroy weapon before impact. + local _destroyweapon=false + + if _weaponalive then + + -- Update last position. + _lastpos={x=_currpos.x, y=_currpos.y, z=_currpos.z} + + -- Coordinate and distance to target. + local _coord=COORDINATE:NewFromVec3(_lastpos) + local _dist=_coord:Get2DDistance(_data.target.coord) + + -- Debug + self:T3(self.lid..string.format("ARTY %s weapon to target dist = %d m", self.groupname,_dist)) + + if _data.target.weapontype==ARTY.WeaponType.IlluminationShells then + + -- Check if within distace. + if _dist<_data.target.radius then + + -- Get random coordinate within certain radius of the target. + local _cr=_data.target.coord:GetRandomCoordinateInRadius(_data.target.radius) + + -- Get random altitude over target. + local _alt=_cr:GetLandHeight()+math.random(self.illuMinalt, self.illuMaxalt) + + -- Adjust explosion height of coordinate. + local _ci=COORDINATE:New(_cr.x,_alt,_cr.z) + + -- Create illumination flare. + _ci:IlluminationBomb(self.illuPower) + + -- Destroy actual shell. + _destroyweapon=true + end + + elseif _data.target.weapontype==ARTY.WeaponType.SmokeShells then + + if _dist<_data.target.radius then + + -- Get random coordinate within a certain radius. + local _cr=_coord:GetRandomCoordinateInRadius(_data.target.radius) + + -- Fire smoke at this coordinate. + _cr:Smoke(self.smokeColor) + + -- Destroy actual shell. + _destroyweapon=true + + end + + end + + if _destroyweapon then + + self:T2(self.lid..string.format("ARTY %s destroying shell, stopping timer.", self.groupname)) + + -- Destroy weapon and stop timer. + _data.weapon:destroy() + return nil + + else + + -- TODO: Make dt input parameter. + local dt=0.02 + + self:T3(self.lid..string.format("ARTY %s tracking weapon again in %.3f seconds", self.groupname, dt)) + + -- Check again in 0.05 seconds. + return timer.getTime() + dt + + end + + else + + -- Get impact coordinate. + local _impactcoord=COORDINATE:NewFromVec3(_lastpos) + + self:I(self.lid..string.format("ARTY %s weapon NOT ALIVE any more.", self.groupname)) + + -- Create a "nuclear" explosion and blast at the impact point. + if _data.target.weapontype==ARTY.WeaponType.TacticalNukes then + self:T(self.lid..string.format("ARTY %s triggering nuclear explosion in one second.", self.groupname)) + SCHEDULER:New(nil, ARTY._NuclearBlast, {self,_impactcoord}, 1.0) + end + + -- Stop timer. + return nil + + end + + end + + -- Start track the shell if we want to model a tactical nuke. + local _tracknuke = self.currentTarget.weapontype==ARTY.WeaponType.TacticalNukes and self.Nukes>0 + local _trackillu = self.currentTarget.weapontype==ARTY.WeaponType.IlluminationShells and self.Nillu>0 + local _tracksmoke = self.currentTarget.weapontype==ARTY.WeaponType.SmokeShells and self.Nsmoke>0 + if _tracknuke or _trackillu or _tracksmoke then + + self:T(self.lid..string.format("ARTY %s: Tracking of weapon starts in two seconds.", self.groupname)) + + local _peter={} + _peter.weapon=EventData.weapon + _peter.target=UTILS.DeepCopy(self.currentTarget) + + timer.scheduleFunction(_TrackWeapon, _peter, timer.getTime() + 2.0) + end + + -- Get current ammo. + local _nammo,_nshells,_nrockets,_nmissiles=self:GetAmmo() + + -- Decrease available nukes because we just fired one. + if self.currentTarget.weapontype==ARTY.WeaponType.TacticalNukes then + self.Nukes=self.Nukes-1 + end + + -- Decrease available illuminatin shells because we just fired one. + if self.currentTarget.weapontype==ARTY.WeaponType.IlluminationShells then + self.Nillu=self.Nillu-1 + end + + -- Decrease available illuminatin shells because we just fired one. + if self.currentTarget.weapontype==ARTY.WeaponType.SmokeShells then + self.Nsmoke=self.Nsmoke-1 + end + + -- Check if we are completely out of ammo. + local _outofammo=false + if _nammo==0 then + self:T(self.lid..string.format("Group %s completely out of ammo.", self.groupname)) + _outofammo=true + end + + -- Check if we are out of ammo of the weapon type used for this target. + -- Note that should not happen because we only open fire with the available number of shots. + local _partlyoutofammo=self:_CheckOutOfAmmo({self.currentTarget}) + + -- Weapon type name for current target. + local _weapontype=self:_WeaponTypeName(self.currentTarget.weapontype) + self:T(self.lid..string.format("Group %s ammo: total=%d, shells=%d, rockets=%d, missiles=%d", self.groupname, _nammo, _nshells, _nrockets, _nmissiles)) + self:T(self.lid..string.format("Group %s uses weapontype %s for current target.", self.groupname, _weapontype)) + + -- Default switches for cease fire and relocation. + local _ceasefire=false + local _relocate=false + + -- Check if number of shots reached max. + if self.Nshots >= self.currentTarget.nshells then + + -- Debug message + local text=string.format("Group %s stop firing on target %s.", self.groupname, self.currentTarget.name) + self:T(self.lid..text) + MESSAGE:New(text, 5):ToAllIf(self.Debug) + + -- Cease fire. + _ceasefire=true + + -- Relocate if enabled. + _relocate=self.relocateafterfire + end + + -- Check if we are (partly) out of ammo. + if _outofammo or _partlyoutofammo then + _ceasefire=true + end + + -- Relocate position. + if _relocate then + self:_Relocate() + end + + -- Cease fire on current target. + if _ceasefire then + self:CeaseFire(self.currentTarget) + end + + else + self:E(self.lid..string.format("WARNING: No current target for group %s?!", self.groupname)) + end + end + end +end + + +--- After "Start" event. Initialized ROE and alarm state. Starts the event handler. +-- @param #ARTY self +-- @param #table Event +function ARTY:onEvent(Event) + + if Event == nil or Event.idx == nil then + self:T3("Skipping onEvent. Event or Event.idx unknown.") + return true + end + + -- Set battery and coalition. + --local batteryname=self.groupname + --local batterycoalition=self.Controllable:GetCoalition() + + self:T2(string.format("Event captured = %s", tostring(self.groupname))) + self:T2(string.format("Event id = %s", tostring(Event.id))) + self:T2(string.format("Event time = %s", tostring(Event.time))) + self:T2(string.format("Event idx = %s", tostring(Event.idx))) + self:T2(string.format("Event coalition = %s", tostring(Event.coalition))) + self:T2(string.format("Event group id = %s", tostring(Event.groupID))) + self:T2(string.format("Event text = %s", tostring(Event.text))) + if Event.initiator~=nil then + local _unitname=Event.initiator:getName() + self:T2(string.format("Event ini unit name = %s", tostring(_unitname))) + end + + if Event.id==world.event.S_EVENT_MARK_ADDED then + self:T2({event="S_EVENT_MARK_ADDED", battery=self.groupname, vec3=Event.pos}) + + elseif Event.id==world.event.S_EVENT_MARK_CHANGE then + self:T({event="S_EVENT_MARK_CHANGE", battery=self.groupname, vec3=Event.pos}) + + -- Handle event. + self:_OnEventMarkChange(Event) + + elseif Event.id==world.event.S_EVENT_MARK_REMOVED then + self:T2({event="S_EVENT_MARK_REMOVED", battery=self.groupname, vec3=Event.pos}) + + -- Hande event. + self:_OnEventMarkRemove(Event) + end + +end + +--- Function called when a F10 map mark was removed. +-- @param #ARTY self +-- @param #table Event Event data. +function ARTY:_OnEventMarkRemove(Event) + + -- Get battery coalition and name. + local batterycoalition=self.coalition + --local batteryname=self.groupname + + if Event.text~=nil and Event.text:find("BATTERY") then + + -- Init defaults. + local _cancelmove=false + local _canceltarget=false + local _name="" + local _id=nil + + -- Check for key phrases of relocation or engagements in marker text. If not, return. + if Event.text:find("Marked Relocation") then + _cancelmove=true + _name=self:_MarkMoveName(Event.idx) + _id=self:_GetMoveIndexByName(_name) + elseif Event.text:find("Marked Target") then + _canceltarget=true + _name=self:_MarkTargetName(Event.idx) + _id=self:_GetTargetIndexByName(_name) + else + return + end + + -- Check if there is a task which matches. + if _id==nil then + return + end + + -- Check if the coalition is the same or an authorization key has been defined. + if (batterycoalition==Event.coalition and self.markkey==nil) or self.markkey~=nil then + + -- Authentify key + local _validkey=self:_MarkerKeyAuthentification(Event.text) + + -- Check if we have the right coalition. + if _validkey then + + -- This should be the unique name of the target or move. + if _cancelmove then + if self.currentMove and self.currentMove.name==_name then + -- We do clear tasks here because in Arrived() it can cause a CTD if the group did actually arrive! + self.Controllable:ClearTasks() + -- Current move is removed here. In contrast to RemoveTarget() there are is no maxengage parameter. + self:Arrived() + else + -- Remove move from queue + self:RemoveMove(_name) + end + elseif _canceltarget then + if self.currentTarget and self.currentTarget.name==_name then + -- Cease fire. + self:CeaseFire(self.currentTarget) + -- We still need to remove the target, because there might be more planned engagements (maxengage>1). + self:RemoveTarget(_name) + else + -- Remove target from queue + self:RemoveTarget(_name) + end + end + + end + end + end +end + +--- Function called when a F10 map mark was changed. This happens when a user enters text. +-- @param #ARTY self +-- @param #table Event Event data. +function ARTY:_OnEventMarkChange(Event) + + -- Check if marker has a text and the "arty" keyword. + if Event.text~=nil and Event.text:lower():find("arty") then + + -- Convert (wrong x-->z, z-->x) vec3 + -- DONE: This needs to be "fixed", once DCS gives the correct numbers for x and z. + -- Was fixed in DCS 2.5.5.34644! + local vec3={y=Event.pos.y, x=Event.pos.x, z=Event.pos.z} + --local vec3={y=Event.pos.y, x=Event.pos.z, z=Event.pos.x} + + -- Get coordinate from vec3. + local _coord=COORDINATE:NewFromVec3(vec3) + + -- Adjust y component to actual land height. When a coordinate is create it uses y=5 m! + _coord.y=_coord:GetLandHeight() + + -- Get battery coalition and name. + local batterycoalition=self.coalition + local batteryname=self.groupname + + -- Check if the coalition is the same or an authorization key has been defined. + if (batterycoalition==Event.coalition and self.markkey==nil) or self.markkey~=nil then + + -- Evaluate marker text and extract parameters. + local _assign=self:_Markertext(Event.text) + + -- Check if ENGAGE or MOVE or REQUEST keywords were found. + if _assign==nil or not (_assign.engage or _assign.move or _assign.request or _assign.cancel or _assign.set) then + self:T(self.lid..string.format("WARNING: %s, no keyword ENGAGE, MOVE, REQUEST, CANCEL or SET in mark text! Command will not be executed. Text:\n%s", self.groupname, Event.text)) + return + end + + -- Check if job is assigned to this ARTY group. Default is for all ARTY groups. + local _assigned=false + + -- If any array is filled something has been assigned. + if _assign.everyone then + + -- Everyone was addressed. + _assigned=true + + else --#_assign.battery>0 or #_assign.aliases>0 or #_assign.cluster>0 then + + -- Loop over batteries. + for _,bat in pairs(_assign.battery) do + if self.groupname==bat then + _assigned=true + end + end + + -- Loop over aliases. + for _,alias in pairs(_assign.aliases) do + if self.alias==alias then + _assigned=true + end + end + + -- Loop over clusters. + for _,bat in pairs(_assign.cluster) do + for _,cluster in pairs(self.clusters) do + if cluster==bat then + _assigned=true + end + end + end + + end + + -- We were not addressed. + if not _assigned then + self:T3(self.lid..string.format("INFO: ARTY group %s was not addressed! Mark text:\n%s", self.groupname, Event.text)) + return + else + if self.Controllable and self.Controllable:IsAlive() then + + else + self:T3(self.lid..string.format("INFO: ARTY group %s was addressed but is NOT alive! Mark text:\n%s", self.groupname, Event.text)) + return + end + end + + -- Coordinate was given in text, e.g. as lat, long. + if _assign.coord then + _coord=_assign.coord + end + + -- Check if the authorization key is required and if it is valid. + local _validkey=self:_MarkerKeyAuthentification(Event.text) + + -- Handle requests and return. + if _assign.request and _validkey then + if _assign.requestammo then + self:_MarkRequestAmmo() + end + if _assign.requestmoves then + self:_MarkRequestMoves() + end + if _assign.requesttargets then + self:_MarkRequestTargets() + end + if _assign.requeststatus then + self:_MarkRequestStatus() + end + if _assign.requestrearming then + self:Rearm() + end + -- Requests Done ==> End of story! + return + end + + -- Cancel stuff and return. + if _assign.cancel and _validkey then + if _assign.cancelmove and self.currentMove then + self.Controllable:ClearTasks() + self:Arrived() + elseif _assign.canceltarget and self.currentTarget then + self.currentTarget.engaged=self.currentTarget.engaged+1 + self:CeaseFire(self.currentTarget) + elseif _assign.cancelrearm and self:is("Rearming") then + local nammo=self:GetAmmo() + if nammo>0 then + self:Rearmed() + else + self:Winchester() + end + end + -- Cancels Done ==> End of story! + return + end + + -- Set stuff and return. + if _assign.set and _validkey then + if _assign.setrearmingplace and self.ismobile then + self:SetRearmingPlace(_coord) + _coord:RemoveMark(Event.idx) + _coord:MarkToCoalition(string.format("Rearming place for battery %s", self.groupname), self.coalition, false, string.format("New rearming place for battery %s defined.", self.groupname)) + if self.Debug then + _coord:SmokeOrange() + end + end + if _assign.setrearminggroup then + _coord:RemoveMark(Event.idx) + local rearminggroupcoord=_assign.setrearminggroup:GetCoordinate() + rearminggroupcoord:MarkToCoalition(string.format("Rearming group for battery %s", self.groupname), self.coalition, false, string.format("New rearming group for battery %s defined.", self.groupname)) + self:SetRearmingGroup(_assign.setrearminggroup) + if self.Debug then + rearminggroupcoord:SmokeOrange() + end + end + -- Set stuff Done ==> End of story! + return + end + + -- Handle engagements and relocations. + if _validkey then + + -- Remove old mark because it might contain confidential data such as the key. + -- Also I don't know who can see the mark which was created. + _coord:RemoveMark(Event.idx) + + -- Anticipate marker ID. + -- WARNING: Make sure, no marks are set until the COORDINATE:MarkToCoalition() is called or the target/move name will be wrong and target cannot be removed by deleting its marker. + local _id=UTILS._MarkID+1 + + if _assign.move then + + -- Create a new name. This determins the string we search when deleting a move! + local _name=self:_MarkMoveName(_id) + + local text=string.format("%s, received new relocation assignment.", self.alias) + text=text..string.format("\nCoordinates %s",_coord:ToStringLLDMS()) + MESSAGE:New(text, 10):ToCoalitionIf(batterycoalition, self.report or self.Debug) + + -- Assign a relocation of the arty group. + local _movename=self:AssignMoveCoord(_coord, _assign.time, _assign.speed, _assign.onroad, _assign.movecanceltarget,_name, true) + + if _movename~=nil then + local _mid=self:_GetMoveIndexByName(_movename) + local _move=self.moves[_mid] + + -- Create new target name. + local clock=tostring(self:_SecondsToClock(_move.time)) + local _markertext=_movename..string.format(", Time=%s, Speed=%d km/h, Use Roads=%s.", clock, _move.speed, tostring(_move.onroad)) + + -- Create a new mark. This will trigger the mark added event. + local _randomcoord=_coord:GetRandomCoordinateInRadius(100) + _randomcoord:MarkToCoalition(_markertext, batterycoalition, self.markreadonly or _assign.readonly) + else + local text=string.format("%s, relocation not possible.", self.alias) + MESSAGE:New(text, 10):ToCoalitionIf(batterycoalition, self.report or self.Debug) + end + + else + + -- Create a new name. + local _name=self:_MarkTargetName(_id) + + local text=string.format("%s, received new target assignment.", self.alias) + text=text..string.format("\nCoordinates %s",_coord:ToStringLLDMS()) + if _assign.time then + text=text..string.format("\nTime %s",_assign.time) + end + if _assign.prio then + text=text..string.format("\nPrio %d",_assign.prio) + end + if _assign.radius then + text=text..string.format("\nRadius %d m",_assign.radius) + end + if _assign.nshells then + text=text..string.format("\nShots %d",_assign.nshells) + end + if _assign.maxengage then + text=text..string.format("\nEngagements %d",_assign.maxengage) + end + if _assign.weapontype then + text=text..string.format("\nWeapon %s",self:_WeaponTypeName(_assign.weapontype)) + end + MESSAGE:New(text, 10):ToCoalitionIf(batterycoalition, self.report or self.Debug) + + -- Assign a new firing engagement. + -- Note, we set unique=true so this target gets only added once. + local _targetname=self:AssignTargetCoord(_coord,_assign.prio,_assign.radius,_assign.nshells,_assign.maxengage,_assign.time,_assign.weapontype, _name, true) + + if _targetname~=nil then + local _tid=self:_GetTargetIndexByName(_targetname) + local _target=self.targets[_tid] + + -- Create new target name. + local clock=tostring(self:_SecondsToClock(_target.time)) + local weapon=self:_WeaponTypeName(_target.weapontype) + local _markertext=_targetname..string.format(", Priority=%d, Radius=%d m, Shots=%d, Engagements=%d, Weapon=%s, Time=%s", _target.prio, _target.radius, _target.nshells, _target.maxengage, weapon, clock) + + -- Create a new mark. This will trigger the mark added event. + local _randomcoord=_coord:GetRandomCoordinateInRadius(250) + _randomcoord:MarkToCoalition(_markertext, batterycoalition, self.markreadonly or _assign.readonly) + end + end + end + + end + end + +end + +--- Event handler for event Dead. +-- @param #ARTY self +-- @param Core.Event#EVENTDATA EventData +function ARTY:OnEventDead(EventData) + self:F(EventData) + + -- Name of controllable. + local _name=self.groupname + + -- Check for correct group. + if EventData and EventData.IniGroupName and EventData.IniGroupName==_name then + + -- Name of the dead unit. + local unitname=tostring(EventData.IniUnitName) + + -- Dead Unit. + self:T(self.lid..string.format("%s: Captured dead event for unit %s.", _name, unitname)) + + -- FSM Dead event. We give one second for update of data base. + --self:__Dead(1, unitname) + self:Dead(unitname) + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Events and States +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- After "Status" event. Report status of group. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARTY:onafterStatus(Controllable, From, Event, To) + self:_EventFromTo("onafterStatus", Event, From, To) + + -- Get ammo. + local nammo, nshells, nrockets, nmissiles=self:GetAmmo() + + -- We have a cargo group ==> check if group was loaded into a carrier. + if self.iscargo and self.cargogroup then + if self.cargogroup:IsLoaded() and not self:is("InTransit") then + -- Group is now InTransit state. Current target is canceled. + self:T(self.lid..string.format("Group %s has been loaded into a carrier and is now transported.", self.alias)) + self:Loaded() + elseif self.cargogroup:IsUnLoaded() then + -- Group has been unloaded and is combat ready again. + self:T(self.lid..string.format("Group %s has been unloaded from the carrier.", self.alias)) + self:UnLoaded() + end + end + + -- FSM state. + local fsmstate=self:GetState() + self:T(self.lid..string.format("Status %s, Ammo total=%d: shells=%d [smoke=%d, illu=%d, nukes=%d*%.3f kT], rockets=%d, missiles=%d", fsmstate, nammo, nshells, self.Nsmoke, self.Nillu, self.Nukes, self.nukewarhead/1000000, nrockets, nmissiles)) + + if self.Controllable and self.Controllable:IsAlive() then + + -- Debug current status info. + if self.Debug then + self:_StatusReport() + end + + -- Group on the move. + if self:is("Moving") then + self:T2(self.lid..string.format("%s: Moving", Controllable:GetName())) + end + + -- Group is rearming. + if self:is("Rearming") then + local _rearmed=self:_CheckRearmed() + if _rearmed then + self:T2(self.lid..string.format("%s: Rearming ==> Rearmed", Controllable:GetName())) + self:Rearmed() + end + end + + -- Group finished rearming. + if self:is("Rearmed") then + local distance=self.Controllable:GetCoordinate():Get2DDistance(self.InitialCoord) + self:T2(self.lid..string.format("%s: Rearmed. Distance ARTY to InitalCoord = %d m", Controllable:GetName(), distance)) + -- Check that ARTY group is back and set it to combat ready. + if distance <= self.RearmingDistance then + self:T2(self.lid..string.format("%s: Rearmed ==> CombatReady", Controllable:GetName())) + self:CombatReady() + end + end + + -- Group arrived at destination. + if self:is("Arrived") then + self:T2(self.lid..string.format("%s: Arrived ==> CombatReady", Controllable:GetName())) + self:CombatReady() + end + + -- Group is firing on target. + if self:is("Firing") then + -- Check that firing started after ~5 min. If not, target is removed. + self:_CheckShootingStarted() + end + + -- Check if targets are in range and update target.inrange value. + self:_CheckTargetsInRange() + + -- Check if selected weapon type for target is possible at all. E.g. request rockets for Paladin. + local notpossible={} + for i=1,#self.targets do + local _target=self.targets[i] + local possible=self:_CheckWeaponTypePossible(_target) + if not possible then + table.insert(notpossible, _target.name) + end + end + for _,targetname in pairs(notpossible) do + self:E(self.lid..string.format("%s: Removing target %s because requested weapon is not possible with this type of unit.", self.groupname, targetname)) + self:RemoveTarget(targetname) + end + + -- Get a valid timed target if it is due to be attacked. + local _timedTarget=self:_CheckTimedTargets() + + -- Get a valid normal target (one that is not timed). + local _normalTarget=self:_CheckNormalTargets() + + -- Get a commaned move to another location. + local _move=self:_CheckMoves() + + if _move then + + -- Command to move. + self:Move(_move) + + elseif _timedTarget then + + -- Cease fire on current target first. + if self.currentTarget then + self:CeaseFire(self.currentTarget) + end + + -- Open fire on timed target. + self:OpenFire(_timedTarget) + + elseif _normalTarget then + + -- Open fire on normal target. + self:OpenFire(_normalTarget) + + end + + -- Get ammo. + --local nammo, nshells, nrockets, nmissiles=self:GetAmmo() + + -- Check if we have a target in the queue for which weapons are still available. + local gotsome=false + if #self.targets>0 then + for i=1,#self.targets do + local _target=self.targets[i] + if self:_CheckWeaponTypeAvailable(_target)>0 then + gotsome=true + end + end + else + -- No targets in the queue. + gotsome=true + end + + -- No ammo available. Either completely blank or only queued targets for ammo which is out. + if (nammo==0 or not gotsome) and not (self:is("Moving") or self:is("Rearming") or self:is("OutOfAmmo")) then + self:Winchester() + end + + -- Group is out of ammo. + if self:is("OutOfAmmo") then + self:T2(self.lid..string.format("%s: OutOfAmmo ==> Rearm ==> Rearming", Controllable:GetName())) + self:Rearm() + end + + -- Call status again in ~10 sec. + self:__Status(self.StatusInterval) + + elseif self.iscargo then + + -- We have a cargo group ==> check if group was loaded into a carrier. + if self.cargogroup and self.cargogroup:IsAlive() then + + -- Group is being transported as cargo ==> skip everything and check again in 5 seconds. + if self:is("InTransit") then + self:__Status(-5) + end + + end + + else + self:E(self.lid..string.format("Arty group %s is not alive!", self.groupname)) + end + + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Before "Loaded" event. Checks if group is currently firing and removes the target by calling CeaseFire. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @return #boolean If true, proceed to onafterLoaded. +function ARTY:onbeforeLoaded(Controllable, From, Event, To) + if self.currentTarget then + self:CeaseFire(self.currentTarget) + end + + return true +end + +--- After "UnLoaded" event. Group is combat ready again. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @return #boolean If true, proceed to onafterLoaded. +function ARTY:onafterUnLoaded(Controllable, From, Event, To) + self:CombatReady() +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Enter "CombatReady" state. Route the group back if necessary. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARTY:onenterCombatReady(Controllable, From, Event, To) + self:_EventFromTo("onenterCombatReady", Event, From, To) + -- Debug info + self:T3(self.lid..string.format("onenterComabReady, from=%s, event=%s, to=%s", From, Event, To)) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Before "OpenFire" event. Checks if group already has a target. Checks for valid min/max range and removes the target if necessary. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #table target Array holding the target info. +-- @return #boolean If true, proceed to onafterOpenfire. +function ARTY:onbeforeOpenFire(Controllable, From, Event, To, target) + self:_EventFromTo("onbeforeOpenFire", Event, From, To) + + -- Check that group has no current target already. + if self.currentTarget then + -- This should not happen. Some earlier check failed. + self:E(self.lid..string.format("ERROR: Group %s already has a target %s!", self.groupname, self.currentTarget.name)) + -- Deny transition. + return false + end + + -- Check if target is in range. + if not self:_TargetInRange(target) then + -- This should not happen. Some earlier check failed. + self:E(self.lid..string.format("ERROR: Group %s, target %s is out of range!", self.groupname, self.currentTarget.name)) + -- Deny transition. + return false + end + + -- Get the number of available shells, rockets or missiles requested for this target. + local nfire=self:_CheckWeaponTypeAvailable(target) + + -- Adjust if less than requested ammo is left. + target.nshells=math.min(target.nshells, nfire) + + -- No ammo left ==> deny transition. + if target.nshells<1 then + local text=string.format("%s, no ammo left to engage target %s with selected weapon type %s.") + return false + end + + return true +end + +--- After "OpenFire" event. Sets the current target and starts the fire at point task. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #ARTY.Target target Array holding the target info. +function ARTY:onafterOpenFire(Controllable, From, Event, To, target) + self:_EventFromTo("onafterOpenFire", Event, From, To) + + -- Get target array index. + local id=self:_GetTargetIndexByName(target.name) + + -- Target is now under fire and has been engaged once more. + if id then + -- Set under fire flag. + self.targets[id].underfire=true + -- Set current target. + self.currentTarget=target + -- Set time the target was assigned. + self.currentTarget.Tassigned=timer.getTime() + end + + -- Distance to target + local range=Controllable:GetCoordinate():Get2DDistance(target.coord) + + -- Get ammo. + local Nammo, Nshells, Nrockets, Nmissiles=self:GetAmmo() + local nfire=Nammo + local _type="shots" + if target.weapontype==ARTY.WeaponType.Auto then + nfire=Nammo + _type="shots" + elseif target.weapontype==ARTY.WeaponType.Cannon then + nfire=Nshells + _type="shells" + elseif target.weapontype==ARTY.WeaponType.TacticalNukes then + nfire=self.Nukes + _type="nuclear shells" + elseif target.weapontype==ARTY.WeaponType.IlluminationShells then + nfire=self.Nillu + _type="illumination shells" + elseif target.weapontype==ARTY.WeaponType.SmokeShells then + nfire=self.Nsmoke + _type="smoke shells" + elseif target.weapontype==ARTY.WeaponType.Rockets then + nfire=Nrockets + _type="rockets" + elseif target.weapontype==ARTY.WeaponType.CruiseMissile then + nfire=Nmissiles + _type="cruise missiles" + end + + -- Adjust if less than requested ammo is left. + target.nshells=math.min(target.nshells, nfire) + + -- Send message. + local text=string.format("%s, opening fire on target %s with %d %s. Distance %.1f km.", Controllable:GetName(), target.name, target.nshells, _type, range/1000) + self:T(self.lid..text) + MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report) + + --if self.Debug then + -- local _coord=target.coord --Core.Point#COORDINATE + -- local text=string.format("ARTY %s, Target %s, n=%d, weapon=%s", self.Controllable:GetName(), target.name, target.nshells, self:_WeaponTypeName(target.weapontype)) + -- _coord:MarkToAll(text) + --end + + -- Start firing. + if target.attackgroup then + self:_AttackGroup(target) + else + self:_FireAtCoord(target.coord, target.radius, target.nshells, target.weapontype) + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- After "CeaseFire" event. Clears task of the group and removes the target if max engagement was reached. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #table target Array holding the target info. +function ARTY:onafterCeaseFire(Controllable, From, Event, To, target) + self:_EventFromTo("onafterCeaseFire", Event, From, To) + + if target then + + -- Send message. + local text=string.format("%s, ceasing fire on target %s.", Controllable:GetName(), target.name) + self:T(self.lid..text) + MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report) + + -- Get target array index. + local id=self:_GetTargetIndexByName(target.name) + + -- We have a target. + if id then + -- Target was actually engaged. (Could happen that engagement was aborted while group was still aiming.) + if self.Nshots>0 then + self.targets[id].engaged=self.targets[id].engaged+1 + -- Clear the attack time. + self.targets[id].time=nil + end + -- Target is not under fire any more. + self.targets[id].underfire=false + end + + -- If number of engagements has been reached, the target is removed. + if target.engaged >= target.maxengage then + self:RemoveTarget(target.name) + end + + -- Set ROE to weapon hold. + self.Controllable:OptionROEHoldFire() + + -- Clear tasks. + self.Controllable:ClearTasks() + + else + self:E(self.lid..string.format("ERROR: No target in cease fire for group %s.", self.groupname)) + end + + -- Set number of shots to zero. + self.Nshots=0 + + -- ARTY group has no current target any more. + self.currentTarget=nil + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- After "Winchester" event. Group is out of ammo. Trigger "Rearm" event. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARTY:onafterWinchester(Controllable, From, Event, To) + self:_EventFromTo("onafterWinchester", Event, From, To) + + -- Send message. + local text=string.format("%s, winchester!", Controllable:GetName()) + self:T(self.lid..text) + MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report or self.Debug) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Before "Rearm" event. Check if a unit to rearm the ARTY group has been defined. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @return #boolean If true, proceed to onafterRearm. +function ARTY:onbeforeRearm(Controllable, From, Event, To) + self:_EventFromTo("onbeforeRearm", Event, From, To) + + local _rearmed=self:_CheckRearmed() + if _rearmed then + self:T(self.lid..string.format("%s, group is already armed to the teeth. Rearming request denied!", self.groupname)) + return false + else + self:T(self.lid..string.format("%s, group might be rearmed.", self.groupname)) + end + + -- Check if a reaming unit or rearming place was specified. + if self.RearmingGroup and self.RearmingGroup:IsAlive() then + return true + elseif self.RearmingPlaceCoord then + return true + else + return false + end + +end + +--- After "Rearm" event. Send message if reporting is on. Route rearming unit to ARTY group. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARTY:onafterRearm(Controllable, From, Event, To) + self:_EventFromTo("onafterRearm", Event, From, To) + + -- Coordinate of ARTY unit. + local coordARTY=self.Controllable:GetCoordinate() + + -- Remember current coordinates so that we find our way back home. + self.InitialCoord=coordARTY + + -- Coordinate of rearming group. + local coordRARM=nil + if self.RearmingGroup then + -- Coordinate of the rearming unit. + coordRARM=self.RearmingGroup:GetCoordinate() + -- Remember the coordinates of the rearming unit. After rearming it will go back to this position. + self.RearmingGroupCoord=coordRARM + end + + if self.RearmingGroup and self.RearmingPlaceCoord and self.ismobile then + + -- CASE 1: Rearming unit and ARTY group meet at rearming place. + + -- Send message. + local text=string.format("%s, %s, request rearming at rearming place.", Controllable:GetName(), self.RearmingGroup:GetName()) + self:T(self.lid..text) + MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report or self.Debug) + + -- Distances. + local dA=coordARTY:Get2DDistance(self.RearmingPlaceCoord) + local dR=coordRARM:Get2DDistance(self.RearmingPlaceCoord) + + -- Route ARTY group to rearming place. + if dA > self.RearmingDistance then + local _tocoord=self:_VicinityCoord(self.RearmingPlaceCoord, self.RearmingDistance/4, self.RearmingDistance/2) + self:AssignMoveCoord(_tocoord, nil, nil, self.RearmingArtyOnRoad, false, "REARMING MOVE TO REARMING PLACE", true) + end + + -- Route Rearming group to rearming place. + if dR > self.RearmingDistance then + local ToCoord=self:_VicinityCoord(self.RearmingPlaceCoord, self.RearmingDistance/4, self.RearmingDistance/2) + self:_Move(self.RearmingGroup, ToCoord, self.RearmingGroupSpeed, self.RearmingGroupOnRoad) + end + + elseif self.RearmingGroup then + + -- CASE 2: Rearming unit drives to ARTY group. + + -- Send message. + local text=string.format("%s, %s, request rearming.", Controllable:GetName(), self.RearmingGroup:GetName()) + self:T(self.lid..text) + MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report or self.Debug) + + -- Distance between ARTY group and rearming unit. + local distance=coordARTY:Get2DDistance(coordRARM) + + -- If distance is larger than ~100 m, the Rearming unit is routed to the ARTY group. + if distance > self.RearmingDistance then + + -- Route rearming group to ARTY group. + self:_Move(self.RearmingGroup, self:_VicinityCoord(coordARTY), self.RearmingGroupSpeed, self.RearmingGroupOnRoad) + end + + elseif self.RearmingPlaceCoord then + + -- CASE 3: ARTY drives to rearming place. + + -- Send message. + local text=string.format("%s, moving to rearming place.", Controllable:GetName()) + self:T(self.lid..text) + MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report or self.Debug) + + -- Distance. + local dA=coordARTY:Get2DDistance(self.RearmingPlaceCoord) + + -- Route ARTY group to rearming place. + if dA > self.RearmingDistance then + local _tocoord=self:_VicinityCoord(self.RearmingPlaceCoord) + self:AssignMoveCoord(_tocoord, nil, nil, self.RearmingArtyOnRoad, false, "REARMING MOVE TO REARMING PLACE", true) + end + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- After "Rearmed" event. Send ARTY and rearming group back to their inital positions. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARTY:onafterRearmed(Controllable, From, Event, To) + self:_EventFromTo("onafterRearmed", Event, From, To) + + -- Send message. + local text=string.format("%s, rearming complete.", Controllable:GetName()) + self:T(self.lid..text) + MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report or self.Debug) + + -- "Rearm" tactical nukes as well. + self.Nukes=self.Nukes0 + self.Nillu=self.Nillu0 + self.Nsmoke=self.Nsmoke0 + + -- Route ARTY group back to where it came from (if distance is > 100 m). + local dist=self.Controllable:GetCoordinate():Get2DDistance(self.InitialCoord) + if dist > self.RearmingDistance then + self:AssignMoveCoord(self.InitialCoord, nil, nil, self.RearmingArtyOnRoad, false, "REARMING MOVE REARMING COMPLETE", true) + end + + -- Route unit back to where it came from (if distance is > 100 m). + if self.RearmingGroup and self.RearmingGroup:IsAlive() then + local d=self.RearmingGroup:GetCoordinate():Get2DDistance(self.RearmingGroupCoord) + if d > self.RearmingDistance then + self:_Move(self.RearmingGroup, self.RearmingGroupCoord, self.RearmingGroupSpeed, self.RearmingGroupOnRoad) + else + -- Clear tasks. + self.RearmingGroup:ClearTasks() + end + end + +end + +--- Check if ARTY group is rearmed, i.e. has its full amount of ammo. +-- @param #ARTY self +-- @return #boolean True if rearming is complete, false otherwise. +function ARTY:_CheckRearmed() + self:F2() + + -- Get current ammo. + local nammo,nshells,nrockets,nmissiles=self:GetAmmo() + + -- Number of units still alive. + local units=self.Controllable:GetUnits() + local nunits=0 + if units then + nunits=#units + end + + -- Full Ammo count. + local FullAmmo=self.Nammo0 * nunits / self.IniGroupStrength + + -- Rearming status in per cent. + local _rearmpc=nammo/FullAmmo*100 + + -- Send message if rearming > 1% complete + if _rearmpc>1 then + local text=string.format("%s, rearming %d %% complete.", self.alias, _rearmpc) + self:T(self.lid..text) + MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report or self.Debug) + end + + -- Return if ammo is full. + -- TODO: Strangely, I got the case that a Paladin got one more shell than it can max carry, i.e. 40 not 39 when rearming when it still had some ammo left. Need to report. + if nammo>=FullAmmo then + return true + else + return false + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Before "Move" event. Check if a unit to rearm the ARTY group has been defined. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #table move Table containing the move parameters. +-- @param Core.Point#COORDINATE ToCoord Coordinate to which the ARTY group should move. +-- @param #boolean OnRoad If true group should move on road mainly. +-- @return #boolean If true, proceed to onafterMove. +function ARTY:onbeforeMove(Controllable, From, Event, To, move) + self:_EventFromTo("onbeforeMove", Event, From, To) + + -- Check if group can actually move... + if not self.ismobile then + return false + end + + -- Check if group is engaging. + if self.currentTarget then + if move.cancel then + -- Cancel current target. + self:CeaseFire(self.currentTarget) + else + -- We should not cancel. + return false + end + end + + return true +end + +--- After "Move" event. Route group to given coordinate. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #table move Table containing the move parameters. +function ARTY:onafterMove(Controllable, From, Event, To, move) + self:_EventFromTo("onafterMove", Event, From, To) + + -- Set alarm state to green and ROE to weapon hold. + self.Controllable:OptionAlarmStateGreen() + self.Controllable:OptionROEHoldFire() + + -- Take care of max speed. + local _Speed=math.min(move.speed, self.SpeedMax) + + -- Smoke coordinate + if self.Debug then + move.coord:SmokeRed() + end + + -- Set current move. + self.currentMove=move + + -- Route group to coodinate. + self:_Move(self.Controllable, move.coord, move.speed, move.onroad) + +end + +--- After "Arrived" event. Group has reached its destination. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARTY:onafterArrived(Controllable, From, Event, To) + self:_EventFromTo("onafterArrived", Event, From, To) + + -- Set alarm state to auto. + self.Controllable:OptionAlarmStateAuto() + + -- WARNING: calling ClearTasks() here causes CTD of DCS when move is over. Dont know why? combotask? + --self.Controllable:ClearTasks() + + -- Send message + local text=string.format("%s, arrived at destination.", Controllable:GetName()) + self:T(self.lid..text) + MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report or self.Debug) + + -- Remove executed move from queue. + if self.currentMove then + self:RemoveMove(self.currentMove.name) + self.currentMove=nil + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- After "NewTarget" event. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #table target Array holding the target parameters. +function ARTY:onafterNewTarget(Controllable, From, Event, To, target) + self:_EventFromTo("onafterNewTarget", Event, From, To) + + -- Debug message. + local text=string.format("Adding new target %s.", target.name) + MESSAGE:New(text, 5):ToAllIf(self.Debug) + self:T(self.lid..text) +end + +--- After "NewMove" event. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #table move Array holding the move parameters. +function ARTY:onafterNewMove(Controllable, From, Event, To, move) + self:_EventFromTo("onafterNewTarget", Event, From, To) + + -- Debug message. + local text=string.format("Adding new move %s.", move.name) + MESSAGE:New(text, 5):ToAllIf(self.Debug) + self:T(self.lid..text) +end + + +--- After "Dead" event, when a unit has died. When all units of a group are dead trigger "Stop" event. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string Unitname Name of the unit that died. +function ARTY:onafterDead(Controllable, From, Event, To, Unitname) + self:_EventFromTo("onafterDead", Event, From, To) + + -- Number of units still alive. + --local nunits=self.Controllable and self.Controllable:CountAliveUnits() or 0 + local nunits=self.Controllable:CountAliveUnits() + + -- Message. + local text=string.format("%s, our unit %s just died! %d units left.", self.groupname, Unitname, nunits) + MESSAGE:New(text, 5):ToAllIf(self.Debug) + self:I(self.lid..text) + + -- Go to stop state. + if nunits==0 then + + -- Cease Fire on current target. + if self.currentTarget then + self:CeaseFire(self.currentTarget) + end + + if self.respawnafterdeath then + -- Respawn group. + if not self.respawning then + self.respawning=true + self:__Respawn(self.respawndelay or 1) + end + else + -- Stop FSM. + self:Stop() + end + end + +end + + +--- After "Dead" event, when a unit has died. When all units of a group are dead trigger "Stop" event. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARTY:onafterRespawn(Controllable, From, Event, To) + self:_EventFromTo("onafterRespawn", Event, From, To) + + env.info("FF Respawning arty group") + + local group=self.Controllable --Wrapper.Group#GROUP + + -- Respawn group. + self.Controllable=group:Respawn() + + self.respawning=false + + -- Call status again. + self:__Status(-1) +end + +--- After "Stop" event. Unhandle events and cease fire on current target. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARTY:onafterStop(Controllable, From, Event, To) + self:_EventFromTo("onafterStop", Event, From, To) + + -- Debug info. + self:I(self.lid..string.format("Stopping ARTY FSM for group %s.", tostring(Controllable:GetName()))) + + -- Cease Fire on current target. + if self.currentTarget then + self:CeaseFire(self.currentTarget) + end + + -- Remove all targets. + --self:RemoveAllTargets() + + -- Unhandle event. + self:UnHandleEvent(EVENTS.Shot) + self:UnHandleEvent(EVENTS.Dead) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set task for firing at a coordinate. +-- @param #ARTY self +-- @param Core.Point#COORDINATE coord Coordinates to fire upon. +-- @param #number radius Radius around coordinate. +-- @param #number nshells Number of shells to fire. +-- @param #number weapontype Type of weapon to use. +function ARTY:_FireAtCoord(coord, radius, nshells, weapontype) + self:F({coord=coord, radius=radius, nshells=nshells}) + + -- Controllable. + local group=self.Controllable --Wrapper.Group#GROUP + + -- Tactical nukes are actually cannon shells. + if weapontype==ARTY.WeaponType.TacticalNukes or weapontype==ARTY.WeaponType.IlluminationShells or weapontype==ARTY.WeaponType.SmokeShells then + weapontype=ARTY.WeaponType.Cannon + end + + -- Set ROE to weapon free. + group:OptionROEOpenFire() + + -- Get Vec2 + local vec2=coord:GetVec2() + + -- Get task. + local fire=group:TaskFireAtPoint(vec2, radius, nshells, weapontype) + + -- Execute task. + group:SetTask(fire) +end + +--- Set task for attacking a group. +-- @param #ARTY self +-- @param #ARTY.Target target Target data. +function ARTY:_AttackGroup(target) + + -- Controllable. + local group=self.Controllable --Wrapper.Group#GROUP + + local weapontype=target.weapontype + + -- Tactical nukes are actually cannon shells. + if weapontype==ARTY.WeaponType.TacticalNukes or weapontype==ARTY.WeaponType.IlluminationShells or weapontype==ARTY.WeaponType.SmokeShells then + weapontype=ARTY.WeaponType.Cannon + end + + -- Set ROE to weapon free. + group:OptionROEOpenFire() + + -- Target group. + local targetgroup=GROUP:FindByName(target.name) + + -- Get task. + local fire=group:TaskAttackGroup(targetgroup, weapontype, AI.Task.WeaponExpend.ONE, 1) + + -- Execute task. + group:SetTask(fire) +end + + + +--- Model a nuclear blast/destruction by creating fires and destroy scenery. +-- @param #ARTY self +-- @param Core.Point#COORDINATE _coord Coordinate of the impact point (center of the blast). +function ARTY:_NuclearBlast(_coord) + + local S0=self.nukewarhead + local R0=self.nukerange + + -- Number of fires + local N0=self.nukefires + + -- Create an explosion at the last known position. + _coord:Explosion(S0) + + -- Huge fire at direct impact point. + --if self.nukefire then + _coord:BigSmokeAndFireHuge() + --end + + -- Create a table of fire coordinates within the demolition zone. + local _fires={} + for i=1,N0 do + local _fire=_coord:GetRandomCoordinateInRadius(R0) + local _dist=_fire:Get2DDistance(_coord) + table.insert(_fires, {distance=_dist, coord=_fire}) + end + + -- Sort scenery wrt to distance from impact point. + local _sort = function(a,b) return a.distance < b.distance end + table.sort(_fires,_sort) + + local function _explosion(R) + -- At R=R0 ==> explosion strength is 1% of S0 at impact point. + local alpha=math.log(100) + local strength=S0*math.exp(-alpha*R/R0) + self:T2(self.lid..string.format("Nuclear explosion strength s(%.1f m) = %.5f (s/s0=%.1f %%), alpha=%.3f", R, strength, strength/S0*100, alpha)) + return strength + end + + local function ignite(_fires) + for _,fire in pairs(_fires) do + local _fire=fire.coord --Core.Point#COORDINATE + + -- Get distance to impact and calc exponential explosion strength. + local R=_fire:Get2DDistance(_coord) + local S=_explosion(R) + self:T2(self.lid..string.format("Explosion r=%.1f, s=%.3f", R, S)) + + -- Get a random Big Smoke and fire object. + local _preset=math.random(0,7) + local _density=S/S0 --math.random()+0.1 + + _fire:BigSmokeAndFire(_preset,_density) + _fire:Explosion(S) + + end + end + + if self.nukefire==true then + ignite(_fires) + end + +--[[ + local ZoneNuke=ZONE_RADIUS:New("Nukezone", _coord:GetVec2(), 2000) + + -- Scan for Scenery objects. + ZoneNuke:Scan(Object.Category.SCENERY) + + -- Array with all possible hideouts, i.e. scenery objects in the vicinity of the group. + local scenery={} + + for SceneryTypeName, SceneryData in pairs(ZoneNuke:GetScannedScenery()) do + for SceneryName, SceneryObject in pairs(SceneryData) do + + local SceneryObject = SceneryObject -- Wrapper.Scenery#SCENERY + + -- Position of the scenery object. + local spos=SceneryObject:GetCoordinate() + + -- Distance from group to impact point. + local distance= spos:Get2DDistance(_coord) + + -- Place markers on every possible scenery object. + if self.Debug then + local MarkerID=spos:MarkToAll(string.format("%s scenery object %s", self.Controllable:GetName(), SceneryObject:GetTypeName())) + local text=string.format("%s scenery: %s, Coord %s", self.Controllable:GetName(), SceneryObject:GetTypeName(), SceneryObject:GetCoordinate():ToStringLLDMS()) + self:T2(SUPPRESSION.id..text) + end + + -- Add to table. + table.insert(scenery, {object=SceneryObject, distance=distance}) + + --SceneryObject:Destroy() + end + end + + -- Sort scenery wrt to distance from impact point. +-- local _sort = function(a,b) return a.distance < b.distance end +-- table.sort(scenery,_sort) + +-- for _,object in pairs(scenery) do +-- local sobject=object -- Wrapper.Scenery#SCENERY +-- sobject:Destroy() +-- end + +]] + +end + +--- Route group to a certain point. +-- @param #ARTY self +-- @param Wrapper.Group#GROUP group Group to route. +-- @param Core.Point#COORDINATE ToCoord Coordinate where we want to go. +-- @param #number Speed (Optional) Speed in km/h. Default is 70% of max speed the group can do. +-- @param #boolean OnRoad If true, use (mainly) roads. +function ARTY:_Move(group, ToCoord, Speed, OnRoad) + self:F2({group=group:GetName(), Speed=Speed, OnRoad=OnRoad}) + + -- Clear all tasks. + group:ClearTasks() + group:OptionAlarmStateGreen() + group:OptionROEHoldFire() + + -- Set formation. + local formation = "Off Road" + + -- Get max speed of group. + local SpeedMax=group:GetSpeedMax() + + -- Set speed. + Speed=Speed or SpeedMax*0.7 + + -- Make sure, we do not go above max speed possible. + Speed=math.min(Speed, SpeedMax) + + -- Current coordinates of group. + local cpini=group:GetCoordinate() -- Core.Point#COORDINATE + + -- Distance between current and final point. + local dist=cpini:Get2DDistance(ToCoord) + + -- Waypoint and task arrays. + local path={} + local task={} + + -- First waypoint is the current position of the group. + path[#path+1]=cpini:WaypointGround(Speed, formation) + task[#task+1]=group:TaskFunction("ARTY._PassingWaypoint", self, #path-1, false) + + -- Route group on road if requested. + if OnRoad then + + -- Get path on road. + local _pathonroad=cpini:GetPathOnRoad(ToCoord) + + -- Check if we actually got a path. There are situations where nil is returned. In that case, we go directly. + if _pathonroad then + + -- Just take the first and last point. + local _first=_pathonroad[1] + local _last=_pathonroad[#_pathonroad] + + if self.Debug then + _first:SmokeGreen() + _last:SmokeGreen() + end + + -- First point on road. + path[#path+1]=_first:WaypointGround(Speed, "On Road") + task[#task+1]=group:TaskFunction("ARTY._PassingWaypoint", self, #path-1, false) + + -- Last point on road. + path[#path+1]=_last:WaypointGround(Speed, "On Road") + task[#task+1]=group:TaskFunction("ARTY._PassingWaypoint", self, #path-1, false) + end + + end + + -- Last waypoint at ToCoord. + path[#path+1]=ToCoord:WaypointGround(Speed, formation) + task[#task+1]=group:TaskFunction("ARTY._PassingWaypoint", self, #path-1, true) + + --if self.Debug then + -- cpini:SmokeBlue() + -- ToCoord:SmokeBlue() + --end + + -- Init waypoints of the group. + local Waypoints={} + + -- New points are added to the default route. + for i=1,#path do + table.insert(Waypoints, i, path[i]) + end + + -- Set task for all waypoints. + for i=1,#Waypoints do + group:SetTaskWaypoint(Waypoints[i], task[i]) + end + + -- Submit task and route group along waypoints. + group:Route(Waypoints) + +end + +--- Function called when group is passing a waypoint. +-- @param Wrapper.Group#GROUP group Group for which waypoint passing should be monitored. +-- @param #ARTY arty ARTY object. +-- @param #number i Waypoint number that has been reached. +-- @param #boolean final True if it is the final waypoint. +function ARTY._PassingWaypoint(group, arty, i, final) + + if group and group:IsAlive() then + + local groupname=tostring(group:GetName()) + + -- Debug message. + local text=string.format("%s, passing waypoint %d.", groupname, i) + if final then + text=string.format("%s, arrived at destination.", groupname) + end + arty:T(arty.lid..text) + + -- Arrived event. + if final and arty.groupname==groupname then + arty:Arrived() + end + + end +end + +--- Relocate to another position, e.g. after an engagement to avoid couter strikes. +-- @param #ARTY self +function ARTY:_Relocate() + + -- Current position. + local _pos=self.Controllable:GetCoordinate() + + local _new=nil + local _gotit=false + local _n=0 + local _nmax=1000 + repeat + -- Get a random coordinate. + _new=_pos:GetRandomCoordinateInRadius(self.relocateRmax, self.relocateRmin) + local _surface=_new:GetSurfaceType() + + -- Check that new coordinate is not water(-ish). + if _surface~=land.SurfaceType.WATER and _surface~=land.SurfaceType.SHALLOW_WATER then + _gotit=true + end + -- Increase counter. + _n=_n+1 + until _gotit or _n>_nmax + + -- Assign relocation. + if _gotit then + self:AssignMoveCoord(_new, nil, nil, false, false, "RELOCATION MOVE AFTER FIRING") + end +end + +--- Get the number of shells a unit or group currently has. For a group the ammo count of all units is summed up. +-- @param #ARTY self +-- @param #boolean display Display ammo table as message to all. Default false. +-- @return #number Total amount of ammo the whole group has left. +-- @return #number Number of shells the group has left. +-- @return #number Number of rockets the group has left. +-- @return #number Number of missiles the group has left. +function ARTY:GetAmmo(display) + self:F3({display=display}) + + -- Default is display false. + if display==nil then + display=false + end + + -- Init counter. + local nammo=0 + local nshells=0 + local nrockets=0 + local nmissiles=0 + + -- Get all units. + local units=self.Controllable:GetUnits() + if units==nil then + return nammo, nshells, nrockets, nmissiles + end + + for _,unit in pairs(units) do + + if unit and unit:IsAlive() then + + -- Output. + local text=string.format("ARTY group %s - unit %s:\n", self.groupname, unit:GetName()) + + -- Get ammo table. + local ammotable=unit:GetAmmo() + + if ammotable ~= nil then + + local weapons=#ammotable + + -- Display ammo table + if display then + self:I(self.lid..string.format("Number of weapons %d.", weapons)) + self:I({ammotable=ammotable}) + self:I(self.lid.."Ammotable:") + for id,bla in pairs(ammotable) do + self:I({id=id, ammo=bla}) + end + end + + -- Loop over all weapons. + for w=1,weapons do + + -- Number of current weapon. + local Nammo=ammotable[w]["count"] + + -- Typename of current weapon + local Tammo=ammotable[w]["desc"]["typeName"] + + local _weaponString = self:_split(Tammo,"%.") + local _weaponName = _weaponString[#_weaponString] + + -- Get the weapon category: shell=0, missile=1, rocket=2, bomb=3 + local Category=ammotable[w].desc.category + + -- Get missile category: Weapon.MissileCategory AAM=1, SAM=2, BM=3, ANTI_SHIP=4, CRUISE=5, OTHER=6 + local MissileCategory=nil + if Category==Weapon.Category.MISSILE then + MissileCategory=ammotable[w].desc.missileCategory + end + + + -- Check for correct shell type. + local _gotshell=false + if #self.ammoshells>0 then + -- User explicitly specified the valid type(s) of shells. + for _,_type in pairs(self.ammoshells) do + if string.match(Tammo, _type) and Category==Weapon.Category.SHELL then + _gotshell=true + end + end + else + if Category==Weapon.Category.SHELL then + _gotshell=true + end + end + + -- Check for correct rocket type. + local _gotrocket=false + if #self.ammorockets>0 then + for _,_type in pairs(self.ammorockets) do + if string.match(Tammo, _type) and Category==Weapon.Category.ROCKET then + _gotrocket=true + end + end + else + if Category==Weapon.Category.ROCKET then + _gotrocket=true + end + end + + -- Check for correct missile type. + local _gotmissile=false + if #self.ammomissiles>0 then + for _,_type in pairs(self.ammomissiles) do + if string.match(Tammo,_type) and Category==Weapon.Category.MISSILE then + _gotmissile=true + end + end + else + if Category==Weapon.Category.MISSILE then + _gotmissile=true + end + end + + -- We are specifically looking for shells or rockets here. + if _gotshell then + + -- Add up all shells. + nshells=nshells+Nammo + + -- Debug info. + text=text..string.format("- %d shells of type %s\n", Nammo, _weaponName) + + elseif _gotrocket then + + -- Add up all rockets. + nrockets=nrockets+Nammo + + -- Debug info. + text=text..string.format("- %d rockets of type %s\n", Nammo, _weaponName) + + elseif _gotmissile then + + -- Add up all cruise missiles (category 5) + if MissileCategory==Weapon.MissileCategory.CRUISE then + nmissiles=nmissiles+Nammo + end + + -- Debug info. + text=text..string.format("- %d %s missiles of type %s\n", Nammo, self:_MissileCategoryName(MissileCategory), _weaponName) + + else + + -- Debug info. + text=text..string.format("- %d unknown ammo of type %s (category=%d, missile category=%s)\n", Nammo, Tammo, Category, tostring(MissileCategory)) + + end + + end + end + + -- Debug text and send message. + if display then + self:I(self.lid..text) + else + self:T3(self.lid..text) + end + MESSAGE:New(text, 10):ToAllIf(display) + + end + end + + -- Total amount of ammunition. + nammo=nshells+nrockets+nmissiles + + return nammo, nshells, nrockets, nmissiles +end + +--- Returns a name of a missile category. +-- @param #ARTY self +-- @param #number categorynumber Number of missile category from weapon missile category enumerator. See https://wiki.hoggitworld.com/view/DCS_Class_Weapon +-- @return #string Missile category name. +function ARTY:_MissileCategoryName(categorynumber) + local cat="unknown" + if categorynumber==Weapon.MissileCategory.AAM then + cat="air-to-air" + elseif categorynumber==Weapon.MissileCategory.SAM then + cat="surface-to-air" + elseif categorynumber==Weapon.MissileCategory.BM then + cat="ballistic" + elseif categorynumber==Weapon.MissileCategory.ANTI_SHIP then + cat="anti-ship" + elseif categorynumber==Weapon.MissileCategory.CRUISE then + cat="cruise" + elseif categorynumber==Weapon.MissileCategory.OTHER then + cat="other" + end + return cat +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Mark Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Extract engagement assignments and parameters from mark text. +-- @param #ARTY self +-- @param #string text Marker text. +-- @return #boolean If true, authentification successful. +function ARTY:_MarkerKeyAuthentification(text) + + -- Set battery and coalition. + --local batteryname=self.groupname + local batterycoalition=self.coalition + + -- Get assignment. + local mykey=nil + if self.markkey~=nil then + + -- keywords are split by "," + local keywords=self:_split(text, ",") + for _,key in pairs(keywords) do + local s=self:_split(key, " ") + local val=s[2] + if key:lower():find("key") then + mykey=tonumber(val) + self:T(self.lid..string.format("Authorisation Key=%s.", val)) + end + end + + end + + -- Check if the authorization key is required and if it is valid. + local _validkey=true + + -- Check if group needs authorization. + if self.markkey~=nil then + -- Assume key is incorrect. + _validkey=false + + -- If key was found, check if matches. + if mykey~=nil then + _validkey=self.markkey==mykey + end + self:T2(self.lid..string.format("%s, authkey=%s == %s=playerkey ==> valid=%s", self.groupname, tostring(self.markkey), tostring(mykey), tostring(_validkey))) + + -- Send message + local text="" + if mykey==nil then + text=string.format("%s, authorization required but did not receive a key!", self.alias) + elseif _validkey==false then + text=string.format("%s, authorization required but did receive an incorrect key (key=%s)!", self.alias, tostring(mykey)) + elseif _validkey==true then + text=string.format("%s, authentification successful!", self.alias) + end + MESSAGE:New(text, 10):ToCoalitionIf(batterycoalition, self.report or self.Debug) + end + + return _validkey +end + +--- Extract engagement assignments and parameters from mark text. +-- @param #ARTY self +-- @param #string text Marker text to be analyzed. +-- @return #table Table with assignment parameters, e.g. number of shots, radius, time etc. +function ARTY:_Markertext(text) + self:F(text) + + -- Assignment parameters. + local assignment={} + assignment.battery={} + assignment.aliases={} + assignment.cluster={} + assignment.everyone=false + assignment.move=false + assignment.engage=false + assignment.request=false + assignment.cancel=false + assignment.set=false + assignment.readonly=false + assignment.movecanceltarget=false + assignment.cancelmove=false + assignment.canceltarget=false + assignment.cancelrearm=false + assignment.setrearmingplace=false + assignment.setrearminggroup=false + + -- Check for correct keywords. + if text:lower():find("arty engage") or text:lower():find("arty attack") then + assignment.engage=true + elseif text:lower():find("arty move") or text:lower():find("arty relocate") then + assignment.move=true + elseif text:lower():find("arty request") then + assignment.request=true + elseif text:lower():find("arty cancel") then + assignment.cancel=true + elseif text:lower():find("arty set") then + assignment.set=true + else + self:E(self.lid..'ERROR: Neither "ARTY ENGAGE" nor "ARTY MOVE" nor "ARTY RELOCATE" nor "ARTY REQUEST" nor "ARTY CANCEL" nor "ARTY SET" keyword specified!') + return nil + end + + -- keywords are split by "," + local keywords=self:_split(text, ",") + self:T({keywords=keywords}) + + for _,keyphrase in pairs(keywords) do + + -- Split keyphrase by space. First one is the key and second, ... the parameter(s) until the next comma. + local str=self:_split(keyphrase, " ") + local key=str[1] + local val=str[2] + + -- Debug output. + self:T3(self.lid..string.format("%s, keyphrase = %s, key = %s, val = %s", self.groupname, tostring(keyphrase), tostring(key), tostring(val))) + + -- Battery name, i.e. which ARTY group should fire. + if key:lower():find("battery") then + + local v=self:_split(keyphrase, '"') + + for i=2,#v,2 do + table.insert(assignment.battery, v[i]) + self:T2(self.lid..string.format("Key Battery=%s.", v[i])) + end + + elseif key:lower():find("alias") then + + local v=self:_split(keyphrase, '"') + + for i=2,#v,2 do + table.insert(assignment.aliases, v[i]) + self:T2(self.lid..string.format("Key Aliases=%s.", v[i])) + end + + elseif key:lower():find("cluster") then + + local v=self:_split(keyphrase, '"') + + for i=2,#v,2 do + table.insert(assignment.cluster, v[i]) + self:T2(self.lid..string.format("Key Cluster=%s.", v[i])) + end + + elseif keyphrase:lower():find("everyone") or keyphrase:lower():find("all batteries") or keyphrase:lower():find("allbatteries") then + + assignment.everyone=true + self:T(self.lid..string.format("Key Everyone=true.")) + + elseif keyphrase:lower():find("irrevocable") or keyphrase:lower():find("readonly") then + + assignment.readonly=true + self:T2(self.lid..string.format("Key Readonly=true.")) + + elseif (assignment.engage or assignment.move) and key:lower():find("time") then + + if val:lower():find("now") then + assignment.time=self:_SecondsToClock(timer.getTime0()+2) + else + assignment.time=val + end + self:T2(self.lid..string.format("Key Time=%s.", val)) + + elseif assignment.engage and key:lower():find("shot") then + + assignment.nshells=tonumber(val) + self:T(self.lid..string.format("Key Shot=%s.", val)) + + elseif assignment.engage and key:lower():find("prio") then + + assignment.prio=tonumber(val) + self:T2(string.format("Key Prio=%s.", val)) + + elseif assignment.engage and key:lower():find("maxengage") then + + assignment.maxengage=tonumber(val) + self:T2(self.lid..string.format("Key Maxengage=%s.", val)) + + elseif assignment.engage and key:lower():find("radius") then + + assignment.radius=tonumber(val) + self:T2(self.lid..string.format("Key Radius=%s.", val)) + + elseif assignment.engage and key:lower():find("weapon") then + + if val:lower():find("cannon") then + assignment.weapontype=ARTY.WeaponType.Cannon + elseif val:lower():find("rocket") then + assignment.weapontype=ARTY.WeaponType.Rockets + elseif val:lower():find("missile") then + assignment.weapontype=ARTY.WeaponType.CruiseMissile + elseif val:lower():find("nuke") then + assignment.weapontype=ARTY.WeaponType.TacticalNukes + elseif val:lower():find("illu") then + assignment.weapontype=ARTY.WeaponType.IlluminationShells + elseif val:lower():find("smoke") then + assignment.weapontype=ARTY.WeaponType.SmokeShells + else + assignment.weapontype=ARTY.WeaponType.Auto + end + self:T2(self.lid..string.format("Key Weapon=%s.", val)) + + elseif (assignment.move or assignment.set) and key:lower():find("speed") then + + assignment.speed=tonumber(val) + self:T2(self.lid..string.format("Key Speed=%s.", val)) + + elseif (assignment.move or assignment.set) and (keyphrase:lower():find("on road") or keyphrase:lower():find("onroad") or keyphrase:lower():find("use road")) then + + assignment.onroad=true + self:T2(self.lid..string.format("Key Onroad=true.")) + + elseif assignment.move and (keyphrase:lower():find("cancel target") or keyphrase:lower():find("canceltarget")) then + + assignment.movecanceltarget=true + self:T2(self.lid..string.format("Key Cancel Target (before move)=true.")) + + elseif assignment.request and keyphrase:lower():find("rearm") then + + assignment.requestrearming=true + self:T2(self.lid..string.format("Key Request Rearming=true.")) + + elseif assignment.request and keyphrase:lower():find("ammo") then + + assignment.requestammo=true + self:T2(self.lid..string.format("Key Request Ammo=true.")) + + elseif assignment.request and keyphrase:lower():find("target") then + + assignment.requesttargets=true + self:T2(self.lid..string.format("Key Request Targets=true.")) + + elseif assignment.request and keyphrase:lower():find("status") then + + assignment.requeststatus=true + self:T2(self.lid..string.format("Key Request Status=true.")) + + elseif assignment.request and (keyphrase:lower():find("move") or keyphrase:lower():find("relocation")) then + + assignment.requestmoves=true + self:T2(self.lid..string.format("Key Request Moves=true.")) + + elseif assignment.cancel and (keyphrase:lower():find("engagement") or keyphrase:lower():find("attack") or keyphrase:lower():find("target")) then + + assignment.canceltarget=true + self:T2(self.lid..string.format("Key Cancel Target=true.")) + + elseif assignment.cancel and (keyphrase:lower():find("move") or keyphrase:lower():find("relocation")) then + + assignment.cancelmove=true + self:T2(self.lid..string.format("Key Cancel Move=true.")) + + elseif assignment.cancel and keyphrase:lower():find("rearm") then + + assignment.cancelrearm=true + self:T2(self.lid..string.format("Key Cancel Rearm=true.")) + + elseif assignment.set and keyphrase:lower():find("rearming place") then + + assignment.setrearmingplace=true + self:T(self.lid..string.format("Key Set Rearming Place=true.")) + + elseif assignment.set and keyphrase:lower():find("rearming group") then + + local v=self:_split(keyphrase, '"') + local groupname=v[2] + + local group=GROUP:FindByName(groupname) + if group and group:IsAlive() then + assignment.setrearminggroup=group + end + + self:T2(self.lid..string.format("Key Set Rearming Group = %s.", tostring(groupname))) + + elseif key:lower():find("lldms") then + + local _flat = "%d+:%d+:%d+%s*[N,S]" + local _flon = "%d+:%d+:%d+%s*[W,E]" + local _lat=keyphrase:match(_flat) + local _lon=keyphrase:match(_flon) + self:T2(self.lid..string.format("Key LLDMS: lat=%s, long=%s format=DMS", _lat,_lon)) + + if _lat and _lon then + + -- Convert DMS string to DD numbers format. + local _latitude, _longitude=self:_LLDMS2DD(_lat, _lon) + self:T2(self.lid..string.format("Key LLDMS: lat=%.3f, long=%.3f format=DD", _latitude,_longitude)) + + -- Convert LL to coordinate object. + if _latitude and _longitude then + assignment.coord=COORDINATE:NewFromLLDD(_latitude,_longitude) + end + + end + end + end + + return assignment +end + +--- Request ammo via mark. +-- @param #ARTY self +function ARTY:_MarkRequestAmmo() + self:GetAmmo(true) +end + +--- Request status via mark. +-- @param #ARTY self +function ARTY:_MarkRequestStatus() + self:_StatusReport(true) +end + +--- Request Moves. +-- @param #ARTY self +function ARTY:_MarkRequestMoves() + local text=string.format("%s, relocations:", self.groupname) + if #self.moves>0 then + for _,move in pairs(self.moves) do + if self.currentMove and move.name == self.currentMove.name then + text=text..string.format("\n- %s (current)", self:_MoveInfo(move)) + else + text=text..string.format("\n- %s", self:_MoveInfo(move)) + end + end + else + text=text..string.format("\n- no queued relocations") + end + MESSAGE:New(text, 20):Clear():ToCoalition(self.coalition) +end + +--- Request Targets. +-- @param #ARTY self +function ARTY:_MarkRequestTargets() + local text=string.format("%s, targets:", self.groupname) + if #self.targets>0 then + for _,target in pairs(self.targets) do + if self.currentTarget and target.name == self.currentTarget.name then + text=text..string.format("\n- %s (current)", self:_TargetInfo(target)) + else + text=text..string.format("\n- %s", self:_TargetInfo(target)) + end + end + else + text=text..string.format("\n- no queued targets") + end + MESSAGE:New(text, 20):Clear():ToCoalition(self.coalition) +end + +--- Create a name for an engagement initiated by placing a marker. +-- @param #ARTY self +-- @param #number markerid ID of the placed marker. +-- @return #string Name of target engagement. +function ARTY:_MarkTargetName(markerid) + return string.format("BATTERY=%s, Marked Target ID=%d", self.groupname, markerid) +end + +--- Create a name for a relocation move initiated by placing a marker. +-- @param #ARTY self +-- @param #number markerid ID of the placed marker. +-- @return #string Name of relocation move. +function ARTY:_MarkMoveName(markerid) + return string.format("BATTERY=%s, Marked Relocation ID=%d", self.groupname, markerid) +end + +--- Get the marker ID from the assigned task name. +-- @param #ARTY self +-- @param #string name Name of the assignment. +-- @return #string Name of the ARTY group or nil +-- @return #number ID of the marked target or nil. +-- @return #number ID of the marked relocation move or nil +function ARTY:_GetMarkIDfromName(name) + + -- keywords are split by "," + local keywords=self:_split(name, ",") + + local battery=nil + local markTID=nil + local markMID=nil + + for _,key in pairs(keywords) do + + local str=self:_split(key, "=") + local par=str[1] + local val=str[2] + + if par:find("BATTERY") then + battery=val + end + if par:find("Marked Target ID") then + markTID=tonumber(val) + end + if par:find("Marked Relocation ID") then + markMID=tonumber(val) + end + + end + + return battery, markTID, markMID +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Helper Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Sort targets with respect to priority and number of times it was already engaged. +-- @param #ARTY self +function ARTY:_SortTargetQueuePrio() + self:F2() + + -- Sort results table wrt times they have already been engaged. + local function _sort(a, b) + return (a.engaged < b.engaged) or (a.engaged==b.engaged and a.prio < b.prio) + end + table.sort(self.targets, _sort) + + -- Debug output. + self:T3(self.lid.."Sorted targets wrt prio and number of engagements:") + for i=1,#self.targets do + local _target=self.targets[i] + self:T3(self.lid..string.format("Target %s", self:_TargetInfo(_target))) + end +end + +--- Sort array with respect to time. Array elements must have a .time entry. +-- @param #ARTY self +-- @param #table queue Array to sort. Should have elemnt .time. +function ARTY:_SortQueueTime(queue) + self:F3({queue=queue}) + + -- Sort targets w.r.t attack time. + local function _sort(a, b) + if a.time == nil and b.time == nil then + return false + end + if a.time == nil then + return false + end + if b.time == nil then + return true + end + return a.time < b.time + end + table.sort(queue, _sort) + + -- Debug output. + self:T3(self.lid.."Sorted queue wrt time:") + for i=1,#queue do + local _queue=queue[i] + local _time=tostring(_queue.time) + local _clock=tostring(self:_SecondsToClock(_queue.time)) + self:T3(self.lid..string.format("%s: time=%s, clock=%s", _queue.name, _time, _clock)) + end + +end + +--- Heading from point a to point b in degrees. +--@param #ARTY self +--@param Core.Point#COORDINATE a Coordinate. +--@param Core.Point#COORDINATE b Coordinate. +--@return #number angle Angle from a to b in degrees. +function ARTY:_GetHeading(a, b) + local dx = b.x-a.x + local dy = b.z-a.z + local angle = math.deg(math.atan2(dy,dx)) + if angle < 0 then + angle = 360 + angle + end + return angle +end + +--- Check all targets whether they are in range. +-- @param #ARTY self +function ARTY:_CheckTargetsInRange() + + local targets2delete={} + + for i=1,#self.targets do + local _target=self.targets[i] + + self:T3(self.lid..string.format("Before: Target %s - in range = %s", _target.name, tostring(_target.inrange))) + + -- Check if target is in range. + local _inrange,_toofar,_tooclose,_remove=self:_TargetInRange(_target) + self:T3(self.lid..string.format("Inbetw: Target %s - in range = %s, toofar = %s, tooclose = %s", _target.name, tostring(_target.inrange), tostring(_toofar), tostring(_tooclose))) + + if _remove then + + -- The ARTY group is immobile and not cargo but the target is not in range! + table.insert(targets2delete, _target.name) + + else + + -- Init default for assigning moves into range. + local _movetowards=false + local _moveaway=false + + if _target.inrange==nil then + + -- First time the check is performed. We call the function again and send a message. + _target.inrange,_toofar,_tooclose=self:_TargetInRange(_target, self.report or self.Debug) + + -- Send group towards/away from target. + if _toofar then + _movetowards=true + elseif _tooclose then + _moveaway=true + end + + elseif _target.inrange==true then + + -- Target was in range at previous check... + + if _toofar then --...but is now too far away. + _movetowards=true + elseif _tooclose then --...but is now too close. + _moveaway=true + end + + elseif _target.inrange==false then + + -- Target was out of range at previous check. + + if _inrange then + -- Inform coalition that target is now in range. + local text=string.format("%s, target %s is now in range.", self.alias, _target.name) + self:T(self.lid..text) + MESSAGE:New(text,10):ToCoalitionIf(self.coalition, self.report or self.Debug) + end + + end + + -- Assign a relocation command so that the unit will be in range of the requested target. + if self.autorelocate and (_movetowards or _moveaway) then + + -- Get current position. + local _from=self.Controllable:GetCoordinate() + local _dist=_from:Get2DDistance(_target.coord) + + if _dist<=self.autorelocatemaxdist then + + local _tocoord --Core.Point#COORDINATE + local _name="" + local _safetymargin=500 + + if _movetowards then + + -- Target was in range on previous check but now we are too far away. + local _waytogo=_dist-self.maxrange+_safetymargin + local _heading=self:_GetHeading(_from,_target.coord) + _tocoord=_from:Translate(_waytogo, _heading) + _name=string.format("%s, relocation to within max firing range of target %s", self.alias, _target.name) + + elseif _moveaway then + + -- Target was in range on previous check but now we are too far away. + local _waytogo=_dist-self.minrange+_safetymargin + local _heading=self:_GetHeading(_target.coord,_from) + _tocoord=_from:Translate(_waytogo, _heading) + _name=string.format("%s, relocation to within min firing range of target %s", self.alias, _target.name) + + end + + -- Send info message. + MESSAGE:New(_name.." assigned.", 10):ToCoalitionIf(self.coalition, self.report or self.Debug) + + -- Assign relocation move. + self:AssignMoveCoord(_tocoord, nil, nil, self.autorelocateonroad, false, _name, true) + + end + + end + + -- Update value. + _target.inrange=_inrange + + self:T3(self.lid..string.format("After: Target %s - in range = %s", _target.name, tostring(_target.inrange))) + end + end + + -- Remove targets not in range. + for _,targetname in pairs(targets2delete) do + self:RemoveTarget(targetname) + end + +end + +--- Check all normal (untimed) targets and return the target with the highest priority which has been engaged the fewest times. +-- @param #ARTY self +-- @return #table Target which is due to be attacked now or nil if no target could be found. +function ARTY:_CheckNormalTargets() + self:F3() + + -- Sort targets w.r.t. prio and number times engaged already. + self:_SortTargetQueuePrio() + + -- No target engagements if rearming! + if self:is("Rearming") then + return nil + end + + -- Loop over all sorted targets. + for i=1,#self.targets do + local _target=self.targets[i] + + -- Debug info. + self:T3(self.lid..string.format("Check NORMAL target %d: %s", i, self:_TargetInfo(_target))) + + -- Check that target no time, is not under fire currently and in range. + if _target.underfire==false and _target.time==nil and _target.maxengage > _target.engaged and self:_TargetInRange(_target) and self:_CheckWeaponTypeAvailable(_target)>0 then + + -- Debug info. + self:T2(self.lid..string.format("Found NORMAL target %s", self:_TargetInfo(_target))) + + return _target + end + end + + return nil +end + +--- Check all timed targets and return the target which should be attacked next. +-- @param #ARTY self +-- @return #table Target which is due to be attacked now. +function ARTY:_CheckTimedTargets() + self:F3() + + -- Current time. + local Tnow=timer.getAbsTime() + + -- Sort Targets wrt time. + self:_SortQueueTime(self.targets) + + -- No target engagements if rearming! + if self:is("Rearming") then + return nil + end + + for i=1,#self.targets do + local _target=self.targets[i] + + -- Debug info. + self:T3(self.lid..string.format("Check TIMED target %d: %s", i, self:_TargetInfo(_target))) + + -- Check if target has an attack time which has already passed. Also check that target is not under fire already and that it is in range. + if _target.time and Tnow>=_target.time and _target.underfire==false and self:_TargetInRange(_target) and self:_CheckWeaponTypeAvailable(_target)>0 then + + -- Check if group currently has a target and whether its priorty is lower than the timed target. + if self.currentTarget then + if self.currentTarget.prio > _target.prio then + -- Current target under attack but has lower priority than this target. + self:T2(self.lid..string.format("Found TIMED HIGH PRIO target %s.", self:_TargetInfo(_target))) + return _target + end + else + -- No current target. + self:T2(self.lid..string.format("Found TIMED target %s.", self:_TargetInfo(_target))) + return _target + end + end + + end + + return nil +end + +--- Check all moves and return the one which should be executed next. +-- @param #ARTY self +-- @return #table Move which is due. +function ARTY:_CheckMoves() + self:F3() + + -- Current time. + local Tnow=timer.getAbsTime() + + -- Sort Targets wrt time. + self:_SortQueueTime(self.moves) + + -- Check if we are currently firing. + local firing=false + if self.currentTarget then + firing=true + end + + -- Loop over all moves in queue. + for i=1,#self.moves do + + -- Shortcut. + local _move=self.moves[i] + + if string.find(_move.name, "REARMING MOVE") and ((self.currentMove and self.currentMove.name~=_move.name) or self.currentMove==nil) then + -- We got an rearming assignment which has priority. + return _move + elseif (Tnow >= _move.time) and (firing==false or _move.cancel) and (not self.currentMove) and (not self:is("Rearming")) then + -- Time for move is reached and maybe current target should be cancelled. + return _move + end + end + + return nil +end + +--- Check whether shooting started within a certain time (~5 min). If not, the current target is considered invalid and removed from the target list. +-- @param #ARTY self +function ARTY:_CheckShootingStarted() + self:F2() + + if self.currentTarget then + + -- Current time. + local Tnow=timer.getTime() + + -- Get name and id of target. + local name=self.currentTarget.name + + -- Time that passed after current target has been assigned. + local dt=Tnow-self.currentTarget.Tassigned + + -- Debug info + if self.Nshots==0 then + self:T(self.lid..string.format("%s, waiting for %d seconds for first shot on target %s.", self.groupname, dt, name)) + end + + -- Check if we waited long enough and no shot was fired. + --if dt > self.WaitForShotTime and self.Nshots==0 then + if dt > self.WaitForShotTime and (self.Nshots==0 or self.currentTarget.nshells >= self.Nshots) then --https://github.com/FlightControl-Master/MOOSE/issues/1356 + + -- Debug info. + self:T(self.lid..string.format("%s, no shot event after %d seconds. Removing current target %s from list.", self.groupname, self.WaitForShotTime, name)) + + -- CeaseFire. + self:CeaseFire(self.currentTarget) + + -- Remove target from list. + self:RemoveTarget(name) + + end + end +end + +--- Get the index of a target by its name. +-- @param #ARTY self +-- @param #string name Name of target. +-- @return #number Arrayindex of target. +function ARTY:_GetTargetIndexByName(name) + self:F2(name) + + for i=1,#self.targets do + local targetname=self.targets[i].name + self:T3(self.lid..string.format("Have target with name %s. Index = %d", targetname, i)) + if targetname==name then + self:T2(self.lid..string.format("Found target with name %s. Index = %d", name, i)) + return i + end + end + + self:T2(self.lid..string.format("WARNING: Target with name %s could not be found. (This can happen.)", name)) + return nil +end + +--- Get the index of a move by its name. +-- @param #ARTY self +-- @param #string name Name of move. +-- @return #number Arrayindex of move. +function ARTY:_GetMoveIndexByName(name) + self:F2(name) + + for i=1,#self.moves do + local movename=self.moves[i].name + self:T3(self.lid..string.format("Have move with name %s. Index = %d", movename, i)) + if movename==name then + self:T2(self.lid..string.format("Found move with name %s. Index = %d", name, i)) + return i + end + end + + self:T2(self.lid..string.format("WARNING: Move with name %s could not be found. (This can happen.)", name)) + return nil +end + +--- Check if group is (partly) out of ammo of a special weapon type. +-- @param #ARTY self +-- @param #table targets Table of targets. +-- @return @boolean True if any target requests a weapon type that is empty. +function ARTY:_CheckOutOfAmmo(targets) + + -- Get current ammo. + local _nammo,_nshells,_nrockets,_nmissiles=self:GetAmmo() + + -- Special weapon type requested ==> Check if corresponding ammo is empty. + local _partlyoutofammo=false + + for _,Target in pairs(targets) do + + if Target.weapontype==ARTY.WeaponType.Auto and _nammo==0 then + + self:T(self.lid..string.format("Group %s, auto weapon requested for target %s but all ammo is empty.", self.groupname, Target.name)) + _partlyoutofammo=true + + elseif Target.weapontype==ARTY.WeaponType.Cannon and _nshells==0 then + + self:T(self.lid..string.format("Group %s, cannons requested for target %s but shells empty.", self.groupname, Target.name)) + _partlyoutofammo=true + + elseif Target.weapontype==ARTY.WeaponType.TacticalNukes and self.Nukes<=0 then + + self:T(self.lid..string.format("Group %s, tactical nukes requested for target %s but nukes empty.", self.groupname, Target.name)) + _partlyoutofammo=true + + elseif Target.weapontype==ARTY.WeaponType.IlluminationShells and self.Nillu<=0 then + + self:T(self.lid..string.format("Group %s, illumination shells requested for target %s but illumination shells empty.", self.groupname, Target.name)) + _partlyoutofammo=true + + elseif Target.weapontype==ARTY.WeaponType.SmokeShells and self.Nsmoke<=0 then + + self:T(self.lid..string.format("Group %s, smoke shells requested for target %s but smoke shells empty.", self.groupname, Target.name)) + _partlyoutofammo=true + + elseif Target.weapontype==ARTY.WeaponType.Rockets and _nrockets==0 then + + self:T(self.lid..string.format("Group %s, rockets requested for target %s but rockets empty.", self.groupname, Target.name)) + _partlyoutofammo=true + + elseif Target.weapontype==ARTY.WeaponType.CruiseMissile and _nmissiles==0 then + + self:T(self.lid..string.format("Group %s, cruise missiles requested for target %s but all missiles empty.", self.groupname, Target.name)) + _partlyoutofammo=true + + end + + end + + return _partlyoutofammo +end + +--- Check if a selected weapon type is available for this target, i.e. if the current amount of ammo of this weapon type is currently available. +-- @param #ARTY self +-- @param #boolean target Target array data structure. +-- @return #number Amount of shells, rockets or missiles available of the weapon type selected for the target. +function ARTY:_CheckWeaponTypeAvailable(target) + + -- Get current ammo of group. + local Nammo, Nshells, Nrockets, Nmissiles=self:GetAmmo() + + -- Check if enough ammo is there for the selected weapon type. + local nfire=Nammo + if target.weapontype==ARTY.WeaponType.Auto then + nfire=Nammo + elseif target.weapontype==ARTY.WeaponType.Cannon then + nfire=Nshells + elseif target.weapontype==ARTY.WeaponType.TacticalNukes then + nfire=self.Nukes + elseif target.weapontype==ARTY.WeaponType.IlluminationShells then + nfire=self.Nillu + elseif target.weapontype==ARTY.WeaponType.SmokeShells then + nfire=self.Nsmoke + elseif target.weapontype==ARTY.WeaponType.Rockets then + nfire=Nrockets + elseif target.weapontype==ARTY.WeaponType.CruiseMissile then + nfire=Nmissiles + end + + return nfire +end +--- Check if a selected weapon type is in principle possible for this group. The current amount of ammo might be zero but the group still can be rearmed at a later point in time. +-- @param #ARTY self +-- @param #boolean target Target array data structure. +-- @return #boolean True if the group can carry this weapon type, false otherwise. +function ARTY:_CheckWeaponTypePossible(target) + + -- Check if enough ammo is there for the selected weapon type. + local possible=false + if target.weapontype==ARTY.WeaponType.Auto then + possible=self.Nammo0>0 + elseif target.weapontype==ARTY.WeaponType.Cannon then + possible=self.Nshells0>0 + elseif target.weapontype==ARTY.WeaponType.TacticalNukes then + possible=self.Nukes0>0 + elseif target.weapontype==ARTY.WeaponType.IlluminationShells then + possible=self.Nillu0>0 + elseif target.weapontype==ARTY.WeaponType.SmokeShells then + possible=self.Nsmoke0>0 + elseif target.weapontype==ARTY.WeaponType.Rockets then + possible=self.Nrockets0>0 + elseif target.weapontype==ARTY.WeaponType.CruiseMissile then + possible=self.Nmissiles0>0 + end + + return possible +end + +--- Check if a name is unique. If not, a new unique name can be created by adding a running index #01, #02, ... +-- @param #ARTY self +-- @param #table givennames Table with entries of already given names. Must contain a .name item. +-- @param #string name Name to check if it already exists in givennames table. +-- @param #boolean makeunique If true, a new unique name is returned by appending the running index. +-- @return #string Unique name, which is not already given for another target. +function ARTY:_CheckName(givennames, name, makeunique) + self:F2({givennames=givennames, name=name}) + + local newname=name + local counter=1 + local n=1 + local nmax=100 + if makeunique==nil then + makeunique=true + end + + repeat -- until a unique name is found. + + -- We assume the name is unique. + local _unique=true + + -- Loop over all targets already defined. + for _,_target in pairs(givennames) do + + -- Target name. + local _givenname=_target.name + + -- Name is already used by another target. + if _givenname==newname then + + -- Name is already used for another target ==> try again with new name. + _unique=false + + end + + -- Debug info. + self:T3(self.lid..string.format("%d: givenname = %s, newname=%s, unique = %s, makeunique = %s", n, tostring(_givenname), newname, tostring(_unique), tostring(makeunique))) + end + + -- Create a new name if requested and try again. + if _unique==false and makeunique==true then + + -- Define newname = "name #01" + newname=string.format("%s #%02d", name, counter) + + -- Increase counter. + counter=counter+1 + end + + -- Name is not unique and we don't want to make it unique. + if _unique==false and makeunique==false then + self:T3(self.lid..string.format("Name %s is not unique. Return false.", tostring(newname))) + + -- Return + return name, false + end + + -- Increase loop counter. We try max 100 times. + n=n+1 + until (_unique or n==nmax) + + -- Debug output and return new name. + self:T3(self.lid..string.format("Original name %s, new name = %s", name, newname)) + return newname, true +end + +--- Check if target is in range. +-- @param #ARTY self +-- @param #table target Target table. +-- @param #boolean message (Optional) If true, send a message to the coalition if the target is not in range. Default is no message is send. +-- @return #boolean True if target is in range, false otherwise. +-- @return #boolean True if ARTY group is too far away from the target, i.e. distance > max firing range. +-- @return #boolean True if ARTY group is too close to the target, i.e. distance < min finring range. +-- @return #boolean True if target should be removed since ARTY group is immobile and not cargo. +function ARTY:_TargetInRange(target, message) + self:F3(target) + + -- Default is no message. + if message==nil then + message=false + end + + -- Distance between ARTY group and target. + self:T3({controllable=self.Controllable, targetcoord=target.coord}) + local _dist=self.Controllable:GetCoordinate():Get2DDistance(target.coord) + + -- Assume we are in range. + local _inrange=true + local _tooclose=false + local _toofar=false + local text="" + + if _dist < self.minrange then + _inrange=false + _tooclose=true + text=string.format("%s, target is out of range. Distance of %.1f km is below min range of %.1f km.", self.alias, _dist/1000, self.minrange/1000) + elseif _dist > self.maxrange then + _inrange=false + _toofar=true + text=string.format("%s, target is out of range. Distance of %.1f km is greater than max range of %.1f km.", self.alias, _dist/1000, self.maxrange/1000) + end + + -- Debug output. + if not _inrange then + self:T(self.lid..text) + MESSAGE:New(text, 5):ToCoalitionIf(self.coalition, (self.report and message) or (self.Debug and message)) + end + + -- Remove target if ARTY group cannot move, e.g. Mortas. No chance to be ever in range - unless they are cargo. + local _remove=false + if not (self.ismobile or self.iscargo) and _inrange==false then + --self:RemoveTarget(target.name) + _remove=true + end + + return _inrange,_toofar,_tooclose,_remove +end + +--- Get the weapon type name, which should be used to attack the target. +-- @param #ARTY self +-- @param #number tnumber Number of weapon type ARTY.WeaponType.XXX +-- @return #number tnumber of weapon type. +function ARTY:_WeaponTypeName(tnumber) + self:F2(tnumber) + local name="unknown" + if tnumber==ARTY.WeaponType.Auto then + name="Auto" -- (Cannon, Rockets, Missiles) + elseif tnumber==ARTY.WeaponType.Cannon then + name="Cannons" + elseif tnumber==ARTY.WeaponType.Rockets then + name="Rockets" + elseif tnumber==ARTY.WeaponType.CruiseMissile then + name="Cruise Missiles" + elseif tnumber==ARTY.WeaponType.TacticalNukes then + name="Tactical Nukes" + elseif tnumber==ARTY.WeaponType.IlluminationShells then + name="Illumination Shells" + elseif tnumber==ARTY.WeaponType.SmokeShells then + name="Smoke Shells" + end + return name +end + +--- Find a random coordinate in the vicinity of another coordinate. +-- @param #ARTY self +-- @param Core.Point#COORDINATE coord Center coordinate. +-- @param #number rmin (Optional) Minimum distance in meters from center coordinate. Default 20 m. +-- @param #number rmax (Optional) Maximum distance in meters from center coordinate. Default 80 m. +-- @return Core.Point#COORDINATE Random coordinate in a certain distance from center coordinate. +function ARTY:_VicinityCoord(coord, rmin, rmax) + self:F2({coord=coord, rmin=rmin, rmax=rmax}) + -- Set default if necessary. + rmin=rmin or 20 + rmax=rmax or 80 + -- Random point withing range. + local vec2=coord:GetRandomVec2InRadius(rmax, rmin) + local pops=COORDINATE:NewFromVec2(vec2) + -- Debug info. + self:T3(self.lid..string.format("Vicinity distance = %d (rmin=%d, rmax=%d)", pops:Get2DDistance(coord), rmin, rmax)) + return pops +end + +--- Print event-from-to string to DCS log file. +-- @param #ARTY self +-- @param #string BA Before/after info. +-- @param #string Event Event. +-- @param #string From From state. +-- @param #string To To state. +function ARTY:_EventFromTo(BA, Event, From, To) + local text=string.format("%s: %s EVENT %s: %s --> %s", BA, self.groupname, Event, From, To) + self:T3(self.lid..text) +end + +--- Split string. C.f. http://stackoverflow.com/questions/1426954/split-string-in-lua +-- @param #ARTY self +-- @param #string str Sting to split. +-- @param #string sep Speparator for split. +-- @return #table Split text. +function ARTY:_split(str, sep) + self:F3({str=str, sep=sep}) + local result = {} + local regex = ("([^%s]+)"):format(sep) + for each in str:gmatch(regex) do + table.insert(result, each) + end + return result +end + +--- Returns the target parameters as formatted string. +-- @param #ARTY self +-- @param #ARTY.Target target The target data. +-- @return #string name, prio, radius, nshells, engaged, maxengage, time, weapontype +function ARTY:_TargetInfo(target) + local clock=tostring(self:_SecondsToClock(target.time)) + local weapon=self:_WeaponTypeName(target.weapontype) + local _underfire=tostring(target.underfire) + return string.format("%s: prio=%d, radius=%d, nshells=%d, engaged=%d/%d, weapontype=%s, time=%s, underfire=%s, attackgroup=%s", + target.name, target.prio, target.radius, target.nshells, target.engaged, target.maxengage, weapon, clock,_underfire, tostring(target.attackgroup)) +end + +--- Returns a formatted string with information about all move parameters. +-- @param #ARTY self +-- @param #table move Move table item. +-- @return #string Info string. +function ARTY:_MoveInfo(move) + self:F3(move) + local _clock=self:_SecondsToClock(move.time) + return string.format("%s: time=%s, speed=%d, onroad=%s, cancel=%s", move.name, _clock, move.speed, tostring(move.onroad), tostring(move.cancel)) +end + +--- Convert Latitude and Lontigude from DMS to DD. +-- @param #ARTY self +-- @param #string l1 Latitude or longitude as string in the format DD:MM:SS N/S/W/E +-- @param #string l2 Latitude or longitude as string in the format DD:MM:SS N/S/W/E +-- @return #number Latitude in decimal degree format. +-- @return #number Longitude in decimal degree format. +function ARTY:_LLDMS2DD(l1,l2) + self:F2(l1,l2) + + -- Make an array of lat and long. + local _latlong={l1,l2} + + local _latitude=nil + local _longitude=nil + + for _,ll in pairs(_latlong) do + + -- Format is expected as "DD:MM:SS" or "D:M:S". + local _format = "%d+:%d+:%d+" + local _ldms=ll:match(_format) + + if _ldms then + + -- Split DMS to degrees, minutes and seconds. + local _dms=self:_split(_ldms, ":") + local _deg=tonumber(_dms[1]) + local _min=tonumber(_dms[2]) + local _sec=tonumber(_dms[3]) + + -- Convert DMS to DD. + local function DMS2DD(d,m,s) + return d+m/60+s/3600 + end + + -- Detect with hemisphere is meant. + if ll:match("N") then + _latitude=DMS2DD(_deg,_min,_sec) + elseif ll:match("S") then + _latitude=-DMS2DD(_deg,_min,_sec) + elseif ll:match("W") then + _longitude=-DMS2DD(_deg,_min,_sec) + elseif ll:match("E") then + _longitude=DMS2DD(_deg,_min,_sec) + end + + -- Debug text. + local text=string.format("DMS %02d Deg %02d min %02d sec",_deg,_min,_sec) + self:T2(self.lid..text) + + end + end + + -- Debug text. + local text=string.format("\nLatitude %s", tostring(_latitude)) + text=text..string.format("\nLongitude %s", tostring(_longitude)) + self:T2(self.lid..text) + + return _latitude,_longitude +end + +--- Convert time in seconds to hours, minutes and seconds. +-- @param #ARTY self +-- @param #number seconds Time in seconds. +-- @return #string Time in format Hours:minutes:seconds. +function ARTY:_SecondsToClock(seconds) + self:F3({seconds=seconds}) + + if seconds==nil then + return nil + end + + -- Seconds + local seconds = tonumber(seconds) + + -- Seconds of this day. + local _seconds=seconds%(60*60*24) + + if seconds <= 0 then + return nil + else + local hours = string.format("%02.f", math.floor(_seconds/3600)) + local mins = string.format("%02.f", math.floor(_seconds/60 - (hours*60))) + local secs = string.format("%02.f", math.floor(_seconds - hours*3600 - mins *60)) + local days = string.format("%d", seconds/(60*60*24)) + return hours..":"..mins..":"..secs.."+"..days + end +end + +--- Convert clock time from hours, minutes and seconds to seconds. +-- @param #ARTY self +-- @param #string clock String of clock time. E.g., "06:12:35". +function ARTY:_ClockToSeconds(clock) + self:F3({clock=clock}) + + if clock==nil then + return nil + end + + -- Seconds init. + local seconds=0 + + -- Split additional days. + local dsplit=self:_split(clock, "+") + + -- Convert days to seconds. + if #dsplit>1 then + seconds=seconds+tonumber(dsplit[2])*60*60*24 + end + + -- Split hours, minutes, seconds + local tsplit=self:_split(dsplit[1], ":") + + -- Get time in seconds + local i=1 + for _,time in ipairs(tsplit) do + if i==1 then + -- Hours + seconds=seconds+tonumber(time)*60*60 + elseif i==2 then + -- Minutes + seconds=seconds+tonumber(time)*60 + elseif i==3 then + -- Seconds + seconds=seconds+tonumber(time) + end + i=i+1 + end + + self:T3(self.lid..string.format("Clock %s = %d seconds", clock, seconds)) + return seconds +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- **Functional** - Suppress fire of ground units when they get hit. +-- +-- === +-- +-- ## Features: +-- +-- * Hold fire of attacked units when being fired upon. +-- * Retreat to a user defined zone. +-- * Fall back on hits. +-- * Take cover on hits. +-- * Gaussian distribution of suppression time. +-- +-- === +-- +-- ## Missions: +-- +-- ## [MOOSE - ALL Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS) +-- +-- === +-- +-- When ground units get hit by (suppressive) enemy fire, they will not be able to shoot back for a certain amount of time. +-- +-- The implementation is based on an idea and script by MBot. See the [DCS forum threat](https://forums.eagle.ru/showthread.php?t=107635) for details. +-- +-- In addition to suppressing the fire, conditions can be specified, which let the group retreat to a defined zone, move away from the attacker +-- or hide at a nearby scenery object. +-- +-- ==== +-- +-- # YouTube Channel +-- +-- ### [MOOSE YouTube Channel](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg) +-- +-- === +-- +-- ### Author: **[funkyfranky](https://forums.eagle.ru/member.php?u=115026)** +-- +-- ### Contributions: [FlightControl](https://forums.eagle.ru/member.php?u=89536) +-- +-- === +-- +-- @module Functional.Suppression +-- @image Suppression.JPG + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- SUPPRESSION class +-- @type SUPPRESSION +-- @field #string ClassName Name of the class. +-- @field #boolean Debug Write Debug messages to DCS log file and send Debug messages to all players. +-- @field #string lid String for DCS log file. +-- @field #boolean flare Flare units when they get hit or die. +-- @field #boolean smoke Smoke places to which the group retreats, falls back or hides. +-- @field #list DCSdesc Table containing all DCS descriptors of the group. +-- @field #string Type Type of the group. +-- @field #number SpeedMax Maximum speed of group in km/h. +-- @field #boolean IsInfantry True if group has attribute Infantry. +-- @field Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the FSM. Must be a ground group. +-- @field #number Tsuppress_ave Average time in seconds a group gets suppressed. Actual value is sampled randomly from a Gaussian distribution. +-- @field #number Tsuppress_min Minimum time in seconds the group gets suppressed. +-- @field #number Tsuppress_max Maximum time in seconds the group gets suppressed. +-- @field #number TsuppressionOver Time at which the suppression will be over. +-- @field #number IniGroupStrength Number of units in a group at start. +-- @field #number Nhit Number of times the group was hit. +-- @field #string Formation Formation which will be used when falling back, taking cover or retreating. Default "Vee". +-- @field #number Speed Speed the unit will use when falling back, taking cover or retreating. Default 999. +-- @field #boolean MenuON If true creates a entry in the F10 menu. +-- @field #boolean FallbackON If true, group can fall back, i.e. move away from the attacking unit. +-- @field #number FallbackWait Time in seconds the unit will wait at the fall back point before it resumes its mission. +-- @field #number FallbackDist Distance in meters the unit will fall back. +-- @field #number FallbackHeading Heading in degrees to which the group should fall back. Default is directly away from the attacking unit. +-- @field #boolean TakecoverON If true, group can hide at a nearby scenery object. +-- @field #number TakecoverWait Time in seconds the group will hide before it will resume its mission. +-- @field #number TakecoverRange Range in which the group will search for scenery objects to hide at. +-- @field Core.Point#COORDINATE hideout Coordinate/place where the group will try to take cover. +-- @field #number PminFlee Minimum probability in percent that a group will flee (fall back or take cover) at each hit event. Default is 10 %. +-- @field #number PmaxFlee Maximum probability in percent that a group will flee (fall back or take cover) at each hit event. Default is 90 %. +-- @field Core.Zone#ZONE RetreatZone Zone to which a group retreats. +-- @field #number RetreatDamage Damage in percent at which the group will be ordered to retreat. +-- @field #number RetreatWait Time in seconds the group will wait in the retreat zone before it resumes its mission. Default two hours. +-- @field #string CurrentAlarmState Alam state the group is currently in. +-- @field #string CurrentROE ROE the group currently has. +-- @field #string DefaultAlarmState Alarm state the group will go to when it is changed back from another state. Default is "Auto". +-- @field #string DefaultROE ROE the group will get once suppression is over. Default is "Free". +-- @field #boolean eventmoose If true, events are handled by MOOSE. If false, events are handled directly by DCS eventhandler. Default true. +-- @field Core.Zone#ZONE BattleZone +-- @field #boolean AutoEngage +-- @extends Core.Fsm#FSM_CONTROLLABLE +-- + +--- Mimic suppressive enemy fire and let groups flee or retreat. +-- +-- ## Suppression Process +-- +-- ![Process](..\Presentations\SUPPRESSION\Suppression_Process.png) +-- +-- The suppression process can be described as follows. +-- +-- ### CombatReady +-- +-- A group starts in the state **CombatReady**. In this state the group is ready to fight. The ROE is set to either "Weapon Free" or "Return Fire". +-- The alarm state is set to either "Auto" or "Red". +-- +-- ### Event Hit +-- The most important event in this scenario is the **Hit** event. This is an event of the FSM and triggered by the DCS event hit. +-- +-- ### Suppressed +-- After the **Hit** event the group changes its state to **Suppressed**. Technically, the ROE of the group is changed to "Weapon Hold". +-- The suppression of the group will last a certain amount of time. It is randomized an will vary each time the group is hit. +-- The expected suppression time is set to 15 seconds by default. But the actual value is sampled from a Gaussian distribution. +-- +-- ![Process](..\Presentations\SUPPRESSION\Suppression_Gaussian.png) +-- +-- The graph shows the distribution of suppression times if a group would be hit 100,000 times. As can be seen, on most hits the group gets +-- suppressed for around 15 seconds. Other values are also possible but they become less likely the further away from the "expected" suppression time they are. +-- Minimal and maximal suppression times can also be specified. By default these are set to 5 and 25 seconds, respectively. This can also be seen in the graph +-- because the tails of the Gaussian distribution are cut off at these values. +-- +-- ### Event Recovered +-- After the suppression time is over, the event **Recovered** is initiated and the group becomes **CombatReady** again. +-- The ROE of the group will be set to "Weapon Free". +-- +-- Of course, it can also happen that a group is hit again while it is still suppressed. In that case a new random suppression time is calculated. +-- If the new suppression time is longer than the remaining suppression of the previous hit, then the group recovers when the suppression time of the last +-- hit has passed. +-- If the new suppression time is shorter than the remaining suppression, the group will recover after the longer time of the first suppression has passed. +-- +-- For example: +-- +-- * A group gets hit the first time and is suppressed for - let's say - 15 seconds. +-- * After 10 seconds, i.e. when 5 seconds of the old suppression are left, the group gets hit a again. +-- * A new suppression time is calculated which can be smaller or larger than the remaining 5 seconds. +-- * If the new suppression time is smaller, e.g. three seconds, than five seconds, the group will recover after the 5 remaining seconds of the first suppression have passed. +-- * If the new suppression time is longer than last suppression time, e.g. 10 seconds, then the group will recover after the 10 seconds of the new hit have passed. +-- +-- Generally speaking, the suppression times are not just added on top of each other. Because this could easily lead to the situation that a group +-- never becomes CombatReady again before it gets destroyed. +-- +-- The mission designer can capture the event **Recovered** by the function @{#SUPPRESSION.OnAfterRecovered}(). +-- +-- ## Flee Events and States +-- Apart from being suppressed the groups can also flee from the enemy under certain conditions. +-- +-- ### Event Retreat +-- The first option is a retreat. This can be enabled by setting a retreat zone, i.e. a trigger zone defined in the mission editor. +-- +-- If the group takes a certain amount of damage, the event **Retreat** will be called and the group will start to move to the retreat zone. +-- The group will be in the state **Retreating**, which means that its ROE is set to "Weapon Hold" and the alarm state is set to "Green". +-- Setting the alarm state to green is necessary to enable the group to move under fire. +-- +-- When the group has reached the retreat zone, the event **Retreated** is triggered and the state will change to **Retreated** (note that both the event and +-- the state of the same name in this case). ROE and alarm state are +-- set to "Return Fire" and "Auto", respectively. The group will stay in the retreat zone and not actively participate in the combat any more. +-- +-- If no option retreat zone has been specified, the option retreat is not available. +-- +-- The mission designer can capture the events **Retreat** and **Retreated** by the functions @{#SUPPRESSION.OnAfterRetreat}() and @{#SUPPRESSION.OnAfterRetreated}(). +-- +-- ### Fallback +-- +-- If a group is attacked by another ground group, it has the option to fall back, i.e. move away from the enemy. The probability of the event **FallBack** to +-- happen depends on the damage of the group that was hit. The more a group gets damaged, the more likely **FallBack** event becomes. +-- +-- If the group enters the state **FallingBack** it will move 100 meters in the opposite direction of the attacking unit. ROE and alarmstate are set to "Weapon Hold" +-- and "Green", respectively. +-- +-- At the fallback point the group will wait for 60 seconds before it resumes its normal mission. +-- +-- The mission designer can capture the event **FallBack** by the function @{#SUPPRESSION.OnAfterFallBack}(). +-- +-- ### TakeCover +-- +-- If a group is hit by either another ground or air unit, it has the option to "take cover" or "hide". This means that the group will move to a random +-- scenery object in it vicinity. +-- +-- Analogously to the fall back case, the probability of a **TakeCover** event to occur, depends on the damage of the group. The more a group is damaged, the more +-- likely it becomes that a group takes cover. +-- +-- When a **TakeCover** event occurs an area with a radius of 300 meters around the hit group is searched for an arbitrary scenery object. +-- If at least one scenery object is found, the group will move there. One it has reached its "hideout", it will wait there for two minutes before it resumes its +-- normal mission. +-- +-- If more than one scenery object is found, the group will move to a random one. +-- If no scenery object is near the group the **TakeCover** event is rejected and the group will not move. +-- +-- The mission designer can capture the event **TakeCover** by the function @{#SUPPRESSION.OnAfterTakeCover}(). +-- +-- ### Choice of FallBack or TakeCover if both are enabled? +-- +-- If both **FallBack** and **TakeCover** events are enabled by the functions @{#SUPPRESSION.Fallback}() and @{#SUPPRESSION.Takecover}() the algorithm does the following: +-- +-- * If the attacking unit is a ground unit, then the **FallBack** event is executed. +-- * Otherwise, i.e. if the attacker is *not* a ground unit, then the **TakeCover** event is triggered. +-- +-- ### FightBack +-- +-- When a group leaves the states **TakingCover** or **FallingBack** the event **FightBack** is triggered. This changes the ROE and the alarm state back to their default values. +-- +-- The mission designer can capture the event **FightBack** by the function @{#SUPPRESSION.OnAfterFightBack}() +-- +-- # Examples +-- +-- ## Simple Suppression +-- This example shows the basic steps to use suppressive fire for a group. +-- +-- ![Process](..\Presentations\SUPPRESSION\Suppression_Example_01.png) +-- +-- +-- # Customization and Fine Tuning +-- The following user functions can be used to change the default values +-- +-- * @{#SUPPRESSION.SetSuppressionTime}() can be used to set the time a goup gets suppressed. +-- * @{#SUPPRESSION.SetRetreatZone}() sets the retreat zone and enables the possiblity for the group to retreat. +-- * @{#SUPPRESSION.SetFallbackDistance}() sets a value how far the unit moves away from the attacker after the fallback event. +-- * @{#SUPPRESSION.SetFallbackWait}() sets the time after which the group resumes its mission after a FallBack event. +-- * @{#SUPPRESSION.SetTakecoverWait}() sets the time after which the group resumes its mission after a TakeCover event. +-- * @{#SUPPRESSION.SetTakecoverRange}() sets the radius in which hideouts are searched. +-- * @{#SUPPRESSION.SetTakecoverPlace}() explicitly sets the place where the group will run at a TakeCover event. +-- * @{#SUPPRESSION.SetMinimumFleeProbability}() sets the minimum probability that a group flees (FallBack or TakeCover) after a hit. Note taht the probability increases with damage. +-- * @{#SUPPRESSION.SetMaximumFleeProbability}() sets the maximum probability that a group flees (FallBack or TakeCover) after a hit. Default is 90%. +-- * @{#SUPPRESSION.SetRetreatDamage}() sets the damage a group/unit can take before it is ordered to retreat. +-- * @{#SUPPRESSION.SetRetreatWait}() sets the time a group waits in the retreat zone after a retreat. +-- * @{#SUPPRESSION.SetDefaultAlarmState}() sets the alarm state a group gets after it becomes CombatReady again. +-- * @{#SUPPRESSION.SetDefaultROE}() set the rules of engagement a group gets after it becomes CombatReady again. +-- * @{#SUPPRESSION.FlareOn}() is mainly for debugging. A flare is fired when a unit is hit, gets suppressed, recovers, dies. +-- * @{#SUPPRESSION.SmokeOn}() is mainly for debugging. Puts smoke on retreat zone, hideouts etc. +-- * @{#SUPPRESSION.MenuON}() is mainly for debugging. Activates a radio menu item where certain functions like retreat etc. can be triggered manually. +-- +-- +-- @field #SUPPRESSION +SUPPRESSION={ + ClassName = "SUPPRESSION", + Debug = false, + lid = nil, + flare = false, + smoke = false, + DCSdesc = nil, + Type = nil, + IsInfantry = nil, + SpeedMax = nil, + Tsuppress_ave = 15, + Tsuppress_min = 5, + Tsuppress_max = 25, + TsuppressOver = nil, + IniGroupStrength = nil, + Nhit = 0, + Formation = "Off road", + Speed = 4, + MenuON = false, + FallbackON = false, + FallbackWait = 60, + FallbackDist = 100, + FallbackHeading = nil, + TakecoverON = false, + TakecoverWait = 120, + TakecoverRange = 300, + hideout = nil, + PminFlee = 10, + PmaxFlee = 90, + RetreatZone = nil, + RetreatDamage = nil, + RetreatWait = 7200, + CurrentAlarmState = "unknown", + CurrentROE = "unknown", + DefaultAlarmState = "Auto", + DefaultROE = "Weapon Free", + eventmoose = true, +} + +--- Enumerator of possible rules of engagement. +-- @type SUPPRESSION.ROE +-- @field #string Hold Hold fire. +-- @field #string Free Weapon fire. +-- @field #string Return Return fire. +SUPPRESSION.ROE={ + Hold="Weapon Hold", + Free="Weapon Free", + Return="Return Fire", +} + +--- Enumerator of possible alarm states. +-- @type SUPPRESSION.AlarmState +-- @field #string Auto Automatic. +-- @field #string Green Green. +-- @field #string Red Red. +SUPPRESSION.AlarmState={ + Auto="Auto", + Green="Green", + Red="Red", +} + +--- Main F10 menu for suppresion, i.e. F10/Suppression. +-- @field #string MenuF10 +SUPPRESSION.MenuF10=nil + +--- PSEUDOATC version. +-- @field #number version +SUPPRESSION.version="0.9.3" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--TODO list +--DONE: Figure out who was shooting and move away from him. +--DONE: Move behind a scenery building if there is one nearby. +--DONE: Retreat to a given zone or point. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Creates a new AI_suppression object. +-- @param #SUPPRESSION self +-- @param Wrapper.Group#GROUP group The GROUP object for which suppression should be applied. +-- @return #SUPPRESSION SUPPRESSION object or *nil* if group does not exist or is not a ground group. +function SUPPRESSION:New(group) + + -- Inherits from FSM_CONTROLLABLE + local self=BASE:Inherit(self, FSM_CONTROLLABLE:New()) -- #SUPPRESSION + + -- Check that group is present. + if group then + self.lid=string.format("SUPPRESSION %s | ", tostring(group:GetName())) + self:T(self.lid..string.format("SUPPRESSION version %s. Activating suppressive fire for group %s", SUPPRESSION.version, group:GetName())) + else + self:E(self.lid.."SUPPRESSION | Requested group does not exist! (Has to be a MOOSE group.)") + return nil + end + + -- Check that we actually have a GROUND group. + if group:IsGround()==false then + self:E(self.lid..string.format("SUPPRESSION fire group %s has to be a GROUND group!", group:GetName())) + return nil + end + + -- Set the controllable for the FSM. + self:SetControllable(group) + + -- Get DCS descriptors of group. + self.DCSdesc=group:GetDCSDesc(1) + + -- Get max speed the group can do and convert to km/h. + self.SpeedMax=group:GetSpeedMax() + + -- Set speed to maximum. + self.Speed=self.SpeedMax + + -- Is this infantry or not. + self.IsInfantry=group:GetUnit(1):HasAttribute("Infantry") + + -- Type of group. + self.Type=group:GetTypeName() + + -- Initial group strength. + self.IniGroupStrength=#group:GetUnits() + + -- Set ROE and Alarm State. + self:SetDefaultROE("Free") + self:SetDefaultAlarmState("Auto") + + -- Transitions + self:AddTransition("*", "Start", "CombatReady") + self:AddTransition("*", "Status", "*") + self:AddTransition("CombatReady", "Hit", "Suppressed") + self:AddTransition("Suppressed", "Hit", "Suppressed") + self:AddTransition("Suppressed", "Recovered", "CombatReady") + self:AddTransition("Suppressed", "TakeCover", "TakingCover") + self:AddTransition("Suppressed", "FallBack", "FallingBack") + self:AddTransition("*", "Retreat", "Retreating") + self:AddTransition("TakingCover", "FightBack", "CombatReady") + self:AddTransition("FallingBack", "FightBack", "CombatReady") + self:AddTransition("Retreating", "Retreated", "Retreated") + self:AddTransition("*", "OutOfAmmo", "*") + self:AddTransition("*", "Dead", "*") + self:AddTransition("*", "Stop", "Stopped") + + self:AddTransition("TakingCover", "Hit", "TakingCover") + self:AddTransition("FallingBack", "Hit", "FallingBack") + + + --- Trigger "Status" event. + -- @function [parent=#SUPPRESSION] Status + -- @param #SUPPRESSION self + + --- Trigger "Status" event after a delay. + -- @function [parent=#SUPPRESSION] __Status + -- @param #SUPPRESSION self + -- @param #number Delay Delay in seconds. + + --- User function for OnAfter "Status" event. + -- @function [parent=#SUPPRESSION] OnAfterStatus + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Trigger "Hit" event. + -- @function [parent=#SUPPRESSION] Hit + -- @param #SUPPRESSION self + -- @param Wrapper.Unit#UNIT Unit Unit that was hit. + -- @param Wrapper.Unit#UNIT AttackUnit Unit that attacked. + + --- Trigger "Hit" event after a delay. + -- @function [parent=#SUPPRESSION] __Hit + -- @param #SUPPRESSION self + -- @param #number Delay Delay in seconds. + -- @param Wrapper.Unit#UNIT Unit Unit that was hit. + -- @param Wrapper.Unit#UNIT AttackUnit Unit that attacked. + + --- User function for OnBefore "Hit" event. + -- @function [parent=#SUPPRESSION] OnBeforeHit + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Unit#UNIT Unit Unit that was hit. + -- @param Wrapper.Unit#UNIT AttackUnit Unit that attacked. + -- @return #boolean + + --- User function for OnAfter "Hit" event. + -- @function [parent=#SUPPRESSION] OnAfterHit + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Unit#UNIT Unit Unit that was hit. + -- @param Wrapper.Unit#UNIT AttackUnit Unit that attacked. + + + --- Trigger "Recovered" event. + -- @function [parent=#SUPPRESSION] Recovered + -- @param #SUPPRESSION self + + --- Trigger "Recovered" event after a delay. + -- @function [parent=#SUPPRESSION] Recovered + -- @param #number Delay Delay in seconds. + -- @param #SUPPRESSION self + + --- User function for OnBefore "Recovered" event. + -- @function [parent=#SUPPRESSION] OnBeforeRecovered + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @return #boolean + + --- User function for OnAfter "Recovered" event. + -- @function [parent=#SUPPRESSION] OnAfterRecovered + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Trigger "TakeCover" event. + -- @function [parent=#SUPPRESSION] TakeCover + -- @param #SUPPRESSION self + -- @param Core.Point#COORDINATE Hideout Place where the group will hide. + + --- Trigger "TakeCover" event after a delay. + -- @function [parent=#SUPPRESSION] __TakeCover + -- @param #SUPPRESSION self + -- @param #number Delay Delay in seconds. + -- @param Core.Point#COORDINATE Hideout Place where the group will hide. + + --- User function for OnBefore "TakeCover" event. + -- @function [parent=#SUPPRESSION] OnBeforeTakeCover + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Point#COORDINATE Hideout Place where the group will hide. + -- @return #boolean + + --- User function for OnAfter "TakeCover" event. + -- @function [parent=#SUPPRESSION] OnAfterTakeCover + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Point#COORDINATE Hideout Place where the group will hide. + + + --- Trigger "FallBack" event. + -- @function [parent=#SUPPRESSION] FallBack + -- @param #SUPPRESSION self + -- @param Wrapper.Unit#UNIT AttackUnit Attacking unit. We will move away from this. + + --- Trigger "FallBack" event after a delay. + -- @function [parent=#SUPPRESSION] __FallBack + -- @param #SUPPRESSION self + -- @param #number Delay Delay in seconds. + -- @param Wrapper.Unit#UNIT AttackUnit Attacking unit. We will move away from this. + + --- User function for OnBefore "FallBack" event. + -- @function [parent=#SUPPRESSION] OnBeforeFallBack + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Unit#UNIT AttackUnit Attacking unit. We will move away from this. + -- @return #boolean + + --- User function for OnAfter "FallBack" event. + -- @function [parent=#SUPPRESSION] OnAfterFallBack + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Unit#UNIT AttackUnit Attacking unit. We will move away from this. + + + --- Trigger "Retreat" event. + -- @function [parent=#SUPPRESSION] Retreat + -- @param #SUPPRESSION self + + --- Trigger "Retreat" event after a delay. + -- @function [parent=#SUPPRESSION] __Retreat + -- @param #SUPPRESSION self + -- @param #number Delay Delay in seconds. + + --- User function for OnBefore "Retreat" event. + -- @function [parent=#SUPPRESSION] OnBeforeRetreat + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @return #boolean + + --- User function for OnAfter "Retreat" event. + -- @function [parent=#SUPPRESSION] OnAfterRetreat + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Trigger "Retreated" event. + -- @function [parent=#SUPPRESSION] Retreated + -- @param #SUPPRESSION self + + --- Trigger "Retreated" event after a delay. + -- @function [parent=#SUPPRESSION] __Retreated + -- @param #SUPPRESSION self + -- @param #number Delay Delay in seconds. + + --- User function for OnBefore "Retreated" event. + -- @function [parent=#SUPPRESSION] OnBeforeRetreated + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @return #boolean + + --- User function for OnAfter "Retreated" event. + -- @function [parent=#SUPPRESSION] OnAfterRetreated + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Trigger "FightBack" event. + -- @function [parent=#SUPPRESSION] FightBack + -- @param #SUPPRESSION self + + --- Trigger "FightBack" event after a delay. + -- @function [parent=#SUPPRESSION] __FightBack + -- @param #SUPPRESSION self + -- @param #number Delay Delay in seconds. + + --- User function for OnBefore "FlightBack" event. + -- @function [parent=#SUPPRESSION] OnBeforeFightBack + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @return #boolean + + --- User function for OnAfter "FlightBack" event. + -- @function [parent=#SUPPRESSION] OnAfterFightBack + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Trigger "OutOfAmmo" event. + -- @function [parent=#SUPPRESSION] OutOfAmmo + -- @param #SUPPRESSION self + + --- Trigger "OutOfAmmo" event after a delay. + -- @function [parent=#SUPPRESSION] __OutOfAmmo + -- @param #SUPPRESSION self + -- @param #number Delay Delay in seconds. + + --- User function for OnAfter "OutOfAmmo" event. + -- @function [parent=#SUPPRESSION] OnAfterOutOfAmmo + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Trigger "Dead" event. + -- @function [parent=#SUPPRESSION] Dead + -- @param #SUPPRESSION self + + --- Trigger "Dead" event after a delay. + -- @function [parent=#SUPPRESSION] __Dead + -- @param #SUPPRESSION self + -- @param #number Delay Delay in seconds. + + --- User function for OnAfter "Dead" event. + -- @function [parent=#SUPPRESSION] OnAfterDead + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set average, minimum and maximum time a unit is suppressed each time it gets hit. +-- @param #SUPPRESSION self +-- @param #number Tave Average time [seconds] a group will be suppressed. Default is 15 seconds. +-- @param #number Tmin (Optional) Minimum time [seconds] a group will be suppressed. Default is 5 seconds. +-- @param #number Tmax (Optional) Maximum time a group will be suppressed. Default is 25 seconds. +function SUPPRESSION:SetSuppressionTime(Tave, Tmin, Tmax) + self:F({Tave=Tave, Tmin=Tmin, Tmax=Tmax}) + + -- Minimum suppression time is input or default but at least 1 second. + self.Tsuppress_min=Tmin or self.Tsuppress_min + self.Tsuppress_min=math.max(self.Tsuppress_min, 1) + + -- Maximum suppression time is input or dault but at least Tmin. + self.Tsuppress_max=Tmax or self.Tsuppress_max + self.Tsuppress_max=math.max(self.Tsuppress_max, self.Tsuppress_min) + + -- Expected suppression time is input or default but at leat Tmin and at most Tmax. + self.Tsuppress_ave=Tave or self.Tsuppress_ave + self.Tsuppress_ave=math.max(self.Tsuppress_min) + self.Tsuppress_ave=math.min(self.Tsuppress_max) + + self:T(self.lid..string.format("Set ave suppression time to %d seconds.", self.Tsuppress_ave)) + self:T(self.lid..string.format("Set min suppression time to %d seconds.", self.Tsuppress_min)) + self:T(self.lid..string.format("Set max suppression time to %d seconds.", self.Tsuppress_max)) +end + +--- Set the zone to which a group retreats after being damaged too much. +-- @param #SUPPRESSION self +-- @param Core.Zone#ZONE zone MOOSE zone object. +function SUPPRESSION:SetRetreatZone(zone) + self:F({zone=zone}) + self.RetreatZone=zone +end + +--- Turn Debug mode on. Enables messages and more output to DCS log file. +-- @param #SUPPRESSION self +function SUPPRESSION:DebugOn() + self:F() + self.Debug=true +end + +--- Flare units when they are hit, die or recover from suppression. +-- @param #SUPPRESSION self +function SUPPRESSION:FlareOn() + self:F() + self.flare=true +end + +--- Smoke positions where units fall back to, hide or retreat. +-- @param #SUPPRESSION self +function SUPPRESSION:SmokeOn() + self:F() + self.smoke=true +end + +--- Set the formation a group uses for fall back, hide or retreat. +-- @param #SUPPRESSION self +-- @param #string formation Formation of the group. Default "Vee". +function SUPPRESSION:SetFormation(formation) + self:F(formation) + self.Formation=formation or "Vee" +end + +--- Set speed a group moves at for fall back, hide or retreat. +-- @param #SUPPRESSION self +-- @param #number speed Speed in km/h of group. Default max speed the group can do. +function SUPPRESSION:SetSpeed(speed) + self:F(speed) + self.Speed=speed or self.SpeedMax + self.Speed=math.min(self.Speed, self.SpeedMax) +end + +--- Enable fall back if a group is hit. +-- @param #SUPPRESSION self +-- @param #boolean switch Enable=true or disable=false fall back of group. +function SUPPRESSION:Fallback(switch) + self:F(switch) + if switch==nil then + switch=true + end + self.FallbackON=switch +end + +--- Set distance a group will fall back when it gets hit. +-- @param #SUPPRESSION self +-- @param #number distance Distance in meters. +function SUPPRESSION:SetFallbackDistance(distance) + self:F(distance) + self.FallbackDist=distance +end + +--- Set time a group waits at its fall back position before it resumes its normal mission. +-- @param #SUPPRESSION self +-- @param #number time Time in seconds. +function SUPPRESSION:SetFallbackWait(time) + self:F(time) + self.FallbackWait=time +end + +--- Enable take cover option if a unit is hit. +-- @param #SUPPRESSION self +-- @param #boolean switch Enable=true or disable=false fall back of group. +function SUPPRESSION:Takecover(switch) + self:F(switch) + if switch==nil then + switch=true + end + self.TakecoverON=switch +end + +--- Set time a group waits at its hideout position before it resumes its normal mission. +-- @param #SUPPRESSION self +-- @param #number time Time in seconds. +function SUPPRESSION:SetTakecoverWait(time) + self:F(time) + self.TakecoverWait=time +end + +--- Set distance a group searches for hideout places. +-- @param #SUPPRESSION self +-- @param #number range Search range in meters. +function SUPPRESSION:SetTakecoverRange(range) + self:F(range) + self.TakecoverRange=range +end + +--- Set hideout place explicitly. +-- @param #SUPPRESSION self +-- @param Core.Point#COORDINATE Hideout Place where the group will hide after the TakeCover event. +function SUPPRESSION:SetTakecoverPlace(Hideout) + self.hideout=Hideout +end + +--- Set minimum probability that a group flees (falls back or takes cover) after a hit event. Default is 10%. +-- @param #SUPPRESSION self +-- @param #number probability Probability in percent. +function SUPPRESSION:SetMinimumFleeProbability(probability) + self:F(probability) + self.PminFlee=probability or 10 +end + +--- Set maximum probability that a group flees (falls back or takes cover) after a hit event. Default is 90%. +-- @param #SUPPRESSION self +-- @param #number probability Probability in percent. +function SUPPRESSION:SetMaximumFleeProbability(probability) + self:F(probability) + self.PmaxFlee=probability or 90 +end + +--- Set damage threshold before a group is ordered to retreat if a retreat zone was defined. +-- If the group consists of only a singe unit, this referrs to the life of the unit. +-- If the group consists of more than one unit, this referrs to the group strength relative to its initial strength. +-- @param #SUPPRESSION self +-- @param #number damage Damage in percent. If group gets damaged above this value, the group will retreat. Default 50 %. +function SUPPRESSION:SetRetreatDamage(damage) + self:F(damage) + self.RetreatDamage=damage or 50 +end + +--- Set time a group waits in the retreat zone before it resumes its mission. Default is two hours. +-- @param #SUPPRESSION self +-- @param #number time Time in seconds. Default 7200 seconds = 2 hours. +function SUPPRESSION:SetRetreatWait(time) + self:F(time) + self.RetreatWait=time or 7200 +end + +--- Set alarm state a group will get after it returns from a fall back or take cover. +-- @param #SUPPRESSION self +-- @param #string alarmstate Alarm state. Possible "Auto", "Green", "Red". Default is "Auto". +function SUPPRESSION:SetDefaultAlarmState(alarmstate) + self:F(alarmstate) + if alarmstate:lower()=="auto" then + self.DefaultAlarmState=SUPPRESSION.AlarmState.Auto + elseif alarmstate:lower()=="green" then + self.DefaultAlarmState=SUPPRESSION.AlarmState.Green + elseif alarmstate:lower()=="red" then + self.DefaultAlarmState=SUPPRESSION.AlarmState.Red + else + self.DefaultAlarmState=SUPPRESSION.AlarmState.Auto + end +end + +--- Set Rules of Engagement (ROE) a group will get when it recovers from suppression. +-- @param #SUPPRESSION self +-- @param #string roe ROE after suppression. Possible "Free", "Hold" or "Return". Default "Free". +function SUPPRESSION:SetDefaultROE(roe) + self:F(roe) + if roe:lower()=="free" then + self.DefaultROE=SUPPRESSION.ROE.Free + elseif roe:lower()=="hold" then + self.DefaultROE=SUPPRESSION.ROE.Hold + elseif roe:lower()=="return" then + self.DefaultROE=SUPPRESSION.ROE.Return + else + self.DefaultROE=SUPPRESSION.ROE.Free + end +end + +--- Create an F10 menu entry for the suppressed group. The menu is mainly for Debugging purposes. +-- @param #SUPPRESSION self +-- @param #boolean switch Enable=true or disable=false menu group. Default is true. +function SUPPRESSION:MenuOn(switch) + self:F(switch) + if switch==nil then + switch=true + end + self.MenuON=switch +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create F10 main menu, i.e. F10/Suppression. The menu is mainly for Debugging purposes. +-- @param #SUPPRESSION self +function SUPPRESSION:_CreateMenuGroup() + local SubMenuName=self.Controllable:GetName() + local MenuGroup=MENU_MISSION:New(SubMenuName, SUPPRESSION.MenuF10) + MENU_MISSION_COMMAND:New("Fallback!", MenuGroup, self.OrderFallBack, self) + MENU_MISSION_COMMAND:New("Take Cover!", MenuGroup, self.OrderTakeCover, self) + MENU_MISSION_COMMAND:New("Retreat!", MenuGroup, self.OrderRetreat, self) + MENU_MISSION_COMMAND:New("Report Status", MenuGroup, self.Status, self, true) +end + +--- Order group to fall back between 100 and 150 meters in a random direction. +-- @param #SUPPRESSION self +function SUPPRESSION:OrderFallBack() + local group=self.Controllable --Wrapper.Controllable#CONTROLLABLE + local vicinity=group:GetCoordinate():GetRandomVec2InRadius(150, 100) + local coord=COORDINATE:NewFromVec2(vicinity) + self:FallBack(self.Controllable) +end + +--- Order group to take cover at a nearby scenery object. +-- @param #SUPPRESSION self +function SUPPRESSION:OrderTakeCover() + -- Search place to hide or take specified one. + local Hideout=self.hideout + if self.hideout==nil then + Hideout=self:_SearchHideout() + end + -- Trigger TakeCover event. + self:TakeCover(Hideout) +end + +--- Order group to retreat to a pre-defined zone. +-- @param #SUPPRESSION self +function SUPPRESSION:OrderRetreat() + self:Retreat() +end + +--- Status of group. Current ROE, alarm state, life. +-- @param #SUPPRESSION self +-- @param #boolean message Send message to all players. +function SUPPRESSION:StatusReport(message) + + local group=self.Controllable --Wrapper.Group#GROUP + + local nunits=group:CountAliveUnits() + local roe=self.CurrentROE + local state=self.CurrentAlarmState + local life_min, life_max, life_ave, life_ave0, groupstrength=self:_GetLife() + local ammotot=group:GetAmmunition() + local detectedG=group:GetDetectedGroupSet():CountAlive() + local detectedU=group:GetDetectedUnitSet():Count() + + local text=string.format("State %s, Units=%d/%d, ROE=%s, AlarmState=%s, Hits=%d, Life(min/max/ave/ave0)=%d/%d/%d/%d, Total Ammo=%d, Detected=%d/%d", + self:GetState(), nunits, self.IniGroupStrength, self.CurrentROE, self.CurrentAlarmState, self.Nhit, life_min, life_max, life_ave, life_ave0, ammotot, detectedG, detectedU) + + MESSAGE:New(text, 10):ToAllIf(message or self.Debug) + self:I(self.lid..text) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- After "Start" event. Initialized ROE and alarm state. Starts the event handler. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function SUPPRESSION:onafterStart(Controllable, From, Event, To) + self:_EventFromTo("onafterStart", Event, From, To) + + local text=string.format("Started SUPPRESSION for group %s.", Controllable:GetName()) + self:I(self.lid..text) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + + local rzone="not defined" + if self.RetreatZone then + rzone=self.RetreatZone:GetName() + end + + -- Set retreat damage value if it was not set by user input. + if self.RetreatDamage==nil then + if self.RetreatZone then + if self.IniGroupStrength==1 then + self.RetreatDamage=60.0 -- 40% of life is left. + elseif self.IniGroupStrength==2 then + self.RetreatDamage=50.0 -- 50% of group left, i.e. 1 of 2. We already order a retreat, because if for a group 2 two a zone is defined it would not be used at all. + else + self.RetreatDamage=66.5 -- 34% of the group is left, e.g. 1 of 3,4 or 5, 2 of 6,7 or 8, 3 of 9,10 or 11, 4/12, 4/13, 4/14, 5/15, ... + end + else + self.RetreatDamage=100 -- If no retreat then this should be set to 100%. + end + end + + -- Create main F10 menu if it is not there yet. + if self.MenuON then + if not SUPPRESSION.MenuF10 then + SUPPRESSION.MenuF10 = MENU_MISSION:New("Suppression") + end + self:_CreateMenuGroup() + end + + -- Set the current ROE and alam state. + self:_SetAlarmState(self.DefaultAlarmState) + self:_SetROE(self.DefaultROE) + + local text=string.format("\n******************************************************\n") + text=text..string.format("Suppressed group = %s\n", Controllable:GetName()) + text=text..string.format("Type = %s\n", self.Type) + text=text..string.format("IsInfantry = %s\n", tostring(self.IsInfantry)) + text=text..string.format("Group strength = %d\n", self.IniGroupStrength) + text=text..string.format("Average time = %5.1f seconds\n", self.Tsuppress_ave) + text=text..string.format("Minimum time = %5.1f seconds\n", self.Tsuppress_min) + text=text..string.format("Maximum time = %5.1f seconds\n", self.Tsuppress_max) + text=text..string.format("Default ROE = %s\n", self.DefaultROE) + text=text..string.format("Default AlarmState = %s\n", self.DefaultAlarmState) + text=text..string.format("Fall back ON = %s\n", tostring(self.FallbackON)) + text=text..string.format("Fall back distance = %5.1f m\n", self.FallbackDist) + text=text..string.format("Fall back wait = %5.1f seconds\n", self.FallbackWait) + text=text..string.format("Fall back heading = %s degrees\n", tostring(self.FallbackHeading)) + text=text..string.format("Take cover ON = %s\n", tostring(self.TakecoverON)) + text=text..string.format("Take cover search = %5.1f m\n", self.TakecoverRange) + text=text..string.format("Take cover wait = %5.1f seconds\n", self.TakecoverWait) + text=text..string.format("Min flee probability = %5.1f\n", self.PminFlee) + text=text..string.format("Max flee probability = %5.1f\n", self.PmaxFlee) + text=text..string.format("Retreat zone = %s\n", rzone) + text=text..string.format("Retreat damage = %5.1f %%\n", self.RetreatDamage) + text=text..string.format("Retreat wait = %5.1f seconds\n", self.RetreatWait) + text=text..string.format("Speed = %5.1f km/h\n", self.Speed) + text=text..string.format("Speed max = %5.1f km/h\n", self.SpeedMax) + text=text..string.format("Formation = %s\n", self.Formation) + text=text..string.format("******************************************************\n") + self:T(self.lid..text) + + -- Add event handler. + if self.eventmoose then + self:HandleEvent(EVENTS.Hit, self._OnEventHit) + self:HandleEvent(EVENTS.Dead, self._OnEventDead) + else + world.addEventHandler(self) + end + + self:__Status(-1) +end + +--- After "Status" event. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function SUPPRESSION:onafterStatus(Controllable, From, Event, To) + + -- Suppressed group. + local group=self.Controllable --Wrapper.Group#GROUP + + -- Check if group object exists. + if group then + + -- Number of alive units. + local nunits=group:CountAliveUnits() + + -- Check if there are units. + if nunits>0 then + + -- Retreat if completely out of ammo and retreat zone defined. + local nammo=group:GetAmmunition() + if nammo==0 then + self:OutOfAmmo() + end + + -- Status report. + self:StatusReport(false) + + -- Call status again if not "Stopped". + if self:GetState()~="Stopped" then + self:__Status(-30) + end + + else + -- Stop FSM as there are no units left. + self:Stop() + end + + else + -- Stop FSM as there group object does not exist. + self:Stop() + end + +end +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- After "Hit" event. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Unit#UNIT Unit Unit that was hit. +-- @param Wrapper.Unit#UNIT AttackUnit Unit that attacked. +function SUPPRESSION:onafterHit(Controllable, From, Event, To, Unit, AttackUnit) + self:_EventFromTo("onafterHit", Event, From, To) + + -- Suppress unit. + if From=="CombatReady" or From=="Suppressed" then + self:_Suppress() + end + + -- Get life of group in %. + local life_min, life_max, life_ave, life_ave0, groupstrength=self:_GetLife() + + -- Damage in %. If group consists only of one unit, we take its life value. + local Damage=100-life_ave0 + + -- Condition for retreat. + local RetreatCondition = Damage >= self.RetreatDamage-0.01 and self.RetreatZone + + -- Probability that a unit flees. The probability increases linearly with the damage of the group/unit. + -- If Damage=0 ==> P=Pmin + -- if Damage=RetreatDamage ==> P=Pmax + -- If no retreat zone has been specified, RetreatDamage is 100. + local Pflee=(self.PmaxFlee-self.PminFlee)/self.RetreatDamage * math.min(Damage, self.RetreatDamage) + self.PminFlee + + -- Evaluate flee condition. + local P=math.random(0,100) + local FleeCondition = P < Pflee + + local text + text=string.format("\nGroup %s: Life min=%5.1f, max=%5.1f, ave=%5.1f, ave0=%5.1f group=%5.1f\n", Controllable:GetName(), life_min, life_max, life_ave, life_ave0, groupstrength) + text=string.format("Group %s: Damage = %8.4f (%8.4f retreat threshold).\n", Controllable:GetName(), Damage, self.RetreatDamage) + text=string.format("Group %s: P_Flee = %5.1f %5.1f=P_rand (P_Flee > Prand ==> Flee)\n", Controllable:GetName(), Pflee, P) + self:T(self.lid..text) + + -- Group is obviously destroyed. + if Damage >= 99.9 then + return + end + + if RetreatCondition then + + -- Trigger Retreat event. + self:Retreat() + + elseif FleeCondition then + + if self.FallbackON and AttackUnit:IsGround() then + + -- Trigger FallBack event. + self:FallBack(AttackUnit) + + elseif self.TakecoverON then + + -- Search place to hide or take specified one. + local Hideout=self.hideout + if self.hideout==nil then + Hideout=self:_SearchHideout() + end + + -- Trigger TakeCover event. + self:TakeCover(Hideout) + end + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Before "Recovered" event. Check if suppression time is over. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @return #boolean +function SUPPRESSION:onbeforeRecovered(Controllable, From, Event, To) + self:_EventFromTo("onbeforeRecovered", Event, From, To) + + -- Current time. + local Tnow=timer.getTime() + + -- Debug info + self:T(self.lid..string.format("onbeforeRecovered: Time now: %d - Time over: %d", Tnow, self.TsuppressionOver)) + + -- Recovery is only possible if enough time since the last hit has passed. + if Tnow >= self.TsuppressionOver then + return true + else + return false + end + +end + +--- After "Recovered" event. Group has recovered and its ROE is set back to the "normal" unsuppressed state. Optionally the group is flared green. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function SUPPRESSION:onafterRecovered(Controllable, From, Event, To) + self:_EventFromTo("onafterRecovered", Event, From, To) + + if Controllable and Controllable:IsAlive() then + + -- Debug message. + local text=string.format("Group %s has recovered!", Controllable:GetName()) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + self:T(self.lid..text) + + -- Set ROE back to default. + self:_SetROE() + + -- Flare unit green. + if self.flare or self.Debug then + Controllable:FlareGreen() + end + + end +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- After "FightBack" event. ROE and Alarm state are set back to default. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function SUPPRESSION:onafterFightBack(Controllable, From, Event, To) + self:_EventFromTo("onafterFightBack", Event, From, To) + + -- Set ROE and alarm state back to default. + self:_SetROE() + self:_SetAlarmState() +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Before "FallBack" event. We check that group is not already falling back. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Unit#UNIT AttackUnit Attacking unit. We will move away from this. +-- @return #boolean +function SUPPRESSION:onbeforeFallBack(Controllable, From, Event, To, AttackUnit) + self:_EventFromTo("onbeforeFallBack", Event, From, To) + + --TODO: Add retreat? Only allowd transition is Suppressed-->Fallback. So in principle no need. + if From == "FallingBack" then + return false + else + return true + end +end + +--- After "FallBack" event. We get the heading away from the attacker and route the group a certain distance in that direction. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Unit#UNIT AttackUnit Attacking unit. We will move away from this. +function SUPPRESSION:onafterFallBack(Controllable, From, Event, To, AttackUnit) + self:_EventFromTo("onafterFallback", Event, From, To) + + -- Debug info + self:T(self.lid..string.format("Group %s is falling back after %d hits.", Controllable:GetName(), self.Nhit)) + + -- Coordinate of the attacker and attacked unit. + local ACoord=AttackUnit:GetCoordinate() + local DCoord=Controllable:GetCoordinate() + + -- Heading from attacker to attacked unit. + local heading=self:_Heading(ACoord, DCoord) + + -- Overwrite heading with user specified heading. + if self.FallbackHeading then + heading=self.FallbackHeading + end + + -- Create a coordinate ~ 100 m in opposite direction of the attacking unit. + local Coord=DCoord:Translate(self.FallbackDist, heading) + + -- Place marker + if self.Debug then + local MarkerID=Coord:MarkToAll("Fall back position for group "..Controllable:GetName()) + end + + -- Smoke the coordinate. + if self.smoke or self.Debug then + Coord:SmokeBlue() + end + + -- Set ROE to weapon hold. + self:_SetROE(SUPPRESSION.ROE.Hold) + + -- Set alarm state to GREEN and let the unit run away. + self:_SetAlarmState(SUPPRESSION.AlarmState.Green) + + -- Make the group run away. + self:_Run(Coord, self.Speed, self.Formation, self.FallbackWait) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Before "TakeCover" event. Search an area around the group for possible scenery objects where the group can hide. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Point#COORDINATE Hideout Place where the group will hide. +-- @return #boolean +function SUPPRESSION:onbeforeTakeCover(Controllable, From, Event, To, Hideout) + self:_EventFromTo("onbeforeTakeCover", Event, From, To) + + --TODO: Need to test this! + if From=="TakingCover" then + return false + end + + -- Block transition if no hideout place is given. + if Hideout ~= nil then + return true + else + return false + end + +end + +--- After "TakeCover" event. Group will run to a nearby scenery object and "hide" there for a certain time. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Point#COORDINATE Hideout Place where the group will hide. +function SUPPRESSION:onafterTakeCover(Controllable, From, Event, To, Hideout) + self:_EventFromTo("onafterTakeCover", Event, From, To) + + if self.Debug then + local MarkerID=Hideout:MarkToAll(string.format("Hideout for group %s", Controllable:GetName())) + end + + -- Smoke place of hideout. + if self.smoke or self.Debug then + Hideout:SmokeBlue() + end + + -- Set ROE to weapon hold. + self:_SetROE(SUPPRESSION.ROE.Hold) + + -- Set the ALARM STATE to GREEN. Then the unit will move even if it is under fire. + self:_SetAlarmState(SUPPRESSION.AlarmState.Green) + + -- Make the group run away. + self:_Run(Hideout, self.Speed, self.Formation, self.TakecoverWait) + +end + +--- After "OutOfAmmo" event. Triggered when group is completely out of ammo. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function SUPPRESSION:onafterOutOfAmmo(Controllable, From, Event, To) + self:_EventFromTo("onafterOutOfAmmo", Event, From, To) + + -- Info to log. + self:I(self.lid..string.format("Out of ammo!")) + + -- Order retreat if retreat zone was specified. + if self.RetreatZone then + self:Retreat() + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Before "Retreat" event. We check that the group is not already retreating. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @return #boolean True if transition is allowed, False if transition is forbidden. +function SUPPRESSION:onbeforeRetreat(Controllable, From, Event, To) + self:_EventFromTo("onbeforeRetreat", Event, From, To) + + if From=="Retreating" then + local text=string.format("Group %s is already retreating.", tostring(Controllable:GetName())) + self:T2(self.lid..text) + return false + else + return true + end + +end + +--- After "Retreat" event. Find a random point in the retreat zone and route the group there. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function SUPPRESSION:onafterRetreat(Controllable, From, Event, To) + self:_EventFromTo("onafterRetreat", Event, From, To) + + -- Route the group to a zone. + local text=string.format("Group %s is retreating! Alarm state green.", Controllable:GetName()) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + self:T(self.lid..text) + + -- Get a random point in the retreat zone. + local ZoneCoord=self.RetreatZone:GetRandomCoordinate() -- Core.Point#COORDINATE + local ZoneVec2=ZoneCoord:GetVec2() + + -- Debug smoke zone and point. + if self.smoke or self.Debug then + ZoneCoord:SmokeBlue() + end + if self.Debug then + self.RetreatZone:SmokeZone(SMOKECOLOR.Red, 12) + end + + -- Set ROE to weapon hold. + self:_SetROE(SUPPRESSION.ROE.Hold) + + -- Set the ALARM STATE to GREEN. Then the unit will move even if it is under fire. + self:_SetAlarmState(SUPPRESSION.AlarmState.Green) + + -- Make unit run to retreat zone and wait there for ~two hours. + self:_Run(ZoneCoord, self.Speed, self.Formation, self.RetreatWait) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Before "Retreateded" event. Check that the group is really in the retreat zone. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function SUPPRESSION:onbeforeRetreated(Controllable, From, Event, To) + self:_EventFromTo("onbeforeRetreated", Event, From, To) + + -- Check that the group is inside the zone. + local inzone=self.RetreatZone:IsVec3InZone(Controllable:GetVec3()) + + return inzone +end + +--- After "Retreateded" event. Group has reached the retreat zone. Set ROE to return fire and alarm state to auto. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function SUPPRESSION:onafterRetreated(Controllable, From, Event, To) + self:_EventFromTo("onafterRetreated", Event, From, To) + + -- Set ROE to weapon return fire. + self:_SetROE(SUPPRESSION.ROE.Return) + + -- Set the ALARM STATE to GREEN. Then the unit will move even if it is under fire. + self:_SetAlarmState(SUPPRESSION.AlarmState.Auto) + + -- TODO: Add hold task? Move from _Run() +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- After "Dead" event, when a unit has died. When all units of a group are dead, FSM is stopped and eventhandler removed. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function SUPPRESSION:onafterDead(Controllable, From, Event, To) + self:_EventFromTo("onafterDead", Event, From, To) + + local group=self.Controllable --Wrapper.Group#GROUP + + if group then + + -- Number of units left in the group. + local nunits=group:CountAliveUnits() + + local text=string.format("Group %s: One of our units just died! %d units left.", self.Controllable:GetName(), nunits) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + self:T(self.lid..text) + + -- Go to stop state. + if nunits==0 then + self:Stop() + end + + else + self:Stop() + end + +end + +--- After "Stop" event. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function SUPPRESSION:onafterStop(Controllable, From, Event, To) + self:_EventFromTo("onafterStop", Event, From, To) + + local text=string.format("Stopping SUPPRESSION for group %s", self.Controllable:GetName()) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + self:I(self.lid..text) + + -- Clear all pending schedules + self.CallScheduler:Clear() + + if self.mooseevents then + self:UnHandleEvent(EVENTS.Dead) + self:UnHandleEvent(EVENTS.Hit) + else + world.removeEventHandler(self) + end + +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Event Handler +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Event handler for suppressed groups. +--@param #SUPPRESSION self +function SUPPRESSION:onEvent(Event) + --self:E(event) + + if Event == nil or Event.initiator == nil or Unit.getByName(Event.initiator:getName()) == nil then + return true + end + + local EventData={} + if Event.initiator then + EventData.IniDCSUnit = Event.initiator + EventData.IniUnitName = Event.initiator:getName() + EventData.IniDCSGroup = Event.initiator:getGroup() + EventData.IniGroupName = Event.initiator:getGroup():getName() + EventData.IniGroup = GROUP:FindByName(EventData.IniGroupName) + EventData.IniUnit = UNIT:FindByName(EventData.IniUnitName) + end + + if Event.target then + EventData.TgtDCSUnit = Event.target + EventData.TgtUnitName = Event.target:getName() + EventData.TgtDCSGroup = Event.target:getGroup() + EventData.TgtGroupName = Event.target:getGroup():getName() + EventData.TgtGroup = GROUP:FindByName(EventData.TgtGroupName) + EventData.TgtUnit = UNIT:FindByName(EventData.TgtUnitName) + end + + + -- Event HIT + if Event.id == world.event.S_EVENT_HIT then + self:_OnEventHit(EventData) + end + + -- Event DEAD + if Event.id == world.event.S_EVENT_DEAD then + self:_OnEventDead(EventData) + end + +end + +--- Event handler for Dead event of suppressed groups. +-- @param #SUPPRESSION self +-- @param Core.Event#EVENTDATA EventData +function SUPPRESSION:_OnEventHit(EventData) + self:F(EventData) + + local GroupNameSelf=self.Controllable:GetName() + local GroupNameTgt=EventData.TgtGroupName + local TgtUnit=EventData.TgtUnit + local tgt=EventData.TgtDCSUnit + local IniUnit=EventData.IniUnit + + -- Check that correct group was hit. + if GroupNameTgt == GroupNameSelf then + + self:T(self.lid..string.format("Hit event at t = %5.1f", timer.getTime())) + + -- Flare unit that was hit. + if self.flare or self.Debug then + TgtUnit:FlareRed() + end + + -- Increase Hit counter. + self.Nhit=self.Nhit+1 + + -- Info on hit times. + self:T(self.lid..string.format("Group %s has just been hit %d times.", self.Controllable:GetName(), self.Nhit)) + + --self:Status() + local life=tgt:getLife()/(tgt:getLife0()+1)*100 + self:T2(self.lid..string.format("Target unit life = %5.1f", life)) + + -- FSM Hit event. + self:__Hit(3, TgtUnit, IniUnit) + end + +end + +--- Event handler for Dead event of suppressed groups. +-- @param #SUPPRESSION self +-- @param Core.Event#EVENTDATA EventData +function SUPPRESSION:_OnEventDead(EventData) + + local GroupNameSelf=self.Controllable:GetName() + local GroupNameIni=EventData.IniGroupName + + -- Check for correct group. + if GroupNameIni==GroupNameSelf then + + -- Dead Unit. + local IniUnit=EventData.IniUnit --Wrapper.Unit#UNIT + local IniUnitName=EventData.IniUnitName + + if EventData.IniUnit then + self:T2(self.lid..string.format("Group %s: Dead MOOSE unit DOES exist! Unit name %s.", GroupNameIni, IniUnitName)) + else + self:T2(self.lid..string.format("Group %s: Dead MOOSE unit DOES NOT not exist! Unit name %s.", GroupNameIni, IniUnitName)) + end + + if EventData.IniDCSUnit then + self:T2(self.lid..string.format("Group %s: Dead DCS unit DOES exist! Unit name %s.", GroupNameIni, IniUnitName)) + else + self:T2(self.lid..string.format("Group %s: Dead DCS unit DOES NOT exist! Unit name %s.", GroupNameIni, IniUnitName)) + end + + -- Flare unit that died. + if IniUnit and (self.flare or self.Debug) then + IniUnit:FlareWhite() + self:T(self.lid..string.format("Flare Dead MOOSE unit.")) + end + + -- Flare unit that died. + if EventData.IniDCSUnit and (self.flare or self.Debug) then + local p=EventData.IniDCSUnit:getPosition().p + trigger.action.signalFlare(p, trigger.flareColor.Yellow , 0) + self:T(self.lid..string.format("Flare Dead DCS unit.")) + end + + -- Get status. + self:Status() + + -- FSM Dead event. + self:__Dead(0.1) + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Suppress fire of a unit by setting its ROE to "Weapon Hold". +-- @param #SUPPRESSION self +function SUPPRESSION:_Suppress() + + -- Current time. + local Tnow=timer.getTime() + + -- Controllable + local Controllable=self.Controllable --Wrapper.Controllable#CONTROLLABLE + + -- Group will hold their weapons. + self:_SetROE(SUPPRESSION.ROE.Hold) + + -- Get randomized time the unit is suppressed. + local sigma=(self.Tsuppress_max-self.Tsuppress_min)/4 + local Tsuppress=self:_Random_Gaussian(self.Tsuppress_ave,sigma,self.Tsuppress_min, self.Tsuppress_max) + + -- Time at which the suppression is over. + local renew=true + if self.TsuppressionOver ~= nil then + if Tsuppress+Tnow > self.TsuppressionOver then + self.TsuppressionOver=Tnow+Tsuppress + else + renew=false + end + else + self.TsuppressionOver=Tnow+Tsuppress + end + + -- Recovery event will be called in Tsuppress seconds. + if renew then + self:__Recovered(self.TsuppressionOver-Tnow) + end + + -- Debug message. + local text=string.format("Group %s is suppressed for %d seconds. Suppression ends at %d:%02d.", Controllable:GetName(), Tsuppress, self.TsuppressionOver/60, self.TsuppressionOver%60) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + self:T(self.lid..text) + +end + + +--- Make group run/drive to a certain point. We put in several intermediate waypoints because sometimes the group stops before it arrived at the desired point. +--@param #SUPPRESSION self +--@param Core.Point#COORDINATE fin Coordinate where we want to go. +--@param #number speed Speed of group. Default is 20. +--@param #string formation Formation of group. Default is "Vee". +--@param #number wait Time the group will wait/hold at final waypoint. Default is 30 seconds. +function SUPPRESSION:_Run(fin, speed, formation, wait) + + speed=speed or 20 + formation=formation or "Off road" + wait=wait or 30 + + local group=self.Controllable -- Wrapper.Controllable#CONTROLLABLE + + if group and group:IsAlive() then + + -- Clear all tasks. + group:ClearTasks() + + -- Current coordinates of group. + local ini=group:GetCoordinate() + + -- Distance between current and final point. + local dist=ini:Get2DDistance(fin) + + -- Heading from ini to fin. + local heading=self:_Heading(ini, fin) + + -- Number of waypoints. + local nx + if dist <= 50 then + nx=2 + elseif dist <= 100 then + nx=3 + elseif dist <= 500 then + nx=4 + else + nx=5 + end + + -- Number of intermediate waypoints. + local dx=dist/(nx-1) + + -- Waypoint and task arrays. + local wp={} + local tasks={} + + -- First waypoint is the current position of the group. + wp[1]=ini:WaypointGround(speed, formation) + tasks[1]=group:TaskFunction("SUPPRESSION._Passing_Waypoint", self, 1, false) + + if self.Debug then + local MarkerID=ini:MarkToAll(string.format("Waypoing %d of group %s (initial)", #wp, self.Controllable:GetName())) + end + + self:T2(self.lid..string.format("Number of waypoints %d", nx)) + for i=1,nx-2 do + + local x=dx*i + local coord=ini:Translate(x, heading) + + wp[#wp+1]=coord:WaypointGround(speed, formation) + tasks[#tasks+1]=group:TaskFunction("SUPPRESSION._Passing_Waypoint", self, #wp, false) + + self:T2(self.lid..string.format("%d x = %4.1f", i, x)) + if self.Debug then + local MarkerID=coord:MarkToAll(string.format("Waypoing %d of group %s", #wp, self.Controllable:GetName())) + end + + end + self:T2(self.lid..string.format("Total distance: %4.1f", dist)) + + -- Final waypoint. + wp[#wp+1]=fin:WaypointGround(speed, formation) + if self.Debug then + local MarkerID=fin:MarkToAll(string.format("Waypoing %d of group %s (final)", #wp, self.Controllable:GetName())) + end + + -- Task to hold. + local ConditionWait=group:TaskCondition(nil, nil, nil, nil, wait, nil) + local TaskHold = group:TaskHold() + + -- Task combo to make group hold at final waypoint. + local TaskComboFin = {} + TaskComboFin[#TaskComboFin+1] = group:TaskFunction("SUPPRESSION._Passing_Waypoint", self, #wp, true) + TaskComboFin[#TaskComboFin+1] = group:TaskControlled(TaskHold, ConditionWait) + + -- Add final task. + tasks[#tasks+1]=group:TaskCombo(TaskComboFin) + + -- Original waypoints of the group. + local Waypoints = group:GetTemplateRoutePoints() + + -- New points are added to the default route. + for i,p in ipairs(wp) do + table.insert(Waypoints, i, wp[i]) + end + + -- Set task for all waypoints. + for i,wp in ipairs(Waypoints) do + group:SetTaskWaypoint(Waypoints[i], tasks[i]) + end + + -- Submit task and route group along waypoints. + group:Route(Waypoints) + + else + self:E(self.lid..string.format("ERROR: Group is not alive!")) + end + +end + +--- Function called when group is passing a waypoint. At the last waypoint we set the group back to CombatReady. +--@param Wrapper.Group#GROUP group Group which is passing a waypoint. +--@param #SUPPRESSION Fsm The suppression object. +--@param #number i Waypoint number that has been reached. +--@param #boolean final True if it is the final waypoint. Start Fightback. +function SUPPRESSION._Passing_Waypoint(group, Fsm, i, final) + + -- Debug message. + local text=string.format("Group %s passing waypoint %d (final=%s)", group:GetName(), i, tostring(final)) + MESSAGE:New(text,10):ToAllIf(Fsm.Debug) + if Fsm.Debug then + env.info(self.lid..text) + end + + if final then + if Fsm:is("Retreating") then + -- Retreated-->Retreated. + Fsm:Retreated() + else + -- FightBack-->Combatready: Change alarm state back to default. + Fsm:FightBack() + end + end +end + + +--- Search a place to hide. This is any scenery object in the vicinity. +--@param #SUPPRESSION self +--@return Core.Point#COORDINATE Coordinate of the hideout place. +--@return nil If no scenery object is within search radius. +function SUPPRESSION:_SearchHideout() + -- We search objects in a zone with radius ~300 m around the group. + local Zone = ZONE_GROUP:New("Zone_Hiding", self.Controllable, self.TakecoverRange) + local gpos = self.Controllable:GetCoordinate() + + -- Scan for Scenery objects to run/drive to. + Zone:Scan(Object.Category.SCENERY) + + -- Array with all possible hideouts, i.e. scenery objects in the vicinity of the group. + local hideouts={} + + for SceneryTypeName, SceneryData in pairs(Zone:GetScannedScenery()) do + for SceneryName, SceneryObject in pairs(SceneryData) do + + local SceneryObject = SceneryObject -- Wrapper.Scenery#SCENERY + + -- Position of the scenery object. + local spos=SceneryObject:GetCoordinate() + + -- Distance from group to hideout. + local distance= spos:Get2DDistance(gpos) + + if self.Debug then + -- Place markers on every possible scenery object. + local MarkerID=SceneryObject:GetCoordinate():MarkToAll(string.format("%s scenery object %s", self.Controllable:GetName(),SceneryObject:GetTypeName())) + local text=string.format("%s scenery: %s, Coord %s", self.Controllable:GetName(), SceneryObject:GetTypeName(), SceneryObject:GetCoordinate():ToStringLLDMS()) + self:T2(self.lid..text) + end + + -- Add to table. + table.insert(hideouts, {object=SceneryObject, distance=distance}) + end + end + + -- Get random hideout place. + local Hideout=nil + if #hideouts>0 then + + -- Debug info. + self:T(self.lid.."Number of hideouts "..#hideouts) + + -- Sort results table wrt number of hits. + local _sort = function(a,b) return a.distance < b.distance end + table.sort(hideouts,_sort) + + -- Pick a random location. + --Hideout=hideouts[math.random(#hideouts)].object + + -- Pick closest location. + Hideout=hideouts[1].object:GetCoordinate() + + else + self:E(self.lid.."No hideouts found!") + end + + return Hideout + +end + +--- Get (relative) life in percent of a group. Function returns the value of the units with the smallest and largest life. Also the average value of all groups is returned. +-- @param #SUPPRESSION self +-- @return #number Smallest life value of all units. +-- @return #number Largest life value of all units. +-- @return #number Average life value of all alife groups +-- @return #number Average life value of all groups including already dead ones. +-- @return #number Relative group strength. +function SUPPRESSION:_GetLife() + + local group=self.Controllable --Wrapper.Group#GROUP + + if group and group:IsAlive() then + + local units=group:GetUnits() + + local life_min=nil + local life_max=nil + local life_ave=0 + local life_ave0=0 + local n=0 + + local groupstrength=#units/self.IniGroupStrength*100 + + self.T2(self.lid..string.format("Group %s _GetLife nunits = %d", self.Controllable:GetName(), #units)) + + for _,unit in pairs(units) do + + local unit=unit -- Wrapper.Unit#UNIT + if unit and unit:IsAlive() then + n=n+1 + local life=unit:GetLife()/(unit:GetLife0()+1)*100 + if life_min==nil or life < life_min then + life_min=life + end + if life_max== nil or life > life_max then + life_max=life + end + life_ave=life_ave+life + if self.Debug then + local text=string.format("n=%02d: Life = %3.1f, Life0 = %3.1f, min=%3.1f, max=%3.1f, ave=%3.1f, group=%3.1f", n, unit:GetLife(), unit:GetLife0(), life_min, life_max, life_ave/n,groupstrength) + self:T2(self.lid..text) + end + end + + end + + -- If the counter did not increase (can happen!) return 0 + if n==0 then + return 0,0,0,0,0 + end + + -- Average life relative to initial group strength including the dead ones. + life_ave0=life_ave/self.IniGroupStrength + + -- Average life of all alive units. + life_ave=life_ave/n + + return life_min, life_max, life_ave, life_ave0, groupstrength + else + return 0, 0, 0, 0, 0 + end +end + + +--- Heading from point a to point b in degrees. +--@param #SUPPRESSION self +--@param Core.Point#COORDINATE a Coordinate. +--@param Core.Point#COORDINATE b Coordinate. +--@return #number angle Angle from a to b in degrees. +function SUPPRESSION:_Heading(a, b) + local dx = b.x-a.x + local dy = b.z-a.z + local angle = math.deg(math.atan2(dy,dx)) + if angle < 0 then + angle = 360 + angle + end + return angle +end + +--- Generate Gaussian pseudo-random numbers. +-- @param #SUPPRESSION self +-- @param #number x0 Expectation value of distribution. +-- @param #number sigma (Optional) Standard deviation. Default 10. +-- @param #number xmin (Optional) Lower cut-off value. +-- @param #number xmax (Optional) Upper cut-off value. +-- @return #number Gaussian random number. +function SUPPRESSION:_Random_Gaussian(x0, sigma, xmin, xmax) + + -- Standard deviation. Default 5 if not given. + sigma=sigma or 5 + + local r + local gotit=false + local i=0 + while not gotit do + + -- Uniform numbers in [0,1). We need two. + local x1=math.random() + local x2=math.random() + + -- Transform to Gaussian exp(-(x-x0)²/(2*sigma²). + r = math.sqrt(-2*sigma*sigma * math.log(x1)) * math.cos(2*math.pi * x2) + x0 + + i=i+1 + if (r>=xmin and r<=xmax) or i>100 then + gotit=true + end + end + + return r + +end + +--- Sets the ROE for the group and updates the current ROE variable. +-- @param #SUPPRESSION self +-- @param #string roe ROE the group will get. Possible "Free", "Hold", "Return". Default is self.DefaultROE. +function SUPPRESSION:_SetROE(roe) + local group=self.Controllable --Wrapper.Controllable#CONTROLLABLE + + -- If no argument is given, we take the default ROE. + roe=roe or self.DefaultROE + + -- Update the current ROE. + self.CurrentROE=roe + + -- Set the ROE. + if roe==SUPPRESSION.ROE.Free then + group:OptionROEOpenFire() + elseif roe==SUPPRESSION.ROE.Hold then + group:OptionROEHoldFire() + elseif roe==SUPPRESSION.ROE.Return then + group:OptionROEReturnFire() + else + self:E(self.lid.."Unknown ROE requested: "..tostring(roe)) + group:OptionROEOpenFire() + self.CurrentROE=SUPPRESSION.ROE.Free + end + + local text=string.format("Group %s now has ROE %s.", self.Controllable:GetName(), self.CurrentROE) + self:T(self.lid..text) +end + +--- Sets the alarm state of the group and updates the current alarm state variable. +-- @param #SUPPRESSION self +-- @param #string state Alarm state the group will get. Possible "Auto", "Green", "Red". Default is self.DefaultAlarmState. +function SUPPRESSION:_SetAlarmState(state) + local group=self.Controllable --Wrapper.Controllable#CONTROLLABLE + + -- Input or back to default alarm state. + state=state or self.DefaultAlarmState + + -- Update the current alam state of the group. + self.CurrentAlarmState=state + + -- Set the alarm state. + if state==SUPPRESSION.AlarmState.Auto then + group:OptionAlarmStateAuto() + elseif state==SUPPRESSION.AlarmState.Green then + group:OptionAlarmStateGreen() + elseif state==SUPPRESSION.AlarmState.Red then + group:OptionAlarmStateRed() + else + self:E(self.lid.."Unknown alarm state requested: "..tostring(state)) + group:OptionAlarmStateAuto() + self.CurrentAlarmState=SUPPRESSION.AlarmState.Auto + end + + local text=string.format("Group %s now has Alarm State %s.", self.Controllable:GetName(), self.CurrentAlarmState) + self:T(self.lid..text) +end + +--- Print event-from-to string to DCS log file. +-- @param #SUPPRESSION self +-- @param #string BA Before/after info. +-- @param #string Event Event. +-- @param #string From From state. +-- @param #string To To state. +function SUPPRESSION:_EventFromTo(BA, Event, From, To) + local text=string.format("\n%s: %s EVENT %s: %s --> %s", BA, self.Controllable:GetName(), Event, From, To) + self:T2(self.lid..text) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- **Functional** - Rudimentary ATC. +-- +-- ![Banner Image](..\Presentations\PSEUDOATC\PSEUDOATC_Main.jpg) +-- +-- ==== +-- +-- The pseudo ATC enhances the standard DCS ATC functions. +-- +-- In particular, a menu entry "Pseudo ATC" is created in the "F10 Other..." radiomenu. +-- +-- ## Features: +-- +-- * Weather report at nearby airbases and mission waypoints. +-- * Report absolute bearing and range to nearest airports and mission waypoints. +-- * Report current altitude AGL of own aircraft. +-- * Upon request, ATC reports altitude until touchdown. +-- * Works with static and dynamic weather. +-- * Player can select the unit system (metric or imperial) in which information is reported. +-- * All maps supported (Caucasus, NTTR, Normandy, Persian Gulf and all future maps). +-- +-- ==== +-- +-- # YouTube Channel +-- +-- ### [MOOSE YouTube Channel](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg) +-- +-- === +-- +-- ### Author: **[funkyfranky](https://forums.eagle.ru/member.php?u=115026)** +-- +-- ### Contributions: [FlightControl](https://forums.eagle.ru/member.php?u=89536) +-- +-- ==== +-- @module Functional.PseudoATC +-- @image Pseudo_ATC.JPG + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- PSEUDOATC class +-- @type PSEUDOATC +-- @field #string ClassName Name of the Class. +-- @field #table player Table comprising each player info. +-- @field #boolean Debug If true, print debug info to dcs.log file. +-- @field #number mdur Duration in seconds how low messages to the player are displayed. +-- @field #number mrefresh Interval in seconds after which the F10 menu is refreshed. E.g. by the closest airports. Default is 120 sec. +-- @field #number talt Interval in seconds between reporting altitude until touchdown. Default 3 sec. +-- @field #boolean chatty Display some messages on events like take-off and touchdown. +-- @field #boolean eventsmoose If true, events are handled by MOOSE. If false, events are handled directly by DCS eventhandler. +-- @extends Core.Base#BASE + +--- Adds some rudimentary ATC functionality via the radio menu. +-- +-- Local weather reports can be requested for nearby airports and player's mission waypoints. +-- The weather report includes +-- +-- * QFE and QNH pressures, +-- * Temperature, +-- * Wind direction and strength. +-- +-- The list of airports is updated every 60 seconds. This interval can be adjusted by the function @{#PSEUDOATC.SetMenuRefresh}(*interval*). +-- +-- Likewise, absolute bearing and range to the close by airports and mission waypoints can be requested. +-- +-- The player can switch the unit system in which all information is displayed during the mission with the MOOSE settings radio menu. +-- The unit system can be set to either imperial or metric. Altitudes are reported in feet or meter, distances in kilometers or nautical miles, +-- temperatures in degrees Fahrenheit or Celsius and QFE/QNH pressues in inHg or mmHg. +-- Note that the pressures are also reported in hPa independent of the unit system setting. +-- +-- In bad weather conditions, the ATC can "talk you down", i.e. will continuously report your altitude on the final approach. +-- Default reporting time interval is 3 seconds. This can be adjusted via the @{#PSEUDOATC.SetReportAltInterval}(*interval*) function. +-- The reporting stops automatically when the player lands or can be stopped manually by clicking on the radio menu item again. +-- So the radio menu item acts as a toggle to switch the reporting on and off. +-- +-- ## Scripting +-- +-- Scripting is almost trivial. Just add the following two lines to your script: +-- +-- pseudoATC=PSEUDOATC:New() +-- pseudoATC:Start() +-- +-- +-- @field #PSEUDOATC +PSEUDOATC={ + ClassName = "PSEUDOATC", + group={}, + Debug=false, + mdur=30, + mrefresh=120, + talt=3, + chatty=true, + eventsmoose=true, +} + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Some ID to identify who we are in output of the DCS.log file. +-- @field #string id +PSEUDOATC.id="PseudoATC | " + +--- PSEUDOATC version. +-- @field #number version +PSEUDOATC.version="0.9.2" + +----------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO list +-- DONE: Add takeoff event. +-- DONE: Add user functions. + +----------------------------------------------------------------------------------------------------------------------------------------- + +--- PSEUDOATC contructor. +-- @param #PSEUDOATC self +-- @return #PSEUDOATC Returns a PSEUDOATC object. +function PSEUDOATC:New() + + -- Inherit BASE. + local self=BASE:Inherit(self, BASE:New()) -- #PSEUDOATC + + -- Debug info + self:E(PSEUDOATC.id..string.format("PseudoATC version %s", PSEUDOATC.version)) + + -- Return object. + return self +end + +--- Starts the PseudoATC event handlers. +-- @param #PSEUDOATC self +function PSEUDOATC:Start() + self:F() + + -- Debug info + self:E(PSEUDOATC.id.."Starting PseudoATC") + + -- Handle events. + if self.eventsmoose then + self:T(PSEUDOATC.id.."Events are handled by MOOSE.") + self:HandleEvent(EVENTS.Birth, self._OnBirth) + self:HandleEvent(EVENTS.Land, self._PlayerLanded) + self:HandleEvent(EVENTS.Takeoff, self._PlayerTakeOff) + self:HandleEvent(EVENTS.PlayerLeaveUnit, self._PlayerLeft) + self:HandleEvent(EVENTS.Crash, self._PlayerLeft) + --self:HandleEvent(EVENTS.Ejection, self._PlayerLeft) + --self:HandleEvent(EVENTS.PilotDead, self._PlayerLeft) + else + self:T(PSEUDOATC.id.."Events are handled by DCS.") + -- Events are handled directly by DCS. + world.addEventHandler(self) + end + +end + +----------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions + +--- Debug mode on. Send messages to everone. +-- @param #PSEUDOATC self +function PSEUDOATC:DebugOn() + self.Debug=true +end + +--- Debug mode off. This is the default setting. +-- @param #PSEUDOATC self +function PSEUDOATC:DebugOff() + self.Debug=false +end + +--- Chatty mode on. Display some messages on take-off and touchdown. +-- @param #PSEUDOATC self +function PSEUDOATC:ChattyOn() + self.chatty=true +end + +--- Chatty mode off. Don't display some messages on take-off and touchdown. +-- @param #PSEUDOATC self +function PSEUDOATC:ChattyOff() + self.chatty=false +end + +--- Set duration how long messages are displayed. +-- @param #PSEUDOATC self +-- @param #number duration Time in seconds. Default is 30 sec. +function PSEUDOATC:SetMessageDuration(duration) + self.mdur=duration or 30 +end + +--- Set time interval after which the F10 radio menu is refreshed. +-- @param #PSEUDOATC self +-- @param #number interval Interval in seconds. Default is every 120 sec. +function PSEUDOATC:SetMenuRefresh(interval) + self.mrefresh=interval or 120 +end + +--- Enable/disable event handling by MOOSE or DCS. +-- @param #PSEUDOATC self +-- @param #boolean switch If true, events are handled by MOOSE (default). If false, events are handled directly by DCS. +function PSEUDOATC:SetEventsMoose(switch) + self.eventsmoose=switch +end + +--- Set time interval for reporting altitude until touchdown. +-- @param #PSEUDOATC self +-- @param #number interval Interval in seconds. Default is every 3 sec. +function PSEUDOATC:SetReportAltInterval(interval) + self.talt=interval or 3 +end + +----------------------------------------------------------------------------------------------------------------------------------------- +-- Event Handling + +--- Event handler for suppressed groups. +--@param #PSEUDOATC self +--@param #table Event Event data table. Holds event.id, event.initiator and event.target etc. +function PSEUDOATC:onEvent(Event) + if Event == nil or Event.initiator == nil or Unit.getByName(Event.initiator:getName()) == nil then + return true + end + + local DCSiniunit = Event.initiator + local DCSplace = Event.place + local DCSsubplace = Event.subplace + + local EventData={} + local _playerunit=nil + local _playername=nil + + if Event.initiator then + EventData.IniUnitName = Event.initiator:getName() + EventData.IniDCSGroup = Event.initiator:getGroup() + EventData.IniGroupName = Event.initiator:getGroup():getName() + -- Get player unit and name. This returns nil,nil if the event was not fired by a player unit. And these are the only events we are interested in. + _playerunit, _playername = self:_GetPlayerUnitAndName(EventData.IniUnitName) + end + + if Event.place then + EventData.Place=Event.place + EventData.PlaceName=Event.place:getName() + end + if Event.subplace then + EventData.SubPlace=Event.subplace + EventData.SubPlaceName=Event.subplace:getName() + end + + -- Event info. + self:T3(PSEUDOATC.id..string.format("EVENT: Event in onEvent with ID = %s", tostring(Event.id))) + self:T3(PSEUDOATC.id..string.format("EVENT: Ini unit = %s" , tostring(EventData.IniUnitName))) + self:T3(PSEUDOATC.id..string.format("EVENT: Ini group = %s" , tostring(EventData.IniGroupName))) + self:T3(PSEUDOATC.id..string.format("EVENT: Ini player = %s" , tostring(_playername))) + self:T3(PSEUDOATC.id..string.format("EVENT: Place = %s" , tostring(EventData.PlaceName))) + self:T3(PSEUDOATC.id..string.format("EVENT: SubPlace = %s" , tostring(EventData.SubPlaceName))) + + -- Event birth. + if Event.id == world.event.S_EVENT_BIRTH and _playername then + self:_OnBirth(EventData) + end + + -- Event takeoff. + if Event.id == world.event.S_EVENT_TAKEOFF and _playername and EventData.Place then + self:_PlayerTakeOff(EventData) + end + + -- Event land. + if Event.id == world.event.S_EVENT_LAND and _playername and EventData.Place then + self:_PlayerLanded(EventData) + end + + -- Event player left unit + if Event.id == world.event.S_EVENT_PLAYER_LEAVE_UNIT and _playername then + self:_PlayerLeft(EventData) + end + + -- Event crash ==> player left unit + if Event.id == world.event.S_EVENT_CRASH and _playername then + self:_PlayerLeft(EventData) + end + +--[[ + -- Event eject ==> player left unit + if Event.id == world.event.S_EVENT_EJECTION and _playername then + self:_PlayerLeft(EventData) + end + + -- Event pilot dead ==> player left unit + if Event.id == world.event.S_EVENT_PILOT_DEAD and _playername then + self:_PlayerLeft(EventData) + end +]] +end + +--- Function called my MOOSE event handler when a player enters a unit. +-- @param #PSEUDOATC self +-- @param Core.Event#EVENTDATA EventData +function PSEUDOATC:_OnBirth(EventData) + self:F({EventData=EventData}) + + -- Get unit and player. + local _unitName=EventData.IniUnitName + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + + -- Check if a player entered. + if _unit and _playername then + self:PlayerEntered(_unit) + end + +end + +--- Function called by MOOSE event handler when a player leaves a unit or dies. +-- @param #PSEUDOATC self +-- @param Core.Event#EVENTDATA EventData +function PSEUDOATC:_PlayerLeft(EventData) + self:F({EventData=EventData}) + + -- Get unit and player. + local _unitName=EventData.IniUnitName + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + + -- Check if a player left. + if _unit and _playername then + self:PlayerLeft(_unit) + end +end + +--- Function called by MOOSE event handler when a player landed. +-- @param #PSEUDOATC self +-- @param Core.Event#EVENTDATA EventData +function PSEUDOATC:_PlayerLanded(EventData) + self:F({EventData=EventData}) + + -- Get unit, player and place. + local _unitName=EventData.IniUnitName + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + local _base=nil + local _baseName=nil + if EventData.place then + _base=EventData.place + _baseName=EventData.place:getName() + end +-- if EventData.subplace then +-- local _subPlace=EventData.subplace +-- local _subPlaceName=EventData.subplace:getName() +-- end + + -- Call landed function. + if _unit and _playername and _base then + self:PlayerLanded(_unit, _baseName) + end +end + +--- Function called by MOOSE/DCS event handler when a player took off. +-- @param #PSEUDOATC self +-- @param Core.Event#EVENTDATA EventData +function PSEUDOATC:_PlayerTakeOff(EventData) + self:F({EventData=EventData}) + + -- Get unit, player and place. + local _unitName=EventData.IniUnitName + local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) + local _base=nil + local _baseName=nil + if EventData.place then + _base=EventData.place + _baseName=EventData.place:getName() + end + + -- Call take-off function. + if _unit and _playername and _base then + self:PlayerTakeOff(_unit, _baseName) + end +end + +----------------------------------------------------------------------------------------------------------------------------------------- +-- Event Functions + +--- Function called when a player enters a unit. +-- @param #PSEUDOATC self +-- @param Wrapper.Unit#UNIT unit Unit the player entered. +function PSEUDOATC:PlayerEntered(unit) + self:F2({unit=unit}) + + -- Get player info. + local group=unit:GetGroup() --Wrapper.Group#GROUP + local GID=group:GetID() + local GroupName=group:GetName() + local PlayerName=unit:GetPlayerName() + local UnitName=unit:GetName() + local CallSign=unit:GetCallsign() + local UID=unit:GetDCSObject():getID() + + if not self.group[GID] then + self.group[GID]={} + self.group[GID].player={} + end + + + -- Init player table. + self.group[GID].player[UID]={} + self.group[GID].player[UID].group=group + self.group[GID].player[UID].unit=unit + self.group[GID].player[UID].groupname=GroupName + self.group[GID].player[UID].unitname=UnitName + self.group[GID].player[UID].playername=PlayerName + self.group[GID].player[UID].callsign=CallSign + self.group[GID].player[UID].waypoints=group:GetTaskRoute() + + -- Info message. + local text=string.format("Player %s entered unit %s of group %s (id=%d).", PlayerName, UnitName, GroupName, GID) + self:T(PSEUDOATC.id..text) + MESSAGE:New(text, 30):ToAllIf(self.Debug) + + -- Create main F10 menu, i.e. "F10/Pseudo ATC" + local countPlayerInGroup = 0 + for _ in pairs(self.group[GID].player) do countPlayerInGroup = countPlayerInGroup + 1 end + if countPlayerInGroup <= 1 then + self.group[GID].menu_main=missionCommands.addSubMenuForGroup(GID, "Pseudo ATC") + end + + -- Create/update custom menu for player + self:MenuCreatePlayer(GID,UID) + + -- Create/update list of nearby airports. + self:LocalAirports(GID,UID) + + -- Create submenu of local airports. + self:MenuAirports(GID,UID) + + -- Create submenu Waypoints. + self:MenuWaypoints(GID,UID) + + -- Start scheduler to refresh the F10 menues. + self.group[GID].player[UID].scheduler, self.group[GID].player[UID].schedulerid=SCHEDULER:New(nil, self.MenuRefresh, {self, GID, UID}, self.mrefresh, self.mrefresh) + +end + +--- Function called when a player has landed. +-- @param #PSEUDOATC self +-- @param Wrapper.Unit#UNIT unit Unit of player which has landed. +-- @param #string place Name of the place the player landed at. +function PSEUDOATC:PlayerLanded(unit, place) + self:F2({unit=unit, place=place}) + + -- Gather some information. + local group=unit:GetGroup() + local GID=group:GetID() + local UID=unit:GetDCSObject():getID() + local PlayerName=self.group[GID].player[UID].playername + local UnitName=self.group[GID].player[UID].unitname + local GroupName=self.group[GID].player[UID].groupname + + -- Debug message. + local text=string.format("Player %s in unit %s of group %s (id=%d) landed at %s.", PlayerName, UnitName, GroupName, GID, place) + self:T(PSEUDOATC.id..text) + MESSAGE:New(text, 30):ToAllIf(self.Debug) + + -- Stop altitude reporting timer if its activated. + self:AltitudeTimerStop(GID,UID) + + -- Welcome message. + if place and self.chatty then + local text=string.format("Touchdown! Welcome to %s pilot %s. Have a nice day!", place,PlayerName) + MESSAGE:New(text, self.mdur):ToGroup(group) + end + +end + +--- Function called when a player took off. +-- @param #PSEUDOATC self +-- @param Wrapper.Unit#UNIT unit Unit of player which has landed. +-- @param #string place Name of the place the player landed at. +function PSEUDOATC:PlayerTakeOff(unit, place) + self:F2({unit=unit, place=place}) + + -- Gather some information. + local group=unit:GetGroup() + local GID=group:GetID() + local UID=unit:GetDCSObject():getID() + local PlayerName=self.group[GID].player[UID].playername + local CallSign=self.group[GID].player[UID].callsign + local UnitName=self.group[GID].player[UID].unitname + local GroupName=self.group[GID].player[UID].groupname + + -- Debug message. + local text=string.format("Player %s in unit %s of group %s (id=%d) took off at %s.", PlayerName, UnitName, GroupName, GID, place) + self:T(PSEUDOATC.id..text) + MESSAGE:New(text, 30):ToAllIf(self.Debug) + + -- Bye-Bye message. + if place and self.chatty then + local text=string.format("%s, %s, you are airborne. Have a safe trip!", place, CallSign) + MESSAGE:New(text, self.mdur):ToGroup(group) + end + +end + +--- Function called when a player leaves a unit or dies. +-- @param #PSEUDOATC self +-- @param Wrapper.Unit#UNIT unit Player unit which was left. +function PSEUDOATC:PlayerLeft(unit) + self:F({unit=unit}) + + -- Get id. + local group=unit:GetGroup() + local GID=group:GetID() + local UID=unit:GetDCSObject():getID() + + if self.group[GID].player[UID] then + local PlayerName=self.group[GID].player[UID].playername + local CallSign=self.group[GID].player[UID].callsign + local UnitName=self.group[GID].player[UID].unitname + local GroupName=self.group[GID].player[UID].groupname + + -- Debug message. + local text=string.format("Player %s (callsign %s) of group %s just left unit %s.", PlayerName, CallSign, GroupName, UnitName) + self:T(PSEUDOATC.id..text) + MESSAGE:New(text, 30):ToAllIf(self.Debug) + + -- Stop scheduler for menu updates + if self.group[GID].player[UID].schedulerid then + self.group[GID].player[UID].scheduler:Stop(self.group[GID].player[UID].schedulerid) + end + + -- Stop scheduler for reporting alt if it runs. + self:AltitudeTimerStop(GID,UID) + + -- Remove own menu. + if self.group[GID].player[UID].menu_own then + missionCommands.removeItemForGroup(GID,self.group[GID].player[UID].menu_own) + end + -- Remove main menu. + -- WARNING: Remove only if last human element of group + + local countPlayerInGroup = 0 + for _ in pairs(self.group[GID].player) do countPlayerInGroup = countPlayerInGroup + 1 end + + if self.group[GID].menu_main and countPlayerInGroup==1 then + missionCommands.removeItemForGroup(GID,self.group[GID].menu_main) + end + + -- Remove player array. + self.group[GID].player[UID]=nil + + end +end + +----------------------------------------------------------------------------------------------------------------------------------------- +-- Menu Functions + +--- Refreshes all player menues. +-- @param #PSEUDOATC self. +-- @param #number GID Group id of player unit. +-- @param #number UID Unit id of player. +function PSEUDOATC:MenuRefresh(GID,UID) + self:F({GID=GID,UID=UID}) + -- Debug message. + local text=string.format("Refreshing menues for player %s in group %s.", self.group[GID].player[UID].playername, self.group[GID].player[UID].groupname) + self:T(PSEUDOATC.id..text) + MESSAGE:New(text,30):ToAllIf(self.Debug) + + -- Clear menu. + self:MenuClear(GID,UID) + + -- Create list of nearby airports. + self:LocalAirports(GID,UID) + + -- Create submenu Local Airports. + self:MenuAirports(GID,UID) + + -- Create submenu Waypoints etc. + self:MenuWaypoints(GID,UID) + +end + +--- Create player menus. +-- @param #PSEUDOATC self. +-- @param #number GID Group id of player unit. +-- @param #number UID Unit id of player. +function PSEUDOATC:MenuCreatePlayer(GID,UID) + self:F({GID=GID,UID=UID}) + -- Table for menu entries. + local PlayerName=self.group[GID].player[UID].playername + self.group[GID].player[UID].menu_own=missionCommands.addSubMenuForGroup(GID, PlayerName, self.group[GID].menu_main) +end + + + +--- Clear player menus. +-- @param #PSEUDOATC self. +-- @param #number GID Group id of player unit. +-- @param #number UID Unit id of player. +function PSEUDOATC:MenuClear(GID,UID) + self:F({GID=GID,UID=UID}) + + -- Debug message. + local text=string.format("Clearing menus for player %s in group %s.", self.group[GID].player[UID].playername, self.group[GID].player[UID].groupname) + self:T(PSEUDOATC.id..text) + MESSAGE:New(text,30):ToAllIf(self.Debug) + + -- Delete Airports menu. + if self.group[GID].player[UID].menu_airports then + missionCommands.removeItemForGroup(GID, self.group[GID].player[UID].menu_airports) + self.group[GID].player[UID].menu_airports=nil + else + self:T2(PSEUDOATC.id.."No airports to clear menus.") + end + + -- Delete waypoints menu. + if self.group[GID].player[UID].menu_waypoints then + missionCommands.removeItemForGroup(GID, self.group[GID].player[UID].menu_waypoints) + self.group[GID].player[UID].menu_waypoints=nil + end + + -- Delete report alt until touchdown menu command. + if self.group[GID].player[UID].menu_reportalt then + missionCommands.removeItemForGroup(GID, self.group[GID].player[UID].menu_reportalt) + self.group[GID].player[UID].menu_reportalt=nil + end + + -- Delete request current alt menu command. + if self.group[GID].player[UID].menu_requestalt then + missionCommands.removeItemForGroup(GID, self.group[GID].player[UID].menu_requestalt) + self.group[GID].player[UID].menu_requestalt=nil + end + +end + +--- Create "F10/Pseudo ATC/Local Airports/Airport Name/" menu items each containing weather report and BR request. +-- @param #PSEUDOATC self +-- @param #number GID Group id of player unit. +-- @param #number UID Unit id of player. +function PSEUDOATC:MenuAirports(GID,UID) + self:F({GID=GID,UID=UID}) + + -- Table for menu entries. + self.group[GID].player[UID].menu_airports=missionCommands.addSubMenuForGroup(GID, "Local Airports", self.group[GID].player[UID].menu_own) + + local i=0 + for _,airport in pairs(self.group[GID].player[UID].airports) do + + i=i+1 + if i > 10 then + break -- Max 10 airports due to 10 menu items restriction. + end + + local name=airport.name + local d=airport.distance + local pos=AIRBASE:FindByName(name):GetCoordinate() + + --F10menu_ATC_airports[ID][name] = missionCommands.addSubMenuForGroup(ID, name, F10menu_ATC) + local submenu=missionCommands.addSubMenuForGroup(GID, name, self.group[GID].player[UID].menu_airports) + + -- Create menu reporting commands + missionCommands.addCommandForGroup(GID, "Weather Report", submenu, self.ReportWeather, self, GID, UID, pos, name) + missionCommands.addCommandForGroup(GID, "Request BR", submenu, self.ReportBR, self, GID, UID, pos, name) + + -- Debug message. + self:T(string.format(PSEUDOATC.id.."Creating airport menu item %s for ID %d", name, GID)) + end +end + +--- Create "F10/Pseudo ATC/Waypoints/ menu items. +-- @param #PSEUDOATC self +-- @param #number GID Group id of player unit. +-- @param #number UID Unit id of player. +function PSEUDOATC:MenuWaypoints(GID, UID) + self:F({GID=GID, UID=UID}) + + -- Player unit and callsign. +-- local unit=self.group[GID].player[UID].unit --Wrapper.Unit#UNIT + local callsign=self.group[GID].player[UID].callsign + + -- Debug info. + self:T(PSEUDOATC.id..string.format("Creating waypoint menu for %s (ID %d).", callsign, GID)) + + if #self.group[GID].player[UID].waypoints>0 then + + -- F10/PseudoATC/Waypoints + self.group[GID].player[UID].menu_waypoints=missionCommands.addSubMenuForGroup(GID, "Waypoints", self.group[GID].player[UID].menu_own) + + local j=0 + for i, wp in pairs(self.group[GID].player[UID].waypoints) do + + -- Increase counter + j=j+1 + + if j>10 then + break -- max ten menu entries + end + + -- Position of Waypoint + local pos=COORDINATE:New(wp.x, wp.alt, wp.y) + local name=string.format("Waypoint %d", i-1) + + -- "F10/PseudoATC/Waypoints/Waypoint X" + local submenu=missionCommands.addSubMenuForGroup(GID, name, self.group[GID].player[UID].menu_waypoints) + + -- Menu commands for each waypoint "F10/PseudoATC/My Aircraft (callsign)/Waypoints/Waypoint X/" + missionCommands.addCommandForGroup(GID, "Weather Report", submenu, self.ReportWeather, self, GID, UID, pos, name) + missionCommands.addCommandForGroup(GID, "Request BR", submenu, self.ReportBR, self, GID, UID, pos, name) + end + end + + self.group[GID].player[UID].menu_reportalt = missionCommands.addCommandForGroup(GID, "Talk me down", self.group[GID].player[UID].menu_own, self.AltidudeTimerToggle, self, GID, UID) + self.group[GID].player[UID].menu_requestalt = missionCommands.addCommandForGroup(GID, "Request altitude", self.group[GID].player[UID].menu_own, self.ReportHeight, self, GID, UID) +end + +----------------------------------------------------------------------------------------------------------------------------------------- +-- Reporting Functions + +--- Weather Report. Report pressure QFE/QNH, temperature, wind at certain location. +-- @param #PSEUDOATC self +-- @param #number GID Group id of player unit. +-- @param #number UID Unit id of player. +-- @param Core.Point#COORDINATE position Coordinates at which the pressure is measured. +-- @param #string location Name of the location at which the pressure is measured. +function PSEUDOATC:ReportWeather(GID, UID, position, location) + self:F({GID=GID, UID=UID, position=position, location=location}) + + -- Player unit system settings. + local settings=_DATABASE:GetPlayerSettings(self.group[GID].player[UID].playername) or _SETTINGS --Core.Settings#SETTINGS + + local text=string.format("Local weather at %s:\n", location) + + -- Get pressure in hPa. + local Pqnh=position:GetPressure(0) -- Get pressure at sea level. + local Pqfe=position:GetPressure() -- Get pressure at (land) height of position. + + -- Pressure conversion + local hPa2inHg=0.0295299830714 + local hPa2mmHg=0.7500615613030 + + -- Unit conversion. + local _Pqnh=string.format("%.2f inHg", Pqnh * hPa2inHg) + local _Pqfe=string.format("%.2f inHg", Pqfe * hPa2inHg) + if settings:IsMetric() then + _Pqnh=string.format("%.1f mmHg", Pqnh * hPa2mmHg) + _Pqfe=string.format("%.1f mmHg", Pqfe * hPa2mmHg) + end + + -- Message text. + text=text..string.format("QFE %.1f hPa = %s.\n", Pqfe, _Pqfe) + text=text..string.format("QNH %.1f hPa = %s.\n", Pqnh, _Pqnh) + + -- Get temperature at position in degrees Celsius. + local T=position:GetTemperature() + + -- Correct unit system. + local _T=string.format('%d°F', UTILS.CelciusToFarenheit(T)) + if settings:IsMetric() then + _T=string.format('%d°C', T) + end + + -- Message text. + local text=text..string.format("Temperature %s\n", _T) + + -- Get wind direction and speed. + local Dir,Vel=position:GetWind() + + -- Get Beaufort wind scale. + local Bn,Bd=UTILS.BeaufortScale(Vel) + + -- Formatted wind direction. + local Ds = string.format('%03d°', Dir) + + -- Velocity in player units. + local Vs=string.format("%.1f knots", UTILS.MpsToKnots(Vel)) + if settings:IsMetric() then + Vs=string.format('%.1f m/s', Vel) + end + + -- Message text. + local text=text..string.format("%s, Wind from %s at %s (%s).", self.group[GID].player[UID].playername, Ds, Vs, Bd) + + -- Send message + self:_DisplayMessageToGroup(self.group[GID].player[UID].unit, text, self.mdur, true) + +end + +--- Report absolute bearing and range form player unit to airport. +-- @param #PSEUDOATC self +-- @param #number GID Group id of player unit. +-- @param #number UID Unit id of player. +-- @param Core.Point#COORDINATE position Coordinates at which the pressure is measured. +-- @param #string location Name of the location at which the pressure is measured. +function PSEUDOATC:ReportBR(GID, UID, position, location) + self:F({GID=GID, UID=UID, position=position, location=location}) + + -- Current coordinates. + local unit=self.group[GID].player[UID].unit --Wrapper.Unit#UNIT + local coord=unit:GetCoordinate() + + -- Direction vector from current position (coord) to target (position). + local angle=coord:HeadingTo(position) + + -- Range from current to + local range=coord:Get2DDistance(position) + + -- Bearing string. + local Bs=string.format('%03d°', angle) + + -- Settings. + local settings=_DATABASE:GetPlayerSettings(self.group[GID].player[UID].playername) or _SETTINGS --Core.Settings#SETTINGS + + + local Rs=string.format("%.1f NM", UTILS.MetersToNM(range)) + if settings:IsMetric() then + Rs=string.format("%.1f km", range/1000) + end + + -- Message text. + local text=string.format("%s: Bearing %s, Range %s.", location, Bs, Rs) + + -- Send message + self:_DisplayMessageToGroup(self.group[GID].player[UID].unit, text, self.mdur, true) + +end + +--- Report altitude above ground level of player unit. +-- @param #PSEUDOATC self +-- @param #number GID Group id of player unit. +-- @param #number UID Unit id of player. +-- @param #number dt (Optional) Duration the message is displayed. +-- @param #boolean _clear (Optional) Clear previouse messages. +-- @return #number Altitude above ground. +function PSEUDOATC:ReportHeight(GID, UID, dt, _clear) + self:F({GID=GID, UID=UID, dt=dt}) + + local dt = dt or self.mdur + if _clear==nil then + _clear=false + end + + -- Return height [m] above ground level. + local function get_AGL(p) + local agl=0 + local vec2={x=p.x,y=p.z} + local ground=land.getHeight(vec2) + local agl=p.y-ground + return agl + end + + -- Get height AGL. + local unit=self.group[GID].player[UID].unit --Wrapper.Unit#UNIT + + if unit and unit:IsAlive() then + + local position=unit:GetCoordinate() + local height=get_AGL(position) + local callsign=unit:GetCallsign() + + -- Settings. + local settings=_DATABASE:GetPlayerSettings(self.group[GID].player[UID].playername) or _SETTINGS --Core.Settings#SETTINGS + + -- Height string. + local Hs=string.format("%d ft", UTILS.MetersToFeet(height)) + if settings:IsMetric() then + Hs=string.format("%d m", height) + end + + -- Message text. + local _text=string.format("%s, your altitude is %s AGL.", callsign, Hs) + + -- Append flight level. + if _clear==false then + _text=_text..string.format(" FL%03d.", position.y/30.48) + end + + -- Send message to player group. + self:_DisplayMessageToGroup(self.group[GID].player[UID].unit,_text, dt,_clear) + + -- Return height + return height + end + + return 0 +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Toggle report altitude reporting on/off. +-- @param #PSEUDOATC self. +-- @param #number GID Group id of player unit. +-- @param #number UID Unit id of player. +function PSEUDOATC:AltidudeTimerToggle(GID,UID) + self:F({GID=GID, UID=UID}) + + if self.group[GID].player[UID].altimerid then + -- If the timer is on, we turn it off. + self:AltitudeTimerStop(GID, UID) + else + -- If the timer is off, we turn it on. + self:AltitudeTimeStart(GID, UID) + end +end + +--- Start altitude reporting scheduler. +-- @param #PSEUDOATC self. +-- @param #number GID Group id of player unit. +-- @param #number UID Unit id of player. +function PSEUDOATC:AltitudeTimeStart(GID, UID) + self:F({GID=GID, UID=UID}) + + -- Debug info. + self:T(PSEUDOATC.id..string.format("Starting altitude report timer for player ID %d.", UID)) + + -- Start timer. Altitude is reported every ~3 seconds. + self.group[GID].player[UID].altimer, self.group[GID].player[UID].altimerid=SCHEDULER:New(nil, self.ReportHeight, {self, GID, UID, 0.1, true}, 1, 3) +end + +--- Stop/destroy DCS scheduler function for reporting altitude. +-- @param #PSEUDOATC self. +-- @param #number GID Group id of player unit. +-- @param #number UID Unit id of player. +function PSEUDOATC:AltitudeTimerStop(GID, UID) + self:F({GID=GID,UID=UID}) + -- Debug info. + self:T(PSEUDOATC.id..string.format("Stopping altitude report timer for player ID %d.", UID)) + + -- Stop timer. + if self.group[GID].player[UID].altimerid then + self.group[GID].player[UID].altimer:Stop(self.group[GID].player[UID].altimerid) + end + + self.group[GID].player[UID].altimer=nil + self.group[GID].player[UID].altimerid=nil +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc + +--- Create list of nearby airports sorted by distance to player unit. +-- @param #PSEUDOATC self +-- @param #number GID Group id of player unit. +-- @param #number UID Unit id of player. +function PSEUDOATC:LocalAirports(GID, UID) + self:F({GID=GID, UID=UID}) + + -- Airports table. + self.group[GID].player[UID].airports=nil + self.group[GID].player[UID].airports={} + + -- Current player position. + local pos=self.group[GID].player[UID].unit:GetCoordinate() + + -- Loop over coalitions. + for i=0,2 do + + -- Get all airbases of coalition. + local airports=coalition.getAirbases(i) + + -- Loop over airbases + for _,airbase in pairs(airports) do + + local name=airbase:getName() + local q=AIRBASE:FindByName(name):GetCoordinate() + local d=q:Get2DDistance(pos) + + -- Add to table. + table.insert(self.group[GID].player[UID].airports, {distance=d, name=name}) + + end + end + + --- compare distance (for sorting airports) + local function compare(a,b) + return a.distance < b.distance + end + + -- Sort airports table w.r.t. distance to player. + table.sort(self.group[GID].player[UID].airports, compare) + +end + +--- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned. +-- @param #PSEUDOATC self +-- @param #string _unitName Name of the player unit. +-- @return Wrapper.Unit#UNIT Unit of player. +-- @return #string Name of the player. +-- @return nil If player does not exist. +function PSEUDOATC:_GetPlayerUnitAndName(_unitName) + self:F(_unitName) + + if _unitName ~= nil then + + -- Get DCS unit from its name. + local DCSunit=Unit.getByName(_unitName) + if DCSunit then + + -- Get the player name to make sure a player entered. + local playername=DCSunit:getPlayerName() + local unit=UNIT:Find(DCSunit) + + -- Debug output. + self:T2({DCSunit=DCSunit, unit=unit, playername=playername}) + + if unit and playername then + -- Return MOOSE unit and player name + return unit, playername + end + + end + end + + return nil,nil +end + + +--- Display message to group. +-- @param #PSEUDOATC self +-- @param Wrapper.Unit#UNIT _unit Player unit. +-- @param #string _text Message text. +-- @param #number _time Duration how long the message is displayed. +-- @param #boolean _clear Clear up old messages. +function PSEUDOATC:_DisplayMessageToGroup(_unit, _text, _time, _clear) + self:F({unit=_unit, text=_text, time=_time, clear=_clear}) + + _time=_time or self.Tmsg + if _clear==nil then + _clear=false + end + + -- Group ID. + local _gid=_unit:GetGroup():GetID() + + if _gid then + if _clear == true then + trigger.action.outTextForGroup(_gid, _text, _time, _clear) + else + trigger.action.outTextForGroup(_gid, _text, _time) + end + end + +end + +--- Returns a string which consits of this callsign and the player name. +-- @param #PSEUDOATC self +-- @param #string unitname Name of the player unit. +function PSEUDOATC:_myname(unitname) + self:F2(unitname) + + local unit=UNIT:FindByName(unitname) + local pname=unit:GetPlayerName() + local csign=unit:GetCallsign() + + return string.format("%s (%s)", csign, pname) +end + + + +--- **Functional** - Simulation of logistic operations. +-- +-- === +-- +-- ## Features: +-- +-- * Holds (virtual) assets in stock and spawns them upon request. +-- * Manages requests of assets from other warehouses. +-- * Queueing system with optional prioritization of requests. +-- * Realistic transportation of assets between warehouses. +-- * Different means of automatic transportation (planes, helicopters, APCs, self propelled). +-- * Strategic components such as capturing, defending and destroying warehouses and their associated infrastructure. +-- * Intelligent spawning of aircraft on airports (only if enough parking spots are available). +-- * Possibility to hook into events and customize actions. +-- * Persistence of assets. Warehouse assets can be saved and loaded from file. +-- * Can be easily interfaced to other MOOSE classes. +-- +-- === +-- +-- ## Youtube Videos: +-- +-- * [Warehouse Trailer](https://www.youtube.com/watch?v=e98jzLi5fGk) +-- * [DCS Warehouse Airbase Resources Proof Of Concept](https://www.youtube.com/watch?v=YeuGL0duEgY) +-- +-- === +-- +-- ## Missions: +-- +-- === +-- +-- The MOOSE warehouse concept simulates the organization and implementation of complex operations regarding the flow of assets between the point of origin and the point of consumption +-- in order to meet requirements of a potential conflict. In particular, this class is concerned with maintaining army supply lines while disrupting those of the enemy, since an armed +-- force without resources and transportation is defenseless. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- ### Co-author: FlightControl (cargo dispatcher classes) +-- +-- === +-- +-- @module Functional.Warehouse +-- @image Warehouse.JPG + +--- WAREHOUSE class. +-- @type WAREHOUSE +-- @field #string ClassName Name of the class. +-- @field #boolean Debug If true, send debug messages to all. +-- @field #number verbosity Verbosity level. +-- @field #string wid Identifier of the warehouse printed before other output to DCS.log file. +-- @field #boolean Report If true, send status messages to coalition. +-- @field Wrapper.Static#STATIC warehouse The phyical warehouse structure. +-- @field #string alias Alias of the warehouse. Name its called when sending messages. +-- @field Core.Zone#ZONE zone Zone around the warehouse. If this zone is captured, the warehouse and all its assets goes to the capturing coaliton. +-- @field Wrapper.Airbase#AIRBASE airbase Airbase the warehouse belongs to. +-- @field #string airbasename Name of the airbase associated to the warehouse. +-- @field Core.Point#COORDINATE road Closest point to warehouse on road. +-- @field Core.Point#COORDINATE rail Closest point to warehouse on rail. +-- @field Core.Zone#ZONE spawnzone Zone in which assets are spawned. +-- @field #number uid Unique ID of the warehouse. +-- @field #boolean markerOn If true, markers are displayed on the F10 map. +-- @field Wrapper.Marker#MARKER markerWarehouse Marker warehouse. +-- @field Wrapper.Marker#MARKER markerRoad Road connection. +-- @field Wrapper.Marker#MARKER markerRail Rail road connection. +-- @field #number markerid ID of the warehouse marker at the airbase. +-- @field #number dTstatus Time interval in seconds of updating the warehouse status and processing new events. Default 30 seconds. +-- @field #number queueid Unit id of each request in the queue. Essentially a running number starting at one and incremented when a new request is added. +-- @field #table stock Table holding all assets in stock. Table entries are of type @{#WAREHOUSE.Assetitem}. +-- @field #table queue Table holding all queued requests. Table entries are of type @{#WAREHOUSE.Queueitem}. +-- @field #table pending Table holding all pending requests, i.e. those that are currently in progress. Table elements are of type @{#WAREHOUSE.Pendingitem}. +-- @field #table transporting Table holding assets currently transporting cargo assets. +-- @field #table delivered Table holding all delivered requests. Table elements are #boolean. If true, all cargo has been delivered. +-- @field #table defending Table holding all defending requests, i.e. self requests that were if the warehouse is under attack. Table elements are of type @{#WAREHOUSE.Pendingitem}. +-- @field Core.Zone#ZONE portzone Zone defining the port of a warehouse. This is where naval assets are spawned. +-- @field #table shippinglanes Table holding the user defined shipping between warehouses. +-- @field #table offroadpaths Table holding user defined paths from one warehouse to another. +-- @field #boolean autodefence When the warehouse is under attack, automatically spawn assets to defend the warehouse. +-- @field #number spawnzonemaxdist Max distance between warehouse and spawn zone. Default 5000 meters. +-- @field #boolean autosave Automatically save assets to file when mission ends. +-- @field #string autosavepath Path where the asset file is saved on auto save. +-- @field #string autosavefile File name of the auto asset save file. Default is auto generated from warehouse id and name. +-- @field #boolean safeparking If true, parking spots for aircraft are considered as occupied if e.g. a client aircraft is parked there. Default false. +-- @field #boolean isunit If true, warehouse is represented by a unit instead of a static. +-- @field #number lowfuelthresh Low fuel threshold. Triggers the event AssetLowFuel if for any unit fuel goes below this number. +-- @field #boolean respawnafterdestroyed If true, warehouse is respawned after it was destroyed. Assets are kept. +-- @field #number respawndelay Delay before respawn in seconds. +-- @field #number runwaydestroyed Time stamp timer.getAbsTime() when the runway was destroyed. +-- @field #number runwayrepairtime Time in seconds until runway will be repaired after it was destroyed. Default is 3600 sec (one hour). +-- @extends Core.Fsm#FSM + +--- Have your assets at the right place at the right time - or not! +-- +-- === +-- +-- # The Warehouse Concept +-- +-- The MOOSE warehouse adds a new logistic component to the DCS World. *Assets*, i.e. ground, airborne and naval units, can be transferred from one place +-- to another in a realistic and highly automatic fashion. In contrast to a "DCS warehouse" these assets have a physical representation in game. In particular, +-- this means they can be destroyed during the transport and add more life to the DCS world. +-- +-- This comes along with some additional interesting strategic aspects since capturing/defending and destroying/protecting an enemy or your +-- own warehouse becomes of critical importance for the development of a conflict. +-- +-- In essence, creating an efficient network of warehouses is vital for the success of a battle or even the whole war. Likewise, of course, cutting off the enemy +-- of important supply lines by capturing or destroying warehouses or their associated infrastructure is equally important. +-- +-- ## What is a warehouse? +-- +-- A warehouse is an abstract object represented by a physical (static) building that can hold virtual assets in stock. +-- It can (but it must not) be associated with a particular airbase. The associated airbase can be an airdrome, a Helipad/FARP or a ship. +-- +-- If another warehouse requests assets, the corresponding troops are spawned at the warehouse and being transported to the requestor or go their +-- by themselfs. Once arrived at the requesting warehouse, the assets go into the stock of the requestor and can be activated/deployed when necessary. +-- +-- ## What assets can be stored? +-- +-- Any kind of ground, airborne or naval asset can be stored and are spawned upon request. +-- The fact that the assets live only virtually in stock and are put into the game only when needed has a positive impact on the game performance. +-- It also alliviates the problem of limited parking spots at smaller airbases. +-- +-- ## What means of transportation are available? +-- +-- Firstly, all mobile assets can be send from warehouse to another on their own. +-- +-- * Ground vehicles will use the road infrastructure. So a good road connection for both warehouses is important but also off road connections can be added if necessary. +-- * Airborne units get a flightplan from the airbase of the sending warehouse to the airbase of the receiving warehouse. This already implies that for airborne +-- assets both warehouses need an airbase. If either one of the warehouses does not have an associated airbase, direct transportation of airborne assets is not possible. +-- * Naval units can be exchanged between warehouses which possess a port, which can be defined by the user. Also shipping lanes must be specified manually but the user since DCS does not provide these. +-- * Trains (would) use the available railroad infrastructure and both warehouses must have a connection to the railroad. Unfortunately, however, trains are not yet implemented to +-- a reasonable degree in DCS at the moment and hence cannot be used yet. +-- +-- Furthermore, ground assets can be transferred between warehouses by transport units. These are APCs, helicopters and airplanes. The transportation process is modeled +-- in a realistic way by using the corresponding cargo dispatcher classes, i.e. +-- +-- * @{AI.AI_Cargo_Dispatcher_APC#AI_DISPATCHER_APC} +-- * @{AI.AI_Cargo_Dispatcher_Helicopter#AI_DISPATCHER_HELICOPTER} +-- * @{AI.AI_Cargo_Dispatcher_Airplane#AI_DISPATCHER_AIRPLANE} +-- +-- Depending on which cargo dispatcher is used (ground or airbore), similar considerations like in the self propelled case are necessary. Howver, note that +-- the dispatchers as of yet cannot use user defined off road paths for example since they are classes of their own and use a different routing logic. +-- +-- === +-- +-- # Creating a Warehouse +-- +-- A MOOSE warehouse must be represented in game by a physical *static* object. For example, the mission editor already has warehouse as static object available. +-- This would be a good first choice but any static object will do. +-- +-- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_Static.png) +-- +-- The positioning of the warehouse static object is very important for a couple of reasons. Firstly, a warehouse needs a good infrastructure so that spawned assets +-- have a proper road connection or can reach the associated airbase easily. +-- +-- ## Constructor and Start +-- +-- Once the static warehouse object is placed in the mission editor it can be used as a MOOSE warehouse by the @{#WAREHOUSE.New}(*warehousestatic*, *alias*) constructor, +-- like for example: +-- +-- warehouseBatumi=WAREHOUSE:New(STATIC:FindByName("Warehouse Batumi"), "My optional Warehouse Alias") +-- warehouseBatumi:Start() +-- +-- The first parameter *warehousestatic* is the static MOOSE object. By default, the name of the warehouse will be the same as the name given to the static object. +-- The second parameter *alias* is optional and can be used to choose a more convenient name if desired. This will be the name the warehouse calls itself when reporting messages. +-- +-- Note that a warehouse also needs to be started in order to be in service. This is done with the @{#WAREHOUSE.Start}() or @{#WAREHOUSE.__Start}(*delay*) functions. +-- The warehouse is now fully operational and requests are being processed. +-- +-- # Adding Assets +-- +-- Assets can be added to the warehouse stock by using the @{#WAREHOUSE.AddAsset}(*group*, *ngroups*, *forceattribute*, *forcecargobay*, *forceweight*, *loadradius*, *skill*, *liveries*, *assignment*) function. +-- The parameter *group* has to be a MOOSE @{Wrapper.Group#GROUP}. This is also the only mandatory parameters. All other parameters are optional and can be used for fine tuning if +-- nessary. The parameter *ngroups* specifies how many clones of this group are added to the stock. +-- +-- infrantry=GROUP:FindByName("Some Infantry Group") +-- warehouseBatumi:AddAsset(infantry, 5) +-- +-- This will add five infantry groups to the warehouse stock. Note that the group should normally be a late activated template group, +-- which was defined in the mission editor. But you can also add other groups which are already spawned and present in the mission. +-- +-- Also note that the coalition of the template group (red, blue or neutral) does not matter. The coalition of the assets is determined by the coalition of the warehouse owner. +-- In other words, it is no problem to add red groups to blue warehouses and vice versa. The assets will automatically have the coalition of the warehouse. +-- +-- You can add assets with a delay by using the @{#WAREHOUSE.__AddAsset}(*delay*, *group*, *ngroups*, *forceattribute*, *forcecargobay*, *forceweight*, *loadradius*, *skill*, *liveries*, *assignment*), +-- where *delay* is the delay in seconds before the asset is added. +-- +-- In game, the warehouse will get a mark which is regularly updated and showing the currently available assets in stock. +-- +-- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_Stock-Marker.png) +-- +-- ## Optional Parameters for Fine Tuning +-- +-- By default, the generalized attribute of the asset is determined automatically from the DCS descriptor attributes. However, this might not always result in the desired outcome. +-- Therefore, it is possible, to force a generalized attribute for the asset with the third optional parameter *forceattribute*, which is of type @{#WAREHOUSE.Attribute}. +-- +-- ### Setting the Generalized Attibute +-- For example, a UH-1H Huey has in DCS the attibute of an attack helicopter. But of course, it can also transport cargo. If you want to use it for transportation, you can specify this +-- manually when the asset is added +-- +-- warehouseBatumi:AddAsset("Huey", 5, WAREHOUSE.Attribute.AIR_TRANSPORTHELO) +-- +-- This becomes important when assets are requested from other warehouses as described below. In this case, the five Hueys are now marked as transport helicopters and +-- not attack helicopters. This is also particularly useful when adding assets to a warehouse with the intention of using them to transport other units that are part of +-- a subsequent request (see below). Setting the attribute will help to ensure that warehouse module can find the correct unit when attempting to service a request in its +-- queue. For example, if we want to add an Amphibious Landing Ship, even though most are indeed armed, it's recommended to do the following: +-- +-- warehouseBatumi:AddAsset("Landing Ship", 1, WAREHOUSE.Attribute.NAVAL_UNARMEDSHIP) +-- +-- Then when adding the request, you can simply specify WAREHOUSE.TransportType.SHIP (which corresponds to NAVAL_UNARMEDSHIP) as the TransportType. +-- +-- ### Setting the Cargo Bay Weight Limit +-- You can ajust the cargo bay weight limit, in case it is not calculated correctly automatically. For example, the cargo bay of a C-17A is much smaller in DCS than that of a C-130, which is +-- unrealistic. This can be corrected by the *forcecargobay* parmeter which is here set to 77,000 kg +-- +-- warehouseBatumi:AddAsset("C-17A", nil, nil, 77000) +-- +-- The size of the cargo bay is only important when the group is used as transport carrier for other assets. +-- +-- ### Setting the Weight +-- If an asset shall be transported by a carrier it important to note that - as in real life - a carrier can only carry cargo up to a certain weight. The weight of the +-- units is automatically determined from the DCS descriptor table. +-- However, in the current DCS version (2.5.3) a mortar unit has a weight of 5 tons. This confuses the transporter logic, because it appears to be too have for, e.g. all APCs. +-- +-- As a workaround, you can manually adjust the weight by the optional *forceweight* parameter: +-- +-- warehouseBatumi:AddAsset("Mortar Alpha", nil, nil, nil, 210) +-- +-- In this case we set it to 210 kg. Note, the weight value set is meant for *each* unit in the group. Therefore, a group consisting of three mortars will have a total weight +-- of 630 kg. This is important as groups cannot be split between carrier units when transporting, i.e. the total weight of the whole group must be smaller than the +-- cargo bay of the transport carrier. +-- +-- ### Setting the Load Radius +-- Boading and loading of cargo into a carrier is modeled in a realistic fashion in the AI\_CARGO\DISPATCHER classes, which are used inernally by the WAREHOUSE class. +-- Meaning that troops (cargo) will board, i.e. run or drive to the carrier, and only once they are in close proximity to the transporter they will be loaded (disappear). +-- +-- Unfortunately, there are some situations where problems can occur. For example, in DCS tanks have the strong tentendcy not to drive around obstacles but rather to roll over them. +-- I have seen cases where an aircraft of the same coalition as the tank was in its way and the tank drove right through the plane waiting on a parking spot and destroying it. +-- +-- As a workaround it is possible to set a larger load radius so that the cargo units are despawned further away from the carrier via the optional **loadradius** parameter: +-- +-- warehouseBatumi:AddAsset("Leopard 2", nil, nil, nil, nil, 250) +-- +-- Adding the asset like this will cause the units to be loaded into the carrier already at a distance of 250 meters. +-- +-- ### Setting the AI Skill +-- +-- By default, the asset has the skill of its template group. The optional parameter *skill* allows to set a different skill when the asset is added. See the +-- [hoggit page](https://wiki.hoggitworld.com/view/DCS_enum_AI) possible values of this enumerator. +-- For example you can use +-- +-- warehouseBatumi:AddAsset("Leopard 2", nil, nil, nil, nil, nil, AI.Skill.EXCELLENT) +-- +-- do set the skill of the asset to excellent. +-- +-- ### Setting Liveries +-- +-- By default ,the asset uses the livery of its template group. The optional parameter *liveries* allows to define one or multiple liveries. +-- If multiple liveries are given in form of a table of livery names, each asset gets a random one. +-- +-- For example +-- +-- warehouseBatumi:AddAsset("Mi-8", nil, nil, nil, nil, nil, nil, "China UN") +-- +-- would spawn the asset with a Chinese UN livery. +-- +-- Or +-- +-- warehouseBatumi:AddAsset("Mi-8", nil, nil, nil, nil, nil, nil, {"China UN", "German"}) +-- +-- would spawn the asset with either a Chinese UN or German livery. Mind the curly brackets **{}** when you want to specify multiple liveries. +-- +-- Four each unit type, the livery names can be found in the DCS root folder under Bazar\Liveries. You have to use the name of the livery subdirectory. The names of the liveries +-- as displayed in the mission editor might be different and won't work in general. +-- +-- ### Setting an Assignment +-- +-- Assets can be added with a specific assignment given as a text, e.g. +-- +-- warehouseBatumi:AddAsset("Mi-8", nil, nil, nil, nil, nil, nil, nil, "Go to Warehouse Kobuleti") +-- +-- This is helpful to establish supply chains once an asset has arrived at its (first) destination and is meant to be forwarded to another warehouse. +-- +-- ## Retrieving the Asset +-- +-- Once a an asset is added to a warehouse, the @{#WAREHOUSE.NewAsset} event is triggered. You can hook into this event with the @{#WAREHOUSE.OnAfterNewAsset}(*asset*, *assignment*) function. +-- +-- The first parameter *asset* is a table of type @{#WAREHOUSE.Assetitem} and contains a lot of information about the asset. The seconed parameter *assignment* is optional and is the specific +-- assignment the asset got when it was added. +-- +-- Note that the assignment is can also be the assignment that was specified when adding a request (see next section). Once an asset that was requested from another warehouse and an assignment +-- was specified in the @{#WAREHOUSE.AddRequest} function, the assignment can be checked when the asset has arrived and is added to the receiving warehouse. +-- +-- === +-- +-- # Requesting Assets +-- +-- Assets of the warehouse can be requested by other MOOSE warehouses. A request will first be scrutinized to check if can be fulfilled at all. If the request is valid, it is +-- put into the warehouse queue and processed as soon as possible. +-- +-- Requested assets spawn in various "Rule of Engagement Rules" (ROE) and Alerts modes. If your assets will cross into dangerous areas, be sure to change these states. You can do this in @{#WAREHOUSE:OnAfterAssetSpawned}(*From, *Event, *To, *group, *asset, *request)) function. +-- +-- Initial Spawn states is as follows: +-- GROUND: ROE, "Return Fire" Alarm, "Green" +-- AIR: ROE, "Return Fire" Reaction to Threat, "Passive Defense" +-- NAVAL ROE, "Return Fire" Alarm,"N/A" +-- +-- A request can be added by the @{#WAREHOUSE.AddRequest}(*warehouse*, *AssetDescriptor*, *AssetDescriptorValue*, *nAsset*, *TransportType*, *nTransport*, *Prio*, *Assignment*) function. +-- The parameters are +-- +-- * *warehouse*: The requesting MOOSE @{#WAREHOUSE}. Assets will be delivered there. +-- * *AssetDescriptor*: The descriptor to describe the asset "type". See the @{#WAREHOUSE.Descriptor} enumerator. For example, assets requested by their generalized attibute. +-- * *AssetDescriptorValue*: The value of the asset descriptor. +-- * *nAsset*: (Optional) Number of asset group requested. Default is one group. +-- * *TransportType*: (Optional) The transport method used to deliver the assets to the requestor. Default is that assets go to the requesting warehouse on their own. +-- * *nTransport*: (Optional) Number of asset groups used to transport the cargo assets from A to B. Default is one group. +-- * *Prio*: (Optional) A number between 1 (high) and 100 (low) describing the priority of the request. Request with high priority are processed first. Default is 50, i.e. medium priority. +-- * *Assignment*: (Optional) A free to choose string describing the assignment. For self requests, this can be used to assign the spawned groups to specific tasks. +-- +-- ## Requesting by Generalized Attribute +-- +-- Generalized attributes are similar to [DCS attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes). However, they are a bit more general and +-- an asset can only have one generalized attribute by which it is characterized. +-- +-- For example: +-- +-- warehouseBatumi:AddRequest(warehouseKobuleti, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 5, WAREHOUSE.TransportType.APC, 2) +-- +-- Here, warehouse Kobuleti requests 5 infantry groups from warehouse Batumi. These "cargo" assets should be transported from Batumi to Kobuleti by 2 APCS. +-- Note that the warehouse at Batumi needs to have at least five infantry groups and two APC groups in their stock if the request can be processed. +-- If either to few infantry or APC groups are available when the request is made, the request is held in the warehouse queue until enough cargo and +-- transport assets are available. +-- +-- Also note that the above request is for five infantry groups. So any group in stock that has the generalized attribute "GROUND_INFANTRY" can be selected for the request. +-- +-- ### Generalized Attributes +-- +-- Currently implemented are: +-- +-- * @{#WAREHOUSE.Attribute.AIR_TRANSPORTPLANE} Airplane with transport capability. This can be used to transport other assets. +-- * @{#WAREHOUSE.Attribute.AIR_AWACS} Airborne Early Warning and Control System. +-- * @{#WAREHOUSE.Attribute.AIR_FIGHTER} Fighter, interceptor, ... airplane. +-- * @{#WAREHOUSE.Attribute.AIR_BOMBER} Aircraft which can be used for strategic bombing. +-- * @{#WAREHOUSE.Attribute.AIR_TANKER} Airplane which can refuel other aircraft. +-- * @{#WAREHOUSE.Attribute.AIR_TRANSPORTHELO} Helicopter with transport capability. This can be used to transport other assets. +-- * @{#WAREHOUSE.Attribute.AIR_ATTACKHELO} Attack helicopter. +-- * @{#WAREHOUSE.Attribute.AIR_UAV} Unpiloted Aerial Vehicle, e.g. drones. +-- * @{#WAREHOUSE.Attribute.AIR_OTHER} Any airborne unit that does not fall into any other airborne category. +-- * @{#WAREHOUSE.Attribute.GROUND_APC} Infantry carriers, in particular Amoured Personell Carrier. This can be used to transport other assets. +-- * @{#WAREHOUSE.Attribute.GROUND_TRUCK} Unarmed ground vehicles, which has the DCS "Truck" attribute. +-- * @{#WAREHOUSE.Attribute.GROUND_INFANTRY} Ground infantry assets. +-- * @{#WAREHOUSE.Attribute.GROUND_ARTILLERY} Artillery assets. +-- * @{#WAREHOUSE.Attribute.GROUND_TANK} Tanks (modern or old). +-- * @{#WAREHOUSE.Attribute.GROUND_TRAIN} Trains. Not that trains are **not** yet properly implemented in DCS and cannot be used currently. +-- * @{#WAREHOUSE.Attribute.GROUND_EWR} Early Warning Radar. +-- * @{#WAREHOUSE.Attribute.GROUND_AAA} Anti-Aircraft Artillery. +-- * @{#WAREHOUSE.Attribute.GROUND_SAM} Surface-to-Air Missile system or components. +-- * @{#WAREHOUSE.Attribute.GROUND_OTHER} Any ground unit that does not fall into any other ground category. +-- * @{#WAREHOUSE.Attribute.NAVAL_AIRCRAFTCARRIER} Aircraft carrier. +-- * @{#WAREHOUSE.Attribute.NAVAL_WARSHIP} War ship, i.e. cruisers, destroyers, firgates and corvettes. +-- * @{#WAREHOUSE.Attribute.NAVAL_ARMEDSHIP} Any armed ship that is not an aircraft carrier, a cruiser, destroyer, firgatte or corvette. +-- * @{#WAREHOUSE.Attribute.NAVAL_UNARMEDSHIP} Any unarmed naval vessel. +-- * @{#WAREHOUSE.Attribute.NAVAL_OTHER} Any naval unit that does not fall into any other naval category. +-- * @{#WAREHOUSE.Attribute.OTHER_UNKNOWN} Anything that does not fall into any other category. +-- +-- ## Requesting a Specific Unit Type +-- +-- A more specific request could look like: +-- +-- warehouseBatumi:AddRequest(warehouseKobuleti, WAREHOUSE.Descriptor.UNITTYPE, "A-10C", 2) +-- +-- Here, Kobuleti requests a specific unit type, in particular two groups of A-10Cs. Note that the spelling is important as it must exacly be the same as +-- what one get's when using the DCS unit type. +-- +-- ## Requesting a Specific Group +-- +-- An even more specific request would be: +-- +-- warehouseBatumi:AddRequest(warehouseKobuleti, WAREHOUSE.Descriptor.GROUPNAME, "Group Name as in ME", 3) +-- +-- In this case three groups named "Group Name as in ME" are requested. This explicitly request the groups named like that in the Mission Editor. +-- +-- ## Requesting a General Category +-- +-- On the other hand, very general and unspecifc requests can be made by the categroy descriptor. The descriptor value parameter can be any [group category](https://wiki.hoggitworld.com/view/DCS_Class_Group), i.e. +-- +-- * Group.Category.AIRPLANE for fixed wing aircraft, +-- * Group.Category.HELICOPTER for helicopters, +-- * Group.Category.GROUND for all ground troops, +-- * Group.Category.SHIP for naval assets, +-- * Group.Category.TRAIN for trains (not implemented and not working in DCS yet). +-- +-- For example, +-- +-- warehouseBatumi:AddRequest(warehouseKobuleti, WAREHOUSE.Descriptor.CATEGORY, Group.Category.GROUND, 10) +-- +-- means that Kubuleti requests 10 ground groups and does not care which ones. This could be a mix of infantry, APCs, trucks etc. +-- +-- **Note** that these general requests should be made with *great care* due to the fact, that depending on what a warehouse has in stock a lot of different unit types can be spawned. +-- +-- ## Requesting Relative Quantities +-- +-- In addition to requesting absolute numbers of assets it is possible to request relative amounts of assets currently in stock. To this end the @{#WAREHOUSE.Quantity} enumerator +-- was introduced: +-- +-- * @{#WAREHOUSE.Quantity.ALL} +-- * @{#WAREHOUSE.Quantity.HALF} +-- * @{#WAREHOUSE.Quantity.QUARTER} +-- * @{#WAREHOUSE.Quantity.THIRD} +-- * @{#WAREHOUSE.Quantity.THREEQUARTERS} +-- +-- For example, +-- +-- warehouseBatumi:AddRequest(warehouseKobuleti, WAREHOUSE.Descriptor.CATEGORY, Group.Category.HELICOPTER, WAREHOUSE.Quantity.HALF) +-- +-- means that Kobuleti warehouse requests half of all available helicopters which Batumi warehouse currently has in stock. +-- +-- # Employing Assets - The Self Request +-- +-- Transferring assets from one warehouse to another is important but of course once the the assets are at the "right" place it is equally important that they +-- can be employed for specific tasks and assignments. +-- +-- Assets in the warehouses stock can be used for user defined tasks quite easily. They can be spawned into the game by a "***self request***", i.e. the warehouse +-- requests the assets from itself: +-- +-- warehouseBatumi:AddRequest(warehouseBatumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 5) +-- +-- Note that the *sending* and *requesting* warehouses are *identical* in this case. +-- +-- This would simply spawn five infantry groups in the spawn zone of the Batumi warehouse if/when they are available. +-- +-- ## Accessing the Assets +-- +-- If a warehouse requests assets from itself, it triggers the event **SelfReqeuest**. The mission designer can capture this event with the associated +-- @{#WAREHOUSE.OnAfterSelfRequest}(*From*, *Event*, *To*, *groupset*, *request*) function. +-- +-- --- OnAfterSelfRequest user function. Access groups spawned from the warehouse for further tasking. +-- -- @param #WAREHOUSE self +-- -- @param #string From From state. +-- -- @param #string Event Event. +-- -- @param #string To To state. +-- -- @param Core.Set#SET_GROUP groupset The set of cargo groups that was delivered to the warehouse itself. +-- -- @param #WAREHOUSE.Pendingitem request Pending self request. +-- function WAREHOUSE:OnAfterSelfRequest(From, Event, To, groupset, request) +-- local groupset=groupset --Core.Set#SET_GROUP +-- local request=request --Functional.Warehouse#WAREHOUSE.Pendingitem +-- +-- for _,group in pairs(groupset:GetSetObjects()) do +-- local group=group --Wrapper.Group#GROUP +-- group:SmokeGreen() +-- end +-- +-- end +-- +-- The variable *groupset* is a @{Core.Set#SET_GROUP} object and holds all asset groups from the request. The code above shows, how the mission designer can access the groups +-- for further tasking. Here, the groups are only smoked but, of course, you can use them for whatever assignment you fancy. +-- +-- Note that airborne groups are spawned in **uncontrolled state** and need to be activated first before they can begin with their assigned tasks and missions. +-- This can be done with the @{Wrapper.Controllable#CONTROLLABLE.StartUncontrolled} function as demonstrated in the example section below. +-- +-- === +-- +-- # Infrastructure +-- +-- A good infrastructure is important for a warehouse to be efficient. Therefore, the location of a warehouse should be chosen with care. +-- This can also help to avoid many DCS related issues such as units getting stuck in buildings, blocking taxi ways etc. +-- +-- ## Spawn Zone +-- +-- By default, the zone were ground assets are spawned is a circular zone around the physical location of the warehouse with a radius of 200 meters. However, the location of the +-- spawn zone can be set by the @{#WAREHOUSE.SetSpawnZone}(*zone*) functions. It is advisable to choose a zone which is clear of obstacles. +-- +-- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_Batumi.png) +-- +-- The parameter *zone* is a MOOSE @{Core.Zone#ZONE} object. So one can, e.g., use trigger zones defined in the mission editor. If a cicular zone is not desired, one +-- can use a polygon zone (see @{Core.Zone#ZONE_POLYGON}). +-- +-- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_SpawnPolygon.png) +-- +-- ## Road Connections +-- +-- Ground assets will use a road connection to travel from one warehouse to another. Therefore, a proper road connection is necessary. +-- +-- By default, the closest point on road to the center of the spawn zone is chosen as road connection automatically. But only, if distance between the spawn zone +-- and the road connection is less than 3 km. +-- +-- The user can set the road connection manually with the @{#WAREHOUSE.SetRoadConnection} function. This is only functional for self propelled assets at the moment +-- and not if using the AI dispatcher classes since these have a different logic to find the route. +-- +-- ## Off Road Connections +-- +-- For ground troops it is also possible to define off road paths between warehouses if no proper road connection is available or should not be used. +-- +-- An off road path can be defined via the @{#WAREHOUSE.AddOffRoadPath}(*remotewarehouse*, *group*, *oneway*) function, where +-- *remotewarehouse* is the warehouse to which the path leads. +-- The parameter *group* is a *late activated* template group. The waypoints of this group are used to define the path between the two warehouses. +-- By default, the reverse paths is automatically added to get *from* the remote warehouse *to* this warehouse unless the parameter *oneway* is set to *true*. +-- +-- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_Off-Road_Paths.png) +-- +-- **Note** that if an off road connection is defined between two warehouses this becomes the default path, i.e. even if there is a path *on road* possible +-- this will not be used. +-- +-- Also note that you can define multiple off road connections between two warehouses. If there are multiple paths defined, the connection is chosen randomly. +-- It is also possible to add the same path multiple times. By this you can influence the probability of the chosen path. For example Path1(A->B) has been +-- added two times while Path2(A->B) was added only once. Hence, the group will choose Path1 with a probability of 66.6 % while Path2 is only chosen with +-- a probability of 33.3 %. +-- +-- ## Rail Connections +-- +-- A rail connection is automatically defined as the closest point on a railway measured from the center of the spawn zone. But only, if the distance is less than 3 km. +-- +-- The mission designer can manually specify a rail connection with the @{#WAREHOUSE.SetRailConnection} function. +-- +-- **NOTE** however, that trains in DCS are currently not implemented in a way so that they can be used. +-- +-- ## Air Connections +-- +-- In order to use airborne assets, a warehouse needs to have an associated airbase. This can be an airdrome, a FARP/HELOPAD or a ship. +-- +-- If there is an airbase within 3 km range of the warehouse it is automatically set as the associated airbase. A user can set an airbase manually +-- with the @{#WAREHOUSE.SetAirbase} function. Keep in mind that sometimes ground units need to walk/drive from the spawn zone to the airport +-- to get to their transport carriers. +-- +-- ## Naval Connections +-- +-- Natively, DCS does not have the concept of a port/habour or shipping lanes. So in order to have a meaningful transfer of naval units between warehouses, these have to be +-- defined by the mission designer. +-- +-- ### Defining a Port +-- +-- A port in this context is the zone where all naval assets are spawned. This zone can be defined with the function @{#WAREHOUSE.SetPortZone}(*zone*), where the parameter +-- *zone* is a MOOSE zone. So again, this can be create from a trigger zone defined in the mission editor or if a general shape is desired by a @{Core.Zone#ZONE_POLYGON}. +-- +-- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_PortZone.png) +-- +-- ### Defining Shipping Lanes +-- +-- A shipping lane between to warehouses can be defined by the @{#WAREHOUSE.AddShippingLane}(*remotewarehouse*, *group*, *oneway*) function. The first parameter *remotewarehouse* +-- is the warehouse which should be connected to the present warehouse. +-- +-- The parameter *group* should be a late activated group defined in the mission editor. The waypoints of this group are used as waypoints of the shipping lane. +-- +-- By default, the reverse lane is automatically added to the remote warehouse. This can be disabled by setting the *oneway* parameter to *true*. +-- +-- Similar to off road connections, you can also define multiple shipping lanes between two warehouse ports. If there are multiple lanes defined, one is chosen randomly. +-- It is possible to add the same lane multiple times. By this you can influence the probability of the chosen lane. For example Lane_1(A->B) has been +-- added two times while Lane_2(A->B) was added only once. Therefore, the ships will choose Lane_1 with a probability of 66.6 % while Path_2 is only chosen with +-- a probability of 33.3 %. +-- +-- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_ShippingLane.png) +-- +-- === +-- +-- # Why is my request not processed? +-- +-- For each request, the warehouse class logic does a lot of consistency and validation checks under the hood. +-- This helps to circumvent a lot of DCS issues and shortcomings. For example, it is checked that enough free +-- parking spots at an airport are available *before* the assets are spawned. +-- However, this also means that sometimes a request is deemed to be *invalid* in which case they are deleted +-- from the queue or considered to be valid but cannot be executed at this very moment. +-- +-- ## Invalid Requests +-- +-- Invalid request are requests which can **never** be processes because there is some logical or physical argument against it. +-- (Or simply because that feature was not implemented (yet).) +-- +-- * All airborne assets need an associated airbase of any kind on the sending *and* receiving warehouse. +-- * Airplanes need an airdrome at the sending and receiving warehouses. +-- * Not enough parking spots of the right terminal type at the sending warehouse. This avoids planes spawning on runways or on top of each other. +-- * No parking spots of the right terminal type at the receiving warehouse. This avoids DCS despawning planes on landing if they have no valid parking spot. +-- * Ground assets need a road connection between both warehouses or an off-road path needs to be added manually. +-- * Ground assets cannot be send directly to ships, i.e. warehouses on ships. +-- * Naval units need a user defined shipping lane between both warehouses. +-- * Warehouses need a user defined port zone to spawn naval assets. +-- * The receiving warehouse is destroyed or stopped. +-- * If transport by airplane, both warehouses must have and airdrome. +-- * If transport by APC, both warehouses must have a road connection. +-- * If transport by helicopter, the sending airbase must have an associated airbase (airdrome or FARP). +-- +-- All invalid requests are cancelled and **removed** from the warehouse queue! +-- +-- ## Temporarily Unprocessable Requests +-- +-- Temporarily unprocessable requests are possible in principle, but cannot be processed at the given time the warehouse checks its queue. +-- +-- * No enough parking spaces are available for all requested assets but the airbase has enough parking spots in total so that this request is possible once other aircraft have taken off. +-- * The requesting warehouse is not in state "Running" (could be paused, not yet started or under attack). +-- * Not enough cargo assets available at this moment. +-- * Not enough free parking spots for all cargo or transport airborne assets at the moment. +-- * Not enough transport assets to carry all cargo assets. +-- +-- Temporarily unprocessable requests are held in the queue. If at some point in time, the situation changes so that these requests can be processed, they are executed. +-- +-- ## Cargo Bay and Weight Limitations +-- +-- The transportation of cargo is handled by the AI\_Dispatcher classes. These take the cargo bay of a carrier and the weight of +-- the cargo into account so that a carrier can only load a realistic amount of cargo. +-- +-- However, if troops are supposed to be transported between warehouses, there is one important limitations one has to keep in mind. +-- This is that **cargo asset groups cannot be split** and divided into separate carrier units! +-- +-- For example, a TPz Fuchs has a cargo bay large enough to carry up to 10 soldiers at once, which is a realistic number. +-- If a group consisting of more than ten soldiers needs to be transported, it cannot be loaded into the APC. +-- Even if two APCs are available, which could in principle carry up to 20 soldiers, a group of, let's say 12 soldiers will not +-- be split into a group of ten soldiers using the first APC and a group two soldiers using the second APC. +-- +-- In other words, **there must be at least one carrier unit available that has a cargo bay large enough to load the heaviest cargo group!** +-- The warehouse logic will automatically search all available transport assets for a large enough carrier. +-- But if none is available, the request will be queued until a suitable carrier becomes available. +-- +-- The only realistic solution in this case is to either provide a transport carrier with a larger cargo bay or to reduce the number of soldiers +-- in the group. +-- +-- A better way would be to have two groups of max. 10 soldiers each and one TPz Fuchs for transport. In this case, the first group is +-- loaded and transported to the receiving warehouse. Once this is done, the carrier will drive back and pick up the remaining +-- group. +-- +-- As an artificial workaround one can manually set the cargo bay size to a larger value or alternatively reduce the weight of the cargo +-- when adding the assets via the @{#WAREHOUSE.AddAsset} function. This might even be unavoidable if, for example, a SAM group +-- should be transported since SAM sites only work when all units are in the same group. +-- +-- ## Processing Speed +-- +-- A warehouse has a limited speed to process requests. Each time the status of the warehouse is updated only one requests is processed. +-- The time interval between status updates is 30 seconds by default and can be adjusted via the @{#WAREHOUSE.SetStatusUpdate}(*interval*) function. +-- However, the status is also updated on other occasions, e.g. when a new request was added. +-- +-- === +-- +-- # Strategic Considerations +-- +-- Due to the fact that a warehouse holds (or can hold) a lot of valuable assets, it makes a (potentially) juicy target for enemy attacks. +-- There are several interesting situations, which can occur. +-- +-- ## Capturing a Warehouses Airbase +-- +-- If a warehouse has an associated airbase, it can be captured by the enemy. In this case, the warehouse looses its ability so employ all airborne assets and is also cut-off +-- from supply by airplanes. Supply of ground troops via helicopters is still possible, because they deliver the troops into the spawn zone. +-- +-- Technically, the capturing of the airbase is triggered by the DCS [S\_EVENT\_BASE\_CAPTURED](https://wiki.hoggitworld.com/view/DCS_event_base_captured) event. +-- So the capturing takes place when only enemy ground units are in the airbase zone whilst no ground units of the present airbase owner are in that zone. +-- +-- The warehouse will also create an event **AirbaseCaptured**, which can be captured by the @{#WAREHOUSE.OnAfterAirbaseCaptured} function. So the warehouse chief can react on +-- this attack and for example deploy ground groups to re-capture its airbase. +-- +-- When an airbase is re-captured the event **AirbaseRecaptured** is triggered and can be captured by the @{#WAREHOUSE.OnAfterAirbaseRecaptured} function. +-- This can be used to put the defending assets back into the warehouse stock. +-- +-- ## Capturing the Warehouse +-- +-- A warehouse can be captured by the enemy coalition. If enemy ground troops enter the warehouse zone the event **Attacked** is triggered which can be captured by the +-- @{#WAREHOUSE.OnAfterAttacked} event. By default the warehouse zone circular zone with a radius of 500 meters located at the center of the physical warehouse. +-- The warehouse zone can be set via the @{#WAREHOUSE.SetWarehouseZone}(*zone*) function. The parameter *zone* must also be a circular zone. +-- +-- The @{#WAREHOUSE.OnAfterAttacked} function can be used by the mission designer to react to the enemy attack. For example by deploying some or all ground troops +-- currently in stock to defend the warehouse. Note that the warehouse also has a self defence option which can be enabled by the @{#WAREHOUSE.SetAutoDefenceOn}() +-- function. In this case, the warehouse will automatically spawn all ground troops. If the spawn zone is further away from the warehouse zone, all mobile troops +-- are routed to the warehouse zone. The self request which is triggered on an automatic defence has the assignment "AutoDefence". So you can use this to +-- give orders to the groups that were spawned using the @{#WAREHOUSE.OnAfterSelfRequest} function. +-- +-- If only ground troops of the enemy coalition are present in the warehouse zone, the warehouse and all its assets falls into the hands of the enemy. +-- In this case the event **Captured** is triggered which can be captured by the @{#WAREHOUSE.OnAfterCaptured} function. +-- +-- The warehouse turns to the capturing coalition, i.e. its physical representation, and all assets as well. In particular, all requests to the warehouse will +-- spawn assets belonging to the new owner. +-- +-- If the enemy troops could be defeated, i.e. no more troops of the opposite coalition are in the warehouse zone, the event **Defeated** is triggered and +-- the @{#WAREHOUSE.OnAfterDefeated} function can be used to adapt to the new situation. For example putting back all spawned defender troops back into +-- the warehouse stock. Note that if the automatic defence is enabled, all defenders are automatically put back into the warehouse on the **Defeated** event. +-- +-- ## Destroying a Warehouse +-- +-- If an enemy destroy the physical warehouse structure, the warehouse will of course stop all its services. In principle, all assets contained in the warehouse are +-- gone as well. So a warehouse should be properly defended. +-- +-- Upon destruction of the warehouse, the event **Destroyed** is triggered, which can be captured by the @{#WAREHOUSE.OnAfterDestroyed} function. +-- So the mission designer can intervene at this point and for example choose to spawn all or particular types of assets before the warehouse is gone for good. +-- +-- === +-- +-- # Hook in and Take Control +-- +-- The Finite State Machine implementation allows mission designers to hook into important events and add their own code. +-- Most of these events have already been mentioned but here is the list at a glance: +-- +-- * "NotReadyYet" --> "Start" --> "Running" (Starting the warehouse) +-- * "*" --> "Status" --> "*" (status updated in regular intervals) +-- * "*" --> "AddAsset" --> "*" (adding a new asset to the warehouse stock) +-- * "*" --> "NewAsset" --> "*" (a new asset has been added to the warehouse stock) +-- * "*" --> "AddRequest" --> "*" (adding a request for the warehouse assets) +-- * "Running" --> "Request" --> "*" (a request is processed when the warehouse is running) +-- * "Attacked" --> "Request" --> "*" (a request is processed when the warehouse is attacked) +-- * "*" --> "Arrived" --> "*" (asset group has arrived at its destination) +-- * "*" --> "Delivered" --> "*" (all assets of a request have been delivered) +-- * "Running" --> "SelfRequest" --> "*" (warehouse is requesting asset from itself when running) +-- * "Attacked" --> "SelfRequest" --> "*" (warehouse is requesting asset from itself while under attack) +-- * "*" --> "Attacked" --> "Attacked" (warehouse is being attacked) +-- * "Attacked" --> "Defeated" --> "Running" (an attack was defeated) +-- * "Attacked" --> "Captured" --> "Running" (warehouse was captured by the enemy) +-- * "*" --> "AirbaseCaptured" --> "*" (airbase belonging to the warehouse was captured by the enemy) +-- * "*" --> "AirbaseRecaptured" --> "*" (airbase was re-captured) +-- * "*" --> "AssetSpawned" --> "*" (an asset has been spawned into the world) +-- * "*" --> "AssetLowFuel" --> "*" (an asset is running low on fuel) +-- * "*" --> "AssetDead" --> "*" (a whole asset, i.e. all its units/groups, is dead) +-- * "*" --> "Destroyed" --> "Destroyed" (warehouse was destroyed) +-- * "Running" --> "Pause" --> "Paused" (warehouse is paused) +-- * "Paused" --> "Unpause" --> "Running" (warehouse is unpaused) +-- * "*" --> "Stop" --> "Stopped" (warehouse is stopped) +-- +-- The transitions are of the general form "From State" --> "Event" --> "To State". The "*" star denotes that the transition is possible from *any* state. +-- Some transitions, however, are only allowed from certain "From States". For example, no requests can be processed if the warehouse is in "Paused" or "Destroyed" or "Stopped" state. +-- +-- Mission designers can capture the events with OnAfterEvent functions, e.g. @{#WAREHOUSE.OnAfterDelivered} or @{#WAREHOUSE.OnAfterAirbaseCaptured}. +-- +-- === +-- +-- # Persistence of Assets +-- +-- Assets in stock of a warehouse can be saved to a file on your hard drive and then loaded from that file at a later point. This enables to restart the mission +-- and restore the warehouse stock. +-- +-- ## Prerequisites +-- +-- **Important** By default, DCS does not allow for writing data to files. Therefore, one first has to comment out the line "sanitizeModule('io')", i.e. +-- +-- do +-- sanitizeModule('os') +-- --sanitizeModule('io') +-- sanitizeModule('lfs') +-- require = nil +-- loadlib = nil +-- end +-- +-- in the file "MissionScripting.lua", which is located in the subdirectory "Scripts" of your DCS installation root directory. +-- +-- ### Don't! +-- +-- Do not use **semi-colons** or **equal signs** in the group names of your assets as these are used as separators in the saved and loaded files texts. +-- If you do, it will cause problems and give you a headache! +-- +-- ## Save Assets +-- +-- Saving asset data to file is achieved by the @{WAREHOUSE.Save}(*path*, *filename*) function. The parameter *path* specifies the path on the file system where the +-- warehouse data is saved. If you do not specify a path, the file is saved your the DCS installation root directory. +-- The parameter *filename* is optional and defines the name of the saved file. By default this is automatically created from the warehouse id and name, for example +-- "Warehouse-1234_Batumi.txt". +-- +-- warehouseBatumi:Save("D:\\My Warehouse Data\\") +-- +-- This will save all asset data to in "D:\\My Warehouse Data\\Warehouse-1234_Batumi.txt". +-- +-- ### Automatic Save at Mission End +-- +-- The assets can be saved automatically when the mission is ended via the @{WAREHOUSE.SetSaveOnMissionEnd}(*path*, *filename*) function, i.e. +-- +-- warehouseBatumi:SetSaveOnMissionEnd("D:\\My Warehouse Data\\") +-- +-- ## Load Assets +-- +-- Loading assets data from file is achieved by the @{WAREHOUSE.Load}(*path*, *filename*) function. The parameter *path* specifies the path on the file system where the +-- warehouse data is loaded from. If you do not specify a path, the file is loaded from your the DCS installation root directory. +-- The parameter *filename* is optional and defines the name of the file to load. By default this is automatically generated from the warehouse id and name, for example +-- "Warehouse-1234_Batumi.txt". +-- +-- Note that the warehouse **must not be started** and in the *Running* state in order to load the assets. In other words, loading should happen after the +-- @{#WAREHOUSE.New} command is specified in the code but before the @{#WAREHOUSE.Start} command is given. +-- +-- Loading the assets is done by +-- +-- warehouseBatumi:New(STATIC:FindByName("Warehouse Batumi")) +-- warehouseBatumi:Load("D:\\My Warehouse Data\\") +-- warehouseBatumi:Start() +-- +-- This sequence loads all assets from file. If a warehouse was captured in the last mission, it also respawns the static warehouse structure with the right coaliton. +-- However, it due to DCS limitations it is not possible to set the airbase coalition. This has to be done manually in the mission editor. Or alternatively, one could +-- spawn some ground units via a self request and let them capture the airbase. +-- +-- === +-- +-- # Examples +-- +-- This section shows some examples how the WAREHOUSE class is used in practice. This is one of the best ways to explain things, in my opinion. +-- +-- But first, let me introduce a convenient way to define several warehouses in a table. This is absolutely *not necessary* but quite handy if you have +-- multiple WAREHOUSE objects in your mission. +-- +-- ## Example 0: Setting up a Warehouse Array +-- +-- If you have multiple warehouses, you can put them in a table. This makes it easier to access them or to loop over them. +-- +-- -- Define Warehouses. +-- local warehouse={} +-- -- Blue warehouses +-- warehouse.Senaki = WAREHOUSE:New(STATIC:FindByName("Warehouse Senaki"), "Senaki") --Functional.Warehouse#WAREHOUSE +-- warehouse.Batumi = WAREHOUSE:New(STATIC:FindByName("Warehouse Batumi"), "Batumi") --Functional.Warehouse#WAREHOUSE +-- warehouse.Kobuleti = WAREHOUSE:New(STATIC:FindByName("Warehouse Kobuleti"), "Kobuleti") --Functional.Warehouse#WAREHOUSE +-- warehouse.Kutaisi = WAREHOUSE:New(STATIC:FindByName("Warehouse Kutaisi"), "Kutaisi") --Functional.Warehouse#WAREHOUSE +-- warehouse.Berlin = WAREHOUSE:New(STATIC:FindByName("Warehouse Berlin"), "Berlin") --Functional.Warehouse#WAREHOUSE +-- warehouse.London = WAREHOUSE:New(STATIC:FindByName("Warehouse London"), "London") --Functional.Warehouse#WAREHOUSE +-- warehouse.Stennis = WAREHOUSE:New(STATIC:FindByName("Warehouse Stennis"), "Stennis") --Functional.Warehouse#WAREHOUSE +-- warehouse.Pampa = WAREHOUSE:New(STATIC:FindByName("Warehouse Pampa"), "Pampa") --Functional.Warehouse#WAREHOUSE +-- -- Red warehouses +-- warehouse.Sukhumi = WAREHOUSE:New(STATIC:FindByName("Warehouse Sukhumi"), "Sukhumi") --Functional.Warehouse#WAREHOUSE +-- warehouse.Gudauta = WAREHOUSE:New(STATIC:FindByName("Warehouse Gudauta"), "Gudauta") --Functional.Warehouse#WAREHOUSE +-- warehouse.Sochi = WAREHOUSE:New(STATIC:FindByName("Warehouse Sochi"), "Sochi") --Functional.Warehouse#WAREHOUSE +-- +-- Remarks: +-- +-- * I defined the array as local, i.e. local warehouse={}. This is personal preference and sometimes causes trouble with the lua garbage collection. You can also define it as a global array/table! +-- * The "--Functional.Warehouse#WAREHOUSE" at the end is only to have the LDT intellisense working correctly. If you don't use LDT (which you should!), it can be omitted. +-- +-- **NOTE** that all examples below need this bit or code at the beginning - or at least the warehouses which are used. +-- +-- The example mission is based on the same template mission, which has defined a lot of airborne, ground and naval assets as templates. Only few of those are used here. +-- +-- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_Assets.png) +-- +-- ## Example 1: Self Request +-- +-- Ground troops are taken from the Batumi warehouse stock and spawned in its spawn zone. After a short delay, they are added back to the warehouse stock. +-- Also a new request is made. Hence, the groups will be spawned, added back to the warehouse, spawned again and so on and so forth... +-- +-- -- Start warehouse Batumi. +-- warehouse.Batumi:Start() +-- +-- -- Add five groups of infantry as assets. +-- warehouse.Batumi:AddAsset(GROUP:FindByName("Infantry Platoon Alpha"), 5) +-- +-- -- Add self request for three infantry at Batumi. +-- warehouse.Batumi:AddRequest(warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 3) +-- +-- +-- --- Self request event. Triggered once the assets are spawned in the spawn zone or at the airbase. +-- function warehouse.Batumi:OnAfterSelfRequest(From, Event, To, groupset, request) +-- local mygroupset=groupset --Core.Set#SET_GROUP +-- +-- -- Loop over all groups spawned from that request. +-- for _,group in pairs(mygroupset:GetSetObjects()) do +-- local group=group --Wrapper.Group#GROUP +-- +-- -- Gree smoke on spawned group. +-- group:SmokeGreen() +-- +-- -- Put asset back to stock after 10 seconds. +-- warehouse.Batumi:__AddAsset(10, group) +-- end +-- +-- -- Add new self request after 20 seconds. +-- warehouse.Batumi:__AddRequest(20, warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 3) +-- +-- end +-- +-- ## Example 2: Self propelled Ground Troops +-- +-- Warehouse Berlin, which is a FARP near Batumi, requests infantry and troop transports from the warehouse at Batumi. +-- The groups are spawned at Batumi and move by themselves from Batumi to Berlin using the roads. +-- Once the troops have arrived at Berlin, the troops are automatically added to the warehouse stock of Berlin. +-- While on the road, Batumi has requested back two APCs from Berlin. Since Berlin does not have the assets in stock, +-- the request is queued. After the troops have arrived, Berlin is sending back the APCs to Batumi. +-- +-- -- Start Warehouse at Batumi. +-- warehouse.Batumi:Start() +-- +-- -- Add 20 infantry groups and ten APCs as assets at Batumi. +-- warehouse.Batumi:AddAsset("Infantry Platoon Alpha", 20) +-- warehouse.Batumi:AddAsset("TPz Fuchs", 10) +-- +-- -- Start Warehouse Berlin. +-- warehouse.Berlin:Start() +-- +-- -- Warehouse Berlin requests 10 infantry groups and 5 APCs from warehouse Batumi. +-- warehouse.Batumi:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 10) +-- warehouse.Batumi:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_APC, 5) +-- +-- -- Request from Batumi for 2 APCs. Initially these are not in stock. When they become available, the request is executed. +-- warehouse.Berlin:AddRequest(warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_APC, 2) +-- +-- ## Example 3: Self Propelled Airborne Assets +-- +-- Warehouse Senaki receives a high priority request from Kutaisi for one Yak-52s. At the same time, Kobuleti requests half of +-- all available Yak-52s. Request from Kutaisi is first executed and then Kobuleti gets half of the remaining assets. +-- Additionally, London requests one third of all available UH-1H Hueys from Senaki. +-- Once the units have arrived they are added to the stock of the receiving warehouses and can be used for further assignments. +-- +-- -- Start warehouses +-- warehouse.Senaki:Start() +-- warehouse.Kutaisi:Start() +-- warehouse.Kobuleti:Start() +-- warehouse.London:Start() +-- +-- -- Add assets to Senaki warehouse. +-- warehouse.Senaki:AddAsset("Yak-52", 10) +-- warehouse.Senaki:AddAsset("Huey", 6) +-- +-- -- Kusaisi requests 3 Yak-52 form Senaki while Kobuleti wants all the rest. +-- warehouse.Senaki:AddRequest(warehouse.Kutaisi, WAREHOUSE.Descriptor.GROUPNAME, "Yak-52", 1, nil, nil, 10) +-- warehouse.Senaki:AddRequest(warehouse.Kobuleti, WAREHOUSE.Descriptor.GROUPNAME, "Yak-52", WAREHOUSE.Quantity.HALF, nil, nil, 70) +-- +-- -- FARP London wants 1/3 of the six available Hueys. +-- warehouse.Senaki:AddRequest(warehouse.London, WAREHOUSE.Descriptor.GROUPNAME, "Huey", WAREHOUSE.Quantity.THIRD) +-- +-- ## Example 4: Transport of Assets by APCs +-- +-- Warehouse at FARP Berlin requests five infantry groups from Batumi. These assets shall be transported using two APC groups. +-- Infantry and APC are spawned in the spawn zone at Batumi. The APCs have a cargo bay large enough to pick up four of the +-- five infantry groups in the first run and will bring them to Berlin. There, they unboard and walk to the warehouse where they will be added to the stock. +-- Meanwhile the APCs go back to Batumi and one will pick up the last remaining soldiers. +-- Once the APCs have completed their mission, they return to Batumi and are added back to stock. +-- +-- -- Start Warehouse at Batumi. +-- warehouse.Batumi:Start() +-- +-- -- Start Warehouse Berlin. +-- warehouse.Berlin:Start() +-- +-- -- Add 20 infantry groups and five APCs as assets at Batumi. +-- warehouse.Batumi:AddAsset("Infantry Platoon Alpha", 20) +-- warehouse.Batumi:AddAsset("TPz Fuchs", 5) +-- +-- -- Warehouse Berlin requests 5 infantry groups from warehouse Batumi using 2 APCs for transport. +-- warehouse.Batumi:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 5, WAREHOUSE.TransportType.APC, 2) +-- +--## Example 5: Transport of Assets by Helicopters +-- +-- Warehouse at FARP Berlin requests five infantry groups from Batumi. They shall be transported by all available transport helicopters. +-- Note that the UH-1H Huey in DCS is an attack and not a transport helo. So the warehouse logic would be default also +-- register it as an @{#WAREHOUSE.Attribute.AIR_ATTACKHELICOPTER}. In order to use it as a transport we need to force +-- it to be added as transport helo. +-- Also note that even though all (here five) helos are requested, only two of them are employed because this number is sufficient to +-- transport all requested assets in one go. +-- +-- -- Start Warehouses. +-- warehouse.Batumi:Start() +-- warehouse.Berlin:Start() +-- +-- -- Add 20 infantry groups as assets at Batumi. +-- warehouse.Batumi:AddAsset("Infantry Platoon Alpha", 20) +-- +-- -- Add five Hueys for transport. Note that a Huey in DCS is an attack and not a transport helo. So we force this attribute! +-- warehouse.Batumi:AddAsset("Huey", 5, WAREHOUSE.Attribute.AIR_TRANSPORTHELO) +-- +-- -- Warehouse Berlin requests 5 infantry groups from warehouse Batumi using all available helos for transport. +-- warehouse.Batumi:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 5, WAREHOUSE.TransportType.HELICOPTER, WAREHOUSE.Quantity.ALL) +-- +--## Example 6: Transport of Assets by Airplanes +-- +-- Warehoues Kobuleti requests all (three) APCs from Batumi using one airplane for transport. +-- The available C-130 is able to carry one APC at a time. So it has to commute three times between Batumi and Kobuleti to deliver all requested cargo assets. +-- Once the cargo is delivered, the C-130 transport returns to Batumi and is added back to stock. +-- +-- -- Start warehouses. +-- warehouse.Batumi:Start() +-- warehouse.Kobuleti:Start() +-- +-- -- Add assets to Batumi warehouse. +-- warehouse.Batumi:AddAsset("C-130", 1) +-- warehouse.Batumi:AddAsset("TPz Fuchs", 3) +-- +-- warehouse.Batumi:AddRequest(warehouse.Kobuleti, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_APC, WAREHOUSE.Quantity.ALL, WAREHOUSE.TransportType.AIRPLANE) +-- +-- ## Example 7: Capturing Airbase and Warehouse +-- +-- A red BMP has made it through our defence lines and drives towards our unprotected airbase at Senaki. +-- Once the BMP captures the airbase (DCS [S\_EVENT\_BASE\_CAPTURED](https://wiki.hoggitworld.com/view/DCS_event_base_captured) is evaluated) +-- the warehouse at Senaki lost its air infrastructure and it is not possible any more to spawn airborne units. All requests for airborne units are rejected and cancelled in this case. +-- +-- The red BMP then drives further to the warehouse. Once it enters the warehouse zone (500 m radius around the warehouse building), the warehouse is +-- considered to be under attack. This triggers the event **Attacked**. The @{#WAREHOUSE.OnAfterAttacked} function can be used to react to this situation. +-- Here, we only broadcast a distress call and launch a flare. However, it would also be reasonable to spawn all or selected ground troops in order to defend +-- the warehouse. Note, that the warehouse has a self defence option which can be activated via the @{#WAREHOUSE.SetAutoDefenceOn}() function. If activated, +-- *all* ground assets are automatically spawned and assigned to defend the warehouse. Once/if the attack is defeated, these assets go automatically back +-- into the warehouse stock. +-- +-- If the red coalition manages to capture our warehouse, all assets go into their possession. Now red tries to steal three F/A-18 flights and send them to +-- Sukhumi. These aircraft will be spawned and begin to taxi. However, ... +-- +-- A blue Bradley is in the area and will attempt to recapture the warehouse. It might also catch the red F/A-18s before they take off. +-- +-- -- Start warehouses. +-- warehouse.Senaki:Start() +-- warehouse.Sukhumi:Start() +-- +-- -- Add some assets. +-- warehouse.Senaki:AddAsset("TPz Fuchs", 5) +-- warehouse.Senaki:AddAsset("Infantry Platoon Alpha", 10) +-- warehouse.Senaki:AddAsset("F/A-18C 2ship", 10) +-- +-- -- Enable auto defence, i.e. spawn all group troups into the spawn zone. +-- --warehouse.Senaki:SetAutoDefenceOn() +-- +-- -- Activate Red BMP trying to capture the airfield and the warehouse. +-- local red1=GROUP:FindByName("Red BMP-80 Senaki"):Activate() +-- +-- -- The red BMP first drives to the airbase which gets captured and changes from blue to red. +-- -- This triggers the "AirbaseCaptured" event where you can hook in and do things. +-- function warehouse.Senaki:OnAfterAirbaseCaptured(From, Event, To, Coalition) +-- -- This request cannot be processed since the warehouse has lost its airbase. In fact it is deleted from the queue. +-- warehouse.Senaki:AddRequest(warehouse.Senaki,WAREHOUSE.Descriptor.CATEGORY, Group.Category.AIRPLANE, 1) +-- end +-- +-- -- Now the red BMP also captures the warehouse. This triggers the "Captured" event where you can hook in. +-- -- So now the warehouse and the airbase are both red and aircraft can be spawned again. +-- function warehouse.Senaki:OnAfterCaptured(From, Event, To, Coalition, Country) +-- -- These units will be spawned as red units because the warehouse has just been captured. +-- if Coalition==coalition.side.RED then +-- -- Sukhumi tries to "steals" three F/A-18 from Senaki and brings them to Sukhumi. +-- -- Well, actually the aircraft wont make it because blue1 will kill it on the taxi way leaving a blood bath. But that's life! +-- warehouse.Senaki:AddRequest(warehouse.Sukhumi, WAREHOUSE.Descriptor.CATEGORY, Group.Category.AIRPLANE, 3) +-- warehouse.Senaki.warehouse:SmokeRed() +-- elseif Coalition==coalition.side.BLUE then +-- warehouse.Senaki.warehouse:SmokeBlue() +-- end +-- +-- -- Activate a blue vehicle to re-capture the warehouse. It will drive to the warehouse zone and kill the red intruder. +-- local blue1=GROUP:FindByName("blue1"):Activate() +-- end +-- +-- ## Example 8: Destroying a Warehouse +-- +-- FARP Berlin requests a Huey from Batumi warehouse. This helo is deployed and will be delivered. +-- After 30 seconds into the mission we create and (artificial) big explosion - or a terrorist attack if you like - which completely destroys the +-- the warehouse at Batumi. All assets are gone and requests cannot be processed anymore. +-- +-- -- Start Batumi and Berlin warehouses. +-- warehouse.Batumi:Start() +-- warehouse.Berlin:Start() +-- +-- -- Add some assets. +-- warehouse.Batumi:AddAsset("Huey", 5, WAREHOUSE.Attribute.AIR_TRANSPORTHELO) +-- warehouse.Berlin:AddAsset("Huey", 5, WAREHOUSE.Attribute.AIR_TRANSPORTHELO) +-- +-- -- Big explosion at the warehose. It has a very nice damage model by the way :) +-- local function DestroyWarehouse() +-- warehouse.Batumi:GetCoordinate():Explosion(999) +-- end +-- SCHEDULER:New(nil, DestroyWarehouse, {}, 30) +-- +-- -- First request is okay since warehouse is still alive. +-- warehouse.Batumi:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.AIR_TRANSPORTHELO, 1) +-- +-- -- These requests should both not be processed any more since the warehouse at Batumi is destroyed. +-- warehouse.Batumi:__AddRequest(35, warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.AIR_TRANSPORTHELO, 1) +-- warehouse.Berlin:__AddRequest(40, warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.AIR_TRANSPORTHELO, 1) +-- +-- ## Example 9: Self Propelled Naval Assets +-- +-- Kobuleti requests all naval assets from Batumi. +-- However, before naval assets can be exchanged, both warehouses need a port and at least one shipping lane defined by the user. +-- See the @{#WAREHOUSE.SetPortZone}() and @{#WAREHOUSE.AddShippingLane}() functions. +-- We do not want to spawn them all at once, because this will probably be a disaster +-- in the port zone. Therefore, each ship is spawned with a delay of five minutes. +-- +-- Batumi has quite a selection of different ships (for testing). +-- +-- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_Naval_Assets.png) +-- +-- -- Start warehouses. +-- warehouse.Batumi:Start() +-- warehouse.Kobuleti:Start() +-- +-- -- Define ports. These are polygon zones created by the waypoints of late activated units. +-- warehouse.Batumi:SetPortZone(ZONE_POLYGON:NewFromGroupName("Warehouse Batumi Port Zone", "Warehouse Batumi Port Zone")) +-- warehouse.Kobuleti:SetPortZone(ZONE_POLYGON:NewFromGroupName("Warehouse Kobuleti Port Zone", "Warehouse Kobuleti Port Zone")) +-- +-- -- Shipping lane. Again, the waypoints of late activated units are taken as points defining the shipping lane. +-- -- Some units will take lane 1 while others will take lane two. But both lead from Batumi to Kobuleti port. +-- warehouse.Batumi:AddShippingLane(warehouse.Kobuleti, GROUP:FindByName("Warehouse Batumi-Kobuleti Shipping Lane 1")) +-- warehouse.Batumi:AddShippingLane(warehouse.Kobuleti, GROUP:FindByName("Warehouse Batumi-Kobuleti Shipping Lane 2")) +-- +-- -- Large selection of available naval units in DCS. +-- warehouse.Batumi:AddAsset("Speedboat") +-- warehouse.Batumi:AddAsset("Perry") +-- warehouse.Batumi:AddAsset("Normandy") +-- warehouse.Batumi:AddAsset("Stennis") +-- warehouse.Batumi:AddAsset("Carl Vinson") +-- warehouse.Batumi:AddAsset("Tarawa") +-- warehouse.Batumi:AddAsset("SSK 877") +-- warehouse.Batumi:AddAsset("SSK 641B") +-- warehouse.Batumi:AddAsset("Grisha") +-- warehouse.Batumi:AddAsset("Molniya") +-- warehouse.Batumi:AddAsset("Neustrashimy") +-- warehouse.Batumi:AddAsset("Rezky") +-- warehouse.Batumi:AddAsset("Moskva") +-- warehouse.Batumi:AddAsset("Pyotr Velikiy") +-- warehouse.Batumi:AddAsset("Kuznetsov") +-- warehouse.Batumi:AddAsset("Zvezdny") +-- warehouse.Batumi:AddAsset("Yakushev") +-- warehouse.Batumi:AddAsset("Elnya") +-- warehouse.Batumi:AddAsset("Ivanov") +-- warehouse.Batumi:AddAsset("Yantai") +-- warehouse.Batumi:AddAsset("Type 052C") +-- warehouse.Batumi:AddAsset("Guangzhou") +-- +-- -- Get Number of ships at Batumi. +-- local nships=warehouse.Batumi:GetNumberOfAssets(WAREHOUSE.Descriptor.CATEGORY, Group.Category.SHIP) +-- +-- -- Send one ship every 3 minutes (ships do not evade each other well, so we need a bit space between them). +-- for i=1, nships do +-- warehouse.Batumi:__AddRequest(180*(i-1)+10, warehouse.Kobuleti, WAREHOUSE.Descriptor.CATEGORY, Group.Category.SHIP, 1) +-- end +-- +-- ## Example 10: Warehouse on Aircraft Carrier +-- +-- This example shows how to spawn assets from a warehouse located on an aircraft carrier. The warehouse must still be represented by a +-- physical static object. However, on a carrier space is limit so we take a smaller static. In priciple one could also take something +-- like a windsock. +-- +-- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_Carrier.png) +-- +-- USS Stennis requests F/A-18s from Batumi. At the same time Kobuleti requests F/A-18s from the Stennis which currently does not have any. +-- So first, Batumi delivers the fighters to the Stennis. After they arrived they are deployed again and send to Kobuleti. +-- +-- -- Start warehouses. +-- warehouse.Batumi:Start() +-- warehouse.Stennis:Start() +-- warehouse.Kobuleti:Start() +-- +-- -- Add F/A-18 2-ship flight to Batmi. +-- warehouse.Batumi:AddAsset("F/A-18C 2ship", 1) +-- +-- -- USS Stennis requests F/A-18 from Batumi. +-- warehouse.Batumi:AddRequest(warehouse.Stennis, WAREHOUSE.Descriptor.GROUPNAME, "F/A-18C 2ship") +-- +-- -- Kobuleti requests F/A-18 from USS Stennis. +-- warehouse.Stennis:AddRequest(warehouse.Kobuleti, WAREHOUSE.Descriptor.GROUPNAME, "F/A-18C 2ship") +-- +-- ## Example 11: Aircraft Carrier - Rescue Helo and Escort +-- +-- After 10 seconds we make a self request for a rescue helicopter. Note, that the @{#WAREHOUSE.AddRequest} function has a parameter which lets you +-- specify an "Assignment". This can be later used to identify the request and take the right actions. +-- +-- Once the request is processed, the @{#WAREHOUSE.OnAfterSelfRequest} function is called. This is where we hook in and postprocess the spawned assets. +-- In particular, we use the @{AI.AI_Formation#AI_FORMATION} class to make some nice escorts for our carrier. +-- +-- When the resue helo is spawned, we can check that this is the correct asset and make the helo go into formation with the carrier. +-- Once the helo runs out of fuel, it will automatically return to the ship and land. For the warehouse, this means that the "cargo", i.e. the helicopter +-- has been delivered - assets can be delivered to other warehouses and to the same warehouse - hence a *self* request. +-- When that happens, the **Delivered** event is triggered and the @{#WAREHOUSE.OnAfterDelivered} function called. This can now be used to spawn +-- a fresh helo. Effectively, there we created an infinite, never ending loop. So a rescue helo will be up at all times. +-- +-- After 30 and 45 seconds requests for five groups of armed speedboats are made. These will be spawned in the port zone right behind the carrier. +-- The first five groups will go port of the carrier an form a left wing formation. The seconds groups will to the analogue on the starboard side. +-- **Note** that in order to spawn naval assets a warehouse needs a port (zone). Since the carrier and hence the warehouse is mobile, we define a moving +-- zone as @{Core.Zone#ZONE_UNIT} with the carrier as reference unit. The "port" of the Stennis at its stern so all naval assets are spawned behind the carrier. +-- +-- -- Start warehouse on USS Stennis. +-- warehouse.Stennis:Start() +-- +-- -- Aircraft carrier gets a moving zone right behind it as port. +-- warehouse.Stennis:SetPortZone(ZONE_UNIT:New("Warehouse Stennis Port Zone", UNIT:FindByName("USS Stennis"), 100, {rho=250, theta=180, relative_to_unit=true})) +-- +-- -- Add speedboat assets. +-- warehouse.Stennis:AddAsset("Speedboat", 10) +-- warehouse.Stennis:AddAsset("CH-53E", 1) +-- +-- -- Self request of speed boats. +-- warehouse.Stennis:__AddRequest(10, warehouse.Stennis, WAREHOUSE.Descriptor.GROUPNAME, "CH-53E", 1, nil, nil, nil, "Rescue Helo") +-- warehouse.Stennis:__AddRequest(30, warehouse.Stennis, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.NAVAL_ARMEDSHIP, 5, nil, nil, nil, "Speedboats Left") +-- warehouse.Stennis:__AddRequest(45, warehouse.Stennis, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.NAVAL_ARMEDSHIP, 5, nil, nil, nil, "Speedboats Right") +-- +-- --- Function called after self request +-- function warehouse.Stennis:OnAfterSelfRequest(From, Event, To,_groupset, request) +-- local groupset=_groupset --Core.Set#SET_GROUP +-- local request=request --Functional.Warehouse#WAREHOUSE.Pendingitem +-- +-- -- USS Stennis is the mother ship. +-- local Mother=UNIT:FindByName("USS Stennis") +-- +-- -- Get assignment of the request. +-- local assignment=warehouse.Stennis:GetAssignment(request) +-- +-- if assignment=="Speedboats Left" then +-- +-- -- Define AI Formation object. +-- -- Note that this has to be a global variable or the garbage collector will remove it for some reason! +-- CarrierFormationLeft = AI_FORMATION:New(Mother, groupset, "Left Formation with Carrier", "Escort Carrier.") +-- +-- -- Formation parameters. +-- CarrierFormationLeft:FormationLeftWing(200 ,50, 0, 0, 500, 50) +-- CarrierFormationLeft:__Start(2) +-- +-- for _,group in pairs(groupset:GetSetObjects()) do +-- local group=group --Wrapper.Group#GROUP +-- group:FlareRed() +-- end +-- +-- elseif assignment=="Speedboats Right" then +-- +-- -- Define AI Formation object. +-- -- Note that this has to be a global variable or the garbage collector will remove it for some reason! +-- CarrierFormationRight = AI_FORMATION:New(Mother, groupset, "Right Formation with Carrier", "Escort Carrier.") +-- +-- -- Formation parameters. +-- CarrierFormationRight:FormationRightWing(200 ,50, 0, 0, 500, 50) +-- CarrierFormationRight:__Start(2) +-- +-- for _,group in pairs(groupset:GetSetObjects()) do +-- local group=group --Wrapper.Group#GROUP +-- group:FlareGreen() +-- end +-- +-- elseif assignment=="Rescue Helo" then +-- +-- -- Start uncontrolled helo. +-- local group=groupset:GetFirst() --Wrapper.Group#GROUP +-- group:StartUncontrolled() +-- +-- -- Define AI Formation object. +-- CarrierFormationHelo = AI_FORMATION:New(Mother, groupset, "Helo Formation with Carrier", "Fly Formation.") +-- +-- -- Formation parameters. +-- CarrierFormationHelo:FormationCenterWing(-150, 50, 20, 50, 100, 50) +-- CarrierFormationHelo:__Start(2) +-- +-- end +-- +-- --- When the helo is out of fuel, it will return to the carrier and should be delivered. +-- function warehouse.Stennis:OnAfterDelivered(From,Event,To,request) +-- local request=request --Functional.Warehouse#WAREHOUSE.Pendingitem +-- +-- -- So we start another request. +-- if request.assignment=="Rescue Helo" then +-- warehouse.Stennis:__AddRequest(10, warehouse.Stennis, WAREHOUSE.Descriptor.GROUPNAME, "CH-53E", 1, nil, nil, nil, "Rescue Helo") +-- end +-- end +-- +-- end +-- +-- ## Example 12: Pause a Warehouse +-- +-- This example shows how to pause and unpause a warehouse. In paused state, requests will not be processed but assets can be added and requests be added. +-- +-- * Warehouse Batumi is paused after 10 seconds. +-- * Request from Berlin after 15 which will not be processed. +-- * New tank assets for Batumi after 20 seconds. This is possible also in paused state. +-- * Batumi unpaused after 30 seconds. Queued request from Berlin can be processed. +-- * Berlin is paused after 60 seconds. +-- * Berlin requests tanks from Batumi after 90 seconds. Request is not processed because Berlin is paused and not running. +-- * Berlin is unpaused after 120 seconds. Queued request for tanks from Batumi can not be processed. +-- +-- Here is the code: +-- +-- -- Start Warehouse at Batumi. +-- warehouse.Batumi:Start() +-- +-- -- Start Warehouse Berlin. +-- warehouse.Berlin:Start() +-- +-- -- Add 20 infantry groups and 5 tank platoons as assets at Batumi. +-- warehouse.Batumi:AddAsset("Infantry Platoon Alpha", 20) +-- +-- -- Pause the warehouse after 10 seconds +-- warehouse.Batumi:__Pause(10) +-- +-- -- Add a request from Berlin after 15 seconds. A request can be added but not be processed while warehouse is paused. +-- warehouse.Batumi:__AddRequest(15, warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 1) +-- +-- -- New asset added after 20 seconds. This is possible even if the warehouse is paused. +-- warehouse.Batumi:__AddAsset(20, "Abrams", 5) +-- +-- -- Unpause warehouse after 30 seconds. Now the request from Berlin can be processed. +-- warehouse.Batumi:__Unpause(30) +-- +-- -- Pause warehouse Berlin +-- warehouse.Berlin:__Pause(60) +-- +-- -- After 90 seconds request from Berlin for tanks. +-- warehouse.Batumi:__AddRequest(90, warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_TANK, 1) +-- +-- -- After 120 seconds unpause Berlin. +-- warehouse.Berlin:__Unpause(120) +-- +-- ## Example 13: Battlefield Air Interdiction +-- +-- This example show how to couple the WAREHOUSE class with the @{AI.AI_Bai} class. +-- Four enemy targets have been located at the famous Kobuleti X. All three available Viggen 2-ship flights are assigned to kill at least one of the BMPs to complete their mission. +-- +-- -- Start Warehouse at Kobuleti. +-- warehouse.Kobuleti:Start() +-- +-- -- Add three 2-ship groups of Viggens. +-- warehouse.Kobuleti:AddAsset("Viggen 2ship", 3) +-- +-- -- Self request for all Viggen assets. +-- warehouse.Kobuleti:AddRequest(warehouse.Kobuleti, WAREHOUSE.Descriptor.GROUPNAME, "Viggen 2ship", WAREHOUSE.Quantity.ALL, nil, nil, nil, "BAI") +-- +-- -- Red targets at Kobuleti X (late activated). +-- local RedTargets=GROUP:FindByName("Red IVF Alpha") +-- +-- -- Activate the targets. +-- RedTargets:Activate() +-- +-- -- Do something with the spawned aircraft. +-- function warehouse.Kobuleti:OnAfterSelfRequest(From,Event,To,groupset,request) +-- local groupset=groupset --Core.Set#SET_GROUP +-- local request=request --Functional.Warehouse#WAREHOUSE.Pendingitem +-- +-- if request.assignment=="BAI" then +-- +-- for _,group in pairs(groupset:GetSetObjects()) do +-- local group=group --Wrapper.Group#GROUP +-- +-- -- Start uncontrolled aircraft. +-- group:StartUncontrolled() +-- +-- local BAI=AI_BAI_ZONE:New(ZONE:New("Patrol Zone Kobuleti"), 500, 1000, 500, 600, ZONE:New("Patrol Zone Kobuleti")) +-- +-- -- Tell the program to use the object (in this case called BAIPlane) as the group to use in the BAI function +-- BAI:SetControllable(group) +-- +-- -- Function checking if targets are still alive +-- local function CheckTargets() +-- local nTargets=RedTargets:GetSize() +-- local nInitial=RedTargets:GetInitialSize() +-- local nDead=nInitial-nTargets +-- local nRequired=1 -- Let's make this easy. +-- if RedTargets:IsAlive() and nDead < nRequired then +-- MESSAGE:New(string.format("BAI Mission: %d of %d red targets still alive. At least %d targets need to be eliminated.", nTargets, nInitial, nRequired), 5):ToAll() +-- else +-- MESSAGE:New("BAI Mission: The required red targets are destroyed.", 30):ToAll() +-- BAI:__Accomplish(1) -- Now they should fly back to the patrolzone and patrol. +-- end +-- end +-- +-- -- Start scheduler to monitor number of targets. +-- local Check, CheckScheduleID = SCHEDULER:New(nil, CheckTargets, {}, 60, 60) +-- +-- -- When the targets in the zone are destroyed, (see scheduled function), the planes will return home ... +-- function BAI:OnAfterAccomplish( Controllable, From, Event, To ) +-- MESSAGE:New( "BAI Mission: Sending the Viggens back to base.", 30):ToAll() +-- Check:Stop(CheckScheduleID) +-- BAI:__RTB(1) +-- end +-- +-- -- Start BAI +-- BAI:Start() +-- +-- -- Engage after 5 minutes. +-- BAI:__Engage(300) +-- +-- -- RTB after 30 min max. +-- BAI:__RTB(-30*60) +-- +-- end +-- end +-- +-- end +-- +-- ## Example 14: Strategic Bombing +-- +-- This example shows how to employ strategic bombers in a mission. Three B-52s are launched at Kobuleti with the assignment to wipe out the enemy warehouse at Sukhumi. +-- The bombers will get a flight path and make their approach from the South at an altitude of 5000 m ASL. After their bombing run, they will return to Kobuleti and +-- added back to stock. +-- +-- -- Start warehouses +-- warehouse.Kobuleti:Start() +-- warehouse.Sukhumi:Start() +-- +-- -- Add a strategic bomber assets +-- warehouse.Kobuleti:AddAsset("B-52H", 3) +-- +-- -- Request bombers for specific task of bombing Sukhumi warehouse. +-- warehouse.Kobuleti:AddRequest(warehouse.Kobuleti, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.AIR_BOMBER, WAREHOUSE.Quantity.ALL, nil, nil, nil, "Bomb Sukhumi") +-- +-- -- Specify assignment after bombers have been spawned. +-- function warehouse.Kobuleti:OnAfterSelfRequest(From, Event, To, groupset, request) +-- local groupset=groupset --Core.Set#SET_GROUP +-- +-- -- Get assignment of this request. +-- local assignment=warehouse.Kobuleti:GetAssignment(request) +-- +-- if assignment=="Bomb Sukhumi" then +-- +-- for _,_group in pairs(groupset:GetSet()) do +-- local group=_group --Wrapper.Group#GROUP +-- +-- -- Start uncontrolled aircraft. +-- group:StartUncontrolled() +-- +-- -- Target coordinate! +-- local ToCoord=warehouse.Sukhumi:GetCoordinate():SetAltitude(5000) +-- +-- -- Home coordinate. +-- local HomeCoord=warehouse.Kobuleti:GetCoordinate():SetAltitude(3000) +-- +-- -- Task bomb Sukhumi warehouse using all bombs (2032) from direction 180 at altitude 5000 m. +-- local task=group:TaskBombing(warehouse.Sukhumi:GetCoordinate():GetVec2(), false, "All", nil , 180, 5000, 2032) +-- +-- -- Define waypoints. +-- local WayPoints={} +-- +-- -- Take off position. +-- WayPoints[1]=warehouse.Kobuleti:GetCoordinate():WaypointAirTakeOffParking() +-- -- Begin bombing run 20 km south of target. +-- WayPoints[2]=ToCoord:Translate(20*1000, 180):WaypointAirTurningPoint(nil, 600, {task}, "Bombing Run") +-- -- Return to base. +-- WayPoints[3]=HomeCoord:WaypointAirTurningPoint() +-- -- Land at homebase. Bombers are added back to stock and can be employed in later assignments. +-- WayPoints[4]=warehouse.Kobuleti:GetCoordinate():WaypointAirLanding() +-- +-- -- Route bombers. +-- group:Route(WayPoints) +-- end +-- +-- end +-- end +-- +-- ## Example 15: Defining Off-Road Paths +-- +-- For self propelled assets it is possible to define custom off-road paths from one warehouse to another via the @{#WAREHOUSE.AddOffRoadPath} function. +-- The waypoints of a path are taken from late activated units. In this example, two paths have been defined between the warehouses Kobuleti and FARP London. +-- Trucks are spawned at each warehouse and are guided along the paths to the other warehouse. +-- Note that if more than one path was defined, each asset group will randomly select its route. +-- +-- -- Start warehouses +-- warehouse.Kobuleti:Start() +-- warehouse.London:Start() +-- +-- -- Define a polygon zone as spawn zone at Kobuleti. +-- warehouse.Kobuleti:SetSpawnZone(ZONE_POLYGON:New("Warehouse Kobuleti Spawn Zone", GROUP:FindByName("Warehouse Kobuleti Spawn Zone"))) +-- +-- -- Add assets. +-- warehouse.Kobuleti:AddAsset("M978", 20) +-- warehouse.London:AddAsset("M818", 20) +-- +-- -- Off two road paths from Kobuleti to London. The reverse path from London to Kobuleti is added automatically. +-- warehouse.Kobuleti:AddOffRoadPath(warehouse.London, GROUP:FindByName("Warehouse Kobuleti-London OffRoad Path 1")) +-- warehouse.Kobuleti:AddOffRoadPath(warehouse.London, GROUP:FindByName("Warehouse Kobuleti-London OffRoad Path 2")) +-- +-- -- London requests all available trucks from Kobuleti. +-- warehouse.Kobuleti:AddRequest(warehouse.London, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_TRUCK, WAREHOUSE.Quantity.ALL) +-- +-- -- Kobuleti requests all available trucks from London. +-- warehouse.London:AddRequest(warehouse.Kobuleti, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_TRUCK, WAREHOUSE.Quantity.HALF) +-- +-- ## Example 16: Resupply of Dead Assets +-- +-- Warehouse at FARP Berlin is located at the front line and sends infantry groups to the battle zone. +-- Whenever a group dies, a new group is send from the warehouse to the battle zone. +-- Additionally, for each dead group, Berlin requests resupply from Batumi. +-- +-- -- Start warehouses. +-- warehouse.Batumi:Start() +-- warehouse.Berlin:Start() +-- +-- -- Front line warehouse. +-- warehouse.Berlin:AddAsset("Infantry Platoon Alpha", 6) +-- +-- -- Resupply warehouse. +-- warehouse.Batumi:AddAsset("Infantry Platoon Alpha", 50) +-- +-- -- Battle zone near FARP Berlin. This is where the action is! +-- local BattleZone=ZONE:New("Virtual Battle Zone") +-- +-- -- Send infantry groups to the battle zone. Two groups every ~60 seconds. +-- for i=1,2 do +-- local time=(i-1)*60+10 +-- warehouse.Berlin:__AddRequest(time, warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 2, nil, nil, nil, "To Battle Zone") +-- end +-- +-- -- Take care of the spawned units. +-- function warehouse.Berlin:OnAfterSelfRequest(From,Event,To,groupset,request) +-- local groupset=groupset --Core.Set#SET_GROUP +-- local request=request --Functional.Warehouse#WAREHOUSE.Pendingitem +-- +-- -- Get assignment of this request. +-- local assignment=warehouse.Berlin:GetAssignment(request) +-- +-- if assignment=="To Battle Zone" then +-- +-- for _,group in pairs(groupset:GetSet()) do +-- local group=group --Wrapper.Group#GROUP +-- +-- -- Route group to Battle zone. +-- local ToCoord=BattleZone:GetRandomCoordinate() +-- group:RouteGroundOnRoad(ToCoord, group:GetSpeedMax()*0.8) +-- +-- -- After 3-5 minutes we create an explosion to destroy the group. +-- SCHEDULER:New(nil, Explosion, {group, 50}, math.random(180, 300)) +-- end +-- +-- end +-- +-- end +-- +-- -- An asset has died ==> request resupply for it. +-- function warehouse.Berlin:OnAfterAssetDead(From, Event, To, asset, request) +-- local asset=asset --Functional.Warehouse#WAREHOUSE.Assetitem +-- local request=request --Functional.Warehouse#WAREHOUSE.Pendingitem +-- +-- -- Get assignment. +-- local assignment=warehouse.Berlin:GetAssignment(request) +-- +-- -- Request resupply for dead asset from Batumi. +-- warehouse.Batumi:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, asset.attribute, nil, nil, nil, nil, "Resupply") +-- +-- -- Send asset to Battle zone either now or when they arrive. +-- warehouse.Berlin:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, asset.attribute, 1, nil, nil, nil, assignment) +-- end +-- +-- ## Example 17: Supply Chains +-- +-- Our remote warehouse "Pampa" south of Batumi needs assets but does not have any air infrastructure (FARP or airdrome). +-- Leopard 2 tanks are transported from Kobuleti to Batumi using two C-17As. From there they go be themselfs to Pampa. +-- Eight infantry groups and two mortar groups are also being transferred from Kobuleti to Batumi by helicopter. +-- The infantry has a higher priority and will be transported first using all available Mi-8 helicopters. +-- Once infantry has arrived at Batumi, it will walk by itself to warehouse Pampa. +-- The mortars can only be transported once the Mi-8 helos are available again, i.e. when the infantry has been delivered. +-- Once the mortars arrive at Batumi, they will be transported by APCs to Pampa. +-- +-- -- Start warehouses. +-- warehouse.Kobuleti:Start() +-- warehouse.Batumi:Start() +-- warehouse.Pampa:Start() +-- +-- -- Add assets to Kobuleti warehouse, which is our main hub. +-- warehouse.Kobuleti:AddAsset("C-130", 2) +-- warehouse.Kobuleti:AddAsset("C-17A", 2, nil, 77000) +-- warehouse.Kobuleti:AddAsset("Mi-8", 2, WAREHOUSE.Attribute.AIR_TRANSPORTHELO, nil, nil, nil, AI.Skill.EXCELLENT, {"Germany", "United Kingdom"}) +-- warehouse.Kobuleti:AddAsset("Leopard 2", 10, nil, nil, 62000, 500) +-- warehouse.Kobuleti:AddAsset("Mortar Alpha", 10, nil, nil, 210) +-- warehouse.Kobuleti:AddAsset("Infantry Platoon Alpha", 20) +-- +-- -- Transports at Batumi. +-- warehouse.Batumi:AddAsset("SPz Marder", 2) +-- warehouse.Batumi:AddAsset("TPz Fuchs", 2) +-- +-- -- Tanks transported by plane from from Kobuleti to Batumi. +-- warehouse.Kobuleti:AddRequest(warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_TANK, 2, WAREHOUSE.TransportType.AIRPLANE, 2, 10, "Assets for Pampa") +-- -- Artillery transported by helicopter from Kobuleti to Batumi. +-- warehouse.Kobuleti:AddRequest(warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_ARTILLERY, 2, WAREHOUSE.TransportType.HELICOPTER, 2, 30, "Assets for Pampa via APC") +-- -- Infantry transported by helicopter from Kobuleti to Batumi. +-- warehouse.Kobuleti:AddRequest(warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 8, WAREHOUSE.TransportType.HELICOPTER, 2, 20, "Assets for Pampa") +-- +-- --- Function handling assets delivered from Kobuleti warehouse. +-- function warehouse.Kobuleti:OnAfterDelivered(From, Event, To, request) +-- local request=request --Functional.Warehouse#WAREHOUSE.Pendingitem +-- +-- -- Get assignment. +-- local assignment=warehouse.Kobuleti:GetAssignment(request) +-- +-- -- Check if these assets were meant for Warehouse Pampa. +-- if assignment=="Assets for Pampa via APC" then +-- -- Forward everything that arrived at Batumi to Pampa via APC. +-- warehouse.Batumi:AddRequest(warehouse.Pampa, WAREHOUSE.Descriptor.ATTRIBUTE, request.cargoattribute, request.ndelivered, WAREHOUSE.TransportType.APC, WAREHOUSE.Quantity.ALL) +-- end +-- end +-- +-- -- Forward all mobile ground assets to Pampa once they arrived. +-- function warehouse.Batumi:OnAfterNewAsset(From, Event, To, asset, assignment) +-- local asset=asset --Functional.Warehouse#WAREHOUSE.Assetitem +-- if assignment=="Assets for Pampa" then +-- if asset.category==Group.Category.GROUND and asset.speedmax>0 then +-- warehouse.Batumi:AddRequest(warehouse.Pampa, WAREHOUSE.Descriptor.GROUPNAME, asset.templatename) +-- end +-- end +-- end +-- +-- +-- @field #WAREHOUSE +WAREHOUSE = { + ClassName = "WAREHOUSE", + Debug = false, + verbosity = 0, + lid = nil, + Report = true, + warehouse = nil, + alias = nil, + zone = nil, + airbase = nil, + airbasename = nil, + road = nil, + rail = nil, + spawnzone = nil, + uid = nil, + dTstatus = 30, + queueid = 0, + stock = {}, + queue = {}, + pending = {}, + transporting = {}, + delivered = {}, + defending = {}, + portzone = nil, + harborzone = nil, + shippinglanes = {}, + offroadpaths = {}, + autodefence = false, + spawnzonemaxdist = 5000, + autosave = false, + autosavepath = nil, + autosavefile = nil, + saveparking = false, + isunit = false, + lowfuelthresh = 0.15, + respawnafterdestroyed=false, + respawndelay = nil, +} + +--- Item of the warehouse stock table. +-- @type WAREHOUSE.Assetitem +-- @field #number uid Unique id of the asset. +-- @field #string templatename Name of the template group. +-- @field #table template The spawn template of the group. +-- @field DCS#Group.Category category Category of the group. +-- @field #string unittype Type of the first unit of the group as obtained by the Object.getTypeName() DCS API function. +-- @field #number nunits Number of units in the group. +-- @field #number range Range of the unit in meters. +-- @field #number speedmax Maximum speed in km/h the group can do. +-- @field #number size Maximum size in length and with of the asset in meters. +-- @field #number weight The weight of the whole asset group in kilo gramms. +-- @field DCS#Object.Desc DCSdesc All DCS descriptors. +-- @field #WAREHOUSE.Attribute attribute Generalized attribute of the group. +-- @field #table cargobay Array of cargo bays of all units in an asset group. +-- @field #number cargobaytot Total weight in kg that fits in the cargo bay of all asset group units. +-- @field #number cargobaymax Largest cargo bay of all units in the group. +-- @field #number loadradius Distance when cargo is loaded into the carrier. +-- @field DCS#AI.Skill skill Skill of AI unit. +-- @field #string livery Livery of the asset. +-- @field #string assignment Assignment of the asset. This could, e.g., be used in the @{#WAREHOUSE.OnAfterNewAsset) function. +-- @field #boolean spawned If true, asset was spawned into the cruel world. If false, it is still in stock. +-- @field #string spawngroupname Name of the spawned group. +-- @field #boolean iscargo If true, asset is cargo. If false asset is transport. Nil if in stock. +-- @field #number rid The request ID of this asset. +-- @field #boolean arrived If true, asset arrived at its destination. +-- @field #number damage Damage of asset group in percent. + +--- Item of the warehouse queue table. +-- @type WAREHOUSE.Queueitem +-- @field #number uid Unique id of the queue item. +-- @field #WAREHOUSE warehouse Requesting warehouse. +-- @field #WAREHOUSE.Descriptor assetdesc Descriptor of the requested asset. Enumerator of type @{#WAREHOUSE.Descriptor}. +-- @field assetdescval Value of the asset descriptor. Type depends on "assetdesc" descriptor. +-- @field #number nasset Number of asset groups requested. +-- @field #WAREHOUSE.TransportType transporttype Transport unit type. +-- @field #number ntransport Max. number of transport units requested. +-- @field #string assignment A keyword or text that later be used to identify this request and postprocess the assets. +-- @field #number prio Priority of the request. Number between 1 (high) and 100 (low). +-- @field Wrapper.Airbase#AIRBASE airbase The airbase beloning to requesting warehouse if any. +-- @field DCS#Airbase.Category category Category of the requesting airbase, i.e. airdrome, helipad/farp or ship. +-- @field #boolean toself Self request, i.e. warehouse requests assets from itself. +-- @field #table assets Table of self propelled (or cargo) and transport assets. Each element of the table is a @{#WAREHOUSE.Assetitem} and can be accessed by their asset ID. +-- @field #table cargoassets Table of cargo (or self propelled) assets. Each element of the table is a @{#WAREHOUSE.Assetitem}. +-- @field #number cargoattribute Attribute of cargo assets of type @{#WAREHOUSE.Attribute}. +-- @field #number cargocategory Category of cargo assets of type @{#WAREHOUSE.Category}. +-- @field #table transportassets Table of transport carrier assets. Each element of the table is a @{#WAREHOUSE.Assetitem}. +-- @field #number transportattribute Attribute of transport assets of type @{#WAREHOUSE.Attribute}. +-- @field #number transportcategory Category of transport assets of type @{#WAREHOUSE.Category}. + +--- Item of the warehouse pending queue table. +-- @type WAREHOUSE.Pendingitem +-- @field #number timestamp Absolute mission time in seconds when the request was processed. +-- @field #table assetproblem Table with assets that might have problems (damage or stuck). +-- @field Core.Set#SET_GROUP cargogroupset Set of cargo groups do be delivered. +-- @field #number ndelivered Number of groups delivered to destination. +-- @field Core.Set#SET_GROUP transportgroupset Set of cargo transport carrier groups. +-- @field Core.Set#SET_CARGO transportcargoset Set of cargo objects. +-- @field #table carriercargo Table holding the cargo groups of each carrier unit. +-- @field #number ntransporthome Number of transports back home. +-- @field #boolean lowfuel If true, at least one asset group is low on fuel. +-- @extends #WAREHOUSE.Queueitem + +--- Descriptors enumerator describing the type of the asset. +-- @type WAREHOUSE.Descriptor +-- @field #string GROUPNAME Name of the asset template. +-- @field #string UNITTYPE Typename of the DCS unit, e.g. "A-10C". +-- @field #string ATTRIBUTE Generalized attribute @{#WAREHOUSE.Attribute}. +-- @field #string CATEGORY Asset category of type DCS#Group.Category, i.e. GROUND, AIRPLANE, HELICOPTER, SHIP, TRAIN. +-- @field #string ASSIGNMENT Assignment of asset when it was added. +-- @field #string ASSETLIST List of specific assets gives as a table of assets. Mind the curly brackets {}. +WAREHOUSE.Descriptor = { + GROUPNAME="templatename", + UNITTYPE="unittype", + ATTRIBUTE="attribute", + CATEGORY="category", + ASSIGNMENT="assignment", + ASSETLIST="assetlist," +} + +--- Generalized asset attributes. Can be used to request assets with certain general characteristics. See [DCS attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes) on hoggit. +-- @type WAREHOUSE.Attribute +-- @field #string AIR_TRANSPORTPLANE Airplane with transport capability. This can be used to transport other assets. +-- @field #string AIR_AWACS Airborne Early Warning and Control System. +-- @field #string AIR_FIGHTER Fighter, interceptor, ... airplane. +-- @field #string AIR_BOMBER Aircraft which can be used for strategic bombing. +-- @field #string AIR_TANKER Airplane which can refuel other aircraft. +-- @field #string AIR_TRANSPORTHELO Helicopter with transport capability. This can be used to transport other assets. +-- @field #string AIR_ATTACKHELO Attack helicopter. +-- @field #string AIR_UAV Unpiloted Aerial Vehicle, e.g. drones. +-- @field #string AIR_OTHER Any airborne unit that does not fall into any other airborne category. +-- @field #string GROUND_APC Infantry carriers, in particular Amoured Personell Carrier. This can be used to transport other assets. +-- @field #string GROUND_TRUCK Unarmed ground vehicles, which has the DCS "Truck" attribute. +-- @field #string GROUND_INFANTRY Ground infantry assets. +-- @field #string GROUND_ARTILLERY Artillery assets. +-- @field #string GROUND_TANK Tanks (modern or old). +-- @field #string GROUND_TRAIN Trains. Not that trains are **not** yet properly implemented in DCS and cannot be used currently. +-- @field #string GROUND_EWR Early Warning Radar. +-- @field #string GROUND_AAA Anti-Aircraft Artillery. +-- @field #string GROUND_SAM Surface-to-Air Missile system or components. +-- @field #string GROUND_OTHER Any ground unit that does not fall into any other ground category. +-- @field #string NAVAL_AIRCRAFTCARRIER Aircraft carrier. +-- @field #string NAVAL_WARSHIP War ship, i.e. cruisers, destroyers, firgates and corvettes. +-- @field #string NAVAL_ARMEDSHIP Any armed ship that is not an aircraft carrier, a cruiser, destroyer, firgatte or corvette. +-- @field #string NAVAL_UNARMEDSHIP Any unarmed naval vessel. +-- @field #string NAVAL_OTHER Any naval unit that does not fall into any other naval category. +-- @field #string OTHER_UNKNOWN Anything that does not fall into any other category. +WAREHOUSE.Attribute = { + AIR_TRANSPORTPLANE="Air_TransportPlane", + AIR_AWACS="Air_AWACS", + AIR_FIGHTER="Air_Fighter", + AIR_BOMBER="Air_Bomber", + AIR_TANKER="Air_Tanker", + AIR_TRANSPORTHELO="Air_TransportHelo", + AIR_ATTACKHELO="Air_AttackHelo", + AIR_UAV="Air_UAV", + AIR_OTHER="Air_OtherAir", + GROUND_APC="Ground_APC", + GROUND_TRUCK="Ground_Truck", + GROUND_INFANTRY="Ground_Infantry", + GROUND_ARTILLERY="Ground_Artillery", + GROUND_TANK="Ground_Tank", + GROUND_TRAIN="Ground_Train", + GROUND_EWR="Ground_EWR", + GROUND_AAA="Ground_AAA", + GROUND_SAM="Ground_SAM", + GROUND_OTHER="Ground_OtherGround", + NAVAL_AIRCRAFTCARRIER="Naval_AircraftCarrier", + NAVAL_WARSHIP="Naval_WarShip", + NAVAL_ARMEDSHIP="Naval_ArmedShip", + NAVAL_UNARMEDSHIP="Naval_UnarmedShip", + NAVAL_OTHER="Naval_OtherNaval", + OTHER_UNKNOWN="Other_Unknown", +} + +--- Cargo transport type. Defines how assets are transported to their destination. +-- @type WAREHOUSE.TransportType +-- @field #string AIRPLANE Transports are carried out by airplanes. +-- @field #string HELICOPTER Transports are carried out by helicopters. +-- @field #string APC Transports are conducted by APCs. +-- @field #string SHIP Transports are conducted by ships. Not implemented yet. +-- @field #string TRAIN Transports are conducted by trains. Not implemented yet. Also trains are buggy in DCS. +-- @field #string SELFPROPELLED Assets go to their destination by themselves. No transport carrier needed. +WAREHOUSE.TransportType = { + AIRPLANE = "Air_TransportPlane", + HELICOPTER = "Air_TransportHelo", + APC = "Ground_APC", + TRAIN = "Ground_Train", + SHIP = "Naval_UnarmedShip", + AIRCRAFTCARRIER = "Naval_AircraftCarrier", + WARSHIP = "Naval_WarShip", + ARMEDSHIP = "Naval_ArmedShip", + SELFPROPELLED = "Selfpropelled", +} + +--- Warehouse quantity enumerator for selecting number of assets, e.g. all, half etc. of what is in stock rather than an absolute number. +-- @type WAREHOUSE.Quantity +-- @field #string ALL All "all" assets currently in stock. +-- @field #string THREEQUARTERS Three quarters "3/4" of assets in stock. +-- @field #string HALF Half "1/2" of assets in stock. +-- @field #string THIRD One third "1/3" of assets in stock. +-- @field #string QUARTER One quarter "1/4" of assets in stock. +WAREHOUSE.Quantity = { + ALL = "all", + THREEQUARTERS = "3/4", + HALF = "1/2", + THIRD = "1/3", + QUARTER = "1/4", +} + +--- Warehouse database. Note that this is a global array to have easier exchange between warehouses. +-- @type _WAREHOUSEDB +-- @field #number AssetID Unique ID of each asset. This is a running number, which is increased each time a new asset is added. +-- @field #table Assets Table holding registered assets, which are of type @{Functional.Warehouse#WAREHOUSE.Assetitem}.# +-- @field #number WarehouseID Unique ID of the warehouse. Running number. +-- @field #table Warehouses Table holding all defined @{#WAREHOUSE} objects by their unique ids. +_WAREHOUSEDB = { + AssetID = 0, + Assets = {}, + WarehouseID = 0, + Warehouses = {} +} + +--- Warehouse class version. +-- @field #string version +WAREHOUSE.version="1.0.2" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO: Warehouse todo list. +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Add check if assets "on the move" are stationary. Can happen if ground units get stuck in buildings. If stationary auto complete transport by adding assets to request warehouse? Time? +-- TODO: Optimize findpathonroad. Do it only once (first time) and safe paths between warehouses similar to off-road paths. +-- NOGO: Spawn assets only virtually, i.e. remove requested assets from stock but do NOT spawn them ==> Interface to A2A dispatcher! Maybe do a negative sign on asset number? +-- TODO: Make more examples: ARTY, CAP, ... +-- TODO: Check also general requests like all ground. Is this a problem for self propelled if immobile units are among the assets? Check if transport. +-- TODO: Handle the case when units of a group die during the transfer. +-- DONE: Added harbours as interface for transport to/from warehouses. Simplifies process of spawning units near the ship, especially if cargo not self-propelled. +-- DONE: Test capturing a neutral warehouse. +-- DONE: Add save/load capability of warehouse <==> persistance after mission restart. Difficult in lua! +-- DONE: Get cargo bay and weight from CARGO_GROUP and GROUP. No necessary any more! +-- DONE: Add possibility to set weight and cargo bay manually in AddAsset function as optional parameters. +-- DONE: Check overlapping aircraft sometimes. +-- DONE: Case when all transports are killed and there is still cargo to be delivered. Put cargo back into warehouse. Should be done now! +-- DONE: Add transport units from dispatchers back to warehouse stock once they completed their mission. +-- DONE: Write documentation. +-- DONE: Add AAA, SAMs and UAVs to generalized attributes. +-- DONE: Add warehouse quantity enumerator. +-- DONE: Test mortars. Immobile units need a transport. +-- DONE: Set ROE for spawned groups. +-- DONE: Add offroad lanes between warehouses if road connection is not available. +-- DONE: Add possibility to add active groups. Need to create a pseudo template before destroy. <== Does not seem to be necessary any more. +-- DONE: Add a time stamp when an asset is added to the stock and for requests. +-- DONE: How to get a specific request once the cargo is delivered? Make addrequest addasset non FSM function? Callback for requests like in SPAWN? +-- DONE: Add autoselfdefence switch and user function. Default should be off. +-- DONE: Warehouse re-capturing not working?! +-- DONE: Naval assets dont go back into stock once arrived. +-- DONE: Take cargo weight into consideration, when selecting transport assets. +-- DONE: Add ports for spawning naval assets. +-- DONE: Add shipping lanes between warehouses. +-- DONE: Handle cases with immobile units <== should be handled by dispatcher classes. +-- DONE: Handle cases for aircraft carriers and other ships. Place warehouse on carrier possible? On others probably not - exclude them? +-- DONE: Add general message function for sending to coaliton or debug. +-- DONE: Fine tune event handlers. +-- DONE: Improve generalized attributes. +-- DONE: If warehouse is destroyed, all asssets are gone. +-- DONE: Add event handlers. +-- DONE: Add AI_CARGO_AIRPLANE +-- DONE: Add AI_CARGO_APC +-- DONE: Add AI_CARGO_HELICOPTER +-- DONE: Switch to AI_CARGO_XXX_DISPATCHER +-- DONE: Add queue. +-- DONE: Put active groups into the warehouse, e.g. when they were transported to this warehouse. +-- NOGO: Spawn warehouse assets as uncontrolled or AI off and activate them when requested. +-- DONE: How to handle multiple units in a transport group? <== Cargo dispatchers. +-- DONE: Add phyical object. +-- DONE: If warehosue is captured, change warehouse and assets to other coalition. +-- NOGO: Use RAT for routing air units. Should be possible but might need some modifications of RAT, e.g. explit spawn place. But flight plan should be better. +-- DONE: Can I make a request with specific assets? E.g., once delivered, make a request for exactly those assests that were in the original request. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor(s) +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- The WAREHOUSE constructor. Creates a new WAREHOUSE object from a static object. Parameters like the coalition and country are taken from the static object structure. +-- @param #WAREHOUSE self +-- @param Wrapper.Static#STATIC warehouse The physical structure representing the warehouse. +-- @param #string alias (Optional) Alias of the warehouse, i.e. the name it will be called when sending messages etc. Default is the name of the static +-- @return #WAREHOUSE self +function WAREHOUSE:New(warehouse, alias) + + -- Check if just a string was given and convert to static. + if type(warehouse)=="string" then + local warehousename=warehouse + warehouse=UNIT:FindByName(warehousename) + if warehouse==nil then + warehouse=STATIC:FindByName(warehousename, true) + self.isunit=false + else + self.isunit=true + end + end + + -- Nil check. + if warehouse==nil then + BASE:E("ERROR: Warehouse does not exist!") + return nil + end + + -- Set alias. + self.alias=alias or warehouse:GetName() + + -- Print version. + env.info(string.format("Adding warehouse v%s for structure %s with alias %s", WAREHOUSE.version, warehouse:GetName(), self.alias)) + + -- Inherit everthing from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #WAREHOUSE + + -- Set some string id for output to DCS.log file. + self.lid=string.format("WAREHOUSE %s | ", self.alias) + + -- Set some variables. + self.warehouse=warehouse + + -- Increase global warehouse counter. + _WAREHOUSEDB.WarehouseID=_WAREHOUSEDB.WarehouseID+1 + + -- Set unique ID for this warehouse. + self.uid=_WAREHOUSEDB.WarehouseID + + -- Coalition of the warehouse. + self.coalition=self.warehouse:GetCoalition() + + -- Country of the warehouse. + self.countryid=self.warehouse:GetCountry() + + -- Closest of the same coalition but within 5 km range. + local _airbase=self:GetCoordinate():GetClosestAirbase(nil, self:GetCoalition()) + if _airbase and _airbase:GetCoordinate():Get2DDistance(self:GetCoordinate()) <= 5000 then + self:SetAirbase(_airbase) + end + + -- Define warehouse and default spawn zone. + self.zone=ZONE_RADIUS:New(string.format("Warehouse zone %s", self.warehouse:GetName()), warehouse:GetVec2(), 500) + self.spawnzone=ZONE_RADIUS:New(string.format("Warehouse %s spawn zone", self.warehouse:GetName()), warehouse:GetVec2(), 250) + + -- Defaults + self:SetMarker(true) + self:SetReportOff() + self:SetRunwayRepairtime() + --self:SetVerbosityLevel(0) + + -- Add warehouse to database. + _WAREHOUSEDB.Warehouses[self.uid]=self + + ----------------------- + --- FSM Transitions --- + ----------------------- + + -- Start State. + self:SetStartState("NotReadyYet") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("NotReadyYet", "Load", "Loaded") -- Load the warehouse state from scatch. + self:AddTransition("Stopped", "Load", "Loaded") -- Load the warehouse state stopped state. + + self:AddTransition("NotReadyYet", "Start", "Running") -- Start the warehouse from scratch. + self:AddTransition("Loaded", "Start", "Running") -- Start the warehouse when loaded from disk. + + self:AddTransition("*", "Status", "*") -- Status update. + + self:AddTransition("*", "AddAsset", "*") -- Add asset to warehouse stock. + self:AddTransition("*", "NewAsset", "*") -- New asset was added to warehouse stock. + + self:AddTransition("*", "AddRequest", "*") -- New request from other warehouse. + self:AddTransition("Running", "Request", "*") -- Process a request. Only in running mode. + self:AddTransition("Running", "RequestSpawned", "*") -- Assets of request were spawned. + self:AddTransition("Attacked", "Request", "*") -- Process a request. Only in running mode. + + self:AddTransition("*", "Unloaded", "*") -- Cargo has been unloaded from the carrier (unused ==> unnecessary?). + self:AddTransition("*", "AssetSpawned", "*") -- Asset has been spawned into the world. + self:AddTransition("*", "AssetLowFuel", "*") -- Asset is low on fuel. + + self:AddTransition("*", "Arrived", "*") -- Cargo or transport group has arrived. + + self:AddTransition("*", "Delivered", "*") -- All cargo groups of a request have been delivered to the requesting warehouse. + self:AddTransition("Running", "SelfRequest", "*") -- Request to warehouse itself. Requested assets are only spawned but not delivered anywhere. + self:AddTransition("Attacked", "SelfRequest", "*") -- Request to warehouse itself. Also possible when warehouse is under attack! + self:AddTransition("Running", "Pause", "Paused") -- Pause the processing of new requests. Still possible to add assets and requests. + self:AddTransition("Paused", "Unpause", "Running") -- Unpause the warehouse. Queued requests are processed again. + self:AddTransition("*", "Stop", "Stopped") -- Stop the warehouse. + self:AddTransition("Stopped", "Restart", "Running") -- Restart the warehouse when it was stopped before. + self:AddTransition("Loaded", "Restart", "Running") -- Restart the warehouse when assets were loaded from file before. + self:AddTransition("*", "Save", "*") -- Save the warehouse state to disk. + self:AddTransition("*", "Attacked", "Attacked") -- Warehouse is under attack by enemy coalition. + self:AddTransition("Attacked", "Defeated", "Running") -- Attack by other coalition was defeated! + self:AddTransition("*", "ChangeCountry", "*") -- Change country (and coalition) of the warehouse. Warehouse is respawned! + self:AddTransition("Attacked", "Captured", "Running") -- Warehouse was captured by another coalition. It must have been attacked first. + self:AddTransition("*", "AirbaseCaptured", "*") -- Airbase was captured by other coalition. + self:AddTransition("*", "AirbaseRecaptured", "*") -- Airbase was re-captured from other coalition. + self:AddTransition("*", "RunwayDestroyed", "*") -- Runway of the airbase was destroyed. + self:AddTransition("*", "RunwayRepaired", "*") -- Runway of the airbase was repaired. + self:AddTransition("*", "AssetDead", "*") -- An asset group died. + self:AddTransition("*", "Destroyed", "Destroyed") -- Warehouse was destroyed. All assets in stock are gone and warehouse is stopped. + self:AddTransition("Destroyed", "Respawn", "Running") -- Respawn warehouse after it was destroyed. + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the warehouse. Initializes parameters and starts event handlers. + -- @function [parent=#WAREHOUSE] Start + -- @param #WAREHOUSE self + + --- Triggers the FSM event "Start" after a delay. Starts the warehouse. Initializes parameters and starts event handlers. + -- @function [parent=#WAREHOUSE] __Start + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the warehouse and all its event handlers. All waiting and pending queue items are deleted as well and all assets are removed from stock. + -- @function [parent=#WAREHOUSE] Stop + -- @param #WAREHOUSE self + + --- Triggers the FSM event "Stop" after a delay. Stops the warehouse and all its event handlers. All waiting and pending queue items are deleted as well and all assets are removed from stock. + -- @function [parent=#WAREHOUSE] __Stop + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Restart". Restarts the warehouse from stopped state by reactivating the event handlers *only*. + -- @function [parent=#WAREHOUSE] Restart + -- @param #WAREHOUSE self + + --- Triggers the FSM event "Restart" after a delay. Restarts the warehouse from stopped state by reactivating the event handlers *only*. + -- @function [parent=#WAREHOUSE] __Restart + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Respawn". + -- @function [parent=#WAREHOUSE] Respawn + -- @param #WAREHOUSE self + + --- Triggers the FSM event "Respawn" after a delay. + -- @function [parent=#WAREHOUSE] __Respawn + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + + --- On after "Respawn" event user function. + -- @function [parent=#WAREHOUSE] OnAfterRespawn + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- Triggers the FSM event "Pause". Pauses the warehouse. Assets can still be added and requests be made. However, requests are not processed. + -- @function [parent=#WAREHOUSE] Pause + -- @param #WAREHOUSE self + + --- Triggers the FSM event "Pause" after a delay. Pauses the warehouse. Assets can still be added and requests be made. However, requests are not processed. + -- @function [parent=#WAREHOUSE] __Pause + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Unpause". Unpauses the warehouse. Processing of queued requests is resumed. + -- @function [parent=#WAREHOUSE] UnPause + -- @param #WAREHOUSE self + + --- Triggers the FSM event "Unpause" after a delay. Unpauses the warehouse. Processing of queued requests is resumed. + -- @function [parent=#WAREHOUSE] __Unpause + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Status". Queue is updated and requests are executed. + -- @function [parent=#WAREHOUSE] Status + -- @param #WAREHOUSE self + + --- Triggers the FSM event "Status" after a delay. Queue is updated and requests are executed. + -- @function [parent=#WAREHOUSE] __Status + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + + + --- Trigger the FSM event "AddAsset". Add a group to the warehouse stock. + -- @function [parent=#WAREHOUSE] AddAsset + -- @param #WAREHOUSE self + -- @param Wrapper.Group#GROUP group Group to be added as new asset. + -- @param #number ngroups (Optional) Number of groups to add to the warehouse stock. Default is 1. + -- @param #WAREHOUSE.Attribute forceattribute (Optional) Explicitly force a generalized attribute for the asset. This has to be an @{#WAREHOUSE.Attribute}. + -- @param #number forcecargobay (Optional) Explicitly force cargobay weight limit in kg for cargo carriers. This is for each *unit* of the group. + -- @param #number forceweight (Optional) Explicitly force weight in kg of each unit in the group. + -- @param #number loadradius (Optional) The distance in meters when the cargo is loaded into the carrier. Default is the bounding box size of the carrier. + -- @param DCS#AI.Skill skill Skill of the asset. + -- @param #table liveries Table of livery names. When the asset is spawned one livery is chosen randomly. + -- @param #string assignment A free to choose string specifying an assignment for the asset. This can be used with the @{#WAREHOUSE.OnAfterNewAsset} function. + + --- Trigger the FSM event "AddAsset" with a delay. Add a group to the warehouse stock. + -- @function [parent=#WAREHOUSE] __AddAsset + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + -- @param Wrapper.Group#GROUP group Group to be added as new asset. + -- @param #number ngroups (Optional) Number of groups to add to the warehouse stock. Default is 1. + -- @param #WAREHOUSE.Attribute forceattribute (Optional) Explicitly force a generalized attribute for the asset. This has to be an @{#WAREHOUSE.Attribute}. + -- @param #number forcecargobay (Optional) Explicitly force cargobay weight limit in kg for cargo carriers. This is for each *unit* of the group. + -- @param #number forceweight (Optional) Explicitly force weight in kg of each unit in the group. + -- @param #number loadradius (Optional) The distance in meters when the cargo is loaded into the carrier. Default is the bounding box size of the carrier. + -- @param DCS#AI.Skill skill Skill of the asset. + -- @param #table liveries Table of livery names. When the asset is spawned one livery is chosen randomly. + -- @param #string assignment A free to choose string specifying an assignment for the asset. This can be used with the @{#WAREHOUSE.OnAfterNewAsset} function. + + + --- Triggers the FSM delayed event "NewAsset" when a new asset has been added to the warehouse stock. + -- @function [parent=#WAREHOUSE] NewAsset + -- @param #WAREHOUSE self + -- @param #WAREHOUSE.Assetitem asset The new asset. + -- @param #string assignment (Optional) Assignment text for the asset. + + --- Triggers the FSM delayed event "NewAsset" when a new asset has been added to the warehouse stock. + -- @function [parent=#WAREHOUSE] __NewAsset + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + -- @param #WAREHOUSE.Assetitem asset The new asset. + -- @param #string assignment (Optional) Assignment text for the asset. + + --- On after "NewAsset" event user function. A new asset has been added to the warehouse stock. + -- @function [parent=#WAREHOUSE] OnAfterNewAsset + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #WAREHOUSE.Assetitem asset The asset that has just been added. + -- @param #string assignment (Optional) Assignment text for the asset. + + + --- Triggers the FSM event "AddRequest". Add a request to the warehouse queue, which is processed when possible. + -- @function [parent=#WAREHOUSE] AddRequest + -- @param #WAREHOUSE self + -- @param #WAREHOUSE warehouse The warehouse requesting supply. + -- @param #WAREHOUSE.Descriptor AssetDescriptor Descriptor describing the asset that is requested. + -- @param AssetDescriptorValue Value of the asset descriptor. Type depends on descriptor, i.e. could be a string, etc. + -- @param #number nAsset Number of groups requested that match the asset specification. + -- @param #WAREHOUSE.TransportType TransportType Type of transport. + -- @param #number nTransport Number of transport units requested. + -- @param #number Prio Priority of the request. Number ranging from 1=high to 100=low. + -- @param #string Assignment A keyword or text that later be used to identify this request and postprocess the assets. + + --- Triggers the FSM event "AddRequest" with a delay. Add a request to the warehouse queue, which is processed when possible. + -- @function [parent=#WAREHOUSE] __AddRequest + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + -- @param #WAREHOUSE warehouse The warehouse requesting supply. + -- @param #WAREHOUSE.Descriptor AssetDescriptor Descriptor describing the asset that is requested. + -- @param AssetDescriptorValue Value of the asset descriptor. Type depends on descriptor, i.e. could be a string, etc. + -- @param #number nAsset Number of groups requested that match the asset specification. + -- @param #WAREHOUSE.TransportType TransportType Type of transport. + -- @param #number nTransport Number of transport units requested. + -- @param #number Prio Priority of the request. Number ranging from 1=high to 100=low. + -- @param #string Assignment A keyword or text that later be used to identify this request and postprocess the assets. + + + --- Triggers the FSM event "Request". Executes a request from the queue if possible. + -- @function [parent=#WAREHOUSE] Request + -- @param #WAREHOUSE self + -- @param #WAREHOUSE.Queueitem Request Information table of the request. + + --- Triggers the FSM event "Request" after a delay. Executes a request from the queue if possible. + -- @function [parent=#WAREHOUSE] __Request + -- @param #WAREHOUSE self + -- @param #number Delay Delay in seconds. + -- @param #WAREHOUSE.Queueitem Request Information table of the request. + + --- On before "Request" user function. The necessary cargo and transport assets will be spawned. Time to set some additional asset parameters. + -- @function [parent=#WAREHOUSE] OnBeforeRequest + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #WAREHOUSE.Queueitem Request Information table of the request. + + --- On after "Request" user function. The necessary cargo and transport assets were spawned. + -- @function [parent=#WAREHOUSE] OnAfterRequest + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #WAREHOUSE.Queueitem Request Information table of the request. + + + --- Triggers the FSM event "Arrived" when a group has arrived at the destination warehouse. + -- This function should always be called from the sending and not the receiving warehouse. + -- If the group is a cargo asset, it is added to the receiving warehouse. If the group is a transporter it + -- is added to the sending warehouse since carriers are supposed to return to their home warehouse once + -- all cargo was delivered. + -- @function [parent=#WAREHOUSE] Arrived + -- @param #WAREHOUSE self + -- @param Wrapper.Group#GROUP group Group that has arrived. + + --- Triggers the FSM event "Arrived" after a delay when a group has arrived at the destination. + -- This function should always be called from the sending and not the receiving warehouse. + -- If the group is a cargo asset, it is added to the receiving warehouse. If the group is a transporter it + -- is added to the sending warehouse since carriers are supposed to return to their home warehouse once + -- @function [parent=#WAREHOUSE] __Arrived + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + -- @param Wrapper.Group#GROUP group Group that has arrived. + + --- On after "Arrived" event user function. Called when a group has arrived at its destination. + -- @function [parent=#WAREHOUSE] OnAfterArrived + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Group#GROUP group Group that has arrived. + + + --- Triggers the FSM event "Delivered". All (cargo) assets of a request have been delivered to the receiving warehouse. + -- @function [parent=#WAREHOUSE] Delivered + -- @param #WAREHOUSE self + -- @param #WAREHOUSE.Pendingitem request Pending request that was now delivered. + + --- Triggers the FSM event "Delivered" after a delay. A group has been delivered from the warehouse to another warehouse. + -- @function [parent=#WAREHOUSE] __Delivered + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + -- @param #WAREHOUSE.Pendingitem request Pending request that was now delivered. + + --- On after "Delivered" event user function. Called when a group has been delivered from the warehouse to another warehouse. + -- @function [parent=#WAREHOUSE] OnAfterDelivered + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #WAREHOUSE.Pendingitem request Pending request that was now delivered. + + + --- Triggers the FSM event "SelfRequest". Request was initiated from the warehouse to itself. Groups are just spawned at the warehouse or the associated airbase. + -- If the warehouse is currently under attack when the self request is made, the self request is added to the defending table. One the attack is defeated, + -- this request is used to put the groups back into the warehouse stock. + -- @function [parent=#WAREHOUSE] SelfRequest + -- @param #WAREHOUSE self + -- @param Core.Set#SET_GROUP groupset The set of cargo groups that was delivered to the warehouse itself. + -- @param #WAREHOUSE.Pendingitem request Pending self request. + + --- Triggers the FSM event "SelfRequest" with a delay. Request was initiated from the warehouse to itself. Groups are just spawned at the warehouse or the associated airbase. + -- If the warehouse is currently under attack when the self request is made, the self request is added to the defending table. One the attack is defeated, + -- this request is used to put the groups back into the warehouse stock. + -- @function [parent=#WAREHOUSE] __SelfRequest + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + -- @param Core.Set#SET_GROUP groupset The set of cargo groups that was delivered to the warehouse itself. + -- @param #WAREHOUSE.Pendingitem request Pending self request. + + --- On after "SelfRequest" event. Request was initiated from the warehouse to itself. Groups are simply spawned at the warehouse or the associated airbase. + -- All requested assets are passed as a @{Core.Set#SET_GROUP} and can be used for further tasks or in other MOOSE classes. + -- Note that airborne assets are spawned in uncontrolled state so they do not simply "fly away" after spawning. + -- + -- @usage + -- --- Self request event. Triggered once the assets are spawned in the spawn zone or at the airbase. + -- function mywarehouse:OnAfterSelfRequest(From, Event, To, groupset, request) + -- local groupset=groupset --Core.Set#SET_GROUP + -- + -- -- Loop over all groups spawned from that request. + -- for _,group in pairs(groupset:GetSetObjects()) do + -- local group=group --Wrapper.Group#GROUP + -- + -- -- Gree smoke on spawned group. + -- group:SmokeGreen() + -- + -- -- Activate uncontrolled airborne group if necessary. + -- group:StartUncontrolled() + -- end + -- end + -- + -- @function [parent=#WAREHOUSE] OnAfterSelfRequest + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Set#SET_GROUP groupset The set of (cargo) groups that was delivered to the warehouse itself. + -- @param #WAREHOUSE.Pendingitem request Pending self request. + + + --- Triggers the FSM event "Attacked" when a warehouse is under attack by an another coalition. + -- @function [parent=#WAREHOUSE] Attacked + -- @param #WAREHOUSE self + -- @param DCS#coalition.side Coalition Coalition side which is attacking the warehouse, i.e. a number of @{DCS#coalition.side} enumerator. + -- @param DCS#country.id Country Country ID, which is attacking the warehouse, i.e. a number @{DCS#country.id} enumerator. + + --- Triggers the FSM event "Attacked" with a delay when a warehouse is under attack by an another coalition. + -- @function [parent=#WAREHOUSE] __Attacked + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + -- @param DCS#coalition.side Coalition Coalition side which is attacking the warehouse, i.e. a number of @{DCS#coalition.side} enumerator. + -- @param DCS#country.id Country Country ID, which is attacking the warehouse, i.e. a number @{DCS#country.id} enumerator. + + --- On after "Attacked" event user function. Called when a warehouse (zone) is under attack by an enemy. + -- @function [parent=#WAREHOUSE] OnAfterAttacked + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param DCS#coalition.side Coalition Coalition side which is attacking the warehouse, i.e. a number of @{DCS#coalition.side} enumerator. + -- @param DCS#country.id Country Country ID, which is attacking the warehouse, i.e. a number @{DCS#country.id} enumerator. + + + --- Triggers the FSM event "Defeated" when an attack from an enemy was defeated. + -- @function [parent=#WAREHOUSE] Defeated + -- @param #WAREHOUSE self + + --- Triggers the FSM event "Defeated" with a delay when an attack from an enemy was defeated. + -- @function [parent=#WAREHOUSE] __Defeated + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + + --- On after "Defeated" event user function. Called when an enemy attack was defeated. + -- @function [parent=#WAREHOUSE] OnAfterDefeate + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "ChangeCountry" so the warehouse is respawned with the new country. + -- @function [parent=#WAREHOUSE] ChangeCountry + -- @param #WAREHOUSE self + -- @param DCS#country.id Country New country id of the warehouse. + + --- Triggers the FSM event "ChangeCountry" after a delay so the warehouse is respawned with the new country. + -- @function [parent=#WAREHOUSE] __ChangeCountry + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + -- @param DCS#country.id Country Country id which has captured the warehouse. + + --- On after "ChangeCountry" event user function. Called when the warehouse has changed its country. + -- @function [parent=#WAREHOUSE] OnAfterChangeCountry + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param DCS#country.id Country New country id of the warehouse, i.e. a number @{DCS#country.id} enumerator. + + + --- Triggers the FSM event "Captured" when a warehouse has been captured by another coalition. + -- @function [parent=#WAREHOUSE] Captured + -- @param #WAREHOUSE self + -- @param DCS#coalition.side Coalition Coalition side which captured the warehouse. + -- @param DCS#country.id Country Country id which has captured the warehouse. + + --- Triggers the FSM event "Captured" with a delay when a warehouse has been captured by another coalition. + -- @function [parent=#WAREHOUSE] __Captured + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + -- @param DCS#coalition.side Coalition Coalition side which captured the warehouse. + -- @param DCS#country.id Country Country id which has captured the warehouse. + + --- On after "Captured" event user function. Called when the warehouse has been captured by an enemy coalition. + -- @function [parent=#WAREHOUSE] OnAfterCaptured + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param DCS#coalition.side Coalition Coalition side which captured the warehouse, i.e. a number of @{DCS#coalition.side} enumerator. + -- @param DCS#country.id Country Country id which has captured the warehouse, i.e. a number @{DCS#country.id} enumerator. + -- + + --- Triggers the FSM event "AirbaseCaptured" when the airbase of the warehouse has been captured by another coalition. + -- @function [parent=#WAREHOUSE] AirbaseCaptured + -- @param #WAREHOUSE self + -- @param DCS#coalition.side Coalition Coalition side which captured the airbase, i.e. a number of @{DCS#coalition.side} enumerator. + + --- Triggers the FSM event "AirbaseCaptured" with a delay when the airbase of the warehouse has been captured by another coalition. + -- @function [parent=#WAREHOUSE] __AirbaseCaptured + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + -- @param DCS#coalition.side Coalition Coalition side which captured the airbase, i.e. a number of @{DCS#coalition.side} enumerator. + + --- On after "AirbaseCaptured" even user function. Called when the airbase of the warehouse has been captured by another coalition. + -- @function [parent=#WAREHOUSE] OnAfterAirbaseCaptured + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param DCS#coalition.side Coalition Coalition side which captured the airbase, i.e. a number of @{DCS#coalition.side} enumerator. + + + --- Triggers the FSM event "AirbaseRecaptured" when the airbase of the warehouse has been re-captured from the other coalition. + -- @param #WAREHOUSE self + -- @function [parent=#WAREHOUSE] AirbaseRecaptured + -- @param DCS#coalition.side Coalition Coalition which re-captured the airbase, i.e. the same as the current warehouse owner coalition. + + --- Triggers the FSM event "AirbaseRecaptured" with a delay when the airbase of the warehouse has been re-captured from the other coalition. + -- @function [parent=#WAREHOUSE] __AirbaseRecaptured + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + -- @param DCS#coalition.side Coalition Coalition which re-captured the airbase, i.e. the same as the current warehouse owner coalition. + + --- On after "AirbaseRecaptured" event user function. Called when the airbase of the warehouse has been re-captured from the other coalition. + -- @function [parent=#WAREHOUSE] OnAfterAirbaseRecaptured + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param DCS#coalition.side Coalition Coalition which re-captured the airbase, i.e. the same as the current warehouse owner coalition. + + + --- Triggers the FSM event "AssetDead" when an asset group has died. + -- @function [parent=#WAREHOUSE] AssetDead + -- @param #WAREHOUSE self + -- @param #WAREHOUSE.Assetitem asset The asset that is dead. + -- @param #WAREHOUSE.Pendingitem request The request of the dead asset. + + --- Triggers the delayed FSM event "AssetDead" when an asset group has died. + -- @function [parent=#WAREHOUSE] __AssetDead + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + -- @param #WAREHOUSE.Assetitem asset The asset that is dead. + -- @param #WAREHOUSE.Pendingitem request The request of the dead asset. + + --- On after "AssetDead" event user function. Called when an asset group died. + -- @function [parent=#WAREHOUSE] OnAfterAssetDead + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #WAREHOUSE.Assetitem asset The asset that is dead. + -- @param #WAREHOUSE.Pendingitem request The request of the dead asset. + + + --- Triggers the FSM event "Destroyed" when the warehouse was destroyed. Services are stopped. + -- @function [parent=#WAREHOUSE] Destroyed + -- @param #WAREHOUSE self + + --- Triggers the FSM event "Destroyed" with a delay when the warehouse was destroyed. Services are stopped. + -- @function [parent=#WAREHOUSE] __Destroyed + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + + --- On after "Destroyed" event user function. Called when the warehouse was destroyed. Services are stopped. + -- @function [parent=#WAREHOUSE] OnAfterDestroyed + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "AssetSpawned" when the warehouse has spawned an asset. + -- @function [parent=#WAREHOUSE] AssetSpawned + -- @param #WAREHOUSE self + -- @param Wrapper.Group#GROUP group the group that was spawned. + -- @param #WAREHOUSE.Assetitem asset The asset that was spawned. + -- @param #WAREHOUSE.Pendingitem request The request of the spawned asset. + + --- Triggers the FSM event "AssetSpawned" with a delay when the warehouse has spawned an asset. + -- @function [parent=#WAREHOUSE] __AssetSpawned + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + -- @param Wrapper.Group#GROUP group the group that was spawned. + -- @param #WAREHOUSE.Assetitem asset The asset that was spawned. + -- @param #WAREHOUSE.Pendingitem request The request of the spawned asset. + + --- On after "AssetSpawned" event user function. Called when the warehouse has spawned an asset. + -- @function [parent=#WAREHOUSE] OnAfterAssetSpawned + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Group#GROUP group the group that was spawned. + -- @param #WAREHOUSE.Assetitem asset The asset that was spawned. + -- @param #WAREHOUSE.Pendingitem request The request of the spawned asset. + + + --- Triggers the FSM event "AssetLowFuel" when an asset runs low on fuel + -- @function [parent=#WAREHOUSE] AssetLowFuel + -- @param #WAREHOUSE self + -- @param #WAREHOUSE.Assetitem asset The asset that is low on fuel. + -- @param #WAREHOUSE.Pendingitem request The request of the asset that is low on fuel. + + --- Triggers the FSM event "AssetLowFuel" with a delay when an asset runs low on fuel. + -- @function [parent=#WAREHOUSE] __AssetLowFuel + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + -- @param #WAREHOUSE.Assetitem asset The asset that is low on fuel. + -- @param #WAREHOUSE.Pendingitem request The request of the asset that is low on fuel. + + --- On after "AssetLowFuel" event user function. Called when the an asset is low on fuel. + -- @function [parent=#WAREHOUSE] OnAfterAssetLowFuel + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #WAREHOUSE.Assetitem asset The asset that is low on fuel. + -- @param #WAREHOUSE.Pendingitem request The request of the asset that is low on fuel. + + + --- Triggers the FSM event "Save" when the warehouse assets are saved to file on disk. + -- @function [parent=#WAREHOUSE] Save + -- @param #WAREHOUSE self + -- @param #string path Path where the file is saved. Default is the DCS installation root directory. + -- @param #string filename (Optional) File name. Default is WAREHOUSE-_.txt. + + --- Triggers the FSM event "Save" with a delay when the warehouse assets are saved to a file. + -- @function [parent=#WAREHOUSE] __Save + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + -- @param #string path Path where the file is saved. Default is the DCS installation root directory. + -- @param #string filename (Optional) File name. Default is WAREHOUSE-_.txt. + + --- On after "Save" event user function. Called when the warehouse assets are saved to disk. + -- @function [parent=#WAREHOUSE] OnAfterSave + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string path Path where the file is saved. Default is the DCS installation root directory. + -- @param #string filename (Optional) File name. Default is WAREHOUSE-_.txt. + + + --- Triggers the FSM event "Load" when the warehouse is loaded from a file on disk. + -- @function [parent=#WAREHOUSE] Load + -- @param #WAREHOUSE self + -- @param #string path Path where the file is located. Default is the DCS installation root directory. + -- @param #string filename (Optional) File name. Default is WAREHOUSE-_.txt. + + --- Triggers the FSM event "Load" with a delay when the warehouse assets are loaded from disk. + -- @function [parent=#WAREHOUSE] __Load + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + -- @param #string path Path where the file is located. Default is the DCS installation root directory. + -- @param #string filename (Optional) File name. Default is WAREHOUSE-_.txt. + + --- On after "Load" event user function. Called when the warehouse assets are loaded from disk. + -- @function [parent=#WAREHOUSE] OnAfterLoad + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string path Path where the file is located. Default is the DCS installation root directory. + -- @param #string filename (Optional) File name. Default is WAREHOUSE-_.txt. + + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set debug mode on. Error messages will be displayed on screen, units will be smoked at some events. +-- @param #WAREHOUSE self +-- @return #WAREHOUSE self +function WAREHOUSE:SetDebugOn() + self.Debug=true + return self +end + +--- Set debug mode off. This is the default +-- @param #WAREHOUSE self +-- @return #WAREHOUSE self +function WAREHOUSE:SetDebugOff() + self.Debug=false + return self +end + +--- Set report on. Messages at events will be displayed on screen to the coalition owning the warehouse. +-- @param #WAREHOUSE self +-- @return #WAREHOUSE self +function WAREHOUSE:SetReportOn() + self.Report=true + return self +end + +--- Set report off. Warehouse does not report about its status and at certain events. +-- @param #WAREHOUSE self +-- @return #WAREHOUSE self +function WAREHOUSE:SetReportOff() + self.Report=false + return self +end + +--- Enable safe parking option, i.e. parking spots at an airbase will be considered as occupied when a client aircraft is parked there (even if the client slot is not taken by a player yet). +-- Note that also incoming aircraft can reserve/occupie parking spaces. +-- @param #WAREHOUSE self +-- @return #WAREHOUSE self +function WAREHOUSE:SetSafeParkingOn() + self.safeparking=true + return self +end + +--- Disable safe parking option. Note that is the default setting. +-- @param #WAREHOUSE self +-- @return #WAREHOUSE self +function WAREHOUSE:SetSafeParkingOff() + self.safeparking=false + return self +end + +--- Set low fuel threshold. If one unit of an asset has less fuel than this number, the event AssetLowFuel will be fired. +-- @param #WAREHOUSE self +-- @param #number threshold Relative low fuel threshold, i.e. a number in [0,1]. Default 0.15 (15%). +-- @return #WAREHOUSE self +function WAREHOUSE:SetLowFuelThreshold(threshold) + self.lowfuelthresh=threshold or 0.15 + return self +end + +--- Set interval of status updates. Note that normally only one request can be processed per time interval. +-- @param #WAREHOUSE self +-- @param #number timeinterval Time interval in seconds. +-- @return #WAREHOUSE self +function WAREHOUSE:SetStatusUpdate(timeinterval) + self.dTstatus=timeinterval + return self +end + +--- Set verbosity level. +-- @param #WAREHOUSE self +-- @param #number VerbosityLevel Level of output (higher=more). Default 0. +-- @return #WAREHOUSE self +function WAREHOUSE:SetVerbosityLevel(VerbosityLevel) + self.verbosity=VerbosityLevel or 0 + return self +end + +--- Set a zone where the (ground) assets of the warehouse are spawned once requested. +-- @param #WAREHOUSE self +-- @param Core.Zone#ZONE zone The spawn zone. +-- @param #number maxdist (Optional) Maximum distance in meters between spawn zone and warehouse. Units are not spawned if distance is larger. Default is 5000 m. +-- @return #WAREHOUSE self +function WAREHOUSE:SetSpawnZone(zone, maxdist) + self.spawnzone=zone + self.spawnzonemaxdist=maxdist or 5000 + return self +end + + +--- Set a warehouse zone. If this zone is captured, the warehouse and all its assets fall into the hands of the enemy. +-- @param #WAREHOUSE self +-- @param Core.Zone#ZONE zone The warehouse zone. Note that this **cannot** be a polygon zone! +-- @return #WAREHOUSE self +function WAREHOUSE:SetWarehouseZone(zone) + self.zone=zone + return self +end + +--- Set auto defence on. When the warehouse is under attack, all ground assets are spawned automatically and will defend the warehouse zone. +-- @param #WAREHOUSE self +-- @return #WAREHOUSE self +function WAREHOUSE:SetAutoDefenceOn() + self.autodefence=true + return self +end + +--- Set auto defence off. This is the default. +-- @param #WAREHOUSE self +-- @return #WAREHOUSE self +function WAREHOUSE:SetAutoDefenceOff() + self.autodefence=false + return self +end + +--- Set valid parking spot IDs. +-- @param #WAREHOUSE self +-- @param #table ParkingIDs Table of numbers. +-- @return #WAREHOUSE self +function WAREHOUSE:SetParkingIDs(ParkingIDs) + if type(ParkingIDs)~="table" then + ParkingIDs={ParkingIDs} + end + self.parkingIDs=ParkingIDs + return self +end + +--- Check parking ID. +-- @param #WAREHOUSE self +-- @param Wrapper.Airbase#AIRBASE.ParkingSpot spot Parking spot. +-- @param Wrapper.Airbase#AIRBASE airbase The airbase. +-- @return #boolean If true, parking is valid. +function WAREHOUSE:_CheckParkingValid(spot, airbase) + + if self.parkingIDs==nil then + return true + end + + for _,id in pairs(self.parkingIDs or {}) do + if spot.TerminalID==id then + return true + end + end + + return false +end + + +--- Enable auto save of warehouse assets at mission end event. +-- @param #WAREHOUSE self +-- @param #string path Path where to save the asset data file. +-- @param #string filename File name. Default is generated automatically from warehouse id. +-- @return #WAREHOUSE self +function WAREHOUSE:SetSaveOnMissionEnd(path, filename) + self.autosave=true + self.autosavepath=path + self.autosavefile=filename + return self +end + +--- Show or don't show markers on the F10 map displaying the Warehouse stock and road/rail connections. +-- @param #WAREHOUSE self +-- @param #boolean switch If true (or nil), markers are on. If false, markers are not displayed. +-- @return #WAREHOUSE self +function WAREHOUSE:SetMarker(switch) + if switch==false then + self.markerOn=false + else + self.markerOn=true + end + return self +end + +--- Set respawn after destroy. +-- @param #WAREHOUSE self +-- @return #WAREHOUSE self +function WAREHOUSE:SetRespawnAfterDestroyed(delay) + self.respawnafterdestroyed=true + self.respawndelay=delay + return self +end + + +--- Set the airbase belonging to this warehouse. +-- Note that it has to be of the same coalition as the warehouse. +-- Also, be reasonable and do not put it too far from the phyiscal warehouse structure because you troops might have a long way to get to their transports. +-- @param #WAREHOUSE self +-- @param Wrapper.Airbase#AIRBASE airbase The airbase object associated to this warehouse. +-- @return #WAREHOUSE self +function WAREHOUSE:SetAirbase(airbase) + self.airbase=airbase + if airbase~=nil then + self.airbasename=airbase:GetName() + else + self.airbasename=nil + end + return self +end + +--- Set the connection of the warehouse to the road. +-- Ground assets spawned in the warehouse spawn zone will first go to this point and from there travel on road to the requesting warehouse. +-- Note that by default the road connection is set to the closest point on road from the center of the spawn zone if it is withing 3000 meters. +-- Also note, that if the parameter "coordinate" is passed as nil, any road connection is disabled and ground assets cannot travel of be transportet on the ground. +-- @param #WAREHOUSE self +-- @param Core.Point#COORDINATE coordinate The road connection. Technically, the closest point on road from this coordinate is determined by DCS API function. So this point must not be exactly on the road. +-- @return #WAREHOUSE self +function WAREHOUSE:SetRoadConnection(coordinate) + if coordinate then + self.road=coordinate:GetClosestPointToRoad() + else + self.road=false + end + return self +end + +--- Set the connection of the warehouse to the railroad. +-- This is the place where train assets or transports will be spawned. +-- @param #WAREHOUSE self +-- @param Core.Point#COORDINATE coordinate The railroad connection. Technically, the closest point on rails from this coordinate is determined by DCS API function. So this point must not be exactly on the a railroad connection. +-- @return #WAREHOUSE self +function WAREHOUSE:SetRailConnection(coordinate) + if coordinate then + self.rail=coordinate:GetClosestPointToRoad(true) + else + self.rail=false + end + return self +end + +--- Set the port zone for this warehouse. +-- The port zone is the zone, where all naval assets of the warehouse are spawned. +-- @param #WAREHOUSE self +-- @param Core.Zone#ZONE zone The zone defining the naval port of the warehouse. +-- @return #WAREHOUSE self +function WAREHOUSE:SetPortZone(zone) + self.portzone=zone + return self +end + +--- Add a Harbor Zone for this warehouse where naval cargo units will spawn and be received. +-- Both warehouses must have the harbor zone defined for units to properly spawn on both the +-- sending and receiving side. The harbor zone should be within 3km of the port zone used for +-- warehouse in order to facilitate the boarding process. +-- @param #WAREHOUSE self +-- @param Core.Zone#ZONE zone The zone defining the naval embarcation/debarcation point for cargo units +-- @return #WAREHOUSE self +function WAREHOUSE:SetHarborZone(zone) + self.harborzone=zone + return self +end + +--- Add a shipping lane from this warehouse to another remote warehouse. +-- Note that both warehouses must have a port zone defined before a shipping lane can be added! +-- Shipping lane is taken from the waypoints of a (late activated) template group. So set up a group, e.g. a ship or a helicopter, and place its +-- waypoints along the shipping lane you want to add. +-- @param #WAREHOUSE self +-- @param #WAREHOUSE remotewarehouse The remote warehouse to where the shipping lane is added +-- @param Wrapper.Group#GROUP group Waypoints of this group will define the shipping lane between to warehouses. +-- @param #boolean oneway (Optional) If true, the lane can only be used from this warehouse to the other but not other way around. Default false. +-- @return #WAREHOUSE self +function WAREHOUSE:AddShippingLane(remotewarehouse, group, oneway) + + -- Check that port zones are defined. + if self.portzone==nil or remotewarehouse.portzone==nil then + local text=string.format("ERROR: Sending or receiving warehouse does not have a port zone defined. Adding shipping lane not possible!") + self:_ErrorMessage(text, 5) + return self + end + + -- Initial and final coordinates are random points within the port zones. + local startcoord=self.portzone:GetRandomCoordinate() + local finalcoord=remotewarehouse.portzone:GetRandomCoordinate() + + -- Create new lane from waypoints of the template group. + local lane=self:_NewLane(group, startcoord, finalcoord) + + -- Debug info. Marks along shipping lane. + if self.Debug then + for i=1,#lane do + local coord=lane[i] --Core.Point#COORDINATE + local text=string.format("Shipping lane %s to %s. Point %d.", self.alias, remotewarehouse.alias, i) + coord:MarkToCoalition(text, self:GetCoalition()) + end + end + + -- Name of the remote warehouse. + local remotename=remotewarehouse.warehouse:GetName() + + -- Create new table if no shipping lane exists yet. + if self.shippinglanes[remotename]==nil then + self.shippinglanes[remotename]={} + end + + -- Add shipping lane. + table.insert(self.shippinglanes[remotename], lane) + + -- Add shipping lane in the opposite direction. + if not oneway then + remotewarehouse:AddShippingLane(self, group, true) + end + + return self +end + + +--- Add an off-road path from this warehouse to another and back. +-- The start and end points are automatically set to one random point in the respective spawn zones of the two warehouses. +-- By default, the reverse path is also added as path from the remote warehouse to this warehouse. +-- @param #WAREHOUSE self +-- @param #WAREHOUSE remotewarehouse The remote warehouse to which the path leads. +-- @param Wrapper.Group#GROUP group Waypoints of this group will define the path between to warehouses. +-- @param #boolean oneway (Optional) If true, the path can only be used from this warehouse to the other but not other way around. Default false. +-- @return #WAREHOUSE self +function WAREHOUSE:AddOffRoadPath(remotewarehouse, group, oneway) + + -- Initial and final points are random points within the spawn zone. + local startcoord=self.spawnzone:GetRandomCoordinate() + local finalcoord=remotewarehouse.spawnzone:GetRandomCoordinate() + + -- Create new path from template group waypoints. + local path=self:_NewLane(group, startcoord, finalcoord) + + if path==nil then + self:E(self.lid.."ERROR: Offroad path could not be added. Group present in ME?") + return + end + + -- Debug info. Marks along path. + if path and self.Debug then + for i=1,#path do + local coord=path[i] --Core.Point#COORDINATE + local text=string.format("Off road path from %s to %s. Point %d.", self.alias, remotewarehouse.alias, i) + coord:MarkToCoalition(text, self:GetCoalition()) + end + end + + -- Name of the remote warehouse. + local remotename=remotewarehouse.warehouse:GetName() + + -- Create new table if no shipping lane exists yet. + if self.offroadpaths[remotename]==nil then + self.offroadpaths[remotename]={} + end + + -- Add off road path. + table.insert(self.offroadpaths[remotename], path) + + -- Add off road path in the opposite direction (if not forbidden). + if not oneway then + remotewarehouse:AddOffRoadPath(self, group, true) + end + + return self +end + +--- Create a new path from a template group. +-- @param #WAREHOUSE self +-- @param Wrapper.Group#GROUP group Group used for extracting the waypoints. +-- @param Core.Point#COORDINATE startcoord First coordinate. +-- @param Core.Point#COORDINATE finalcoord Final coordinate. +-- @return #table Table with route points. +function WAREHOUSE:_NewLane(group, startcoord, finalcoord) + + local lane=nil + + if group then + + -- Get route from template. + local lanepoints=group:GetTemplateRoutePoints() + + -- First and last waypoints + local laneF=lanepoints[1] + local laneL=lanepoints[#lanepoints] + + -- Get corresponding coordinates. + local coordF=COORDINATE:New(laneF.x, 0, laneF.y) + local coordL=COORDINATE:New(laneL.x, 0, laneL.y) + + -- Figure out which point is closer to the port of this warehouse. + local distF=startcoord:Get2DDistance(coordF) + local distL=startcoord:Get2DDistance(coordL) + + -- Add the lane. Need to take care of the wrong "direction". + lane={} + if distF0 then + + -- Check if coalition is right. + local samecoalition=anycoalition or Coalition==warehouse:GetCoalition() + + -- Check that warehouse is in service. + if samecoalition and not (warehouse:IsNotReadyYet() or warehouse:IsStopped() or warehouse:IsDestroyed()) then + + -- Get number of assets. Whole stock is returned if no descriptor/value is given. + local nassets=warehouse:GetNumberOfAssets(Descriptor, DescriptorValue) + + --env.info(string.format("FF warehouse %s nassets = %d for %s=%s", warehouse.alias, nassets, tostring(Descriptor), tostring(DescriptorValue))) + + -- Assume we have enough. + local enough=true + -- If specifc assets need to be present... + if Descriptor and DescriptorValue then + -- Check that enough assets (default 1) are available. + enough = nassets>=MinAssets + end + + -- Check distance. + if enough and (distmin==nil or dist=1 then + + local FSMstate=self:GetState() + + local coalition=self:GetCoalitionName() + local country=self:GetCountryName() + + -- Info. + self:I(self.lid..string.format("State=%s %s [%s]: Assets=%d, Requests: waiting=%d, pending=%d", FSMstate, country, coalition, #self.stock, #self.queue, #self.pending)) + end + + -- Check if any pending jobs are done and can be deleted from the queue. + self:_JobDone() + + -- Print status. + self:_DisplayStatus() + + -- Check if warehouse is being attacked or has even been captured. + self:_CheckConquered() + + if self:IsRunwayOperational()==false then + local Trepair=self:GetRunwayRepairtime() + self:I(self.lid..string.format("Runway destroyed! Will be repaired in %d sec", Trepair)) + if Trepair==0 then + self:RunwayRepaired() + end + end + + -- Check if requests are valid and remove invalid one. + self:_CheckRequestConsistancy(self.queue) + + -- If warehouse is running than requests can be processed. + if self:IsRunning() or self:IsAttacked() then + + -- Check queue and handle requests if possible. + local request=self:_CheckQueue() + + -- Execute the request. If the request is really executed, it is also deleted from the queue. + if request then + self:Request(request) + end + + end + + -- Print queue after processing requests. + self:_PrintQueue(self.queue, "Queue waiting") + self:_PrintQueue(self.pending, "Queue pending") + + -- Check fuel for all assets. + self:_CheckFuel() + + -- Update warhouse marker on F10 map. + self:_UpdateWarehouseMarkText() + + -- Display complete list of stock itmes. + if self.Debug then + self:_DisplayStockItems(self.stock) + end + + -- Call status again in ~30 sec (user choice). + self:__Status(-self.dTstatus) +end + + +--- Function that checks if a pending job is done and can be removed from queue. +-- @param #WAREHOUSE self +function WAREHOUSE:_JobDone() + + -- For jobs that are done, i.e. all cargo and transport assets are delivered, home or dead! + local done={} + + -- Loop over all pending requests of this warehouse. + for _,request in pairs(self.pending) do + local request=request --#WAREHOUSE.Pendingitem + + if request.born then + + -- Count number of cargo groups. + local ncargo=0 + if request.cargogroupset then + ncargo=request.cargogroupset:Count() + end + + -- Count number of transport groups (if any). + local ntransport=0 + if request.transportgroupset then + ntransport=request.transportgroupset:Count() + end + + local ncargotot=request.nasset + local ncargodelivered=request.ndelivered + + -- Dead cargo: Ndead=Ntot-Ndeliverd-Nalive, + local ncargodead=ncargotot-ncargodelivered-ncargo + + + local ntransporttot=request.ntransport + local ntransporthome=request.ntransporthome + + -- Dead transport: Ndead=Ntot-Nhome-Nalive. + local ntransportdead=ntransporttot-ntransporthome-ntransport + + local text=string.format("Request id=%d: Cargo: Ntot=%d, Nalive=%d, Ndelivered=%d, Ndead=%d | Transport: Ntot=%d, Nalive=%d, Nhome=%d, Ndead=%d", + request.uid, ncargotot, ncargo, ncargodelivered, ncargodead, ntransporttot, ntransport, ntransporthome, ntransportdead) + self:T(self.lid..text) + + + -- Handle different cases depending on what asset are still around. + if ncargo==0 then + --------------------- + -- Cargo delivered -- + --------------------- + + -- Trigger delivered event. + if not self.delivered[request.uid] then + self:Delivered(request) + end + + -- Check if transports are back home? + if ntransport==0 then + --------------- + -- Job done! -- + --------------- + + -- Info on job. + if self.verbosity>=1 then + local text=string.format("Warehouse %s: Job on request id=%d for warehouse %s done!\n", self.alias, request.uid, request.warehouse.alias) + text=text..string.format("- %d of %d assets delivered. Casualties %d.", ncargodelivered, ncargotot, ncargodead) + if request.ntransport>0 then + text=text..string.format("\n- %d of %d transports returned home. Casualties %d.", ntransporthome, ntransporttot, ntransportdead) + end + self:_InfoMessage(text, 20) + end + + -- Mark request for deletion. + table.insert(done, request) + + else + ----------------------------------- + -- No cargo but still transports -- + ----------------------------------- + + -- This is difficult! How do I know if transports were unused? They could also be just on their way back home. + -- ==> Need to do a lot of checks. + + -- All transports are dead but there is still cargo left ==> Put cargo back into stock. + for _,_group in pairs(request.transportgroupset:GetSetObjects()) do + local group=_group --Wrapper.Group#GROUP + + -- Check if group is alive. + if group and group:IsAlive() then + + -- Check if group is in the spawn zone? + local category=group:GetCategory() + + -- Get current speed. + local speed=group:GetVelocityKMH() + local notmoving=speed<1 + + -- Closest airbase. + local airbase=group:GetCoordinate():GetClosestAirbase():GetName() + local athomebase=self.airbase and self.airbase:GetName()==airbase + + -- On ground + local onground=not group:InAir() + + -- In spawn zone. + local inspawnzone=group:IsPartlyOrCompletelyInZone(self.spawnzone) + + -- Check conditions for being back home. + local ishome=false + if category==Group.Category.GROUND or category==Group.Category.HELICOPTER then + -- Units go back to the spawn zone, helicopters land and they should not move any more. + ishome=inspawnzone and onground and notmoving + elseif category==Group.Category.AIRPLANE then + -- Planes need to be on ground at their home airbase and should not move any more. + ishome=athomebase and onground and notmoving + end + + -- Debug text. + local text=string.format("Group %s: speed=%d km/h, onground=%s , airbase=%s, spawnzone=%s ==> ishome=%s", group:GetName(), speed, tostring(onground), airbase, tostring(inspawnzone), tostring(ishome)) + self:T(self.lid..text) + + if ishome then + + -- Info message. + local text=string.format("Warehouse %s: Transport group arrived back home and no cargo left for request id=%d.\nSending transport group %s back to stock.", self.alias, request.uid, group:GetName()) + self:T(self.lid..text) + + -- Debug smoke. + if self.Debug then + group:SmokeRed() + end + + -- Group arrived. + self:Arrived(group) + end + end + end + + end + + else + + if ntransport==0 and request.ntransport>0 then + ----------------------------------- + -- Still cargo but no transports -- + ----------------------------------- + + local ncargoalive=0 + + -- All transports are dead but there is still cargo left ==> Put cargo back into stock. + for _,_group in pairs(request.cargogroupset:GetSetObjects()) do + --local group=group --Wrapper.Group#GROUP + + -- These groups have been respawned as cargo, i.e. their name changed! + local groupname=_group:GetName() + local group=GROUP:FindByName(groupname.."#CARGO") + + -- Check if group is alive. + if group and group:IsAlive() then + + -- Check if group is in spawn zone? + if group:IsPartlyOrCompletelyInZone(self.spawnzone) then + -- Debug smoke. + if self.Debug then + group:SmokeBlue() + end + -- Add asset group back to stock. + self:AddAsset(group) + ncargoalive=ncargoalive+1 + end + end + + end + + -- Info message. + self:_InfoMessage(string.format("Warehouse %s: All transports of request id=%s dead! Putting remaining %s cargo assets back into warehouse!", self.alias, request.uid, ncargoalive)) + end + + end + end -- born check + end -- loop over requests + + -- Remove pending requests if done. + for _,request in pairs(done) do + self:_DeleteQueueItem(request, self.pending) + end +end + +--- Function that checks if an asset group is still okay. +-- @param #WAREHOUSE self +function WAREHOUSE:_CheckAssetStatus() + + -- Check if a unit of the group has problems. + local function _CheckGroup(_request, _group) + local request=_request --#WAREHOUSE.Pendingitem + local group=_group --Wrapper.Group#GROUP + + if group and group:IsAlive() then + + -- Category of group. + local category=group:GetCategory() + + for _,_unit in pairs(group:GetUnits()) do + local unit=_unit --Wrapper.Unit#UNIT + + if unit and unit:IsAlive() then + local unitid=unit:GetID() + local life9=unit:GetLife() + local life0=unit:GetLife0() + local life=life9/life0*100 + local speed=unit:GetVelocityMPS() + local onground=unit:InAir() + + local problem=false + if life<10 then + self:T(string.format("Unit %s is heavily damaged!", unit:GetName())) + end + if speed<1 and unit:GetSpeedMax()>1 and onground then + self:T(string.format("Unit %s is not moving!", unit:GetName())) + problem=true + end + + if problem then + if request.assetproblem[unitid] then + local deltaT=timer.getAbsTime()-request.assetproblem[unitid] + if deltaT>300 then + --Todo: which event to generate? Removeunit or Dead/Creash or both? + unit:Destroy() + end + else + request.assetproblem[unitid]=timer.getAbsTime() + end + end + end + + end + end + end + + + for _,request in pairs(self.pending) do + local request=request --#WAREHOUSE.Pendingitem + + -- Cargo groups. + if request.cargogroupset then + for _,_group in pairs(request.cargogroupset:GetSet()) do + local group=_group --Wrapper.Group#GROUP + + _CheckGroup(request, group) + + end + end + + -- Transport groups. + if request.transportgroupset then + for _,group in pairs(request.transportgroupset:GetSet()) do + + _CheckGroup(request, group) + end + end + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "AddAsset" event. Add a group to the warehouse stock. If the group is alive, it is destroyed. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Group#GROUP group Group or template group to be added to the warehouse stock. +-- @param #number ngroups Number of groups to add to the warehouse stock. Default is 1. +-- @param #WAREHOUSE.Attribute forceattribute (Optional) Explicitly force a generalized attribute for the asset. This has to be an @{#WAREHOUSE.Attribute}. +-- @param #number forcecargobay (Optional) Explicitly force cargobay weight limit in kg for cargo carriers. This is for each *unit* of the group. +-- @param #number forceweight (Optional) Explicitly force weight in kg of each unit in the group. +-- @param #number loadradius (Optional) Radius in meters when the cargo is loaded into the carrier. +-- @param DCS#AI.Skill skill Skill of the asset. +-- @param #table liveries Table of livery names. When the asset is spawned one livery is chosen randomly. +-- @param #string assignment A free to choose string specifying an assignment for the asset. This can be used with the @{#WAREHOUSE.OnAfterNewAsset} function. +-- @param #table other (Optional) Table of other useful data. Can be collected via WAREHOUSE.OnAfterNewAsset() function for example +function WAREHOUSE:onafterAddAsset(From, Event, To, group, ngroups, forceattribute, forcecargobay, forceweight, loadradius, skill, liveries, assignment, other) + self:T({group=group, ngroups=ngroups, forceattribute=forceattribute, forcecargobay=forcecargobay, forceweight=forceweight}) + + -- Set default. + local n=ngroups or 1 + + -- Handle case where just a string is passed. + if type(group)=="string" then + group=GROUP:FindByName(group) + end + + if liveries and type(liveries)=="string" then + liveries={liveries} + end + + if group then + + -- Try to get UIDs from group name. Is this group a known or a new asset? + local wid,aid,rid=self:_GetIDsFromGroup(group) + + if wid and aid and rid then + + --------------------------- + -- This is a KNOWN asset -- + --------------------------- + + -- Get the original warehouse this group belonged to. + local warehouse=self:FindWarehouseInDB(wid) + + if warehouse then + + local request=warehouse:_GetRequestOfGroup(group, warehouse.pending) + + if request then + + -- Increase number of cargo delivered and transports home. + local istransport=warehouse:_GroupIsTransport(group,request) + + if istransport==true then + request.ntransporthome=request.ntransporthome+1 + request.transportgroupset:Remove(group:GetName(), true) + local ntrans=request.transportgroupset:Count() + self:T2(warehouse.lid..string.format("Transport %d of %s returned home. TransportSet=%d", request.ntransporthome, tostring(request.ntransport), ntrans)) + elseif istransport==false then + request.ndelivered=request.ndelivered+1 + local namewo=self:_GetNameWithOut(group) + request.cargogroupset:Remove(namewo, true) + local ncargo=request.cargogroupset:Count() + self:T2(warehouse.lid..string.format("Cargo %s: %d of %s delivered. CargoSet=%d", namewo, request.ndelivered, tostring(request.nasset), ncargo)) + else + self:E(warehouse.lid..string.format("WARNING: Group %s is neither cargo nor transport! Need to investigate...", group:GetName())) + end + + -- If no assignment was given we take the assignment of the request if there is any. + if assignment==nil and request.assignment~=nil then + assignment=request.assignment + end + + end + end + + -- Get the asset from the global DB. + local asset=self:FindAssetInDB(group) + + -- Note the group is only added once, i.e. the ngroups parameter is ignored here. + -- This is because usually these request comes from an asset that has been transfered from another warehouse and hence should only be added once. + if asset~=nil then + self:_DebugMessage(string.format("Warehouse %s: Adding KNOWN asset uid=%d with attribute=%s to stock.", self.alias, asset.uid, asset.attribute), 5) + + -- Set livery. + if liveries then + if type(liveries)=="table" then + asset.livery=liveries[math.random(#liveries)] + else + asset.livery=liveries + end + end + + -- Set skill. + asset.skill=skill or asset.skill + + -- Asset now belongs to this warehouse. Set warehouse ID. + asset.wid=self.uid + + -- No request associated with this asset. + asset.rid=nil + + -- Asset is not spawned. + asset.spawned=false + asset.iscargo=nil + asset.arrived=nil + + -- Destroy group if it is alive. + if group:IsAlive()==true then + asset.damage=asset.life0-group:GetLife() + end + + -- Add asset to stock. + table.insert(self.stock, asset) + + -- Trigger New asset event. + self:__NewAsset(0.1, asset, assignment or "") + else + self:_ErrorMessage(string.format("ERROR: Known asset could not be found in global warehouse db!"), 0) + end + + else + + ------------------------- + -- This is a NEW asset -- + ------------------------- + + -- Debug info. + self:_DebugMessage(string.format("Warehouse %s: Adding %d NEW assets of group %s to stock", self.alias, n, tostring(group:GetName())), 5) + + -- This is a group that is not in the db yet. Add it n times. + local assets=self:_RegisterAsset(group, n, forceattribute, forcecargobay, forceweight, loadradius, liveries, skill, assignment) + + -- Add created assets to stock of this warehouse. + for _,asset in pairs(assets) do + + -- Asset belongs to this warehouse. Set warehouse ID. + asset.wid=self.uid + + -- No request associated with this asset. + asset.rid=nil + + -- Add asset to stock. + table.insert(self.stock, asset) + + -- Trigger NewAsset event. Delay a bit for OnAfterNewAsset functions to work properly. + self:__NewAsset(0.1, asset, assignment or "") + end + + end + + -- Destroy group if it is alive. + if group:IsAlive()==true then + self:_DebugMessage(string.format("Removing group %s", group:GetName()), 5) + -- Setting parameter to false, i.e. creating NO dead or remove unit event, seems to not confuse the dispatcher logic. + -- TODO: It would be nice, however, to have the remove event. + group:Destroy() --(false) + end + + else + self:E(self.lid.."ERROR: Unknown group added as asset!") + self:E({unknowngroup=group}) + end + +end + +--- Register new asset in globase warehouse data base. +-- @param #WAREHOUSE self +-- @param Wrapper.Group#GROUP group The group that will be added to the warehouse stock. +-- @param #number ngroups Number of groups to be added. +-- @param #string forceattribute Forced generalized attribute. +-- @param #number forcecargobay Cargo bay weight limit in kg. +-- @param #number forceweight Weight of units in kg. +-- @param #number loadradius Radius in meters when cargo is loaded into the carrier. +-- @param #table liveries Table of liveries. +-- @param DCS#AI.Skill skill Skill of AI. +-- @param #string assignment Assignment attached to the asset item. +-- @return #table A table containing all registered assets. +function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, forceweight, loadradius, liveries, skill, assignment) + self:F({groupname=group:GetName(), ngroups=ngroups, forceattribute=forceattribute, forcecargobay=forcecargobay, forceweight=forceweight}) + + -- Set default. + local n=ngroups or 1 + + -- Get the size of an object. + local function _GetObjectSize(DCSdesc) + if DCSdesc.box then + local x=DCSdesc.box.max.x+math.abs(DCSdesc.box.min.x) --length + local y=DCSdesc.box.max.y+math.abs(DCSdesc.box.min.y) --height + local z=DCSdesc.box.max.z+math.abs(DCSdesc.box.min.z) --width + return math.max(x,z), x , y, z + end + return 0,0,0,0 + end + + -- Get name of template group. + local templategroupname=group:GetName() + + local Descriptors=group:GetUnit(1):GetDesc() + local Category=group:GetCategory() + local TypeName=group:GetTypeName() + local SpeedMax=group:GetSpeedMax() + local RangeMin=group:GetRange() + local smax,sx,sy,sz=_GetObjectSize(Descriptors) + + --self:E(Descriptors) + + -- Get weight and cargo bay size in kg. + local weight=0 + local cargobay={} + local cargobaytot=0 + local cargobaymax=0 + for _i,_unit in pairs(group:GetUnits()) do + local unit=_unit --Wrapper.Unit#UNIT + local Desc=unit:GetDesc() + + -- Weight. We sum up all units in the group. + local unitweight=forceweight or Desc.massEmpty + if unitweight then + weight=weight+unitweight + end + + local cargomax=0 + local massfuel=Desc.fuelMassMax or 0 + local massempty=Desc.massEmpty or 0 + local massmax=Desc.massMax or 0 + + -- Calcuate cargo bay limit value. + cargomax=massmax-massfuel-massempty + self:T3(self.lid..string.format("Unit name=%s: mass empty=%.1f kg, fuel=%.1f kg, max=%.1f kg ==> cargo=%.1f kg", unit:GetName(), unitweight, massfuel, massmax, cargomax)) + + -- Cargo bay size. + local bay=forcecargobay or unit:GetCargoBayFreeWeight() + + -- Add bay size to table. + table.insert(cargobay, bay) + + -- Sum up total bay size. + cargobaytot=cargobaytot+bay + + -- Get max bay size. + if bay>cargobaymax then + cargobaymax=bay + end + end + + -- Set/get the generalized attribute. + local attribute=forceattribute or self:_GetAttribute(group) + + -- Table for returned assets. + local assets={} + + -- Add this n times to the table. + for i=1,n do + local asset={} --#WAREHOUSE.Assetitem + + -- Increase asset unique id counter. + _WAREHOUSEDB.AssetID=_WAREHOUSEDB.AssetID+1 + + -- Set parameters. + asset.uid=_WAREHOUSEDB.AssetID + asset.templatename=templategroupname + asset.template=UTILS.DeepCopy(_DATABASE.Templates.Groups[templategroupname].Template) + asset.category=Category + asset.unittype=TypeName + asset.nunits=#asset.template.units + asset.range=RangeMin + asset.speedmax=SpeedMax + asset.size=smax + asset.weight=weight + asset.DCSdesc=Descriptors + asset.attribute=attribute + asset.cargobay=cargobay + asset.cargobaytot=cargobaytot + asset.cargobaymax=cargobaymax + asset.loadradius=loadradius + if liveries then + asset.livery=liveries[math.random(#liveries)] + end + asset.skill=skill + asset.assignment=assignment + asset.spawned=false + asset.life0=group:GetLife0() + asset.damage=0 + asset.spawngroupname=string.format("%s_AID-%d", templategroupname, asset.uid) + + if i==1 then + self:_AssetItemInfo(asset) + end + + -- Add asset to global db. + _WAREHOUSEDB.Assets[asset.uid]=asset + + -- Add asset to the table that is retured. + table.insert(assets,asset) + end + + return assets +end + +--- Asset item characteristics. +-- @param #WAREHOUSE self +-- @param #WAREHOUSE.Assetitem asset The asset for which info in printed in trace mode. +function WAREHOUSE:_AssetItemInfo(asset) + -- Info about asset: + local text=string.format("\nNew asset with id=%d for warehouse %s:\n", asset.uid, self.alias) + text=text..string.format("Spawngroup name= %s\n", asset.spawngroupname) + text=text..string.format("Template name = %s\n", asset.templatename) + text=text..string.format("Unit type = %s\n", asset.unittype) + text=text..string.format("Attribute = %s\n", asset.attribute) + text=text..string.format("Category = %d\n", asset.category) + text=text..string.format("Units # = %d\n", asset.nunits) + text=text..string.format("Speed max = %5.2f km/h\n", asset.speedmax) + text=text..string.format("Range max = %5.2f km\n", asset.range/1000) + text=text..string.format("Size max = %5.2f m\n", asset.size) + text=text..string.format("Weight total = %5.2f kg\n", asset.weight) + text=text..string.format("Cargo bay tot = %5.2f kg\n", asset.cargobaytot) + text=text..string.format("Cargo bay max = %5.2f kg\n", asset.cargobaymax) + text=text..string.format("Load radius = %s m\n", tostring(asset.loadradius)) + text=text..string.format("Skill = %s\n", tostring(asset.skill)) + text=text..string.format("Livery = %s", tostring(asset.livery)) + self:I(self.lid..text) + self:T({DCSdesc=asset.DCSdesc}) + self:T3({Template=asset.template}) +end + +--- On after "NewAsset" event. A new asset has been added to the warehouse stock. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #WAREHOUSE.Assetitem asset The asset that has just been added. +-- @param #string assignment The (optional) assignment for the asset. +function WAREHOUSE:onafterNewAsset(From, Event, To, asset, assignment) + self:T(self.lid..string.format("New asset %s id=%d with assignment %s.", tostring(asset.templatename), asset.uid, tostring(assignment))) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On before "AddRequest" event. Checks some basic properties of the given parameters. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #WAREHOUSE warehouse The warehouse requesting supply. +-- @param #WAREHOUSE.Descriptor AssetDescriptor Descriptor describing the asset that is requested. +-- @param AssetDescriptorValue Value of the asset descriptor. Type depends on descriptor, i.e. could be a string, etc. +-- @param #number nAsset Number of groups requested that match the asset specification. +-- @param #WAREHOUSE.TransportType TransportType Type of transport. +-- @param #number nTransport Number of transport units requested. +-- @param #number Prio Priority of the request. Number ranging from 1=high to 100=low. +-- @param #string Assignment A keyword or text that later be used to identify this request and postprocess the assets. +-- @return #boolean If true, request is okay at first glance. +function WAREHOUSE:onbeforeAddRequest(From, Event, To, warehouse, AssetDescriptor, AssetDescriptorValue, nAsset, TransportType, nTransport, Assignment, Prio) + + -- Request is okay. + local okay=true + + if AssetDescriptor==WAREHOUSE.Descriptor.ATTRIBUTE then + + -- Check if a valid attibute was given. + local gotit=false + for _,attribute in pairs(WAREHOUSE.Attribute) do + if AssetDescriptorValue==attribute then + gotit=true + end + end + if not gotit then + self:_ErrorMessage("ERROR: Invalid request. Asset attribute is unknown!", 5) + okay=false + end + + elseif AssetDescriptor==WAREHOUSE.Descriptor.CATEGORY then + + -- Check if a valid category was given. + local gotit=false + for _,category in pairs(Group.Category) do + if AssetDescriptorValue==category then + gotit=true + end + end + if not gotit then + self:_ErrorMessage("ERROR: Invalid request. Asset category is unknown!", 5) + okay=false + end + + elseif AssetDescriptor==WAREHOUSE.Descriptor.GROUPNAME then + + if type(AssetDescriptorValue)~="string" then + self:_ErrorMessage("ERROR: Invalid request. Asset template name must be passed as a string!", 5) + okay=false + end + + elseif AssetDescriptor==WAREHOUSE.Descriptor.UNITTYPE then + + if type(AssetDescriptorValue)~="string" then + self:_ErrorMessage("ERROR: Invalid request. Asset unit type must be passed as a string!", 5) + okay=false + end + + elseif AssetDescriptor==WAREHOUSE.Descriptor.ASSIGNMENT then + + if type(AssetDescriptorValue)~="string" then + self:_ErrorMessage("ERROR: Invalid request. Asset assignment type must be passed as a string!", 5) + okay=false + end + + elseif AssetDescriptor==WAREHOUSE.Descriptor.ASSETLIST then + + if type(AssetDescriptorValue)~="table" then + self:_ErrorMessage("ERROR: Invalid request. Asset assignment type must be passed as a table!", 5) + okay=false + end + + else + self:_ErrorMessage("ERROR: Invalid request. Asset descriptor is not ATTRIBUTE, CATEGORY, GROUPNAME, UNITTYPE or ASSIGNMENT!", 5) + okay=false + end + + -- Warehouse is stopped? + if self:IsStopped() then + self:_ErrorMessage("ERROR: Invalid request. Warehouse is stopped!", 0) + okay=false + end + + -- Warehouse is destroyed? + if self:IsDestroyed() and not self.respawnafterdestroyed then + self:_ErrorMessage("ERROR: Invalid request. Warehouse is destroyed!", 0) + okay=false + end + + return okay +end + +--- On after "AddRequest" event. Add a request to the warehouse queue, which is processed when possible. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #WAREHOUSE warehouse The warehouse requesting supply. +-- @param #WAREHOUSE.Descriptor AssetDescriptor Descriptor describing the asset that is requested. +-- @param AssetDescriptorValue Value of the asset descriptor. Type depends on descriptor, i.e. could be a string, etc. +-- @param #number nAsset Number of groups requested that match the asset specification. +-- @param #WAREHOUSE.TransportType TransportType Type of transport. +-- @param #number nTransport Number of transport units requested. +-- @param #number Prio Priority of the request. Number ranging from 1=high to 100=low. +-- @param #string Assignment A keyword or text that can later be used to identify this request and postprocess the assets. +function WAREHOUSE:onafterAddRequest(From, Event, To, warehouse, AssetDescriptor, AssetDescriptorValue, nAsset, TransportType, nTransport, Prio, Assignment) + + -- Defaults. + nAsset=nAsset or 1 + TransportType=TransportType or WAREHOUSE.TransportType.SELFPROPELLED + Prio=Prio or 50 + if nTransport==nil then + if TransportType==WAREHOUSE.TransportType.SELFPROPELLED then + nTransport=0 + else + nTransport=1 + end + end + + -- Self request? + local toself=false + if self.warehouse:GetName()==warehouse.warehouse:GetName() then + toself=true + end + + -- Increase id. + self.queueid=self.queueid+1 + + -- Request queue table item. + local request={ + uid=self.queueid, + prio=Prio, + warehouse=warehouse, + assetdesc=AssetDescriptor, + assetdescval=AssetDescriptorValue, + nasset=nAsset, + transporttype=TransportType, + ntransport=nTransport, + assignment=tostring(Assignment), + airbase=warehouse:GetAirbase(), + category=warehouse:GetAirbaseCategory(), + ndelivered=0, + ntransporthome=0, + assets={}, + toself=toself, + } --#WAREHOUSE.Queueitem + + -- Add request to queue. + table.insert(self.queue, request) + + local text=string.format("Warehouse %s: New request from warehouse %s.\nDescriptor %s=%s, #assets=%s; Transport=%s, #transports =%s.", + self.alias, warehouse.alias, request.assetdesc, tostring(request.assetdescval), tostring(request.nasset), request.transporttype, tostring(request.ntransport)) + self:_DebugMessage(text, 5) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On before "Request" event. Checks if the request can be fulfilled. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #WAREHOUSE.Queueitem Request Information table of the request. +-- @return #boolean If true, request is granted. +function WAREHOUSE:onbeforeRequest(From, Event, To, Request) + self:T3({warehouse=self.alias, request=Request}) + + -- Distance from warehouse to requesting warehouse. + local distance=self:GetCoordinate():Get2DDistance(Request.warehouse:GetCoordinate()) + + -- Shortcut to cargoassets. + local _assets=Request.cargoassets + + if Request.nasset==0 then + local text=string.format("Warehouse %s: Request denied! Zero assets were requested.", self.alias) + self:_InfoMessage(text, 10) + return false + end + + -- Check if destination is in range for all requested assets. + for _,_asset in pairs(_assets) do + local asset=_asset --#WAREHOUSE.Assetitem + + -- Check if destination is in range. + if asset.range=1 then + local text=string.format("Warehouse %s: Processing request id=%d from warehouse %s.\n", self.alias, Request.uid, Request.warehouse.alias) + text=text..string.format("Requested %s assets of %s=%s.\n", tostring(Request.nasset), Request.assetdesc, Request.assetdesc==WAREHOUSE.Descriptor.ASSETLIST and "Asset list" or Request.assetdescval) + text=text..string.format("Transports %s of type %s.", tostring(Request.ntransport), tostring(Request.transporttype)) + self:_InfoMessage(text, 5) + end + + ------------------------------------------------------------------------------------------------------------------------------------ + -- Cargo assets. + ------------------------------------------------------------------------------------------------------------------------------------ + + -- Set time stamp. + Request.timestamp=timer.getAbsTime() + + -- Spawn assets of this request. + self:_SpawnAssetRequest(Request) + + ------------------------------------------------------------------------------------------------------------------------------------ + -- Transport assets + ------------------------------------------------------------------------------------------------------------------------------------ + + -- Shortcut to transport assets. + local _assetstock=Request.transportassets + + -- Now we try to find all parking spots for all cargo groups in advance. Due to the for loop, the parking spots do not get updated while spawning. + local Parking={} + if Request.transportcategory==Group.Category.AIRPLANE or Request.transportcategory==Group.Category.HELICOPTER then + Parking=self:_FindParkingForAssets(self.airbase,_assetstock) + end + + -- Transport assets table. + local _transportassets={} + + ---------------------------- + -- Spawn Transport Groups -- + ---------------------------- + + -- Spawn the transport groups. + for i=1,Request.ntransport do + + -- Get stock item. + local _assetitem=_assetstock[i] --#WAREHOUSE.Assetitem + + -- Spawn group name + local _alias=_assetitem.spawngroupname + + -- Set Request ID. + _assetitem.rid=Request.uid + + -- Asset is transport. + _assetitem.spawned=false + _assetitem.iscargo=false + _assetitem.arrived=false + + local spawngroup=nil --Wrapper.Group#GROUP + + -- Add asset by id to all assets table. + Request.assets[_assetitem.uid]=_assetitem + + -- Spawn assets depending on type. + if Request.transporttype==WAREHOUSE.TransportType.AIRPLANE then + + -- Spawn plane at airport in uncontrolled state. Will get activated when cargo is loaded. + spawngroup=self:_SpawnAssetAircraft(_alias,_assetitem, Request, Parking[_assetitem.uid], true) + + elseif Request.transporttype==WAREHOUSE.TransportType.HELICOPTER then + + -- Spawn helos at airport in controlled state. They need to fly to the spawn zone. + spawngroup=self:_SpawnAssetAircraft(_alias,_assetitem, Request, Parking[_assetitem.uid], false) + + elseif Request.transporttype==WAREHOUSE.TransportType.APC then + + -- Spawn APCs in spawn zone. + spawngroup=self:_SpawnAssetGroundNaval(_alias, _assetitem, Request, self.spawnzone) + + elseif Request.transporttype==WAREHOUSE.TransportType.TRAIN then + + self:_ErrorMessage("ERROR: Cargo transport by train not supported yet!") + return + + elseif Request.transporttype==WAREHOUSE.TransportType.SHIP or Request.transporttype==WAREHOUSE.TransportType.NAVALCARRIER then + + -- Spawn Ship in port zone + spawngroup=self:_SpawnAssetGroundNaval(_alias, _assetitem, Request, self.portzone) + + elseif Request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then + + self:_ErrorMessage("ERROR: Transport type selfpropelled was already handled above. We should not get here!") + return + + else + self:_ErrorMessage("ERROR: Unknown transport type!") + return + end + + end + + -- Init problem table. + Request.assetproblem={} + + -- Add request to pending queue. + table.insert(self.pending, Request) + + -- Delete request from queue. + self:_DeleteQueueItem(Request, self.queue) + +end + +--- On after "RequestSpawned" event. Initiates the transport of the assets to the requesting warehouse. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #WAREHOUSE.Pendingitem Request Information table of the request. +-- @param Core.Set#SET_GROUP CargoGroupSet Set of cargo groups. +-- @param Core.Set#SET_GROUP TransportGroupSet Set of transport groups if any. +function WAREHOUSE:onafterRequestSpawned(From, Event, To, Request, CargoGroupSet, TransportGroupSet) + + -- General type and category. + local _cargotype=Request.cargoattribute --#WAREHOUSE.Attribute + local _cargocategory=Request.cargocategory --DCS#Group.Category + + -- Add groups to pending item. + --Request.cargogroupset=CargoGroupSet + + ------------------------------------------------------------------------------------------------------------------------------------ + -- Self request: assets are spawned at warehouse but not transported anywhere. + ------------------------------------------------------------------------------------------------------------------------------------ + + -- Self request! Assets are only spawned but not routed or transported anywhere. + if Request.toself then + self:_DebugMessage(string.format("Selfrequest! Current status %s", self:GetState())) + + -- Start self request. + self:__SelfRequest(1, CargoGroupSet, Request) + + return + end + + ------------------------------------------------------------------------------------------------------------------------------------ + -- Self propelled: assets go to the requesting warehouse by themselfs. + ------------------------------------------------------------------------------------------------------------------------------------ + + -- No transport unit requested. Assets go by themselfes. + if Request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then + self:T2(self.lid..string.format("Got selfpropelled request for %d assets.", CargoGroupSet:Count())) + + for _,_group in pairs(CargoGroupSet:GetSetObjects()) do + local group=_group --Wrapper.Group#GROUP + + -- Route cargo to their destination. + if _cargocategory==Group.Category.GROUND then + self:T2(self.lid..string.format("Route ground group %s.", group:GetName())) + + -- Random place in the spawn zone of the requesting warehouse. + local ToCoordinate=Request.warehouse.spawnzone:GetRandomCoordinate() + + -- Debug marker. + if self.Debug then + ToCoordinate:MarkToAll(string.format("Destination of group %s", group:GetName())) + end + + -- Route ground. + self:_RouteGround(group, Request) + + elseif _cargocategory==Group.Category.AIRPLANE or _cargocategory==Group.Category.HELICOPTER then + self:T2(self.lid..string.format("Route airborne group %s.", group:GetName())) + + -- Route plane to the requesting warehouses airbase. + -- Actually, the route is already set. We only need to activate the uncontrolled group. + self:_RouteAir(group) + + elseif _cargocategory==Group.Category.SHIP then + self:T2(self.lid..string.format("Route naval group %s.", group:GetName())) + + -- Route plane to the requesting warehouses airbase. + self:_RouteNaval(group, Request) + + elseif _cargocategory==Group.Category.TRAIN then + self:T2(self.lid..string.format("Route train group %s.", group:GetName())) + + -- Route train to the rail connection of the requesting warehouse. + self:_RouteTrain(group, Request.warehouse.rail) + + else + self:E(self.lid..string.format("ERROR: unknown category %s for self propelled cargo %s!", tostring(_cargocategory), tostring(group:GetName()))) + end + + end + + -- Transport group set. + Request.transportgroupset=TransportGroupSet + + -- No cargo transport necessary. + return + end + + ------------------------------------------------------------------------------------------------------------------------------------ + -- Prepare cargo groups for transport + ------------------------------------------------------------------------------------------------------------------------------------ + + -- Board radius, i.e. when the cargo will begin to board the carrier + local _boardradius=500 + + if Request.transporttype==WAREHOUSE.TransportType.AIRPLANE then + _boardradius=5000 + elseif Request.transporttype==WAREHOUSE.TransportType.HELICOPTER then + --_loadradius=1000 + --_boardradius=nil + elseif Request.transporttype==WAREHOUSE.TransportType.APC then + --_boardradius=nil + elseif Request.transporttype==WAREHOUSE.TransportType.SHIP or Request.transporttype==WAREHOUSE.TransportType.AIRCRAFTCARRIER + or Request.transporttype==WAREHOUSE.TransportType.ARMEDSHIP or Request.transporttype==WAREHOUSE.TransportType.WARSHIP then + _boardradius=6000 + end + + -- Empty cargo group set. + local CargoGroups=SET_CARGO:New() + + -- Add cargo groups to set. + for _,_group in pairs(CargoGroupSet:GetSetObjects()) do + + -- Find asset belonging to this group. + local asset=self:FindAssetInDB(_group) + -- New cargo group object. + local cargogroup=CARGO_GROUP:New(_group, _cargotype,_group:GetName(),_boardradius, asset.loadradius) + + -- Set weight for this group. + cargogroup:SetWeight(asset.weight) + + -- Add group to group set. + CargoGroups:AddCargo(cargogroup) + + end + + ------------------------ + -- Create Dispatchers -- + ------------------------ + + -- Cargo dispatcher. + local CargoTransport --AI.AI_Cargo_Dispatcher#AI_CARGO_DISPATCHER + + if Request.transporttype==WAREHOUSE.TransportType.AIRPLANE then + + -- Pickup and deploy zones. + local PickupAirbaseSet = SET_ZONE:New():AddZone(ZONE_AIRBASE:New(self.airbase:GetName())) + local DeployAirbaseSet = SET_ZONE:New():AddZone(ZONE_AIRBASE:New(Request.airbase:GetName())) + + -- Define dispatcher for this task. + CargoTransport = AI_CARGO_DISPATCHER_AIRPLANE:New(TransportGroupSet, CargoGroups, PickupAirbaseSet, DeployAirbaseSet) + + -- Set home zone. + CargoTransport:SetHomeZone(ZONE_AIRBASE:New(self.airbase:GetName())) + + elseif Request.transporttype==WAREHOUSE.TransportType.HELICOPTER then + + -- Pickup and deploy zones. + local PickupZoneSet = SET_ZONE:New():AddZone(self.spawnzone) + local DeployZoneSet = SET_ZONE:New():AddZone(Request.warehouse.spawnzone) + + -- Define dispatcher for this task. + CargoTransport = AI_CARGO_DISPATCHER_HELICOPTER:New(TransportGroupSet, CargoGroups, PickupZoneSet, DeployZoneSet) + + -- Home zone. + CargoTransport:SetHomeZone(self.spawnzone) + + elseif Request.transporttype==WAREHOUSE.TransportType.APC then + + -- Pickup and deploy zones. + local PickupZoneSet = SET_ZONE:New():AddZone(self.spawnzone) + local DeployZoneSet = SET_ZONE:New():AddZone(Request.warehouse.spawnzone) + + -- Define dispatcher for this task. + CargoTransport = AI_CARGO_DISPATCHER_APC:New(TransportGroupSet, CargoGroups, PickupZoneSet, DeployZoneSet, 0) + + -- Set home zone. + CargoTransport:SetHomeZone(self.spawnzone) + + elseif Request.transporttype==WAREHOUSE.TransportType.SHIP or Request.transporttype==WAREHOUSE.TransportType.AIRCRAFTCARRIER + or Request.transporttype==WAREHOUSE.TransportType.ARMEDSHIP or Request.transporttype==WAREHOUSE.TransportType.WARSHIP then + + -- Pickup and deploy zones. + local PickupZoneSet = SET_ZONE:New():AddZone(self.portzone) + PickupZoneSet:AddZone(self.harborzone) + local DeployZoneSet = SET_ZONE:New():AddZone(Request.warehouse.harborzone) + + + -- Get the shipping lane to use and pass it to the Dispatcher + local remotename = Request.warehouse.warehouse:GetName() + local ShippingLane = self.shippinglanes[remotename][math.random(#self.shippinglanes[remotename])] + + -- Define dispatcher for this task. + CargoTransport = AI_CARGO_DISPATCHER_SHIP:New(TransportGroupSet, CargoGroups, PickupZoneSet, DeployZoneSet, ShippingLane) + + -- Set home zone + CargoTransport:SetHomeZone(self.portzone) + + else + self:E(self.lid.."ERROR: Unknown transporttype!") + end + + -- Set pickup and deploy radii. + -- The 20 m inner radius are to ensure that the helo does not land on the warehouse itself in the middle of the default spawn zone. + local pickupouter = 200 + local pickupinner = 0 + local deployouter = 200 + local deployinner = 0 + if Request.transporttype==WAREHOUSE.TransportType.SHIP or Request.transporttype==WAREHOUSE.TransportType.AIRCRAFTCARRIER + or Request.transporttype==WAREHOUSE.TransportType.ARMEDSHIP or Request.transporttype==WAREHOUSE.TransportType.WARSHIP then + pickupouter=1000 + pickupinner=20 + deployouter=1000 + deployinner=0 + else + pickupouter=200 + pickupinner=0 + if self.spawnzone.Radius~=nil then + pickupouter=self.spawnzone.Radius + pickupinner=20 + end + deployouter=200 + deployinner=0 + if self.spawnzone.Radius~=nil then + deployouter=Request.warehouse.spawnzone.Radius + deployinner=20 + end + end + CargoTransport:SetPickupRadius(pickupouter, pickupinner) + CargoTransport:SetDeployRadius(deployouter, deployinner) + + + -- Adjust carrier units. This has to come AFTER the dispatchers have been defined because they set the cargobay free weight! + Request.carriercargo={} + for _,carriergroup in pairs(TransportGroupSet:GetSetObjects()) do + local asset=self:FindAssetInDB(carriergroup) + for _i,_carrierunit in pairs(carriergroup:GetUnits()) do + local carrierunit=_carrierunit --Wrapper.Unit#UNIT + + -- Create empty tables which will be filled with the cargo groups of each carrier unit. Needed in case a carrier unit dies. + Request.carriercargo[carrierunit:GetName()]={} + + -- Adjust cargo bay of carrier unit. + local cargobay=asset.cargobay[_i] + carrierunit:SetCargoBayWeightLimit(cargobay) + + -- Debug info. + self:T2(self.lid..string.format("Cargo bay weight limit of carrier unit %s: %.1f kg.", carrierunit:GetName(), carrierunit:GetCargoBayFreeWeight())) + end + end + + -------------------------------- + -- Dispatcher Event Functions -- + -------------------------------- + + --- Function called after carrier picked up something. + function CargoTransport:OnAfterPickedUp(From, Event, To, Carrier, PickupZone) + + -- Get warehouse state. + local warehouse=Carrier:GetState(Carrier, "WAREHOUSE") --#WAREHOUSE + + -- Debug message. + local text=string.format("Carrier group %s picked up at pickup zone %s.", Carrier:GetName(), PickupZone:GetName()) + warehouse:T(warehouse.lid..text) + + end + + --- Function called if something was deployed. + function CargoTransport:OnAfterDeployed(From, Event, To, Carrier, DeployZone) + + -- Get warehouse state. + local warehouse=Carrier:GetState(Carrier, "WAREHOUSE") --#WAREHOUSE + + -- Debug message. + -- TODO: Depoloy zone is nil! + --local text=string.format("Carrier group %s deployed at deploy zone %s.", Carrier:GetName(), DeployZone:GetName()) + --warehouse:T(warehouse.lid..text) + + end + + --- Function called if carrier group is going home. + function CargoTransport:OnAfterHome(From, Event, To, Carrier, Coordinate, Speed, Height, HomeZone) + + -- Get warehouse state. + local warehouse=Carrier:GetState(Carrier, "WAREHOUSE") --#WAREHOUSE + + -- Debug message. + local text=string.format("Carrier group %s going home to zone %s.", Carrier:GetName(), HomeZone:GetName()) + warehouse:T(warehouse.lid..text) + + end + + --- Function called when a carrier unit has loaded a cargo group. + function CargoTransport:OnAfterLoaded(From, Event, To, Carrier, Cargo, CarrierUnit, PickupZone) + + -- Get warehouse state. + local warehouse=Carrier:GetState(Carrier, "WAREHOUSE") --#WAREHOUSE + + -- Debug message. + local text=string.format("Carrier group %s loaded cargo %s into unit %s in pickup zone %s", Carrier:GetName(), Cargo:GetName(), CarrierUnit:GetName(), PickupZone:GetName()) + warehouse:T(warehouse.lid..text) + + -- Get cargo group object. + local group=Cargo:GetObject() --Wrapper.Group#GROUP + + -- Get request. + local request=warehouse:_GetRequestOfGroup(group, warehouse.pending) + + -- Add cargo group to this carrier. + table.insert(request.carriercargo[CarrierUnit:GetName()], warehouse:_GetNameWithOut(Cargo:GetName())) + + end + + --- Function called when cargo has arrived and was unloaded. + function CargoTransport:OnAfterUnloaded(From, Event, To, Carrier, Cargo, CarrierUnit, DeployZone) + + -- Get warehouse state. + local warehouse=Carrier:GetState(Carrier, "WAREHOUSE") --#WAREHOUSE + + -- Get group obejet. + local group=Cargo:GetObject() --Wrapper.Group#GROUP + + -- Debug message. + local text=string.format("Cargo group %s was unloaded from carrier unit %s.", tostring(group:GetName()), tostring(CarrierUnit:GetName())) + warehouse:T(warehouse.lid..text) + + -- Load the cargo in the warehouse. + --Cargo:Load(warehouse.warehouse) + + -- Trigger Arrived event. + warehouse:Arrived(group) + end + + --- On after BackHome event. + function CargoTransport:OnAfterBackHome(From, Event, To, Carrier) + + -- Intellisense. + local carrier=Carrier --Wrapper.Group#GROUP + + -- Get warehouse state. + local warehouse=carrier:GetState(carrier, "WAREHOUSE") --#WAREHOUSE + carrier:SmokeWhite() + + -- Debug info. + local text=string.format("Carrier %s is back home at warehouse %s.", tostring(Carrier:GetName()), tostring(warehouse.warehouse:GetName())) + MESSAGE:New(text, 5):ToAllIf(warehouse.Debug) + warehouse:I(warehouse.lid..text) + + -- Call arrived event for carrier. + warehouse:__Arrived(1, Carrier) + + end + + -- Start dispatcher. + CargoTransport:__Start(5) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "Unloaded" event. Triggered when a group was unloaded from the carrier. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Group#GROUP group The group that was delivered. +function WAREHOUSE:onafterUnloaded(From, Event, To, group) + -- Debug info. + self:_DebugMessage(string.format("Cargo %s unloaded!", tostring(group:GetName())), 5) + + if group and group:IsAlive() then + + -- Debug smoke. + if self.Debug then + group:SmokeWhite() + end + + -- Get max speed of group. + local speedmax=group:GetSpeedMax() + + if group:IsGround() then + -- Route group to spawn zone. + if speedmax>1 then + group:RouteGroundTo(self.spawnzone:GetRandomCoordinate(), speedmax*0.5, AI.Task.VehicleFormation.RANK, 3) + else + -- Immobile ground unit ==> directly put it into the warehouse. + self:Arrived(group) + end + elseif group:IsAir() then + -- Not sure if air units will be allowed as cargo even though it might be possible. Best put them into warehouse immediately. + self:Arrived(group) + elseif group:IsShip() then + -- Not sure if naval units will be allowed as cargo even though it might be possible. Best put them into warehouse immediately. + self:Arrived(group) + end + + else + self:E(self.lid..string.format("ERROR unloaded Cargo group is not alive!")) + end +end + +--- On before "Arrived" event. Triggered when a group has arrived at its destination warehouse. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Group#GROUP group The group that was delivered. +function WAREHOUSE:onbeforeArrived(From, Event, To, group) + + local asset=self:FindAssetInDB(group) + + if asset then + if asset.arrived==true then + -- Asset already arrived (e.g. if multiple units trigger the event via landing). + return false + else + asset.arrived=true --ensure this is not called again from the same asset group. + return true + end + end + +end + +--- On after "Arrived" event. Triggered when a group has arrived at its destination warehouse. +-- The routine should be called by the warehouse sending this asset and not by the receiving warehouse. +-- It is checked if this asset is cargo (or self propelled) or transport. If it is cargo it is put into the stock of receiving warehouse. +-- If it is a transporter it is put back into the sending warehouse since transports are supposed to return their home warehouse. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Group#GROUP group The group that was delivered. +function WAREHOUSE:onafterArrived(From, Event, To, group) + + -- Debug message and smoke. + if self.Debug then + group:SmokeOrange() + end + + -- Get pending request this group belongs to. + local request=self:_GetRequestOfGroup(group, self.pending) + + if request then + + -- Get the right warehouse to put the asset into + -- Transports go back to the warehouse which called this function while cargo goes into the receiving warehouse. + local warehouse=request.warehouse + local istransport=self:_GroupIsTransport(group,request) + if istransport==true then + warehouse=self + elseif istransport==false then + warehouse=request.warehouse + else + self:E(self.lid..string.format("ERROR: Group %s is neither cargo nor transport", group:GetName())) + return + end + + -- Debug message. + self:_DebugMessage(string.format("Group %s arrived at warehouse %s!", tostring(group:GetName()), warehouse.alias), 5) + + -- Route mobile ground group to the warehouse. Group has 60 seconds to get there or it is despawned and added as asset to the new warehouse regardless. + if group:IsGround() and group:GetSpeedMax()>1 then + group:RouteGroundTo(warehouse:GetCoordinate(), group:GetSpeedMax()*0.3, "Off Road") + end + + -- Move asset from pending queue into new warehouse. + self:T(self.lid.."Asset arrived at warehouse adding in 60 sec") + warehouse:__AddAsset(60, group) + end + +end + +--- On after "Delivered" event. Triggered when all asset groups have reached their destination. Corresponding request is deleted from the pending queue. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #WAREHOUSE.Pendingitem request The pending request that is finished and deleted from the pending queue. +function WAREHOUSE:onafterDelivered(From, Event, To, request) + + -- Debug info + if self.verbosity>=1 then + local text=string.format("Warehouse %s: All assets delivered to warehouse %s!", self.alias, request.warehouse.alias) + self:_InfoMessage(text, 5) + end + + -- Make some noise :) + if self.Debug then + self:_Fireworks(request.warehouse:GetCoordinate()) + end + + -- Set delivered status for this request uid. + self.delivered[request.uid]=true + +end + + +--- On after "SelfRequest" event. Request was initiated to the warehouse itself. Groups are just spawned at the warehouse or the associated airbase. +-- If the warehouse is currently under attack when the self request is made, the self request is added to the defending table. One the attack is defeated, +-- this request is used to put the groups back into the warehouse stock. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Set#SET_GROUP groupset The set of asset groups that was delivered to the warehouse itself. +-- @param #WAREHOUSE.Pendingitem request Pending self request. +function WAREHOUSE:onafterSelfRequest(From, Event, To, groupset, request) + + -- Debug info. + self:_DebugMessage(string.format("Assets spawned at warehouse %s after self request!", self.alias)) + + -- Debug info. + for _,_group in pairs(groupset:GetSetObjects()) do + local group=_group --Wrapper.Group#GROUP + if self.Debug then + group:FlareGreen() + end + end + + -- Add a "defender request" to be able to despawn all assets once defeated. + if self:IsAttacked() then + + -- Route (mobile) ground troops to warehouse zone if they are not alreay there. + if self.autodefence then + for _,_group in pairs(groupset:GetSetObjects()) do + local group=_group --Wrapper.Group#GROUP + local speedmax=group:GetSpeedMax() + if group:IsGround() and speedmax>1 and group:IsNotInZone(self.zone) then + group:RouteGroundTo(self.zone:GetRandomCoordinate(), 0.8*speedmax, "Off Road") + end + end + end + + -- Add request to defenders. + table.insert(self.defending, request) + end + +end + +--- On after "Attacked" event. Warehouse is under attack by an another coalition. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param DCS#coalition.side Coalition which is attacking the warehouse. +-- @param DCS#country.id Country which is attacking the warehouse. +function WAREHOUSE:onafterAttacked(From, Event, To, Coalition, Country) + + -- Warning. + local text=string.format("Warehouse %s: We are under attack!", self.alias) + self:_InfoMessage(text) + + -- Debug smoke. + if self.Debug then + self:GetCoordinate():SmokeOrange() + end + + -- Spawn all ground units in the spawnzone? + if self.autodefence then + local nground=self:GetNumberOfAssets(WAREHOUSE.Descriptor.CATEGORY, Group.Category.GROUND) + local text=string.format("Warehouse auto defence activated.\n") + + if nground>0 then + text=text..string.format("Deploying all %d ground assets.", nground) + + -- Add self request. + self:AddRequest(self, WAREHOUSE.Descriptor.CATEGORY, Group.Category.GROUND, WAREHOUSE.Quantity.ALL, nil, nil , 0, "AutoDefence") + else + text=text..string.format("No ground assets currently available.") + end + self:_InfoMessage(text) + else + local text=string.format("Warehouse auto defence inactive.") + self:I(self.lid..text) + end +end + +--- On after "Defeated" event. Warehouse defeated an attack by another coalition. Defender assets are added back to warehouse stock. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function WAREHOUSE:onafterDefeated(From, Event, To) + + -- Message. + local text=string.format("Warehouse %s: Enemy attack was defeated!", self.alias) + self:_InfoMessage(text) + + -- Debug smoke. + if self.Debug then + self:GetCoordinate():SmokeGreen() + end + + -- Auto defence: put assets back into stock. + if self.autodefence then + for _,request in pairs(self.defending) do + + -- Route defenders back to warehoue (for visual reasons only) and put them back into stock. + for _,_group in pairs(request.cargogroupset:GetSetObjects()) do + local group=_group --Wrapper.Group#GROUP + + -- Get max speed of group and route it back slowly to the warehouse. + local speed=group:GetSpeedMax() + if group:IsGround() and speed>1 then + group:RouteGroundTo(self:GetCoordinate(), speed*0.3) + end + + -- Add asset group back to stock after 60 seconds. + self:__AddAsset(60, group) + end + + end + + self.defending=nil + self.defending={} + end +end + +--- Respawn warehouse. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function WAREHOUSE:onafterRespawn(From, Event, To) + + -- Info message. + local text=string.format("Respawning warehouse %s", self.alias) + self:_InfoMessage(text) + + -- Respawn warehouse. + self.warehouse:ReSpawn() + +end + +--- On before "ChangeCountry" event. Checks whether a change of country is necessary by comparing the actual country to the the requested one. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param DCS#country.id Country which has captured the warehouse. +function WAREHOUSE:onbeforeChangeCountry(From, Event, To, Country) + + local currentCountry=self:GetCountry() + + -- Message. + local text=string.format("Warehouse %s: request to change country %d-->%d", self.alias, currentCountry, Country) + self:_DebugMessage(text, 10) + + -- Check if current or requested coalition or country match. + if currentCountry~=Country then + return true + end + + return false +end + +--- On after "ChangeCountry" event. Warehouse is respawned with the specified country. All queued requests are deleted and the owned airbase is reset if the coalition is changed by changing the +-- country. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param DCS#country.id Country Country which has captured the warehouse. +function WAREHOUSE:onafterChangeCountry(From, Event, To, Country) + + local CoalitionOld=self:GetCoalition() + + self.warehouse:ReSpawn(Country) + + local CoalitionNew=self:GetCoalition() + + -- Delete all waiting requests because they are not valid any more. + self.queue=nil + self.queue={} + + if self.airbasename then + + -- Get airbase of this warehouse. + local airbase=AIRBASE:FindByName(self.airbasename) + + -- Get coalition of the airbase. + local airbaseCoalition=airbase:GetCoalition() + + if CoalitionNew==airbaseCoalition then + -- Airbase already owned by the coalition that captured the warehouse. Airbase can be used by this warehouse. + self.airbase=airbase + else + -- Airbase is owned by other coalition. So this warehouse does not have an airbase until it is captured. + self.airbase=nil + end + + end + + -- Debug smoke. + if self.Debug then + if CoalitionNew==coalition.side.RED then + self:GetCoordinate():SmokeRed() + elseif CoalitionNew==coalition.side.BLUE then + self:GetCoordinate():SmokeBlue() + end + end + +end + +--- On before "Captured" event. Warehouse has been captured by another coalition. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param DCS#coalition.side Coalition which captured the warehouse. +-- @param DCS#country.id Country which has captured the warehouse. +function WAREHOUSE:onbeforeCaptured(From, Event, To, Coalition, Country) + + -- Warehouse respawned. + self:ChangeCountry(Country) + +end + +--- On after "Captured" event. Warehouse has been captured by another coalition. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param DCS#coalition.side Coalition which captured the warehouse. +-- @param DCS#country.id Country which has captured the warehouse. +function WAREHOUSE:onafterCaptured(From, Event, To, Coalition, Country) + + -- Message. + local text=string.format("Warehouse %s: We were captured by enemy coalition (side=%d)!", self.alias, Coalition) + self:_InfoMessage(text) + +end + + +--- On after "AirbaseCaptured" event. Airbase of warehouse has been captured by another coalition. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param DCS#coalition.side Coalition which captured the warehouse. +function WAREHOUSE:onafterAirbaseCaptured(From, Event, To, Coalition) + + -- Message. + local text=string.format("Warehouse %s: Our airbase %s was captured by the enemy (coalition=%d)!", self.alias, self.airbasename, Coalition) + self:_InfoMessage(text) + + -- Debug smoke. + if self.Debug then + if Coalition==coalition.side.RED then + self.airbase:GetCoordinate():SmokeRed() + elseif Coalition==coalition.side.BLUE then + self.airbase:GetCoordinate():SmokeBlue() + end + end + + -- Set airbase to nil and category to no airbase. + self.airbase=nil +end + +--- On after "AirbaseRecaptured" event. Airbase of warehouse has been re-captured from other coalition. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param DCS#coalition.side Coalition Coalition side which originally captured the warehouse. +function WAREHOUSE:onafterAirbaseRecaptured(From, Event, To, Coalition) + + -- Message. + local text=string.format("Warehouse %s: We recaptured our airbase %s from the enemy (coalition=%d)!", self.alias, self.airbasename, Coalition) + self:_InfoMessage(text) + + -- Set airbase and category. + self.airbase=AIRBASE:FindByName(self.airbasename) + + -- Debug smoke. + if self.Debug then + if Coalition==coalition.side.RED then + self.airbase:GetCoordinate():SmokeRed() + elseif Coalition==coalition.side.BLUE then + self.airbase:GetCoordinate():SmokeBlue() + end + end + +end + +--- On after "RunwayDestroyed" event. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param DCS#coalition.side Coalition Coalition side which originally captured the warehouse. +function WAREHOUSE:onafterRunwayDestroyed(From, Event, To) + + -- Message. + local text=string.format("Warehouse %s: Runway %s destroyed!", self.alias, self.airbasename) + self:_InfoMessage(text) + + self.runwaydestroyed=timer.getAbsTime() + +end + +--- On after "RunwayRepaired" event. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function WAREHOUSE:onafterRunwayRepaired(From, Event, To) + + -- Message. + local text=string.format("Warehouse %s: Runway %s repaired!", self.alias, self.airbasename) + self:_InfoMessage(text) + + self.runwaydestroyed=nil + +end + + +--- On before "AssetSpawned" event. Checks whether the asset was already set to "spawned" for groups with multiple units. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Group#GROUP group The group spawned. +-- @param #WAREHOUSE.Assetitem asset The asset that is dead. +-- @param #WAREHOUSE.Pendingitem request The request of the dead asset. +function WAREHOUSE:onbeforeAssetSpawned(From, Event, To, group, asset, request) + if asset.spawned then + --return false + else + --return true + end + + return true +end + +--- On after "AssetSpawned" event triggered when an asset group is spawned into the cruel world. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Group#GROUP group The group spawned. +-- @param #WAREHOUSE.Assetitem asset The asset that is dead. +-- @param #WAREHOUSE.Pendingitem request The request of the dead asset. +function WAREHOUSE:onafterAssetSpawned(From, Event, To, group, asset, request) + local text=string.format("Asset %s from request id=%d was spawned!", asset.spawngroupname, request.uid) + self:T(self.lid..text) + + -- Sete asset state to spawned. + asset.spawned=true + + -- Check if all assets groups are spawned and trigger events. + local n=0 + for _,_asset in pairs(request.assets) do + local assetitem=_asset --#WAREHOUSE.Assetitem + + -- Debug info. + self:T(self.lid..string.format("Asset %s spawned %s as %s", assetitem.templatename, tostring(assetitem.spawned), tostring(assetitem.spawngroupname))) + + if assetitem.spawned then + n=n+1 + else + -- Now this can happend if multiple groups need to be spawned in one request. + --self:I(self.lid.."FF What?! This should not happen!") + end + + end + + -- Trigger event. + if n==request.nasset+request.ntransport then + self:T(self.lid..string.format("All assets %d (ncargo=%d + ntransport=%d) of request rid=%d spawned. Calling RequestSpawned", n, request.nasset, request.ntransport, request.uid)) + self:RequestSpawned(request, request.cargogroupset, request.transportgroupset) + else + self:T(self.lid..string.format("Not all assets %d (ncargo=%d + ntransport=%d) of request rid=%d spawned YET", n, request.nasset, request.ntransport, request.uid)) + end + +end + +--- On after "AssetDead" event triggered when an asset group died. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #WAREHOUSE.Assetitem asset The asset that is dead. +-- @param #WAREHOUSE.Pendingitem request The request of the dead asset. +function WAREHOUSE:onafterAssetDead(From, Event, To, asset, request) + local text=string.format("Asset %s from request id=%d is dead!", asset.templatename, request.uid) + self:T(self.lid..text) + self:_DebugMessage(text) +end + + +--- On after "Destroyed" event. Warehouse was destroyed. All services are stopped. Warehouse is going to "Stopped" state in one minute. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function WAREHOUSE:onafterDestroyed(From, Event, To) + + -- Message. + local text=string.format("Warehouse %s was destroyed! Assets lost %d. Respawn=%s", self.alias, #self.stock, tostring(self.respawnafterdestroyed)) + self:_InfoMessage(text) + + if self.respawnafterdestroyed then + + if self.respawndelay then + self:Pause() + self:__Respawn(self.respawndelay) + else + self:Respawn() + end + + else + + -- Remove all table entries from waiting queue and stock. + for k,_ in pairs(self.queue) do + self.queue[k]=nil + end + + for k,_ in pairs(self.stock) do + --self.stock[k]=nil + end + + for k=#self.stock,1,-1 do + --local asset=self.stock[k] --#WAREHOUSE.Assetitem + --self:AssetDead(asset, nil) + self.stock[k]=nil + end + + --self.queue=nil + --self.queue={} + + --self.stock=nil + --self.stock={} + end + +end + + +--- On after "Save" event. Warehouse assets are saved to file on disk. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string path Path where the file is saved. If nil, file is saved in the DCS root installtion directory. +-- @param #string filename (Optional) Name of the file containing the asset data. +function WAREHOUSE:onafterSave(From, Event, To, path, filename) + + local function _savefile(filename, data) + local f = assert(io.open(filename, "wb")) + f:write(data) + f:close() + end + + -- Set file name. + filename=filename or string.format("WAREHOUSE-%d_%s.txt", self.uid, self.alias) + + -- Set path. + if path~=nil then + filename=path.."\\"..filename + end + + -- Info + local text=string.format("Saving warehouse assets to file %s", filename) + MESSAGE:New(text,30):ToAllIf(self.Debug or self.Report) + self:I(self.lid..text) + + local warehouseassets="" + warehouseassets=warehouseassets..string.format("coalition=%d\n", self:GetCoalition()) + warehouseassets=warehouseassets..string.format("country=%d\n", self:GetCountry()) + + -- Loop over all assets in stock. + for _,_asset in pairs(self.stock) do + local asset=_asset -- #WAREHOUSE.Assetitem + + -- Loop over asset parameters. + local assetstring="" + for key,value in pairs(asset) do + + -- Only save keys which are needed to restore the asset. + if key=="templatename" or key=="attribute" or key=="cargobay" or key=="weight" or key=="loadradius" or key=="livery" or key=="skill" or key=="assignment" then + local name + if type(value)=="table" then + name=string.format("%s=%s;", key, value[1]) + else + name=string.format("%s=%s;", key, value) + end + assetstring=assetstring..name + end + self:I(string.format("Loaded asset: %s", assetstring)) + end + + -- Add asset string. + warehouseassets=warehouseassets..assetstring.."\n" + end + + -- Save file. + _savefile(filename, warehouseassets) + +end + + +--- On before "Load" event. Checks if the file the warehouse data should be loaded from exists. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string path Path where the file is loaded from. +-- @param #string filename (Optional) Name of the file containing the asset data. +function WAREHOUSE:onbeforeLoad(From, Event, To, path, filename) + + + local function _fileexists(name) + local f=io.open(name,"r") + if f~=nil then + io.close(f) + return true + else + return false + end + end + + -- Set file name. + filename=filename or string.format("WAREHOUSE-%d_%s.txt", self.uid, self.alias) + + -- Set path. + if path~=nil then + filename=path.."\\"..filename + end + + -- Check if file exists. + local exists=_fileexists(filename) + + if exists then + return true + else + self:_ErrorMessage(string.format("ERROR: file %s does not exist! Cannot load assets.", filename), 60) + return false + end + +end + + +--- On after "Load" event. Warehouse assets are loaded from file on disk. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string path Path where the file is loaded from. +-- @param #string filename (Optional) Name of the file containing the asset data. +function WAREHOUSE:onafterLoad(From, Event, To, path, filename) + + local function _loadfile(filename) + local f = assert(io.open(filename, "rb")) + local data = f:read("*all") + f:close() + return data + end + + -- Set file name. + filename=filename or string.format("WAREHOUSE-%d_%s.txt", self.uid, self.alias) + + -- Set path. + if path~=nil then + filename=path.."\\"..filename + end + + -- Info + local text=string.format("Loading warehouse assets from file %s", filename) + MESSAGE:New(text,30):ToAllIf(self.Debug or self.Report) + self:I(self.lid..text) + + -- Load asset data from file. + local data=_loadfile(filename) + + -- Split by line break. + local assetdata=UTILS.Split(data,"\n") + + -- Coalition and coutrny. + local Coalition + local Country + + -- Loop over asset lines. + local assets={} + for _,asset in pairs(assetdata) do + + -- Parameters are separated by semi-colons + local descriptors=UTILS.Split(asset,";") + + local asset={} + local isasset=false + for _,descriptor in pairs(descriptors) do + + local keyval=UTILS.Split(descriptor,"=") + + if #keyval==2 then + + if keyval[1]=="coalition" then + -- Get coalition side. + Coalition=tonumber(keyval[2]) + elseif keyval[1]=="country" then + -- Get country id. + Country=tonumber(keyval[2]) + else + + -- This is an asset. + isasset=true + + local key=keyval[1] + local val=keyval[2] + + --env.info(string.format("FF asset key=%s val=%s", key, val)) + + -- Livery or skill could be "nil". + if val=="nil" then + val=nil + end + + -- Convert string to number where necessary. + if key=="cargobay" or key=="weight" or key=="loadradius" then + asset[key]=tonumber(val) + else + asset[key]=val + end + end + + end + end + + -- Add to table. + if isasset then + table.insert(assets, asset) + end + end + + -- Respawn warehouse with prev coalition if necessary. + if Country~=self:GetCountry() then + self:T(self.lid..string.format("Changing warehouse country %d-->%d on loading assets.", self:GetCountry(), Country)) + self:ChangeCountry(Country) + end + + for _,_asset in pairs(assets) do + local asset=_asset --#WAREHOUSE.Assetitem + + local group=GROUP:FindByName(asset.templatename) + if group then + self:AddAsset(group, 1, asset.attribute, asset.cargobay, asset.weight, asset.loadradius, asset.skill, asset.livery, asset.assignment) + else + self:E(string.format("ERROR: Group %s doest not exit. Cannot be loaded as asset.", tostring(asset.templatename))) + end + end + +end + +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Spawn functions +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Spawns requested assets at warehouse or associated airbase. +-- @param #WAREHOUSE self +-- @param #WAREHOUSE.Queueitem Request Information table of the request. +function WAREHOUSE:_SpawnAssetRequest(Request) + self:F2({requestUID=Request.uid}) + + -- Shortcut to cargo assets. + local cargoassets=Request.cargoassets + + -- Now we try to find all parking spots for all cargo groups in advance. Due to the for loop, the parking spots do not get updated while spawning. + local Parking={} + if Request.cargocategory==Group.Category.AIRPLANE or Request.cargocategory==Group.Category.HELICOPTER then + Parking=self:_FindParkingForAssets(self.airbase, cargoassets) or {} + end + + -- Spawn aircraft in uncontrolled state. + local UnControlled=true + + -- Loop over cargo requests. + for i=1,#cargoassets do + + -- Get stock item. + local asset=cargoassets[i] --#WAREHOUSE.Assetitem + + -- Set asset status to not spawned until we capture its birth event. + asset.spawned=false + asset.iscargo=true + + -- Set request ID. + asset.rid=Request.uid + + -- Spawn group name. + local _alias=asset.spawngroupname + + --Request add asset by id. + Request.assets[asset.uid]=asset + + -- Spawn an asset group. + local _group=nil --Wrapper.Group#GROUP + if asset.category==Group.Category.GROUND then + + -- Spawn ground troops. + _group=self:_SpawnAssetGroundNaval(_alias, asset, Request, self.spawnzone) + + elseif asset.category==Group.Category.AIRPLANE or asset.category==Group.Category.HELICOPTER then + + -- Spawn air units. + if Parking[asset.uid] then + _group=self:_SpawnAssetAircraft(_alias, asset, Request, Parking[asset.uid], UnControlled) + else + _group=self:_SpawnAssetAircraft(_alias, asset, Request, nil, UnControlled) + end + + elseif asset.category==Group.Category.TRAIN then + + -- Spawn train. + if self.rail then + --TODO: Rail should only get one asset because they would spawn on top! + + -- Spawn naval assets. + _group=self:_SpawnAssetGroundNaval(_alias, asset, Request, self.spawnzone) + end + + --self:E(self.lid.."ERROR: Spawning of TRAIN assets not possible yet!") + + elseif asset.category==Group.Category.SHIP then + + -- Spawn naval assets. + _group=self:_SpawnAssetGroundNaval(_alias, asset, Request, self.portzone) + + else + self:E(self.lid.."ERROR: Unknown asset category!") + end + + end + +end + + +--- Spawn a ground or naval asset in the corresponding spawn zone of the warehouse. +-- @param #WAREHOUSE self +-- @param #string alias Alias name of the asset group. +-- @param #WAREHOUSE.Assetitem asset Ground asset that will be spawned. +-- @param #WAREHOUSE.Queueitem request Request belonging to this asset. Needed for the name/alias. +-- @param Core.Zone#ZONE spawnzone Zone where the assets should be spawned. +-- @param #boolean aioff If true, AI of ground units are set to off. +-- @return Wrapper.Group#GROUP The spawned group or nil if the group could not be spawned. +function WAREHOUSE:_SpawnAssetGroundNaval(alias, asset, request, spawnzone, aioff) + + if asset and (asset.category==Group.Category.GROUND or asset.category==Group.Category.SHIP or asset.category==Group.Category.TRAIN) then + + -- Prepare spawn template. + local template=self:_SpawnAssetPrepareTemplate(asset, alias) + + -- Initial spawn point. + template.route.points[1]={} + + -- Get a random coordinate in the spawn zone. + local coord=spawnzone:GetRandomCoordinate() + + -- For trains, we use the rail connection point. + if asset.category==Group.Category.TRAIN then + coord=self.rail + end + + -- Translate the position of the units. + for i=1,#template.units do + + -- Unit template. + local unit = template.units[i] + + -- Translate position. + local SX = unit.x or 0 + local SY = unit.y or 0 + local BX = asset.template.route.points[1].x + local BY = asset.template.route.points[1].y + local TX = coord.x + (SX-BX) + local TY = coord.z + (SY-BY) + + template.units[i].x = TX + template.units[i].y = TY + + if asset.livery then + unit.livery_id = asset.livery + end + if asset.skill then + unit.skill= asset.skill + end + + end + + template.route.points[1].x = coord.x + template.route.points[1].y = coord.z + + template.x = coord.x + template.y = coord.z + template.alt = coord.y + + -- Spawn group. + local group=_DATABASE:Spawn(template) --Wrapper.Group#GROUP + + -- Activate group. Should only be necessary for late activated groups. + --group:Activate() + + -- Switch AI off if desired. This works only for ground and naval groups. + if aioff then + group:SetAIOff() + end + + return group + end + + return nil +end + +--- Spawn an aircraft asset (plane or helo) at the airbase associated with the warehouse. +-- @param #WAREHOUSE self +-- @param #string alias Alias name of the asset group. +-- @param #WAREHOUSE.Assetitem asset Ground asset that will be spawned. +-- @param #WAREHOUSE.Queueitem request Request belonging to this asset. Needed for the name/alias. +-- @param #table parking Parking data for this asset. +-- @param #boolean uncontrolled Spawn aircraft in uncontrolled state. +-- @return Wrapper.Group#GROUP The spawned group or nil if the group could not be spawned. +function WAREHOUSE:_SpawnAssetAircraft(alias, asset, request, parking, uncontrolled) + + if asset and asset.category==Group.Category.AIRPLANE or asset.category==Group.Category.HELICOPTER then + + -- Prepare the spawn template. + local template=self:_SpawnAssetPrepareTemplate(asset, alias) + + -- Cold start (default). + local _type=COORDINATE.WaypointType.TakeOffParking + local _action=COORDINATE.WaypointAction.FromParkingArea + + -- Hot start. + if asset.takeoffType and asset.takeoffType==COORDINATE.WaypointType.TakeOffParkingHot then + _type=COORDINATE.WaypointType.TakeOffParkingHot + _action=COORDINATE.WaypointAction.FromParkingAreaHot + uncontrolled=false + end + + + -- Set route points. + if request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then + + -- Get flight path if the group goes to another warehouse by itself. + if request.toself then + local wp=self.airbase:GetCoordinate():WaypointAir("RADIO", _type, _action, 0, false, self.airbase, {}, "Parking") + template.route.points={wp} + else + template.route.points=self:_GetFlightplan(asset, self.airbase, request.warehouse.airbase) + end + + else + + -- First route point is the warehouse airbase. + template.route.points[1]=self.airbase:GetCoordinate():WaypointAir("BARO", _type, _action, 0, true, self.airbase, nil, "Spawnpoint") + + end + + -- Get airbase ID and category. + local AirbaseID = self.airbase:GetID() + local AirbaseCategory = self:GetAirbaseCategory() + + -- Check enough parking spots. + if AirbaseCategory==Airbase.Category.HELIPAD or AirbaseCategory==Airbase.Category.SHIP then + + --TODO Figure out what's necessary in this case. + + else + + if #parking<#template.units then + local text=string.format("ERROR: Not enough parking! Free parking = %d < %d aircraft to be spawned.", #parking, #template.units) + self:_DebugMessage(text) + return nil + end + + end + + -- Position the units. + for i=1,#template.units do + + -- Unit template. + local unit = template.units[i] + + if AirbaseCategory == Airbase.Category.HELIPAD or AirbaseCategory == Airbase.Category.SHIP then + + -- Helipads we take the position of the airbase location, since the exact location of the spawn point does not make sense. + local coord=self.airbase:GetCoordinate() + + unit.x=coord.x + unit.y=coord.z + unit.alt=coord.y + + unit.parking_id = nil + unit.parking = nil + + else + + local coord=parking[i].Coordinate --Core.Point#COORDINATE + local terminal=parking[i].TerminalID --#number + + if self.Debug then + coord:MarkToAll(string.format("Spawnplace unit %s terminal %d.", unit.name, terminal)) + end + + unit.x=coord.x + unit.y=coord.z + unit.alt=coord.y + + unit.parking_id = nil + unit.parking = terminal + + end + + if asset.livery then + unit.livery_id = asset.livery + end + + if asset.skill then + unit.skill= asset.skill + end + + if asset.payload then + unit.payload=asset.payload.pylons + end + + if asset.modex then + unit.onboard_num=asset.modex[i] + end + if asset.callsign then + unit.callsign=asset.callsign[i] + end + + end + + -- And template position. + template.x = template.units[1].x + template.y = template.units[1].y + + -- DCS bug workaround. Spawning helos in uncontrolled state on carriers causes a big spash! + -- See https://forums.eagle.ru/showthread.php?t=219550 + -- Should be solved in latest OB update 2.5.3.21708 + --if AirbaseCategory == Airbase.Category.SHIP and asset.category==Group.Category.HELICOPTER then + -- uncontrolled=false + --end + + -- Uncontrolled spawning. + template.uncontrolled=uncontrolled + + -- Debug info. + self:T2({airtemplate=template}) + + -- Spawn group. + local group=_DATABASE:Spawn(template) --Wrapper.Group#GROUP + + return group + end + + return nil +end + + +--- Prepare a spawn template for the asset. Deep copy of asset template, adjusting template and unit names, nillifying group and unit ids. +-- @param #WAREHOUSE self +-- @param #WAREHOUSE.Assetitem asset Ground asset that will be spawned. +-- @param #string alias Alias name of the group. +-- @return #table Prepared new spawn template. +function WAREHOUSE:_SpawnAssetPrepareTemplate(asset, alias) + + -- Create an own copy of the template! + local template=UTILS.DeepCopy(asset.template) + + -- Set unique name. + template.name=alias + + -- Set current(!) coalition and country. + template.CoalitionID=self:GetCoalition() + template.CountryID=self:GetCountry() + + -- Nillify the group ID. + template.groupId=nil + + -- No late activation. + template.lateActivation=false + + if asset.missionTask then + self:T(self.lid..string.format("Setting mission task to %s", tostring(asset.missionTask))) + template.task=asset.missionTask + end + + -- No predefined task. + --template.taskSelected=false + + -- Set and empty route. + template.route = {} + template.route.routeRelativeTOT=true + template.route.points = {} + + -- Handle units. + for i=1,#template.units do + + -- Unit template. + local unit = template.units[i] + + -- Nillify the unit ID. + unit.unitId=nil + + -- Set unit name: -01, -02, ... + unit.name=string.format("%s-%02d", template.name , i) + + end + + return template +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Routing functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Route ground units to destination. ROE is set to return fire and alarm state to green. +-- @param #WAREHOUSE self +-- @param Wrapper.Group#GROUP group The ground group to be routed +-- @param #WAREHOUSE.Queueitem request The request for this group. +function WAREHOUSE:_RouteGround(group, request) + + if group and group:IsAlive() then + + -- Set speed to 70% of max possible. + local _speed=group:GetSpeedMax()*0.7 + + -- Route waypoints. + local Waypoints={} + + -- Check if an off road path has been defined. + local hasoffroad=self:HasConnectionOffRoad(request.warehouse, self.Debug) + + -- Check if any off road paths have be defined. They have priority! + if hasoffroad then + + -- Get off road path to remote warehouse. If more have been defined, pick one randomly. + local remotename=request.warehouse.warehouse:GetName() + local path=self.offroadpaths[remotename][math.random(#self.offroadpaths[remotename])] + + -- Loop over user defined shipping lanes. + for i=1,#path do + + -- Shortcut and coordinate intellisense. + local coord=path[i] --Core.Point#COORDINATE + + -- Get waypoint for coordinate. + local Waypoint=coord:WaypointGround(_speed, "Off Road") + + -- Add waypoint to route. + table.insert(Waypoints, Waypoint) + end + + else + + -- Waypoints for road-to-road connection. + Waypoints = group:TaskGroundOnRoad(request.warehouse.road, _speed, "Off Road", false, self.road) + + -- First waypoint = current position of the group. + local FromWP=group:GetCoordinate():WaypointGround(_speed, "Off Road") + table.insert(Waypoints, 1, FromWP) + + -- Final coordinate. Note, this can lead to errors if the final WP is too close the the point on the road. The vehicle will stop driving and not reach the final WP! + --local ToCO=request.warehouse.spawnzone:GetRandomCoordinate() + --local ToWP=ToCO:WaypointGround(_speed, "Off Road") + --table.insert(Waypoints, #Waypoints+1, ToWP) + + end + + for n,wp in ipairs(Waypoints) do + env.info(n) + local tf=self:_SimpleTaskFunctionWP("warehouse:_PassingWaypoint",group, n, #Waypoints) + group:SetTaskWaypoint(wp, tf) + end + + -- Task function triggering the arrived event at the last waypoint. + --local TaskFunction = self:_SimpleTaskFunction("warehouse:_Arrived", group) + + -- Put task function on last waypoint. + --local Waypoint = Waypoints[#Waypoints] + --group:SetTaskWaypoint(Waypoint, TaskFunction) + + -- Route group to destination. + group:Route(Waypoints, 1) + + -- Set ROE and alaram state. + group:OptionROEReturnFire() + group:OptionAlarmStateGreen() + end +end + +--- Route naval units along user defined shipping lanes to destination warehouse. ROE is set to return fire. +-- @param #WAREHOUSE self +-- @param Wrapper.Group#GROUP group The naval group to be routed +-- @param #WAREHOUSE.Queueitem request The request for this group. +function WAREHOUSE:_RouteNaval(group, request) + + -- Check if we have a group and it is alive. + if group and group:IsAlive() then + + -- Set speed to 80% of max possible. + local _speed=group:GetSpeedMax()*0.8 + + -- Get shipping lane to remote warehouse. If more have been defined, pick one randomly. + local remotename=request.warehouse.warehouse:GetName() + local lane=self.shippinglanes[remotename][math.random(#self.shippinglanes[remotename])] + + if lane then + + -- Route waypoints. + local Waypoints={} + + -- Loop over user defined shipping lanes. + for i=1,#lane do + + -- Shortcut and coordinate intellisense. + local coord=lane[i] --Core.Point#COORDINATE + + -- Get waypoint for coordinate. + local Waypoint=coord:WaypointGround(_speed) + + -- Add waypoint to route. + table.insert(Waypoints, Waypoint) + end + + -- Task function triggering the arrived event at the last waypoint. + local TaskFunction = self:_SimpleTaskFunction("warehouse:_Arrived", group) + + -- Put task function on last waypoint. + local Waypoint = Waypoints[#Waypoints] + group:SetTaskWaypoint(Waypoint, TaskFunction) + + -- Route group to destination. + group:Route(Waypoints, 1) + + -- Set ROE (Naval units dont have and alaram state.) + group:OptionROEReturnFire() + + else + -- This should not happen! Existance of shipping lane was checked before executing this request. + self:E(self.lid..string.format("ERROR: No shipping lane defined for Naval asset!")) + end + + end +end + + +--- Route the airplane from one airbase another. Activates uncontrolled aircraft and sets ROE/ROT for ferry flights. +-- ROE is set to return fire and ROT to passive defence. +-- @param #WAREHOUSE self +-- @param Wrapper.Group#GROUP aircraft Airplane group to be routed. +function WAREHOUSE:_RouteAir(aircraft) + + if aircraft and aircraft:IsAlive()~=nil then + + -- Debug info. + self:T2(self.lid..string.format("RouteAir aircraft group %s alive=%s", aircraft:GetName(), tostring(aircraft:IsAlive()))) + + -- Give start command to activate uncontrolled aircraft within the next 60 seconds. + local starttime=math.random(60) + + aircraft:StartUncontrolled(starttime) + + -- Debug info. + self:T2(self.lid..string.format("RouteAir aircraft group %s alive=%s (after start command)", aircraft:GetName(), tostring(aircraft:IsAlive()))) + + -- Set ROE and alaram state. + aircraft:OptionROEReturnFire() + aircraft:OptionROTPassiveDefense() + + else + self:E(string.format("ERROR: aircraft %s cannot be routed since it does not exist or is not alive %s!", tostring(aircraft:GetName()), tostring(aircraft:IsAlive()))) + end +end + +--- Route trains to their destination - or at least to the closest point on rail of the desired final destination. +-- @param #WAREHOUSE self +-- @param Wrapper.Group#GROUP Group The train group. +-- @param Core.Point#COORDINATE Coordinate of the destination. Tail will be routed to the closest point +-- @param #number Speed Speed in km/h to drive to the destination coordinate. Default is 60% of max possible speed the unit can go. +function WAREHOUSE:_RouteTrain(Group, Coordinate, Speed) + + if Group and Group:IsAlive() then + + local _speed=Speed or Group:GetSpeedMax()*0.6 + + -- Create a + local Waypoints = Group:TaskGroundOnRailRoads(Coordinate, Speed) + + -- Task function triggering the arrived event at the last waypoint. + local TaskFunction = self:_SimpleTaskFunction("warehouse:_Arrived", Group) + + -- Put task function on last waypoint. + local Waypoint = Waypoints[#Waypoints] + Group:SetTaskWaypoint( Waypoint, TaskFunction ) + + -- Route group to destination. + Group:Route(Waypoints, 1) + end +end + +--- Task function for last waypoint. Triggering the "Arrived" event. +-- @param #WAREHOUSE self +-- @param Wrapper.Group#GROUP group The group that arrived. +function WAREHOUSE:_Arrived(group) + self:_DebugMessage(string.format("Group %s arrived!", tostring(group:GetName()))) + + if group then + --Trigger "Arrived event. + self:__Arrived(1, group) + end + +end + +--- Task function for when passing a waypoint. +-- @param #WAREHOUSE self +-- @param Wrapper.Group#GROUP group The group that arrived. +-- @param #number n Waypoint passed. +-- @param #number N Final waypoint. +function WAREHOUSE:_PassingWaypoint(group, n, N) + self:T(self.lid..string.format("Group %s passing waypoint %d of %d!", tostring(group:GetName()), n, N)) + + -- Final waypoint reached. + if n==N then + self:__Arrived(1, group) + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Event handler functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get a warehouse asset from its unique id. +-- @param #WAREHOUSE self +-- @param #number id Asset ID. +-- @return #WAREHOUSE.Assetitem The warehouse asset. +function WAREHOUSE:GetAssetByID(id) + if id then + return _WAREHOUSEDB.Assets[id] + else + return nil + end +end + +--- Get a warehouse asset from its name. +-- @param #WAREHOUSE self +-- @param #string GroupName Spawn group name. +-- @return #WAREHOUSE.Assetitem The warehouse asset. +function WAREHOUSE:GetAssetByName(GroupName) + + local name=self:_GetNameWithOut(GroupName) + local _,aid,_=self:_GetIDsFromGroup(GROUP:FindByName(name)) + + if aid then + return _WAREHOUSEDB.Assets[aid] + else + return nil + end +end + +--- Get a warehouse request from its unique id. +-- @param #WAREHOUSE self +-- @param #number id Request ID. +-- @return #WAREHOUSE.Pendingitem The warehouse requested - either queued or pending. +-- @return #boolean If *true*, request is queued, if *false*, request is pending, if *nil*, request could not be found. +function WAREHOUSE:GetRequestByID(id) + + if id then + + for _,_request in pairs(self.queue) do + local request=_request --#WAREHOUSE.Queueitem + if request.uid==id then + return request, true + end + end + + for _,_request in pairs(self.pending) do + local request=_request --#WAREHOUSE.Pendingitem + if request.uid==id then + return request, false + end + end + + end + + return nil,nil +end + +--- Warehouse event function, handling the birth of a unit. +-- @param #WAREHOUSE self +-- @param Core.Event#EVENTDATA EventData Event data. +function WAREHOUSE:_OnEventBirth(EventData) + self:T3(self.lid..string.format("Warehouse %s (id=%s) captured event birth!", self.alias, self.uid)) + + if EventData and EventData.IniGroup then + local group=EventData.IniGroup + + -- Note: Remember, group:IsAlive might(?) not return true here. + local wid,aid,rid=self:_GetIDsFromGroup(group) + + if wid==self.uid then + + -- Get asset and request from id. + local asset=self:GetAssetByID(aid) + local request=self:GetRequestByID(rid) + + if asset and request then + + -- Debug message. + self:T(self.lid..string.format("Warehouse %s captured event birth of request ID=%d, asset ID=%d, unit %s spawned=%s", self.alias, request.uid, asset.uid, EventData.IniUnitName, tostring(asset.spawned))) + + -- Set born to true. + request.born=true + + -- Birth is triggered for each unit. We need to make sure not to call this too often! + if not asset.spawned then + + -- Remove asset from stock. + self:_DeleteStockItem(asset) + + -- Set spawned switch. + asset.spawned=true + asset.spawngroupname=group:GetName() + + -- Add group. + if asset.iscargo==true then + request.cargogroupset=request.cargogroupset or SET_GROUP:New() + request.cargogroupset:AddGroup(group) + else + request.transportgroupset=request.transportgroupset or SET_GROUP:New() + request.transportgroupset:AddGroup(group) + end + + -- Set warehouse state. + group:SetState(group, "WAREHOUSE", self) + + -- Asset spawned FSM function. + --self:__AssetSpawned(1, group, asset, request) + --env.info(string.format("FF asset spawned %s, %s", asset.spawngroupname, EventData.IniUnitName)) + self:AssetSpawned(group, asset, request) + + end + + else + self:E(self.lid..string.format("ERROR: Either asset AID=%s or request RID=%s are nil in event birth of unit %s", tostring(aid), tostring(rid), tostring(EventData.IniUnitName))) + end + + else + --self:T3({wid=wid, uid=self.uid, match=(wid==self.uid), tw=type(wid), tu=type(self.uid)}) + end + + end +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Function handling the event when a (warehouse) unit starts its engines. +-- @param #WAREHOUSE self +-- @param Core.Event#EVENTDATA EventData Event data. +function WAREHOUSE:_OnEventEngineStartup(EventData) + self:T3(self.lid..string.format("Warehouse %s captured event engine startup!",self.alias)) + + if EventData and EventData.IniGroup then + local group=EventData.IniGroup + local wid,aid,rid=self:_GetIDsFromGroup(group) + if wid==self.uid then + self:T(self.lid..string.format("Warehouse %s captured event engine startup of its asset unit %s.", self.alias, EventData.IniUnitName)) + end + end +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Function handling the event when a (warehouse) unit takes off. +-- @param #WAREHOUSE self +-- @param Core.Event#EVENTDATA EventData Event data. +function WAREHOUSE:_OnEventTakeOff(EventData) + self:T3(self.lid..string.format("Warehouse %s captured event takeoff!",self.alias)) + + if EventData and EventData.IniGroup then + local group=EventData.IniGroup + local wid,aid,rid=self:_GetIDsFromGroup(group) + if wid==self.uid then + self:T(self.lid..string.format("Warehouse %s captured event takeoff of its asset unit %s.", self.alias, EventData.IniUnitName)) + end + end +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Function handling the event when a (warehouse) unit lands. +-- @param #WAREHOUSE self +-- @param Core.Event#EVENTDATA EventData Event data. +function WAREHOUSE:_OnEventLanding(EventData) + self:T3(self.lid..string.format("Warehouse %s captured event landing!", self.alias)) + + if EventData and EventData.IniGroup then + local group=EventData.IniGroup + + -- Try to get UIDs from group name. + local wid,aid,rid=self:_GetIDsFromGroup(group) + + -- Check that this group belongs to this warehouse. + if wid~=nil and wid==self.uid then + + -- Debug info. + self:T(self.lid..string.format("Warehouse %s captured event landing of its asset unit %s.", self.alias, EventData.IniUnitName)) + + end + end +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Function handling the event when a (warehouse) unit shuts down its engines. +-- @param #WAREHOUSE self +-- @param Core.Event#EVENTDATA EventData Event data. +function WAREHOUSE:_OnEventEngineShutdown(EventData) + self:T3(self.lid..string.format("Warehouse %s captured event engine shutdown!", self.alias)) + + if EventData and EventData.IniGroup then + local group=EventData.IniGroup + local wid,aid,rid=self:_GetIDsFromGroup(group) + if wid==self.uid then + self:T(self.lid..string.format("Warehouse %s captured event engine shutdown of its asset unit %s.", self.alias, EventData.IniUnitName)) + end + end +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Arrived event if an air unit/group arrived at its destination. This can be an engine shutdown or a landing event. +-- @param #WAREHOUSE self +-- @param Core.Event#EVENTDATA EventData Event data table. +function WAREHOUSE:_OnEventArrived(EventData) + + if EventData and EventData.IniUnit then + + -- Unit that arrived. + local unit=EventData.IniUnit + + -- Check if unit is alive and on the ground. Engine shutdown can also be triggered in other situations! + if unit and unit:IsAlive()==true and unit:InAir()==false then + + -- Get group. + local group=EventData.IniGroup + + -- Get unique IDs from group name. + local wid,aid,rid=self:_GetIDsFromGroup(group) + + -- If all IDs are good we can assume it is a warehouse asset. + if wid~=nil and aid~=nil and rid~=nil then + + -- Check that warehouse ID is right. + if self.uid==wid then + + local request=self:_GetRequestOfGroup(group, self.pending) + + -- Better check that the request still exists, because for a group with more units, the + if request then + + local istransport=self:_GroupIsTransport(group, request) + + -- Get closest airbase. + local closest=group:GetCoordinate():GetClosestAirbase() + + -- Check if engine shutdown happend at right airbase because the event is also triggered in other situations. + local rightairbase=closest:GetName()==request.warehouse:GetAirbase():GetName() + + -- Check that group is cargo and not transport. + if istransport==false and rightairbase then + + -- Trigger arrived event for this group. Note that each unit of a group will trigger this event. So the onafterArrived function needs to take care of that. + -- Actually, we only take the first unit of the group that arrives. If it does, we assume the whole group arrived, which might not be the case, since + -- some units might still be taxiing or whatever. Therefore, we add 10 seconds for each additional unit of the group until the first arrived event is triggered. + local nunits=#group:GetUnits() + local dt=10*(nunits-1)+1 -- one unit = 1 sec, two units = 11 sec, three units = 21 sec before we call the group arrived. + + -- Debug info. + if self.verbosity>=1 then + local text=string.format("Air asset group %s from warehouse %s arrived at its destination. Trigger Arrived event in %d sec", group:GetName(), self.alias, dt) + self:_InfoMessage(text) + end + + -- Arrived event. + self:__Arrived(dt, group) + end + + end + end + + else + self:T3(string.format("Group that arrived did not belong to a warehouse. Warehouse ID=%s, Asset ID=%s, Request ID=%s.", tostring(wid), tostring(aid), tostring(rid))) + end + end + end + +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Warehouse event handling function. +-- @param #WAREHOUSE self +-- @param Core.Event#EVENTDATA EventData Event data. +function WAREHOUSE:_OnEventCrashOrDead(EventData) + self:T3(self.lid..string.format("Warehouse %s captured event dead or crash!", self.alias)) + + if EventData then + + -- Check if warehouse was destroyed. We compare the name of the destroyed unit. + if EventData.IniUnitName then + local warehousename=self.warehouse:GetName() + if EventData.IniUnitName==warehousename then + self:_DebugMessage(string.format("Warehouse %s alias %s was destroyed!", warehousename, self.alias)) + + -- Trigger Destroyed event. + self:Destroyed() + end + if self.airbase and self.airbasename and self.airbasename==EventData.IniUnitName then + self:RunwayDestroyed() + end + end + + --self:I(self.lid..string.format("Warehouse %s captured event dead or crash or unit %s.", self.alias, tostring(EventData.IniUnitName))) + + -- Check if an asset unit was destroyed. + if EventData.IniGroup then + + -- Group initiating the event. + local group=EventData.IniGroup + + -- Get warehouse, asset and request IDs from the group name. + local wid,aid,rid=self:_GetIDsFromGroup(group) + + -- Check that we have the right warehouse. + if wid==self.uid then + + -- Debug message. + self:T(self.lid..string.format("Warehouse %s captured event dead or crash of its asset unit %s.", self.alias, EventData.IniUnitName)) + + -- Loop over all pending requests and get the one belonging to this unit. + for _,request in pairs(self.pending) do + local request=request --#WAREHOUSE.Pendingitem + + -- This is the right request. + if request.uid==rid then + + -- Update cargo and transport group sets of this request. We need to know if this job is finished. + self:_UnitDead(EventData.IniUnit, request) + + end + end + end + end + end +end + +--- A unit of a group just died. Update group sets in request. +-- This is important in order to determine if a job is done and can be removed from the (pending) queue. +-- @param #WAREHOUSE self +-- @param Wrapper.Unit#UNIT deadunit Unit that died. +-- @param #WAREHOUSE.Pendingitem request Request that needs to be updated. +function WAREHOUSE:_UnitDead(deadunit, request) + + -- Flare unit. + if self.Debug then + deadunit:FlareRed() + end + + -- Group the dead unit belongs to. + local group=deadunit:GetGroup() + + -- Number of alive units in group. + local nalive=group:CountAliveUnits() + + -- Whole group is dead? + local groupdead=true + if nalive>0 then + groupdead=false + end + + -- Here I need to get rid of the #CARGO at the end to obtain the original name again! + local unitname=self:_GetNameWithOut(deadunit) + local groupname=self:_GetNameWithOut(group) + + -- Group is dead! + if groupdead then + self:T(self.lid..string.format("Group %s (transport=%s) is dead!", groupname, tostring(self:_GroupIsTransport(group,request)))) + if self.Debug then + group:SmokeWhite() + end + -- Trigger AssetDead event. + local asset=self:FindAssetInDB(group) + self:AssetDead(asset, request) + end + + + -- Dont trigger a Remove event for the group sets. + local NoTriggerEvent=true + + if request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then + + --- + -- Easy case: Group can simply be removed from the cargogroupset. + --- + + -- Remove dead group from cargo group set. + if groupdead==true then + request.cargogroupset:Remove(groupname, NoTriggerEvent) + self:T(self.lid..string.format("Removed selfpropelled cargo %s: ncargo=%d.", groupname, request.cargogroupset:Count())) + end + + else + + --- + -- Complicated case: Dead unit could be: + -- 1.) A Cargo unit (e.g. waiting to be picked up). + -- 2.) A Transport unit which itself holds cargo groups. + --- + + -- Check if this a cargo or transport group. + local istransport=self:_GroupIsTransport(group,request) + + if istransport==true then + + -- Get the carrier unit table holding the cargo groups inside this carrier. + local cargogroupnames=request.carriercargo[unitname] + + if cargogroupnames then + + -- Loop over all groups inside the destroyed carrier ==> all dead. + for _,cargoname in pairs(cargogroupnames) do + request.cargogroupset:Remove(cargoname, NoTriggerEvent) + self:T(self.lid..string.format("Removed transported cargo %s inside dead carrier %s: ncargo=%d", cargoname, unitname, request.cargogroupset:Count())) + end + + end + + -- Whole carrier group is dead. Remove it from the carrier group set. + if groupdead then + request.transportgroupset:Remove(groupname, NoTriggerEvent) + self:T(self.lid..string.format("Removed transport %s: ntransport=%d", groupname, request.transportgroupset:Count())) + end + + elseif istransport==false then + + -- This must have been an alive cargo group that was killed outside the carrier, e.g. waiting to be transported or waiting to be put back. + -- Remove dead group from cargo group set. + if groupdead==true then + request.cargogroupset:Remove(groupname, NoTriggerEvent) + self:T(self.lid..string.format("Removed transported cargo %s outside carrier: ncargo=%d", groupname, request.cargogroupset:Count())) + -- This as well? + --request.transportcargoset:RemoveCargosByName(RemoveCargoNames) + end + + else + self:E(self.lid..string.format("ERROR: Group %s is neither cargo nor transport!", group:GetName())) + end + end + +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Warehouse event handling function. +-- Handles the case when the airbase associated with the warehous is captured. +-- @param #WAREHOUSE self +-- @param Core.Event#EVENTDATA EventData Event data. +function WAREHOUSE:_OnEventBaseCaptured(EventData) + self:T3(self.lid..string.format("Warehouse %s captured event base captured!",self.alias)) + + -- This warehouse does not have an airbase and never had one. So it could not have been captured. + if self.airbasename==nil then + return + end + + if EventData and EventData.Place then + + -- Place is the airbase that was captured. + local airbase=EventData.Place --Wrapper.Airbase#AIRBASE + + -- Check that this airbase belongs or did belong to this warehouse. + if EventData.PlaceName==self.airbasename then + + -- New coalition of airbase after it was captured. + local NewCoalitionAirbase=airbase:GetCoalition() + + -- Debug info + self:T(self.lid..string.format("Airbase of warehouse %s (coalition ID=%d) was captured! New owner coalition ID=%d.",self.alias, self:GetCoalition(), NewCoalitionAirbase)) + + -- So what can happen? + -- Warehouse is blue, airbase is blue and belongs to warehouse and red captures it ==> self.airbase=nil + -- Warehouse is blue, airbase is blue self.airbase is nil and blue (re-)captures it ==> self.airbase=Event.Place + if self.airbase==nil then + -- New coalition is the same as of the warehouse ==> warehouse previously lost this airbase and now it was re-captured. + if NewCoalitionAirbase == self:GetCoalition() then + self:AirbaseRecaptured(NewCoalitionAirbase) + end + else + -- Captured airbase belongs to this warehouse but was captured by other coaltion. + if NewCoalitionAirbase ~= self:GetCoalition() then + self:AirbaseCaptured(NewCoalitionAirbase) + end + end + + end + end +end + +--- Warehouse event handling function. +-- Handles the case when the mission is ended. +-- @param #WAREHOUSE self +-- @param Core.Event#EVENTDATA EventData Event data. +function WAREHOUSE:_OnEventMissionEnd(EventData) + self:T3(self.lid..string.format("Warehouse %s captured event mission end!",self.alias)) + + if self.autosave then + self:Save(self.autosavepath, self.autosavefile) + end +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Helper functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Checks if the warehouse zone was conquered by antoher coalition. +-- @param #WAREHOUSE self +function WAREHOUSE:_CheckConquered() + + -- Get coordinate and radius to check. + local coord=self.zone:GetCoordinate() + local radius=self.zone:GetRadius() + + -- Scan units in zone. + local gotunits,_,_,units,_,_=coord:ScanObjects(radius, true, false, false) + + local Nblue=0 + local Nred=0 + local Nneutral=0 + + local CountryBlue=nil + local CountryRed=nil + local CountryNeutral=nil + + if gotunits then + -- Loop over all units. + for _,_unit in pairs(units) do + local unit=_unit --Wrapper.Unit#UNIT + + local distance=coord:Get2DDistance(unit:GetCoordinate()) + + -- Filter only alive groud units. Also check distance again, because the scan routine might give some larger distances. + if unit:IsGround() and unit:IsAlive() and distance <= radius then + + -- Get coalition and country. + local _coalition=unit:GetCoalition() + local _country=unit:GetCountry() + + -- Debug info. + self:T2(self.lid..string.format("Unit %s in warehouse zone of radius=%d m. Coalition=%d, country=%d. Distance = %d m.",unit:GetName(), radius,_coalition,_country, distance)) + + -- Add up units for each side. + if _coalition==coalition.side.BLUE then + Nblue=Nblue+1 + CountryBlue=_country + elseif _coalition==coalition.side.RED then + Nred=Nred+1 + CountryRed=_country + else + Nneutral=Nneutral+1 + CountryNeutral=_country + end + + end + end + end + + -- Debug info. + self:T(self.lid..string.format("Ground troops in warehouse zone: blue=%d, red=%d, neutral=%d", Nblue, Nred, Nneutral)) + + + -- Figure out the new coalition if any. + -- Condition is that only units of one coalition are within the zone. + local newcoalition=self:GetCoalition() + local newcountry=self:GetCountry() + if Nblue>0 and Nred==0 and Nneutral==0 then + -- Only blue units in zone ==> Zone goes to blue. + newcoalition=coalition.side.BLUE + newcountry=CountryBlue + elseif Nblue==0 and Nred>0 and Nneutral==0 then + -- Only red units in zone ==> Zone goes to red. + newcoalition=coalition.side.RED + newcountry=CountryRed + elseif Nblue==0 and Nred==0 and Nneutral>0 then + -- Only neutral units in zone but neutrals do not attack or even capture! + --newcoalition=coalition.side.NEUTRAL + --newcountry=CountryNeutral + end + + -- Coalition has changed ==> warehouse was captured! This should be before the attack check. + if self:IsAttacked() and newcoalition ~= self:GetCoalition() then + self:Captured(newcoalition, newcountry) + return + end + + -- Before a warehouse can be captured, it has to be attacked. + -- That is, even if only enemy units are present it is not immediately captured in order to spawn all ground assets for defence. + if self:GetCoalition()==coalition.side.BLUE then + -- Blue warehouse is running and we have red units in the zone. + if self:IsRunning() and Nred>0 then + self:Attacked(coalition.side.RED, CountryRed) + end + -- Blue warehouse was under attack by blue but no more blue units in zone. + if self:IsAttacked() and Nred==0 then + self:Defeated() + end + elseif self:GetCoalition()==coalition.side.RED then + -- Red Warehouse is running and we have blue units in the zone. + if self:IsRunning() and Nblue>0 then + self:Attacked(coalition.side.BLUE, CountryBlue) + end + -- Red warehouse was under attack by blue but no more blue units in zone. + if self:IsAttacked() and Nblue==0 then + self:Defeated() + end + elseif self:GetCoalition()==coalition.side.NEUTRAL then + -- Neutrals dont attack! + if self:IsRunning() and Nred>0 then + self:Attacked(coalition.side.RED, CountryRed) + elseif self:IsRunning() and Nblue>0 then + self:Attacked(coalition.side.BLUE, CountryBlue) + end + end + +end + +--- Checks if the associated airbase still belongs to the warehouse. +-- @param #WAREHOUSE self +function WAREHOUSE:_CheckAirbaseOwner() + -- The airbasename is set at start and not deleted if the airbase was captured. + if self.airbasename then + + local airbase=AIRBASE:FindByName(self.airbasename) + local airbasecurrentcoalition=airbase:GetCoalition() + + if self.airbase then + + -- Warehouse has lost its airbase. + if self:GetCoalition()~=airbasecurrentcoalition then + self.airbase=nil + end + + else + + -- Warehouse has re-captured the airbase. + if self:GetCoalition()==airbasecurrentcoalition then + self.airbase=airbase + end + + end + + end +end + +--- Checks if the request can be fulfilled in general. If not, it is removed from the queue. +-- Check if departure and destination bases are of the right type. +-- @param #WAREHOUSE self +-- @param #table queue The queue which is holding the requests to check. +-- @return #boolean If true, request can be executed. If false, something is not right. +function WAREHOUSE:_CheckRequestConsistancy(queue) + self:T3(self.lid..string.format("Number of queued requests = %d", #queue)) + + -- Requests to delete. + local invalid={} + + for _,_request in pairs(queue) do + local request=_request --#WAREHOUSE.Queueitem + + -- Debug info. + self:T2(self.lid..string.format("Checking request id=%d.", request.uid)) + + -- Let's assume everything is fine. + local valid=true + + -- Check if at least one asset was requested. + if request.nasset==0 then + self:E(self.lid..string.format("ERROR: INVALID request. Request for zero assets not possible. Can happen when, e.g. \"all\" ground assets are requests but none in stock.")) + valid=false + end + + -- Request from enemy coalition? + if self:GetCoalition()~=request.warehouse:GetCoalition() then + self:E(self.lid..string.format("ERROR: INVALID request. Requesting warehouse is of wrong coaltion! Own coalition %s != %s of requesting warehouse.", self:GetCoalitionName(), request.warehouse:GetCoalitionName())) + valid=false + end + + -- Is receiving warehouse stopped? + if request.warehouse:IsStopped() then + self:E(self.lid..string.format("ERROR: INVALID request. Requesting warehouse is stopped!")) + valid=false + end + + -- Is receiving warehouse destroyed? + if request.warehouse:IsDestroyed() and not self.respawnafterdestroyed then + self:E(self.lid..string.format("ERROR: INVALID request. Requesting warehouse is destroyed!")) + valid=false + end + + -- Add request as unvalid and delete it later. + if valid==false then + self:E(self.lid..string.format("Got invalid request id=%d.", request.uid)) + table.insert(invalid, request) + else + self:T3(self.lid..string.format("Got valid request id=%d.", request.uid)) + end + end + + -- Delete invalid requests. + for _,_request in pairs(invalid) do + self:E(self.lid..string.format("Deleting INVALID request id=%d.",_request.uid)) + self:_DeleteQueueItem(_request, self.queue) + end + +end + +--- Check if a request is valid in general. If not, it will be removed from the queue. +-- This routine needs to have at least one asset in stock that matches the request descriptor in order to determine whether the request category of troops. +-- If no asset is in stock, the request will remain in the queue but cannot be executed. +-- @param #WAREHOUSE self +-- @param #WAREHOUSE.Queueitem request The request to be checked. +-- @return #boolean If true, request can be executed. If false, something is not right. +function WAREHOUSE:_CheckRequestValid(request) + + -- Check if number of requested assets is in stock. + local _assets,_nassets,_enough=self:_FilterStock(self.stock, request.assetdesc, request.assetdescval, request.nasset) + + -- No assets in stock? Checks cannot be performed. + if #_assets==0 then + return true + end + + -- Convert relative to absolute number if necessary. + local nasset=request.nasset + if type(request.nasset)=="string" then + nasset=self:_QuantityRel2Abs(request.nasset,_nassets) + end + + -- Debug check, request.nasset might be a string Quantity enumerator. + local text=string.format("Request valid? Number of assets: requested=%s=%d, selected=%d, total=%d, enough=%s.", tostring(request.nasset), nasset,#_assets,_nassets, tostring(_enough)) + self:T(text) + + -- First asset. Is representative for all filtered items in stock. + local asset=_assets[1] --#WAREHOUSE.Assetitem + + -- Asset is air, ground etc. + local asset_plane = asset.category==Group.Category.AIRPLANE + local asset_helo = asset.category==Group.Category.HELICOPTER + local asset_ground = asset.category==Group.Category.GROUND + local asset_train = asset.category==Group.Category.TRAIN + local asset_naval = asset.category==Group.Category.SHIP + + -- General air request. + local asset_air=asset_helo or asset_plane + + -- Assume everything is okay. + local valid=true + + -- Category of the requesting warehouse airbase. + local requestcategory=request.warehouse:GetAirbaseCategory() + + if request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then + ------------------------------------------- + -- Case where the units go my themselves -- + ------------------------------------------- + + if asset_air then + + if asset_plane then + + -- No airplane to or from FARPS. + if requestcategory==Airbase.Category.HELIPAD or self:GetAirbaseCategory()==Airbase.Category.HELIPAD then + self:E("ERROR: Incorrect request. Asset airplane requested but warehouse or requestor is HELIPAD/FARP!") + valid=false + end + + -- Category SHIP is not general enough! Fighters can go to carriers. Which fighters, is there an attibute? + -- Also for carriers, attibute? + + elseif asset_helo then + + -- Helos need a FARP or AIRBASE or SHIP for spawning. Also at the the receiving warehouse. So even if they could go there they "cannot" be spawned again. + -- Unless I allow spawning of helos in the the spawn zone. But one should place at least a FARP there. + if self:GetAirbaseCategory()==-1 or requestcategory==-1 then + self:E("ERROR: Incorrect request. Helos need a AIRBASE/HELIPAD/SHIP as home/destination base!") + valid=false + end + + end + + -- All aircraft need an airbase of any type at depature and destination. + if self.airbase==nil or request.airbase==nil then + + self:E("ERROR: Incorrect request. Either warehouse or requesting warehouse does not have any kind of airbase!") + valid=false + + else + + -- Check if enough parking spots are available. This checks the spots available in general, i.e. not the free spots. + -- TODO: For FARPS/ships, is it possible to send more assets than parking spots? E.g. a FARPS has only four (or even one). + -- TODO: maybe only check if spots > 0 for the necessary terminal type? At least for FARPS. + + -- Get necessary terminal type. + local termtype_dep=asset.terminalType or self:_GetTerminal(asset.attribute, self:GetAirbaseCategory()) + local termtype_des=asset.terminalType or self:_GetTerminal(asset.attribute, request.warehouse:GetAirbaseCategory()) + + -- Get number of parking spots. + local np_departure=self.airbase:GetParkingSpotsNumber(termtype_dep) + local np_destination=request.airbase:GetParkingSpotsNumber(termtype_des) + + -- Debug info. + self:T(string.format("Asset attribute = %s, DEPARTURE: terminal type = %d, spots = %d, DESTINATION: terminal type = %d, spots = %d", asset.attribute, termtype_dep, np_departure, termtype_des, np_destination)) + + -- Not enough parking at sending warehouse. + --if (np_departure < request.nasset) and not (self.category==Airbase.Category.SHIP or self.category==Airbase.Category.HELIPAD) then + if np_departure < nasset then + self:E(string.format("ERROR: Incorrect request. Not enough parking spots of terminal type %d at warehouse. Available spots %d < %d necessary.", termtype_dep, np_departure, nasset)) + valid=false + end + + -- No parking at requesting warehouse. + if np_destination == 0 then + self:E(string.format("ERROR: Incorrect request. No parking spots of terminal type %d at requesting warehouse. Available spots = %d!", termtype_des, np_destination)) + valid=false + end + + end + + elseif asset_ground then + + -- Check that both spawn zones are not in water. + local inwater=self.spawnzone:GetCoordinate():IsSurfaceTypeWater() or request.warehouse.spawnzone:GetCoordinate():IsSurfaceTypeWater() + + if inwater then + self:E("ERROR: Incorrect request. Ground asset requested but at least one spawn zone is in water!") + --valid=false + valid=false + end + + -- No ground assets directly to or from ships. + -- TODO: May needs refinement if warehouse is on land and requestor is ship in harbour?! + --if (requestcategory==Airbase.Category.SHIP or self:GetAirbaseCategory()==Airbase.Category.SHIP) then + -- self:E("ERROR: Incorrect request. Ground asset requested but warehouse or requestor is SHIP!") + -- valid=false + --end + + if asset_train then + + -- Check if there is a valid path on rail. + local hasrail=self:HasConnectionRail(request.warehouse) + if not hasrail then + self:E("ERROR: Incorrect request. No valid path on rail for train assets!") + valid=false + end + + else + + if self.warehouse:GetName()~=request.warehouse.warehouse:GetName() then + + -- Check if there is a valid path on road. + local hasroad=self:HasConnectionRoad(request.warehouse) + + -- Check if there is a valid off road path. + local hasoffroad=self:HasConnectionOffRoad(request.warehouse) + + if not (hasroad or hasoffroad) then + self:E("ERROR: Incorrect request. No valid path on or off road for ground assets!") + valid=false + end + + end + + end + + elseif asset_naval then + + -- Check shipping lane. + local shippinglane=self:HasConnectionNaval(request.warehouse) + + if not shippinglane then + self:E("ERROR: Incorrect request. No shipping lane has been defined between warehouses!") + valid=false + end + + end + + else + ------------------------------- + -- Assests need a transport --- + ------------------------------- + + if request.transporttype==WAREHOUSE.TransportType.AIRPLANE then + + -- Airplanes only to AND from airdromes. + if self:GetAirbaseCategory()~=Airbase.Category.AIRDROME or requestcategory~=Airbase.Category.AIRDROME then + self:E("ERROR: Incorrect request. Warehouse or requestor does not have an airdrome. No transport by plane possible!") + valid=false + end + + --TODO: Not sure if there are any transport planes that can land on a carrier? + + elseif request.transporttype==WAREHOUSE.TransportType.APC then + + -- Transport by ground units. + + -- No transport to or from ships + if self:GetAirbaseCategory()==Airbase.Category.SHIP or requestcategory==Airbase.Category.SHIP then + self:E("ERROR: Incorrect request. Warehouse or requestor is SHIP. No transport by APC possible!") + valid=false + end + + -- Check if there is a valid path on road. + local hasroad=self:HasConnectionRoad(request.warehouse) + if not hasroad then + self:E("ERROR: Incorrect request. No valid path on road for ground transport assets!") + valid=false + end + + elseif request.transporttype==WAREHOUSE.TransportType.HELICOPTER then + + -- Transport by helicopters ==> need airbase for spawning but not for delivering to the spawn zone of the receiver. + if self:GetAirbaseCategory()==-1 then + self:E("ERROR: Incorrect request. Warehouse has no airbase. Transport by helicopter not possible!") + valid=false + end + + elseif request.transporttype==WAREHOUSE.TransportType.SHIP or request.transporttype==WAREHOUSE.TransportType.AIRCRAFTCARRIER + or request.transporttype==WAREHOUSE.TransportType.ARMEDSHIP or request.transporttype==WAREHOUSE.TransportType.WARSHIP then + + -- Transport by ship. + local shippinglane=self:HasConnectionNaval(request.warehouse) + + if not shippinglane then + self:E("ERROR: Incorrect request. No shipping lane has been defined between warehouses!") + valid=false + end + + elseif request.transporttype==WAREHOUSE.TransportType.TRAIN then + + -- Transport by train. + self:E("ERROR: Incorrect request. Transport by TRAIN not implemented yet!") + valid=false + + else + -- No match. + self:E("ERROR: Incorrect request. Transport type unknown!") + valid=false + end + + -- Airborne assets: check parking situation. + if request.transporttype==WAREHOUSE.TransportType.AIRPLANE or request.transporttype==WAREHOUSE.TransportType.HELICOPTER then + + -- Check if number of requested assets is in stock. + local _assets,_nassets,_enough=self:_FilterStock(self.stock, WAREHOUSE.Descriptor.ATTRIBUTE, request.transporttype, request.ntransport, true) + + -- Convert relative to absolute number if necessary. + local nasset=request.ntransport + if type(request.ntransport)=="string" then + nasset=self:_QuantityRel2Abs(request.ntransport,_nassets) + end + + -- Debug check, request.nasset might be a string Quantity enumerator. + local text=string.format("Request valid? Number of transports: requested=%s=%d, selected=%d, total=%d, enough=%s.", tostring(request.ntransport), nasset,#_assets,_nassets, tostring(_enough)) + self:T(text) + + -- Get necessary terminal type for helos or transport aircraft. + local termtype=self:_GetTerminal(request.transporttype, self:GetAirbaseCategory()) + + -- Get number of parking spots. + local np_departure=self.airbase:GetParkingSpotsNumber(termtype) + + -- Debug info. + self:T(self.lid..string.format("Transport attribute = %s, terminal type = %d, spots at departure = %d.", request.transporttype, termtype, np_departure)) + + -- Not enough parking at sending warehouse. + --if (np_departure < request.nasset) and not (self.category==Airbase.Category.SHIP or self.category==Airbase.Category.HELIPAD) then + if np_departure < nasset then + self:E(self.lid..string.format("ERROR: Incorrect request. Not enough parking spots of terminal type %d at warehouse. Available spots %d < %d necessary.", termtype, np_departure, nasset)) + valid=false + end + + -- Planes also need parking at the receiving warehouse. + if request.transporttype==WAREHOUSE.TransportType.AIRPLANE then + + -- Total number of parking spots for transport planes at destination. + termtype=self:_GetTerminal(request.transporttype, request.warehouse:GetAirbaseCategory()) + local np_destination=request.airbase:GetParkingSpotsNumber(termtype) + + -- Debug info. + self:T(self.lid..string.format("Transport attribute = %s: total # of spots (type=%d) at destination = %d.", asset.attribute, termtype, np_destination)) + + -- No parking at requesting warehouse. + if np_destination == 0 then + self:E(string.format("ERROR: Incorrect request. No parking spots of terminal type %d at requesting warehouse for transports. Available spots = %d!", termtype, np_destination)) + valid=false + end + end + + end + + + end + + -- Add request as unvalid and delete it later. + if valid==false then + self:E(self.lid..string.format("ERROR: Got invalid request id=%d.", request.uid)) + else + self:T3(self.lid..string.format("Request id=%d valid :)", request.uid)) + end + + return valid +end + + +--- Checks if the request can be fulfilled right now. +-- Check for current parking situation, number of assets and transports currently in stock. +-- @param #WAREHOUSE self +-- @param #WAREHOUSE.Queueitem request The request to be checked. +-- @return #boolean If true, request can be executed. If false, something is not right. +function WAREHOUSE:_CheckRequestNow(request) + + -- Check if receiving warehouse is running. We do allow self requests if the warehouse is under attack though! + if (request.warehouse:IsRunning()==false) and not (request.toself and self:IsAttacked()) then + local text=string.format("Warehouse %s: Request denied! Receiving warehouse %s is not running. Current state %s.", self.alias, request.warehouse.alias, request.warehouse:GetState()) + self:_InfoMessage(text, 5) + + return false + end + + -- If no transport is requested, assets need to be mobile unless it is a self request. + local onlymobile=false + if type(request.ntransport)=="number" and request.ntransport==0 and not request.toself then + onlymobile=true + end + + -- Check if number of requested assets is in stock. + local _assets,_nassets,_enough=self:_FilterStock(self.stock, request.assetdesc, request.assetdescval, request.nasset, onlymobile) + + + -- Check if enough assets are in stock. + if not _enough then + local text=string.format("Warehouse %s: Request ID=%d denied! Not enough (cargo) assets currently available.", self.alias, request.uid) + self:_InfoMessage(text, 5) + text=string.format("Enough=%s, #assets=%d, nassets=%d, request.nasset=%s", tostring(_enough), #_assets,_nassets, tostring(request.nasset)) + self:T(self.lid..text) + return false + end + + local _transports + local _assetattribute + local _assetcategory + + -- Check if at least one (cargo) asset is available. + if _nassets>0 then + + -- Get the attibute of the requested asset. + _assetattribute=_assets[1].attribute + _assetcategory=_assets[1].category + + -- Check available parking for air asset units. + if _assetcategory==Group.Category.AIRPLANE or _assetcategory==Group.Category.HELICOPTER then + + if self.airbase and self.airbase:GetCoalition()==self:GetCoalition() then + + if self:IsRunwayOperational() then + + local Parking=self:_FindParkingForAssets(self.airbase,_assets) + + --if Parking==nil and not (self.category==Airbase.Category.HELIPAD) then + if Parking==nil then + local text=string.format("Warehouse %s: Request denied! Not enough free parking spots for all requested assets at the moment.", self.alias) + self:_InfoMessage(text, 5) + return false + end + + else + -- Runway destroyed. + local text=string.format("Warehouse %s: Request denied! Runway is still destroyed", self.alias) + self:_InfoMessage(text, 5) + return false + end + + else + + -- No airbase! + local text=string.format("Warehouse %s: Request denied! No airbase", self.alias) + self:_InfoMessage(text, 5) + return false + + end + + end + + -- Add this here or gettransport fails + request.cargoassets=_assets + + end + + -- Check that a transport units. + if request.transporttype ~= WAREHOUSE.TransportType.SELFPROPELLED then + + -- Get best transports for this asset pack. + _transports=self:_GetTransportsForAssets(request) + + -- Check if at least one transport asset is available. + if #_transports>0 then + + -- Get the attibute of the transport units. + local _transportattribute=_transports[1].attribute + local _transportcategory=_transports[1].category + + -- Check available parking for transport units. + if _transportcategory==Group.Category.AIRPLANE or _transportcategory==Group.Category.HELICOPTER then + + if self.airbase and self.airbase:GetCoalition()==self:GetCoalition() then + + if self:IsRunwayOperational() then + + local Parking=self:_FindParkingForAssets(self.airbase,_transports) + + -- No parking ==> return false + if Parking==nil then + local text=string.format("Warehouse %s: Request denied! Not enough free parking spots for all transports at the moment.", self.alias) + self:_InfoMessage(text, 5) + return false + end + + else + + -- Runway destroyed. + local text=string.format("Warehouse %s: Request denied! Runway is still destroyed", self.alias) + self:_InfoMessage(text, 5) + return false + + end + + else + -- No airbase + local text=string.format("Warehouse %s: Request denied! No airbase currently!", self.alias) + self:_InfoMessage(text, 5) + return false + end + + end + + else + + -- Not enough or the right transport carriers. + local text=string.format("Warehouse %s: Request denied! Not enough transport carriers available at the moment.", self.alias) + self:_InfoMessage(text, 5) + + return false + end + + else + + --- + -- Self propelled case + --- + + -- Ground asset checks. + if _assetcategory==Group.Category.GROUND then + + -- Distance between warehouse and spawn zone. + local dist=self.warehouse:GetCoordinate():Get2DDistance(self.spawnzone:GetCoordinate()) + + -- Check min dist to spawn zone. + if dist>self.spawnzonemaxdist then + -- Not close enough to spawn zone. + local text=string.format("Warehouse %s: Request denied! Not close enough to spawn zone. Distance = %d m. We need to be at least within %d m range to spawn.", self.alias, dist, self.spawnzonemaxdist) + self:_InfoMessage(text, 5) + return false + end + + end + + end + + + -- Set chosen cargo assets. + request.cargoassets=_assets + request.cargoattribute=_assets[1].attribute + request.cargocategory=_assets[1].category + request.nasset=#_assets + + -- Debug info: + local text=string.format("Selected cargo assets, attibute=%s, category=%d:\n", request.cargoattribute, request.cargocategory) + for _i,_asset in pairs(_assets) do + local asset=_asset --#WAREHOUSE.Assetitem + text=text..string.format("%d) name=%s, type=%s, category=%d, #units=%d",_i, asset.templatename, asset.unittype, asset.category, asset.nunits) + end + self:T(self.lid..text) + + if request.transporttype ~= WAREHOUSE.TransportType.SELFPROPELLED then + + -- Set chosen transport assets. + request.transportassets=_transports + request.transportattribute=_transports[1].attribute + request.transportcategory=_transports[1].category + request.ntransport=#_transports + + -- Debug info: + local text=string.format("Selected transport assets, attibute=%s, category=%d:\n", request.transportattribute, request.transportcategory) + for _i,_asset in pairs(_transports) do + local asset=_asset --#WAREHOUSE.Assetitem + text=text..string.format("%d) name=%s, type=%s, category=%d, #units=%d\n",_i, asset.templatename, asset.unittype, asset.category, asset.nunits) + end + self:T(self.lid..text) + + end + + return true +end + +---Get (optimized) transport carriers for the given assets to be transported. +-- @param #WAREHOUSE self +-- @param #WAREHOUSE.Pendingitem Chosen request. +function WAREHOUSE:_GetTransportsForAssets(request) + + -- Get all transports of the requested type in stock. + local transports=self:_FilterStock(self.stock, WAREHOUSE.Descriptor.ATTRIBUTE, request.transporttype, nil, true) + + -- Copy asset. + local cargoassets=UTILS.DeepCopy(request.cargoassets) + local cargoset=request.transportcargoset + + -- TODO: Get weight and cargo bay from CARGO_GROUP + --local cargogroup=CARGO_GROUP:New(CargoGroup,Type,Name,LoadRadius,NearRadius) + --cargogroup:GetWeight() + + -- Sort transport carriers w.r.t. cargo bay size. + local function sort_transports(a,b) + return a.cargobaymax>b.cargobaymax + end + + -- Sort cargo assets w.r.t. weight in assending order. + local function sort_cargoassets(a,b) + return a.weight>b.weight + end + + -- Sort tables. + table.sort(transports, sort_transports) + table.sort(cargoassets, sort_cargoassets) + + -- Total cargo bay size of all groups. + self:T2(self.lid.."Transport capability:") + local totalbay=0 + for i=1,#transports do + local transport=transports[i] --#WAREHOUSE.Assetitem + for j=1,transport.nunits do + totalbay=totalbay+transport.cargobay[j] + self:T2(self.lid..string.format("Cargo bay = %d (unit=%d)", transport.cargobay[j], j)) + end + end + self:T2(self.lid..string.format("Total capacity = %d", totalbay)) + + -- Total cargo weight of all assets to transports. + self:T2(self.lid.."Cargo weight:") + local totalcargoweight=0 + for i=1,#cargoassets do + local asset=cargoassets[i] --#WAREHOUSE.Assetitem + totalcargoweight=totalcargoweight+asset.weight + self:T2(self.lid..string.format("weight = %d", asset.weight)) + end + self:T2(self.lid..string.format("Total weight = %d", totalcargoweight)) + + -- Transports used. + local used_transports={} + + -- Loop over all transport groups, largest cargobaymax to smallest. + for i=1,#transports do + + -- Shortcut for carrier and cargo bay + local transport=transports[i] + + -- Cargo put into carrier. + local putintocarrier={} + + -- Cargo assigned to this transport group? + local used=false + + -- Loop over all units + for k=1,transport.nunits do + + -- Get cargo bay of this carrier. + local cargobay=transport.cargobay[k] + + -- Loop over cargo assets. + for j,asset in pairs(cargoassets) do + local asset=asset --#WAREHOUSE.Assetitem + + -- How many times does the cargo fit into the carrier? + local delta=cargobay-asset.weight + --env.info(string.format("k=%d, j=%d delta=%d cargobay=%d weight=%d", k, j, delta, cargobay, asset.weight)) + + --self:E(self.lid..string.format("%s unit %d loads cargo uid=%d: bayempty=%02d, bayloaded = %02d - weight=%02d", transport.templatename, k, asset.uid, transport.cargobay[k], cargobay, asset.weight)) + + -- Cargo fits into carrier + if delta>=0 then + -- Reduce remaining cargobay. + cargobay=cargobay-asset.weight + self:T3(self.lid..string.format("%s unit %d loads cargo uid=%d: bayempty=%02d, bayloaded = %02d - weight=%02d", transport.templatename, k, asset.uid, transport.cargobay[k], cargobay, asset.weight)) + + -- Remember this cargo and remove it so it does not get loaded into other carriers. + table.insert(putintocarrier, j) + + -- This transport group is used. + used=true + else + self:T2(self.lid..string.format("Carrier unit %s too small for cargo asset %s ==> cannot be used! Cargo bay - asset weight = %d kg", transport.templatename, asset.templatename, delta)) + end + + end -- loop over assets + end -- loop over units + + -- Remove cargo assets from list. Needs to be done back-to-front in order not to confuse the loop. + for j=#putintocarrier,1, -1 do + + local nput=putintocarrier[j] + local cargo=cargoassets[nput] + + -- Need to check if multiple units in a group and the group has already been removed! + -- TODO: This might need to be improved but is working okay so far. + if cargo then + -- Remove this group because it was used. + self:T2(self.lid..string.format("Cargo id=%d assigned for carrier id=%d", cargo.uid, transport.uid)) + table.remove(cargoassets, nput) + end + end + + -- Cargo was assined for this carrier. + if used then + table.insert(used_transports, transport) + end + + -- Convert relative quantity (all, half) to absolute number if necessary. + local ntrans=self:_QuantityRel2Abs(request.ntransport, #transports) + + -- Max number of transport groups reached? + if #used_transports >= ntrans then + request.ntransport=#used_transports + break + end + end + + -- Debug info. + local text=string.format("Used Transports for request %d to warehouse %s:\n", request.uid, request.warehouse.alias) + local totalcargobay=0 + for _i,_transport in pairs(used_transports) do + local transport=_transport --#WAREHOUSE.Assetitem + text=text..string.format("%d) %s: cargobay tot = %d kg, cargobay max = %d kg, nunits=%d\n", _i, transport.unittype, transport.cargobaytot, transport.cargobaymax, transport.nunits) + totalcargobay=totalcargobay+transport.cargobaytot + --for _,cargobay in pairs(transport.cargobay) do + -- env.info(string.format("cargobay %d", cargobay)) + --end + end + text=text..string.format("Total cargo bay capacity = %.1f kg\n", totalcargobay) + text=text..string.format("Total cargo weight = %.1f kg\n", totalcargoweight) + text=text..string.format("Minimum number of runs = %.1f", totalcargoweight/totalcargobay) + self:_DebugMessage(text) + + return used_transports +end + +---Relative to absolute quantity. +-- @param #WAREHOUSE self +-- @param #string relative Relative number in terms of @{#WAREHOUSE.Quantity}. +-- @param #number ntot Total number. +-- @return #number Absolute number. +function WAREHOUSE:_QuantityRel2Abs(relative, ntot) + + local nabs=0 + + -- Handle string input for nmax. + if type(relative)=="string" then + if relative==WAREHOUSE.Quantity.ALL then + nabs=ntot + elseif relative==WAREHOUSE.Quantity.THREEQUARTERS then + nabs=UTILS.Round(ntot*3/4) + elseif relative==WAREHOUSE.Quantity.HALF then + nabs=UTILS.Round(ntot/2) + elseif relative==WAREHOUSE.Quantity.THIRD then + nabs=UTILS.Round(ntot/3) + elseif relative==WAREHOUSE.Quantity.QUARTER then + nabs=UTILS.Round(ntot/4) + else + nabs=math.min(1, ntot) + end + else + nabs=relative + end + + self:T2(self.lid..string.format("Relative %s: tot=%d, abs=%.2f", tostring(relative), ntot, nabs)) + + return nabs +end + +---Sorts the queue and checks if the request can be fulfilled. +-- @param #WAREHOUSE self +-- @return #WAREHOUSE.Queueitem Chosen request. +function WAREHOUSE:_CheckQueue() + + -- Sort queue wrt to first prio and then qid. + self:_SortQueue() + + -- Search for a request we can execute. + local request=nil --#WAREHOUSE.Queueitem + + local invalid={} + local gotit=false + for _,_qitem in ipairs(self.queue) do + local qitem=_qitem --#WAREHOUSE.Queueitem + + -- Check if request is valid in general. + local valid=self:_CheckRequestValid(qitem) + + -- Check if request is possible now. + local okay=false + if valid then + okay=self:_CheckRequestNow(qitem) + else + -- Remember invalid request and delete later in order not to confuse the loop. + table.insert(invalid, qitem) + end + + -- Get the first valid request that can be executed now. + if okay and valid and not gotit then + request=qitem + gotit=true + break + end + end + + -- Delete invalid requests. + for _,_request in pairs(invalid) do + self:T(self.lid..string.format("Deleting invalid request id=%d.",_request.uid)) + self:_DeleteQueueItem(_request, self.queue) + end + + -- Execute request. + return request +end + +--- Simple task function. Can be used to call a function which has the warehouse and the executing group as parameters. +-- @param #WAREHOUSE self +-- @param #string Function The name of the function to call passed as string. +-- @param Wrapper.Group#GROUP group The group which is meant. +function WAREHOUSE:_SimpleTaskFunction(Function, group) + self:F2({Function}) + + -- Name of the warehouse (static) object. + local warehouse=self.warehouse:GetName() + local groupname=group:GetName() + + -- Task script. + local DCSScript = {} + + DCSScript[#DCSScript+1] = string.format('local mygroup = GROUP:FindByName(\"%s\") ', groupname) -- The group that executes the task function. Very handy with the "...". + if self.isunit then + DCSScript[#DCSScript+1] = string.format("local mywarehouse = UNIT:FindByName(\"%s\") ", warehouse) -- The unit that holds the warehouse self object. + else + DCSScript[#DCSScript+1] = string.format("local mywarehouse = STATIC:FindByName(\"%s\") ", warehouse) -- The static that holds the warehouse self object. + end + DCSScript[#DCSScript+1] = string.format('local warehouse = mywarehouse:GetState(mywarehouse, \"WAREHOUSE\") ') -- Get the warehouse self object from the static. + DCSScript[#DCSScript+1] = string.format('%s(mygroup)', Function) -- Call the function, e.g. myfunction.(warehouse,mygroup) + + -- Create task. + local DCSTask = CONTROLLABLE.TaskWrappedAction(self, CONTROLLABLE.CommandDoScript(self, table.concat(DCSScript))) + + return DCSTask +end + +--- Simple task function. Can be used to call a function which has the warehouse and the executing group as parameters. +-- @param #WAREHOUSE self +-- @param #string Function The name of the function to call passed as string. +-- @param Wrapper.Group#GROUP group The group which is meant. +-- @param #number n Waypoint passed. +-- @param #number N Final waypoint number. +function WAREHOUSE:_SimpleTaskFunctionWP(Function, group, n, N) + self:F2({Function}) + + -- Name of the warehouse (static) object. + local warehouse=self.warehouse:GetName() + local groupname=group:GetName() + + -- Task script. + local DCSScript = {} + + DCSScript[#DCSScript+1] = string.format('local mygroup = GROUP:FindByName(\"%s\") ', groupname) -- The group that executes the task function. Very handy with the "...". + if self.isunit then + DCSScript[#DCSScript+1] = string.format("local mywarehouse = UNIT:FindByName(\"%s\") ", warehouse) -- The unit that holds the warehouse self object. + else + DCSScript[#DCSScript+1] = string.format("local mywarehouse = STATIC:FindByName(\"%s\") ", warehouse) -- The static that holds the warehouse self object. + end + DCSScript[#DCSScript+1] = string.format('local warehouse = mywarehouse:GetState(mywarehouse, \"WAREHOUSE\") ') -- Get the warehouse self object from the static. + DCSScript[#DCSScript+1] = string.format('%s(mygroup, %d, %d)', Function, n ,N) -- Call the function, e.g. myfunction.(warehouse,mygroup) + + -- Create task. + local DCSTask = CONTROLLABLE.TaskWrappedAction(self, CONTROLLABLE.CommandDoScript(self, table.concat(DCSScript))) + + return DCSTask +end + +--- Get the proper terminal type based on generalized attribute of the group. +--@param #WAREHOUSE self +--@param #WAREHOUSE.Attribute _attribute Generlized attibute of unit. +--@param #number _category Airbase category. +--@return Wrapper.Airbase#AIRBASE.TerminalType Terminal type for this group. +function WAREHOUSE:_GetTerminal(_attribute, _category) + + -- Default terminal is "large". + local _terminal=AIRBASE.TerminalType.OpenBig + + if _attribute==WAREHOUSE.Attribute.AIR_FIGHTER then + -- Fighter ==> small. + _terminal=AIRBASE.TerminalType.FighterAircraft + elseif _attribute==WAREHOUSE.Attribute.AIR_BOMBER or _attribute==WAREHOUSE.Attribute.AIR_TRANSPORTPLANE or _attribute==WAREHOUSE.Attribute.AIR_TANKER or _attribute==WAREHOUSE.Attribute.AIR_AWACS then + -- Bigger aircraft. + _terminal=AIRBASE.TerminalType.OpenBig + elseif _attribute==WAREHOUSE.Attribute.AIR_TRANSPORTHELO or _attribute==WAREHOUSE.Attribute.AIR_ATTACKHELO then + -- Helicopter. + _terminal=AIRBASE.TerminalType.HelicopterUsable + else + --_terminal=AIRBASE.TerminalType.OpenMedOrBig + end + + -- For ships, we allow medium spots for all fixed wing aircraft. There are smaller tankers and AWACS aircraft that can use a carrier. + if _category==Airbase.Category.SHIP then + if not (_attribute==WAREHOUSE.Attribute.AIR_TRANSPORTHELO or _attribute==WAREHOUSE.Attribute.AIR_ATTACKHELO) then + _terminal=AIRBASE.TerminalType.OpenMedOrBig + end + end + + return _terminal +end + + +--- Seach unoccupied parking spots at the airbase for a list of assets. For each asset group a list of parking spots is returned. +-- During the search also the not yet spawned asset aircraft are considered. +-- If not enough spots for all asset units could be found, the routine returns nil! +-- @param #WAREHOUSE self +-- @param Wrapper.Airbase#AIRBASE airbase The airbase where we search for parking spots. +-- @param #table assets A table of assets for which the parking spots are needed. +-- @return #table Table of coordinates and terminal IDs of free parking spots. Each table entry has the elements .Coordinate and .TerminalID. +function WAREHOUSE:_FindParkingForAssets(airbase, assets) + + -- Init default + local scanradius=25 + local scanunits=true + local scanstatics=true + local scanscenery=false + local verysafe=false + + -- Function calculating the overlap of two (square) objects. + local function _overlap(l1,l2,dist) + local safedist=(l1/2+l2/2)*1.05 -- 5% safety margine added to safe distance! + local safe = (dist > safedist) + self:T3(string.format("l1=%.1f l2=%.1f s=%.1f d=%.1f ==> safe=%s", l1,l2,safedist,dist,tostring(safe))) + return safe + end + + -- Get client coordinates. + local function _clients() + local clients=_DATABASE.CLIENTS + local coords={} + for clientname, client in pairs(clients) do + local template=_DATABASE:GetGroupTemplateFromUnitName(clientname) + local units=template.units + for i,unit in pairs(units) do + local coord=COORDINATE:New(unit.x, unit.alt, unit.y) + coords[unit.name]=coord + end + end + return coords + end + + -- Get parking spot data table. This contains all free and "non-free" spots. + local parkingdata=airbase.parking --airbase:GetParkingSpotsTable() + + --- + -- Find all obstacles + --- + + -- List of obstacles. + local obstacles={} + + -- Check all clients. Clients dont change so we can put that out of the loop. + self.clientcoords=self.clientcoords or _clients() + for clientname,_coord in pairs(self.clientcoords) do + table.insert(obstacles, {coord=_coord, size=15, name=clientname, type="client"}) + end + + -- Loop over all parking spots and get the currently present obstacles. + -- How long does this take on very large airbases, i.e. those with hundereds of parking spots? Seems to be okay! + for _,parkingspot in pairs(parkingdata) do + + -- Coordinate of the parking spot. + local _spot=parkingspot.Coordinate -- Core.Point#COORDINATE + local _termid=parkingspot.TerminalID + + -- Scan a radius of 100 meters around the spot. + local _,_,_,_units,_statics,_sceneries=_spot:ScanObjects(scanradius, scanunits, scanstatics, scanscenery) + + -- Check all units. + for _,_unit in pairs(_units) do + local unit=_unit --Wrapper.Unit#UNIT + local _coord=unit:GetVec3() + local _size=self:_GetObjectSize(unit:GetDCSObject()) + local _name=unit:GetName() + table.insert(obstacles, {coord=_coord, size=_size, name=_name, type="unit"}) + end + + -- Check all statics. + for _,static in pairs(_statics) do + local _coord=static:getPoint() + --local _coord=COORDINATE:NewFromVec3(_vec3) + local _name=static:getName() + local _size=self:_GetObjectSize(static) + table.insert(obstacles, {coord=_coord, size=_size, name=_name, type="static"}) + end + + -- Check all scenery. + for _,scenery in pairs(_sceneries) do + local _coord=scenery:getPoint() + --local _coord=COORDINATE:NewFromVec3(_vec3) + local _name=scenery:getTypeName() + local _size=self:_GetObjectSize(scenery) + table.insert(obstacles, {coord=_coord, size=_size, name=_name, type="scenery"}) + end + + end + + --- + -- Get Parking Spots + --- + + -- Parking data for all assets. + local parking={} + + -- Loop over all assets that need a parking psot. + for _,asset in pairs(assets) do + local _asset=asset --#WAREHOUSE.Assetitem + + -- Get terminal type of this asset + local terminaltype=asset.terminalType or self:_GetTerminal(asset.attribute, self:GetAirbaseCategory()) + + -- Asset specific parking. + parking[_asset.uid]={} + + -- Loop over all units - each one needs a spot. + for i=1,_asset.nunits do + + -- Loop over all parking spots. + local gotit=false + for _,_parkingspot in pairs(parkingdata) do + local parkingspot=_parkingspot --Wrapper.Airbase#AIRBASE.ParkingSpot + + -- Check correct terminal type for asset. We don't want helos in shelters etc. + if AIRBASE._CheckTerminalType(parkingspot.TerminalType, terminaltype) and self:_CheckParkingValid(parkingspot, airbase) and airbase:_CheckParkingLists(parkingspot.TerminalID) then + + -- Coordinate of the parking spot. + local _spot=parkingspot.Coordinate -- Core.Point#COORDINATE + local _termid=parkingspot.TerminalID + local free=true + local problem=nil + + -- Loop over all obstacles. + for _,obstacle in pairs(obstacles) do + + -- Check if aircraft overlaps with any obstacle. + local dist=_spot:Get2DDistance(obstacle.coord) + local safe=_overlap(_asset.size, obstacle.size, dist) + + -- Spot is blocked. + if not safe then + --env.info(string.format("FF asset=%s (id=%d): spot id=%d dist=%.1fm is NOT SAFE", _asset.templatename, _asset.uid, _termid, dist)) + free=false + problem=obstacle + problem.dist=dist + break + else + --env.info(string.format("FF asset=%s (id=%d): spot id=%d dist=%.1fm is SAFE", _asset.templatename, _asset.uid, _termid, dist)) + end + + end + + -- Check if spot is free + if free then + + -- Add parkingspot for this asset unit. + table.insert(parking[_asset.uid], parkingspot) + + -- Debug + self:T(self.lid..string.format("Parking spot %d is free for asset id=%d!", _termid, _asset.uid)) + + -- Add the unit as obstacle so that this spot will not be available for the next unit. + table.insert(obstacles, {coord=_spot, size=_asset.size, name=_asset.templatename, type="asset"}) + + gotit=true + break + + else + + -- Debug output for occupied spots. + self:T(self.lid..string.format("Parking spot %d is occupied or not big enough!", _termid)) + if self.Debug then + local coord=problem.coord --Core.Point#COORDINATE + local text=string.format("Obstacle blocking spot #%d is %s type %s with size=%.1f m and distance=%.1f m.", _termid, problem.name, problem.type, problem.size, problem.dist) + coord:MarkToAll(string.format(text)) + end + + end + + end -- check terminal type + end -- loop over parking spots + + -- No parking spot for at least one asset :( + if not gotit then + self:I(self.lid..string.format("WARNING: No free parking spot for asset id=%d",_asset.uid)) + return nil + end + end -- loop over asset units + end -- loop over asset groups + + return parking +end + + +--- Get the request belonging to a group. +-- @param #WAREHOUSE self +-- @param Wrapper.Group#GROUP group The group from which the info is gathered. +-- @param #table queue Queue holding all requests. +-- @return #WAREHOUSE.Pendingitem The request belonging to this group. +function WAREHOUSE:_GetRequestOfGroup(group, queue) + + -- Get warehouse, asset and request ID from group name. + local wid,aid,rid=self:_GetIDsFromGroup(group) + + -- Find the request. + for _,_request in pairs(queue) do + local request=_request --#WAREHOUSE.Queueitem + if request.uid==rid then + return request + end + end + +end + +--- Is the group a used as transporter for a given request? +-- @param #WAREHOUSE self +-- @param Wrapper.Group#GROUP group The group from which the info is gathered. +-- @param #WAREHOUSE.Pendingitem request Request. +-- @return #boolean True if group is transport, false if group is cargo and nil otherwise. +function WAREHOUSE:_GroupIsTransport(group, request) + + local asset=self:FindAssetInDB(group) + + if asset and asset.iscargo~=nil then + return not asset.iscargo + else + + -- Name of the group under question. + local groupname=self:_GetNameWithOut(group) + + if request.transportgroupset then + local transporters=request.transportgroupset:GetSetObjects() + + for _,transport in pairs(transporters) do + if transport:GetName()==groupname then + return true + end + end + end + + if request.cargogroupset then + local cargos=request.cargogroupset:GetSetObjects() + + for _,cargo in pairs(cargos) do + if self:_GetNameWithOut(cargo)==groupname then + return false + end + end + end + end + + return nil +end + + +--- Get group name without any spawn or cargo suffix #CARGO etc. +-- @param #WAREHOUSE self +-- @param Wrapper.Group#GROUP group The group from which the info is gathered. +-- @return #string Name of the object without trailing #... +function WAREHOUSE:_GetNameWithOut(group) + + local groupname=type(group)=="string" and group or group:GetName() + + if groupname:find("CARGO") then + local name=groupname:gsub("#CARGO", "") + return name + else + return groupname + end + +end + + +--- Get warehouse id, asset id and request id from group name (alias). +-- @param #WAREHOUSE self +-- @param Wrapper.Group#GROUP group The group from which the info is gathered. +-- @return #number Warehouse ID. +-- @return #number Asset ID. +-- @return #number Request ID. +function WAREHOUSE:_GetIDsFromGroup(group) + + ---@param #string text The text to analyse. + local function analyse(text) + + -- Get rid of #0001 tail from spawn. + local unspawned=UTILS.Split(text, "#")[1] + + -- Split keywords. + local keywords=UTILS.Split(unspawned, "_") + local _wid=nil -- warehouse UID + local _aid=nil -- asset UID + local _rid=nil -- request UID + + -- Loop over keys. + for _,keys in pairs(keywords) do + local str=UTILS.Split(keys, "-") + local key=str[1] + local val=str[2] + if key:find("WID") then + _wid=tonumber(val) + elseif key:find("AID") then + _aid=tonumber(val) + elseif key:find("RID") then + _rid=tonumber(val) + end + end + + return _wid,_aid,_rid + end + + if group then + + -- Group name + local name=group:GetName() + + -- Get asset id from group name. + local wid,aid,rid=analyse(name) + + -- Get Asset. + local asset=self:GetAssetByID(aid) + + -- Get warehouse and request id from asset table. + if asset then + wid=asset.wid + rid=asset.rid + end + + -- Debug info + self:T(self.lid..string.format("Group Name = %s", tostring(name))) + self:T(self.lid..string.format("Warehouse ID = %s", tostring(wid))) + self:T(self.lid..string.format("Asset ID = %s", tostring(aid))) + self:T(self.lid..string.format("Request ID = %s", tostring(rid))) + + return wid,aid,rid + else + self:E("WARNING: Group not found in GetIDsFromGroup() function!") + end + +end + + +--- Get warehouse id, asset id and request id from group name (alias). +-- @param #WAREHOUSE self +-- @param Wrapper.Group#GROUP group The group from which the info is gathered. +-- @return #number Warehouse ID. +-- @return #number Asset ID. +-- @return #number Request ID. +function WAREHOUSE:_GetIDsFromGroupOLD(group) + + ---@param #string text The text to analyse. + local function analyse(text) + + -- Get rid of #0001 tail from spawn. + local unspawned=UTILS.Split(text, "#")[1] + + -- Split keywords. + local keywords=UTILS.Split(unspawned, "_") + local _wid=nil -- warehouse UID + local _aid=nil -- asset UID + local _rid=nil -- request UID + + -- Loop over keys. + for _,keys in pairs(keywords) do + local str=UTILS.Split(keys, "-") + local key=str[1] + local val=str[2] + if key:find("WID") then + _wid=tonumber(val) + elseif key:find("AID") then + _aid=tonumber(val) + elseif key:find("RID") then + _rid=tonumber(val) + end + end + + return _wid,_aid,_rid + end + + if group then + + -- Group name + local name=group:GetName() + + -- Get ids + local wid,aid,rid=analyse(name) + + -- Debug info + self:T3(self.lid..string.format("Group Name = %s", tostring(name))) + self:T3(self.lid..string.format("Warehouse ID = %s", tostring(wid))) + self:T3(self.lid..string.format("Asset ID = %s", tostring(aid))) + self:T3(self.lid..string.format("Request ID = %s", tostring(rid))) + + return wid,aid,rid + else + self:E("WARNING: Group not found in GetIDsFromGroup() function!") + end + +end + +--- Filter stock assets by descriptor and attribute. +-- @param #WAREHOUSE self +-- @param #string descriptor Descriptor describing the filtered assets. +-- @param attribute Value of the descriptor. +-- @param #number nmax (Optional) Maximum number of items that will be returned. Default nmax=nil is all matching items are returned. +-- @param #boolean mobile (Optional) If true, filter only mobile assets. +-- @return #table Filtered assets in stock with the specified descriptor value. +-- @return #number Total number of (requested) assets available. +-- @return #boolean If true, enough assets are available. +function WAREHOUSE:FilterStock(descriptor, attribute, nmax, mobile) + return self:_FilterStock(self.stock, descriptor, attribute, nmax, mobile) +end + +--- Filter stock assets by table entry. +-- @param #WAREHOUSE self +-- @param #table stock Table holding all assets in stock of the warehouse. Each entry is of type @{#WAREHOUSE.Assetitem}. +-- @param #string descriptor Descriptor describing the filtered assets. +-- @param attribute Value of the descriptor. +-- @param #number nmax (Optional) Maximum number of items that will be returned. Default nmax=nil is all matching items are returned. +-- @param #boolean mobile (Optional) If true, filter only mobile assets. +-- @return #table Filtered stock items table. +-- @return #number Total number of (requested) assets available. +-- @return #boolean If true, enough assets are available. +function WAREHOUSE:_FilterStock(stock, descriptor, attribute, nmax, mobile) + + -- Default all. + nmax=nmax or WAREHOUSE.Quantity.ALL + if mobile==nil then + mobile=false + end + + -- Filtered array. + local filtered={} + + -- A specific list of assets was required. + if descriptor==WAREHOUSE.Descriptor.ASSETLIST then + + -- Count total number in stock. + local ntot=0 + for _,_rasset in pairs(attribute) do + local rasset=_rasset --#WAREHOUSE.Assetitem + for _,_asset in ipairs(stock) do + local asset=_asset --#WAREHOUSE.Assetitem + if rasset.uid==asset.uid then + table.insert(filtered, asset) + break + end + end + end + + return filtered, #filtered, #filtered>=#attribute + end + + -- Count total number in stock. + local ntot=0 + for _,_asset in ipairs(stock) do + local asset=_asset --#WAREHOUSE.Assetitem + local ismobile=asset.speedmax>0 + if asset[descriptor]==attribute then + if (mobile==true and ismobile) or mobile==false then + ntot=ntot+1 + end + end + end + + -- Treat case where ntot=0, i.e. no assets at all. + if ntot==0 then + return filtered, ntot, false + end + + -- Convert relative to absolute number if necessary. + nmax=self:_QuantityRel2Abs(nmax,ntot) + + -- Loop over stock items. + for _i,_asset in ipairs(stock) do + local asset=_asset --#WAREHOUSE.Assetitem + + -- Check if asset has the right attribute. + if asset[descriptor]==attribute then + + -- Check if asset has to be mobile. + if (mobile and asset.speedmax>0) or (not mobile) then + + -- Add asset to filtered table. + table.insert(filtered, asset) + + -- Break loop if nmax was reached. + if nmax~=nil and #filtered>=nmax then + return filtered, ntot, true + end + + end + end + end + + return filtered, ntot, ntot>=nmax +end + +--- Check if a group has a generalized attribute. +-- @param #WAREHOUSE self +-- @param Wrapper.Group#GROUP group MOOSE group object. +-- @param #WAREHOUSE.Attribute attribute Attribute to check. +-- @return #boolean True if group has the specified attribute. +function WAREHOUSE:_HasAttribute(group, attribute) + + if group then + local groupattribute=self:_GetAttribute(group) + return groupattribute==attribute + end + + return false +end + +--- Get the generalized attribute of a group. +-- Note that for a heterogenious group, the attribute is determined from the attribute of the first unit! +-- @param #WAREHOUSE self +-- @param Wrapper.Group#GROUP group MOOSE group object. +-- @return #WAREHOUSE.Attribute Generalized attribute of the group. +function WAREHOUSE:_GetAttribute(group) + + -- Default + local attribute=WAREHOUSE.Attribute.OTHER_UNKNOWN --#WAREHOUSE.Attribute + + if group then + + ----------- + --- Air --- + ----------- + -- Planes + local transportplane=group:HasAttribute("Transports") and group:HasAttribute("Planes") + local awacs=group:HasAttribute("AWACS") + local fighter=group:HasAttribute("Fighters") or group:HasAttribute("Interceptors") or group:HasAttribute("Multirole fighters") or (group:HasAttribute("Bombers") and not group:HasAttribute("Strategic bombers")) + local bomber=group:HasAttribute("Strategic bombers") + local tanker=group:HasAttribute("Tankers") + local uav=group:HasAttribute("UAVs") + -- Helicopters + local transporthelo=group:HasAttribute("Transport helicopters") + local attackhelicopter=group:HasAttribute("Attack helicopters") + + -------------- + --- Ground --- + -------------- + -- Ground + local apc=group:HasAttribute("Infantry carriers") + local truck=group:HasAttribute("Trucks") and group:GetCategory()==Group.Category.GROUND + local infantry=group:HasAttribute("Infantry") + local artillery=group:HasAttribute("Artillery") + local tank=group:HasAttribute("Old Tanks") or group:HasAttribute("Modern Tanks") + local aaa=group:HasAttribute("AAA") + local ewr=group:HasAttribute("EWR") + local sam=group:HasAttribute("SAM elements") and (not group:HasAttribute("AAA")) + -- Train + local train=group:GetCategory()==Group.Category.TRAIN + + ------------- + --- Naval --- + ------------- + -- Ships + local aircraftcarrier=group:HasAttribute("Aircraft Carriers") + local warship=group:HasAttribute("Heavy armed ships") + local armedship=group:HasAttribute("Armed ships") or group:HasAttribute("Armed Ship") + local unarmedship=group:HasAttribute("Unarmed ships") + + -- Define attribute. Order is important. + if transportplane then + attribute=WAREHOUSE.Attribute.AIR_TRANSPORTPLANE + elseif awacs then + attribute=WAREHOUSE.Attribute.AIR_AWACS + elseif fighter then + attribute=WAREHOUSE.Attribute.AIR_FIGHTER + elseif bomber then + attribute=WAREHOUSE.Attribute.AIR_BOMBER + elseif tanker then + attribute=WAREHOUSE.Attribute.AIR_TANKER + elseif transporthelo then + attribute=WAREHOUSE.Attribute.AIR_TRANSPORTHELO + elseif attackhelicopter then + attribute=WAREHOUSE.Attribute.AIR_ATTACKHELO + elseif uav then + attribute=WAREHOUSE.Attribute.AIR_UAV + elseif apc then + attribute=WAREHOUSE.Attribute.GROUND_APC + elseif infantry then + attribute=WAREHOUSE.Attribute.GROUND_INFANTRY + elseif artillery then + attribute=WAREHOUSE.Attribute.GROUND_ARTILLERY + elseif tank then + attribute=WAREHOUSE.Attribute.GROUND_TANK + elseif aaa then + attribute=WAREHOUSE.Attribute.GROUND_AAA + elseif ewr then + attribute=WAREHOUSE.Attribute.GROUND_EWR + elseif sam then + attribute=WAREHOUSE.Attribute.GROUND_SAM + elseif truck then + attribute=WAREHOUSE.Attribute.GROUND_TRUCK + elseif train then + attribute=WAREHOUSE.Attribute.GROUND_TRAIN + elseif aircraftcarrier then + attribute=WAREHOUSE.Attribute.NAVAL_AIRCRAFTCARRIER + elseif warship then + attribute=WAREHOUSE.Attribute.NAVAL_WARSHIP + elseif armedship then + attribute=WAREHOUSE.Attribute.NAVAL_ARMEDSHIP + elseif unarmedship then + attribute=WAREHOUSE.Attribute.NAVAL_UNARMEDSHIP + else + if group:IsGround() then + attribute=WAREHOUSE.Attribute.GROUND_OTHER + elseif group:IsShip() then + attribute=WAREHOUSE.Attribute.NAVAL_OTHER + elseif group:IsAir() then + attribute=WAREHOUSE.Attribute.AIR_OTHER + else + attribute=WAREHOUSE.Attribute.OTHER_UNKNOWN + end + end + end + + return attribute +end + +--- Size of the bounding box of a DCS object derived from the DCS descriptor table. If boundinb box is nil, a size of zero is returned. +-- @param #WAREHOUSE self +-- @param DCS#Object DCSobject The DCS object for which the size is needed. +-- @return #number Max size of object in meters (length (x) or width (z) components not including height (y)). +-- @return #number Length (x component) of size. +-- @return #number Height (y component) of size. +-- @return #number Width (z component) of size. +function WAREHOUSE:_GetObjectSize(DCSobject) + local DCSdesc=DCSobject:getDesc() + if DCSdesc.box then + local x=DCSdesc.box.max.x+math.abs(DCSdesc.box.min.x) --length + local y=DCSdesc.box.max.y+math.abs(DCSdesc.box.min.y) --height + local z=DCSdesc.box.max.z+math.abs(DCSdesc.box.min.z) --width + return math.max(x,z), x , y, z + end + return 0,0,0,0 +end + +--- Returns the number of assets for each generalized attribute. +-- @param #WAREHOUSE self +-- @param #table stock The stock of the warehouse. +-- @return #table Data table holding the numbers, i.e. data[attibute]=n. +function WAREHOUSE:GetStockInfo(stock) + + local _data={} + for _j,_attribute in pairs(WAREHOUSE.Attribute) do + + local n=0 + for _i,_item in pairs(stock) do + local _ite=_item --#WAREHOUSE.Assetitem + if _ite.attribute==_attribute then + n=n+1 + end + end + + _data[_attribute]=n + end + + return _data +end + +--- Delete an asset item from stock. +-- @param #WAREHOUSE self +-- @param #WAREHOUSE.Assetitem stockitem Asset item to delete from stock table. +function WAREHOUSE:_DeleteStockItem(stockitem) + for i=1,#self.stock do + local item=self.stock[i] --#WAREHOUSE.Assetitem + if item.uid==stockitem.uid then + table.remove(self.stock,i) + break + end + end +end + +--- Delete item from queue. +-- @param #WAREHOUSE self +-- @param #WAREHOUSE.Queueitem qitem Item of queue to be removed. +-- @param #table queue The queue from which the item should be deleted. +function WAREHOUSE:_DeleteQueueItem(qitem, queue) + self:F({qitem=qitem, queue=queue}) + + for i=1,#queue do + local _item=queue[i] --#WAREHOUSE.Queueitem + if _item.uid==qitem.uid then + self:T(self.lid..string.format("Deleting queue item id=%d.", qitem.uid)) + table.remove(queue,i) + break + end + end +end + +--- Delete item from queue. +-- @param #WAREHOUSE self +-- @param #number qitemID ID of queue item to be removed. +-- @param #table queue The queue from which the item should be deleted. +function WAREHOUSE:_DeleteQueueItemByID(qitemID, queue) + + for i=1,#queue do + local _item=queue[i] --#WAREHOUSE.Queueitem + if _item.uid==qitemID then + self:T(self.lid..string.format("Deleting queue item id=%d.", qitemID)) + table.remove(queue,i) + break + end + end +end + +--- Sort requests queue wrt prio and request uid. +-- @param #WAREHOUSE self +function WAREHOUSE:_SortQueue() + self:F3() + -- Sort. + local function _sort(a, b) + return (a.prio < b.prio) or (a.prio==b.prio and a.uid < b.uid) + end + table.sort(self.queue, _sort) +end + +--- Checks fuel on all pening assets. +-- @param #WAREHOUSE self +function WAREHOUSE:_CheckFuel() + + for i,qitem in ipairs(self.pending) do + local qitem=qitem --#WAREHOUSE.Pendingitem + + if qitem.transportgroupset then + for _,_group in pairs(qitem.transportgroupset:GetSet()) do + local group=_group --Wrapper.Group#GROUP + + if group and group:IsAlive() then + + -- Get min fuel of group. + local fuel=group:GetFuelMin() + + -- Debug info. + self:T2(self.lid..string.format("Transport group %s min fuel state = %.2f", group:GetName(), fuel)) + + -- Check if fuel is below threshold for first time. + if fuel=2 then + + local total="Empty" + if #queue>0 then + total=string.format("Total = %d", #queue) + end + + -- Init string. + local text=string.format("%s at %s: %s",name, self.alias, total) + + for i,qitem in ipairs(queue) do + local qitem=qitem --#WAREHOUSE.Pendingitem + + local uid=qitem.uid + local prio=qitem.prio + local clock="N/A" + if qitem.timestamp then + clock=tostring(UTILS.SecondsToClock(qitem.timestamp)) + end + local assignment=tostring(qitem.assignment) + local requestor=qitem.warehouse.alias + local airbasename=qitem.warehouse:GetAirbaseName() + local requestorAirbaseCat=qitem.warehouse:GetAirbaseCategory() + local assetdesc=qitem.assetdesc + local assetdescval=qitem.assetdescval + if assetdesc==WAREHOUSE.Descriptor.ASSETLIST then + assetdescval="Asset list" + end + local nasset=tostring(qitem.nasset) + local ndelivered=tostring(qitem.ndelivered) + local ncargogroupset="N/A" + if qitem.cargogroupset then + ncargogroupset=tostring(qitem.cargogroupset:Count()) + end + local transporttype="N/A" + if qitem.transporttype then + transporttype=qitem.transporttype + end + local ntransport="N/A" + if qitem.ntransport then + ntransport=tostring(qitem.ntransport) + end + local ntransportalive="N/A" + if qitem.transportgroupset then + ntransportalive=tostring(qitem.transportgroupset:Count()) + end + local ntransporthome="N/A" + if qitem.ntransporthome then + ntransporthome=tostring(qitem.ntransporthome) + end + + -- Output text: + text=text..string.format( + "\n%d) UID=%d, Prio=%d, Clock=%s, Assignment=%s | Requestor=%s [Airbase=%s, category=%d] | Assets(%s)=%s: #requested=%s / #alive=%s / #delivered=%s | Transport=%s: #requested=%s / #alive=%s / #home=%s", + i, uid, prio, clock, assignment, requestor, airbasename, requestorAirbaseCat, assetdesc, assetdescval, nasset, ncargogroupset, ndelivered, transporttype, ntransport, ntransportalive, ntransporthome) + + end + + if #queue==0 then + self:I(self.lid..text) + else + if total~="Empty" then + self:I(self.lid..text) + end + end + end +end + +--- Display status of warehouse. +-- @param #WAREHOUSE self +function WAREHOUSE:_DisplayStatus() + if self.verbosity>=3 then + local text=string.format("\n------------------------------------------------------\n") + text=text..string.format("Warehouse %s status: %s\n", self.alias, self:GetState()) + text=text..string.format("------------------------------------------------------\n") + text=text..string.format("Coalition name = %s\n", self:GetCoalitionName()) + text=text..string.format("Country name = %s\n", self:GetCountryName()) + text=text..string.format("Airbase name = %s (category=%d)\n", self:GetAirbaseName(), self:GetAirbaseCategory()) + text=text..string.format("Queued requests = %d\n", #self.queue) + text=text..string.format("Pending requests = %d\n", #self.pending) + text=text..string.format("------------------------------------------------------\n") + text=text..self:_GetStockAssetsText() + self:I(text) + end +end + +--- Get text about warehouse stock. +-- @param #WAREHOUSE self +-- @param #boolean messagetoall If true, send message to all. +-- @return #string Text about warehouse stock +function WAREHOUSE:_GetStockAssetsText(messagetoall) + + -- Get assets in stock. + local _data=self:GetStockInfo(self.stock) + + -- Text. + local text="Stock:\n" + local total=0 + for _attribute,_count in pairs(_data) do + if _count>0 then + local attribute=tostring(UTILS.Split(_attribute, "_")[2]) + text=text..string.format("%s = %d\n", attribute,_count) + total=total+_count + end + end + text=text..string.format("===================\n") + text=text..string.format("Total = %d\n", total) + text=text..string.format("------------------------------------------------------\n") + + -- Send message? + MESSAGE:New(text, 10):ToAllIf(messagetoall) + + return text +end + +--- Create or update mark text at warehouse, which is displayed in F10 map showing how many assets of each type are in stock. +-- Only the coaliton of the warehouse owner is able to see it. +-- @param #WAREHOUSE self +-- @return #string Text about warehouse stock +function WAREHOUSE:_UpdateWarehouseMarkText() + + if self.markerOn then + + -- Marker text. + local text=string.format("Warehouse state: %s\nTotal assets in stock %d:\n", self:GetState(), #self.stock) + for _attribute,_count in pairs(self:GetStockInfo(self.stock) or {}) do + if _count>0 then + local attribute=tostring(UTILS.Split(_attribute, "_")[2]) + text=text..string.format("%s=%d, ", attribute,_count) + end + end + + local coordinate=self:GetCoordinate() + local coalition=self:GetCoalition() + + if not self.markerWarehouse then + + -- Create a new marker. + self.markerWarehouse=MARKER:New(coordinate, text):ToCoalition(coalition) + + else + + local refresh=false + + if self.markerWarehouse.text~=text then + self.markerWarehouse.text=text + refresh=true + end + + if self.markerWarehouse.coordinate~=coordinate then + self.markerWarehouse.coordinate=coordinate + refresh=true + end + + if self.markerWarehouse.coalition~=coalition then + self.markerWarehouse.coalition=coalition + refresh=true + end + + if refresh then + self.markerWarehouse:Refresh() + end + + end + end + +end + +--- Display stock items of warehouse. +-- @param #WAREHOUSE self +-- @param #table stock Table holding all assets in stock of the warehouse. Each entry is of type @{#WAREHOUSE.Assetitem}. +function WAREHOUSE:_DisplayStockItems(stock) + + local text=self.lid..string.format("Warehouse %s stock assets:", self.alias) + for _i,_stock in pairs(stock) do + local mystock=_stock --#WAREHOUSE.Assetitem + local name=mystock.templatename + local category=mystock.category + local cargobaymax=mystock.cargobaymax + local cargobaytot=mystock.cargobaytot + local nunits=mystock.nunits + local range=mystock.range + local size=mystock.size + local speed=mystock.speedmax + local uid=mystock.uid + local unittype=mystock.unittype + local weight=mystock.weight + local attribute=mystock.attribute + text=text..string.format("\n%02d) uid=%d, name=%s, unittype=%s, category=%d, attribute=%s, nunits=%d, speed=%.1f km/h, range=%.1f km, size=%.1f m, weight=%.1f kg, cargobax max=%.1f kg tot=%.1f kg", + _i, uid, name, unittype, category, attribute, nunits, speed, range/1000, size, weight, cargobaymax, cargobaytot) + end + + self:T3(text) +end + +--- Fireworks! +-- @param #WAREHOUSE self +-- @param Core.Point#COORDINATE coord +function WAREHOUSE:_Fireworks(coord) + + -- Place. + coord=coord or self:GetCoordinate() + + -- Fireworks! + for i=1,91 do + local color=math.random(0,3) + coord:Flare(color, i-1) + end +end + +--- Info Message. Message send to coalition if reports or debug mode activated (and duration > 0). Text self:I(text) added to DCS.log file. +-- @param #WAREHOUSE self +-- @param #string text The text of the error message. +-- @param #number duration Message display duration in seconds. Default 20 sec. If duration is zero, no message is displayed. +function WAREHOUSE:_InfoMessage(text, duration) + duration=duration or 20 + if duration>0 and self.Debug or self.Report then + MESSAGE:New(text, duration):ToCoalition(self:GetCoalition()) + end + self:I(self.lid..text) +end + + +--- Debug message. Message send to all if debug mode is activated (and duration > 0). Text self:T(text) added to DCS.log file. +-- @param #WAREHOUSE self +-- @param #string text The text of the error message. +-- @param #number duration Message display duration in seconds. Default 20 sec. If duration is zero, no message is displayed. +function WAREHOUSE:_DebugMessage(text, duration) + duration=duration or 20 + if duration>0 then + MESSAGE:New(text, duration):ToAllIf(self.Debug) + end + self:T(self.lid..text) +end + +--- Error message. Message send to all (if duration > 0). Text self:E(text) added to DCS.log file. +-- @param #WAREHOUSE self +-- @param #string text The text of the error message. +-- @param #number duration Message display duration in seconds. Default 20 sec. If duration is zero, no message is displayed. +function WAREHOUSE:_ErrorMessage(text, duration) + duration=duration or 20 + if duration>0 then + MESSAGE:New(text, duration):ToAll() + end + self:E(self.lid..text) +end + + +--- Calculate the maximum height an aircraft can reach for the given parameters. +-- @param #WAREHOUSE self +-- @param #number D Total distance in meters from Departure to holding point at destination. +-- @param #number alphaC Climb angle in rad. +-- @param #number alphaD Descent angle in rad. +-- @param #number Hdep AGL altitude of departure point. +-- @param #number Hdest AGL altitude of destination point. +-- @param #number Deltahhold Relative altitude of holding point above destination. +-- @return #number Maximum height the aircraft can reach. +function WAREHOUSE:_GetMaxHeight(D, alphaC, alphaD, Hdep, Hdest, Deltahhold) + + local Hhold=Hdest+Deltahhold + local hdest=Hdest-Hdep + local hhold=hdest+Deltahhold + + local Dp=math.sqrt(D^2 + hhold^2) + + local alphaS=math.atan(hdest/D) -- slope angle + local alphaH=math.atan(hhold/D) -- angle to holding point (could be necative!) + + local alphaCp=alphaC-alphaH -- climb angle with slope + local alphaDp=alphaD+alphaH -- descent angle with slope + + -- ASA triangle. + local gammap=math.pi-alphaCp-alphaDp + local sCp=Dp*math.sin(alphaDp)/math.sin(gammap) + local sDp=Dp*math.sin(alphaCp)/math.sin(gammap) + + -- Max height from departure. + local hmax=sCp*math.sin(alphaC) + + -- Debug info. + if self.Debug then + env.info(string.format("Hdep = %.3f km", Hdep/1000)) + env.info(string.format("Hdest = %.3f km", Hdest/1000)) + env.info(string.format("DetaHold= %.3f km", Deltahhold/1000)) + env.info() + env.info(string.format("D = %.3f km", D/1000)) + env.info(string.format("Dp = %.3f km", Dp/1000)) + env.info() + env.info(string.format("alphaC = %.3f Deg", math.deg(alphaC))) + env.info(string.format("alphaCp = %.3f Deg", math.deg(alphaCp))) + env.info() + env.info(string.format("alphaD = %.3f Deg", math.deg(alphaD))) + env.info(string.format("alphaDp = %.3f Deg", math.deg(alphaDp))) + env.info() + env.info(string.format("alphaS = %.3f Deg", math.deg(alphaS))) + env.info(string.format("alphaH = %.3f Deg", math.deg(alphaH))) + env.info() + env.info(string.format("sCp = %.3f km", sCp/1000)) + env.info(string.format("sDp = %.3f km", sDp/1000)) + env.info() + env.info(string.format("hmax = %.3f km", hmax/1000)) + env.info() + + -- Descent height + local hdescent=hmax-hhold + + local dClimb = hmax/math.tan(alphaC) + local dDescent = (hmax-hhold)/math.tan(alphaD) + local dCruise = D-dClimb-dDescent + + env.info(string.format("hmax = %.3f km", hmax/1000)) + env.info(string.format("hdescent = %.3f km", hdescent/1000)) + env.info(string.format("Dclimb = %.3f km", dClimb/1000)) + env.info(string.format("Dcruise = %.3f km", dCruise/1000)) + env.info(string.format("Ddescent = %.3f km", dDescent/1000)) + env.info() + end + + return hmax +end + + +--- Make a flight plan from a departure to a destination airport. +-- @param #WAREHOUSE self +-- @param #WAREHOUSE.Assetitem asset +-- @param Wrapper.Airbase#AIRBASE departure Departure airbase. +-- @param Wrapper.Airbase#AIRBASE destination Destination airbase. +-- @return #table Table of flightplan waypoints. +-- @return #table Table of flightplan coordinates. +function WAREHOUSE:_GetFlightplan(asset, departure, destination) + + -- Parameters in SI units (m/s, m). + local Vmax=asset.speedmax/3.6 + local Range=asset.range + local category=asset.category + local ceiling=asset.DCSdesc.Hmax + local Vymax=asset.DCSdesc.VyMax + + -- Max cruise speed 90% of max speed. + local VxCruiseMax=0.90*Vmax + + -- Min cruise speed 70% of max cruise or 600 km/h whichever is lower. + local VxCruiseMin = math.min(VxCruiseMax*0.70, 166) + + -- Cruise speed (randomized). Expectation value at midpoint between min and max. + local VxCruise = UTILS.RandomGaussian((VxCruiseMax-VxCruiseMin)/2+VxCruiseMin, (VxCruiseMax-VxCruiseMax)/4, VxCruiseMin, VxCruiseMax) + + -- Climb speed 90% ov Vmax but max 720 km/h. + local VxClimb = math.min(Vmax*0.90, 200) + + -- Descent speed 60% of Vmax but max 500 km/h. + local VxDescent = math.min(Vmax*0.60, 140) + + -- Holding speed is 90% of descent speed. + local VxHolding = VxDescent*0.9 + + -- Final leg is 90% of holding speed. + local VxFinal = VxHolding*0.9 + + -- Reasonably civil climb speed Vy=1500 ft/min = 7.6 m/s but max aircraft specific climb rate. + local VyClimb=math.min(7.6, Vymax) + + -- Climb angle in rad. + --local AlphaClimb=math.asin(VyClimb/VxClimb) + local AlphaClimb=math.rad(4) + + -- Descent angle in rad. Moderate 4 degrees. + local AlphaDescent=math.rad(4) + + -- Expected cruise level (peak of Gaussian distribution) + local FLcruise_expect=150*RAT.unit.FL2m + if category==Group.Category.HELICOPTER then + FLcruise_expect=1000 -- 1000 m ASL + end + + ------------------------- + --- DEPARTURE AIRPORT --- + ------------------------- + + -- Coordinates of departure point. + local Pdeparture=departure:GetCoordinate() + + -- Height ASL of departure point. + local H_departure=Pdeparture.y + + --------------------------- + --- DESTINATION AIRPORT --- + --------------------------- + + -- Position of destination airport. + local Pdestination=destination:GetCoordinate() + + -- Height ASL of destination airport/zone. + local H_destination=Pdestination.y + + ----------------------------- + --- DESCENT/HOLDING POINT --- + ----------------------------- + + -- Get a random point between 5 and 10 km away from the destination. + local Rhmin=5000 + local Rhmax=10000 + + -- For helos we set a distance between 500 to 1000 m. + if category==Group.Category.HELICOPTER then + Rhmin=500 + Rhmax=1000 + end + + -- Coordinates of the holding point. y is the land height at that point. + local Pholding=Pdestination:GetRandomCoordinateInRadius(Rhmax, Rhmin) + + -- Distance from holding point to final destination (not used). + local d_holding=Pholding:Get2DDistance(Pdestination) + + -- AGL height of holding point. + local H_holding=Pholding.y + + --------------- + --- GENERAL --- + --------------- + + -- We go directly to the holding point not the destination airport. From there, planes are guided by DCS to final approach. + local heading=Pdeparture:HeadingTo(Pholding) + local d_total=Pdeparture:Get2DDistance(Pholding) + + ------------------------------ + --- Holding Point Altitude --- + ------------------------------ + + -- Holding point altitude. For planes between 1600 and 2400 m AGL. For helos 160 to 240 m AGL. + local h_holding=1200 + if category==Group.Category.HELICOPTER then + h_holding=150 + end + h_holding=UTILS.Randomize(h_holding, 0.2) + + -- Max holding altitude. + local DeltaholdingMax=self:_GetMaxHeight(d_total, AlphaClimb, AlphaDescent, H_departure, H_holding, 0) + + if h_holding>DeltaholdingMax then + h_holding=math.abs(DeltaholdingMax) + end + + -- This is the height ASL of the holding point we want to fly to. + local Hh_holding=H_holding+h_holding + + --------------------------- + --- Max Flight Altitude --- + --------------------------- + + -- Get max flight altitude relative to H_departure. + local h_max=self:_GetMaxHeight(d_total, AlphaClimb, AlphaDescent, H_departure, H_holding, h_holding) + + -- Max flight level ASL aircraft can reach for given angles and distance. + local FLmax = h_max+H_departure + + --CRUISE + -- Min cruise alt is just above holding point at destination or departure height, whatever is larger. + local FLmin=math.max(H_departure, Hh_holding) + + -- Ensure that FLmax not above its service ceiling. + FLmax=math.min(FLmax, ceiling) + + -- If the route is very short we set FLmin a bit lower than FLmax. + if FLmin>FLmax then + FLmin=FLmax + end + + -- Expected cruise altitude - peak of gaussian distribution. + if FLcruise_expectFLmax then + FLcruise_expect=FLmax + end + + -- Set cruise altitude. Selected from Gaussian distribution but limited to FLmin and FLmax. + local FLcruise=UTILS.RandomGaussian(FLcruise_expect, math.abs(FLmax-FLmin)/4, FLmin, FLmax) + + -- Climb and descent heights. + local h_climb = FLcruise - H_departure + local h_descent = FLcruise - Hh_holding + + -- Get distances. + local d_climb = h_climb/math.tan(AlphaClimb) + local d_descent = h_descent/math.tan(AlphaDescent) + local d_cruise = d_total-d_climb-d_descent + + -- Debug. + local text=string.format("Flight plan:\n") + text=text..string.format("Vx max = %.2f km/h\n", Vmax*3.6) + text=text..string.format("Vx climb = %.2f km/h\n", VxClimb*3.6) + text=text..string.format("Vx cruise = %.2f km/h\n", VxCruise*3.6) + text=text..string.format("Vx descent = %.2f km/h\n", VxDescent*3.6) + text=text..string.format("Vx holding = %.2f km/h\n", VxHolding*3.6) + text=text..string.format("Vx final = %.2f km/h\n", VxFinal*3.6) + text=text..string.format("Vy max = %.2f m/s\n", Vymax) + text=text..string.format("Vy climb = %.2f m/s\n", VyClimb) + text=text..string.format("Alpha Climb = %.2f Deg\n", math.deg(AlphaClimb)) + text=text..string.format("Alpha Descent = %.2f Deg\n", math.deg(AlphaDescent)) + text=text..string.format("Dist climb = %.3f km\n", d_climb/1000) + text=text..string.format("Dist cruise = %.3f km\n", d_cruise/1000) + text=text..string.format("Dist descent = %.3f km\n", d_descent/1000) + text=text..string.format("Dist total = %.3f km\n", d_total/1000) + text=text..string.format("h_climb = %.3f km\n", h_climb/1000) + text=text..string.format("h_desc = %.3f km\n", h_descent/1000) + text=text..string.format("h_holding = %.3f km\n", h_holding/1000) + text=text..string.format("h_max = %.3f km\n", h_max/1000) + text=text..string.format("FL min = %.3f km\n", FLmin/1000) + text=text..string.format("FL expect = %.3f km\n", FLcruise_expect/1000) + text=text..string.format("FL cruise * = %.3f km\n", FLcruise/1000) + text=text..string.format("FL max = %.3f km\n", FLmax/1000) + text=text..string.format("Ceiling = %.3f km\n", ceiling/1000) + text=text..string.format("Max range = %.3f km\n", Range/1000) + self:T(self.lid..text) + + -- Ensure that cruise distance is positve. Can be slightly negative in special cases. And we don't want to turn back. + if d_cruise<0 then + d_cruise=100 + end + + ------------------------ + --- Create Waypoints --- + ------------------------ + + -- Waypoints and coordinates + local wp={} + local c={} + + -- Cold start (default). + local _type=COORDINATE.WaypointType.TakeOffParking + local _action=COORDINATE.WaypointAction.FromParkingArea + + -- Hot start. + if asset.takeoffType and asset.takeoffType==COORDINATE.WaypointType.TakeOffParkingHot then + env.info("FF hot") + _type=COORDINATE.WaypointType.TakeOffParkingHot + _action=COORDINATE.WaypointAction.FromParkingAreaHot + else + env.info("FF cold") + end + + + --- Departure/Take-off + c[#c+1]=Pdeparture + wp[#wp+1]=Pdeparture:WaypointAir("RADIO", _type, _action, VxClimb*3.6, true, departure, nil, "Departure") + + --- Begin of Cruise + local Pcruise=Pdeparture:Translate(d_climb, heading) + Pcruise.y=FLcruise + c[#c+1]=Pcruise + wp[#wp+1]=Pcruise:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxCruise*3.6, true, nil, nil, "Cruise") + + --- Descent + local Pdescent=Pcruise:Translate(d_cruise, heading) + Pdescent.y=FLcruise + c[#c+1]=Pdescent + wp[#wp+1]=Pdescent:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxDescent*3.6, true, nil, nil, "Descent") + + --- Holding point + Pholding.y=H_holding+h_holding + c[#c+1]=Pholding + wp[#wp+1]=Pholding:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxHolding*3.6, true, nil, nil, "Holding") + + --- Final destination. + c[#c+1]=Pdestination + wp[#wp+1]=Pdestination:WaypointAir("RADIO", COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, VxFinal*3.6, true, destination, nil, "Final Destination") + + + -- Mark points at waypoints for debugging. + if self.Debug then + for i,coord in pairs(c) do + local coord=coord --Core.Point#COORDINATE + local dist=0 + if i>1 then + dist=coord:Get2DDistance(c[i-1]) + end + coord:MarkToAll(string.format("Waypoint %i, distance = %.2f km",i, dist/1000)) + end + end + + return wp,c +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- **Functional** - (R2.5) - Yet Another Missile Trainer. +-- +-- +-- Practice to evade missiles without being destroyed. +-- +-- +-- ## Main Features: +-- +-- * Handles air-to-air and surface-to-air missiles. +-- * Define your own training zones on the map. Players in this zone will be protected. +-- * Define launch zones. Only missiles launched in these zones are tracked. +-- * Define protected AI groups. +-- * F10 radio menu to adjust settings for each player. +-- * Alert on missile launch (optional). +-- * Marker of missile launch position (optional). +-- * Adaptive update of missile-to-player distance. +-- * Finite State Machine (FSM) implementation. +-- * Easy to use. See examples below. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Functional.FOX +-- @image Functional_FOX.png + + +--- FOX class. +-- @type FOX +-- @field #string ClassName Name of the class. +-- @field #boolean Debug Debug mode. Messages to all about status. +-- @field #string lid Class id string for output to DCS log file. +-- @field #table menuadded Table of groups the menu was added for. +-- @field #boolean menudisabled If true, F10 menu for players is disabled. +-- @field #boolean destroy Default player setting for destroying missiles. +-- @field #boolean launchalert Default player setting for launch alerts. +-- @field #boolean marklaunch Default player setting for mark launch coordinates. +-- @field #table players Table of players. +-- @field #table missiles Table of tracked missiles. +-- @field #table safezones Table of practice zones. +-- @field #table launchzones Table of launch zones. +-- @field Core.Set#SET_GROUP protectedset Set of protected groups. +-- @field #number explosionpower Power of explostion when destroying the missile in kg TNT. Default 5 kg TNT. +-- @field #number explosiondist Missile player distance in meters for destroying smaller missiles. Default 200 m. +-- @field #number explosiondist2 Missile player distance in meters for destroying big missiles. Default 500 m. +-- @field #number bigmissilemass Explosion power of big missiles. Default 50 kg TNT. Big missiles will be destroyed earlier. +-- @field #number dt50 Time step [sec] for missile position updates if distance to target > 50 km. Default 5 sec. +-- @field #number dt10 Time step [sec] for missile position updates if distance to target > 10 km and < 50 km. Default 1 sec. +-- @field #number dt05 Time step [sec] for missile position updates if distance to target > 5 km and < 10 km. Default 0.5 sec. +-- @field #number dt01 Time step [sec] for missile position updates if distance to target > 1 km and < 5 km. Default 0.1 sec. +-- @field #number dt00 Time step [sec] for missile position updates if distance to target < 1 km. Default 0.01 sec. +-- @field #boolean +-- @extends Core.Fsm#FSM + +--- Fox 3! +-- +-- === +-- +-- ![Banner Image](..\Presentations\FOX\FOX_Main.png) +-- +-- # The FOX Concept +-- +-- As you probably know [Fox](https://en.wikipedia.org/wiki/Fox_(code_word)) is a NATO brevity code for launching air-to-air munition. Therefore, the class name is not 100% accurate as this +-- script handles air-to-air but also surface-to-air missiles. +-- +-- # Basic Script +-- +-- -- Create a new missile trainer object. +-- fox=FOX:New() +-- +-- -- Start missile trainer. +-- fox:Start() +-- +-- # Training Zones +-- +-- Players are only protected if they are inside one of the training zones. +-- +-- -- Create a new missile trainer object. +-- fox=FOX:New() +-- +-- -- Add training zones. +-- fox:AddSafeZone(ZONE:New("Training Zone Alpha")) +-- fox:AddSafeZone(ZONE:New("Training Zone Bravo")) +-- +-- -- Start missile trainer. +-- fox:Start() +-- +-- # Launch Zones +-- +-- Missile launches are only monitored if the shooter is inside the defined launch zone. +-- +-- -- Create a new missile trainer object. +-- fox=FOX:New() +-- +-- -- Add training zones. +-- fox:AddLaunchZone(ZONE:New("Launch Zone SA-10 Krim")) +-- fox:AddLaunchZone(ZONE:New("Training Zone Bravo")) +-- +-- -- Start missile trainer. +-- fox:Start() +-- +-- # Protected AI Groups +-- +-- Define AI protected groups. These groups cannot be harmed by missiles. +-- +-- ## Add Individual Groups +-- +-- -- Create a new missile trainer object. +-- fox=FOX:New() +-- +-- -- Add single protected group(s). +-- fox:AddProtectedGroup(GROUP:FindByName("A-10 Protected")) +-- fox:AddProtectedGroup(GROUP:FindByName("Yak-40")) +-- +-- -- Start missile trainer. +-- fox:Start() +-- +-- # Fine Tuning +-- +-- Todo! +-- +-- # Special Events +-- +-- Todo! +-- +-- +-- @field #FOX +FOX = { + ClassName = "FOX", + Debug = false, + lid = nil, + menuadded = {}, + menudisabled = nil, + destroy = nil, + launchalert = nil, + marklaunch = nil, + missiles = {}, + players = {}, + safezones = {}, + launchzones = {}, + protectedset = nil, + explosionpower = 0.1, + explosiondist = 200, + explosiondist2 = 500, + bigmissilemass = 50, + destroy = nil, + dt50 = 5, + dt10 = 1, + dt05 = 0.5, + dt01 = 0.1, + dt00 = 0.01, +} + + +--- Player data table holding all important parameters of each player. +-- @type FOX.PlayerData +-- @field Wrapper.Unit#UNIT unit Aircraft of the player. +-- @field #string unitname Name of the unit. +-- @field Wrapper.Client#CLIENT client Client object of player. +-- @field #string callsign Callsign of player. +-- @field Wrapper.Group#GROUP group Aircraft group of player. +-- @field #string groupname Name of the the player aircraft group. +-- @field #string name Player name. +-- @field #number coalition Coalition number of player. +-- @field #boolean destroy Destroy missile. +-- @field #boolean launchalert Alert player on detected missile launch. +-- @field #boolean marklaunch Mark position of launched missile on F10 map. +-- @field #number defeated Number of missiles defeated. +-- @field #number dead Number of missiles not defeated. +-- @field #boolean inzone Player is inside a protected zone. + +--- Missile data table. +-- @type FOX.MissileData +-- @field Wrapper.Unit#UNIT weapon Missile weapon unit. +-- @field #boolean active If true the missile is active. +-- @field #string missileType Type of missile. +-- @field #string missileName Name of missile. +-- @field #number missileRange Range of missile in meters. +-- @field #number fuseDist Fuse distance in meters. +-- @field #number explosive Explosive mass in kg TNT. +-- @field Wrapper.Unit#UNIT shooterUnit Unit that shot the missile. +-- @field Wrapper.Group#GROUP shooterGroup Group that shot the missile. +-- @field #number shooterCoalition Coalition side of the shooter. +-- @field #string shooterName Name of the shooter unit. +-- @field #number shotTime Abs. mission time in seconds the missile was fired. +-- @field Core.Point#COORDINATE shotCoord Coordinate where the missile was fired. +-- @field Wrapper.Unit#UNIT targetUnit Unit that was targeted. +-- @field #string targetName Name of the target unit or "unknown". +-- @field #string targetOrig Name of the "original" target, i.e. the one right after launched. +-- @field #FOX.PlayerData targetPlayer Player that was targeted or nil. + +--- Main radio menu on group level. +-- @field #table MenuF10 Root menu table on group level. +FOX.MenuF10={} + +--- Main radio menu on mission level. +-- @field #table MenuF10Root Root menu on mission level. +FOX.MenuF10Root=nil + +--- FOX class version. +-- @field #string version +FOX.version="0.6.1" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ToDo list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO list: +-- DONE: safe zones +-- DONE: mark shooter on F10 + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new FOX class object. +-- @param #FOX self +-- @return #FOX self. +function FOX:New() + + self.lid="FOX | " + + -- Inherit everthing from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #FOX + + -- Defaults: + self:SetDefaultMissileDestruction(true) + self:SetDefaultLaunchAlerts(true) + self:SetDefaultLaunchMarks(true) + + -- Explosion/destruction defaults. + self:SetExplosionDistance() + self:SetExplosionDistanceBigMissiles() + self:SetExplosionPower() + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start FOX script. + self:AddTransition("*", "Status", "*") -- Status update. + self:AddTransition("*", "MissileLaunch", "*") -- Missile was launched. + self:AddTransition("*", "MissileDestroyed", "*") -- Missile was destroyed before impact. + self:AddTransition("*", "EnterSafeZone", "*") -- Player enters a safe zone. + self:AddTransition("*", "ExitSafeZone", "*") -- Player exists a safe zone. + self:AddTransition("Running", "Stop", "Stopped") -- Stop FOX script. + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the FOX. Initializes parameters and starts event handlers. + -- @function [parent=#FOX] Start + -- @param #FOX self + + --- Triggers the FSM event "Start" after a delay. Starts the FOX. Initializes parameters and starts event handlers. + -- @function [parent=#FOX] __Start + -- @param #FOX self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the FOX and all its event handlers. + -- @param #FOX self + + --- Triggers the FSM event "Stop" after a delay. Stops the FOX and all its event handlers. + -- @function [parent=#FOX] __Stop + -- @param #FOX self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Status". + -- @function [parent=#FOX] Status + -- @param #FOX self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#FOX] __Status + -- @param #FOX self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "MissileLaunch". + -- @function [parent=#FOX] MissileLaunch + -- @param #FOX self + -- @param #FOX.MissileData missile Data of the fired missile. + + --- Triggers the FSM delayed event "MissileLaunch". + -- @function [parent=#FOX] __MissileLaunch + -- @param #FOX self + -- @param #number delay Delay in seconds before the function is called. + -- @param #FOX.MissileData missile Data of the fired missile. + + --- On after "MissileLaunch" event user function. Called when a missile was launched. + -- @function [parent=#FOX] OnAfterMissileLaunch + -- @param #FOX self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #FOX.MissileData missile Data of the fired missile. + + --- Triggers the FSM event "MissileDestroyed". + -- @function [parent=#FOX] MissileDestroyed + -- @param #FOX self + -- @param #FOX.MissileData missile Data of the destroyed missile. + + --- Triggers the FSM delayed event "MissileDestroyed". + -- @function [parent=#FOX] __MissileDestroyed + -- @param #FOX self + -- @param #number delay Delay in seconds before the function is called. + -- @param #FOX.MissileData missile Data of the destroyed missile. + + --- On after "MissileDestroyed" event user function. Called when a missile was destroyed. + -- @function [parent=#FOX] OnAfterMissileDestroyed + -- @param #FOX self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #FOX.MissileData missile Data of the destroyed missile. + + + --- Triggers the FSM event "EnterSafeZone". + -- @function [parent=#FOX] EnterSafeZone + -- @param #FOX self + -- @param #FOX.PlayerData player Player data. + + --- Triggers the FSM delayed event "EnterSafeZone". + -- @function [parent=#FOX] __EnterSafeZone + -- @param #FOX self + -- @param #number delay Delay in seconds before the function is called. + -- @param #FOX.PlayerData player Player data. + + --- On after "EnterSafeZone" event user function. Called when a player enters a safe zone. + -- @function [parent=#FOX] OnAfterEnterSafeZone + -- @param #FOX self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #FOX.PlayerData player Player data. + + + --- Triggers the FSM event "ExitSafeZone". + -- @function [parent=#FOX] ExitSafeZone + -- @param #FOX self + -- @param #FOX.PlayerData player Player data. + + --- Triggers the FSM delayed event "ExitSafeZone". + -- @function [parent=#FOX] __ExitSafeZone + -- @param #FOX self + -- @param #number delay Delay in seconds before the function is called. + -- @param #FOX.PlayerData player Player data. + + --- On after "ExitSafeZone" event user function. Called when a player exists a safe zone. + -- @function [parent=#FOX] OnAfterExitSafeZone + -- @param #FOX self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #FOX.PlayerData player Player data. + + + return self +end + +--- On after Start event. Starts the missile trainer and adds event handlers. +-- @param #FOX self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FOX:onafterStart(From, Event, To) + + -- Short info. + local text=string.format("Starting FOX Missile Trainer %s", FOX.version) + env.info(text) + + -- Handle events: + self:HandleEvent(EVENTS.Birth) + self:HandleEvent(EVENTS.Shot) + + if self.Debug then + self:HandleEvent(EVENTS.Hit) + end + + if self.Debug then + self:TraceClass(self.ClassName) + self:TraceLevel(2) + end + + self:__Status(-20) +end + +--- On after Stop event. Stops the missile trainer and unhandles events. +-- @param #FOX self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FOX:onafterStop(From, Event, To) + + -- Short info. + local text=string.format("Stopping FOX Missile Trainer %s", FOX.version) + env.info(text) + + -- Handle events: + self:UnHandleEvent(EVENTS.Birth) + self:UnHandleEvent(EVENTS.Shot) + + if self.Debug then + self:UnhandleEvent(EVENTS.Hit) + end + +end + +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add a training zone. Players in the zone are safe. +-- @param #FOX self +-- @param Core.Zone#ZONE zone Training zone. +-- @return #FOX self +function FOX:AddSafeZone(zone) + + table.insert(self.safezones, zone) + + return self +end + +--- Add a launch zone. Only missiles launched within these zones will be tracked. +-- @param #FOX self +-- @param Core.Zone#ZONE zone Training zone. +-- @return #FOX self +function FOX:AddLaunchZone(zone) + + table.insert(self.launchzones, zone) + + return self +end + +--- Add a protected set of groups. +-- @param #FOX self +-- @param Core.Set#SET_GROUP groupset The set of groups. +-- @return #FOX self +function FOX:SetProtectedGroupSet(groupset) + self.protectedset=groupset + return self +end + +--- Add a group to the protected set. +-- @param #FOX self +-- @param Wrapper.Group#GROUP group Protected group. +-- @return #FOX self +function FOX:AddProtectedGroup(group) + + if not self.protectedset then + self.protectedset=SET_GROUP:New() + end + + self.protectedset:AddGroup(group) + + return self +end + +--- Set explosion power. This is an "artificial" explosion generated when the missile is destroyed. Just for the visual effect. +-- Don't set the explosion power too big or it will harm the aircraft in the vicinity. +-- @param #FOX self +-- @param #number power Explosion power in kg TNT. Default 0.1 kg. +-- @return #FOX self +function FOX:SetExplosionPower(power) + + self.explosionpower=power or 0.1 + + return self +end + +--- Set missile-player distance when missile is destroyed. +-- @param #FOX self +-- @param #number distance Distance in meters. Default 200 m. +-- @return #FOX self +function FOX:SetExplosionDistance(distance) + + self.explosiondist=distance or 200 + + return self +end + +--- Set missile-player distance when BIG missiles are destroyed. +-- @param #FOX self +-- @param #number distance Distance in meters. Default 500 m. +-- @param #number explosivemass Explosive mass of missile threshold in kg TNT. Default 50 kg. +-- @return #FOX self +function FOX:SetExplosionDistanceBigMissiles(distance, explosivemass) + + self.explosiondist2=distance or 500 + + self.bigmissilemass=explosivemass or 50 + + return self +end + +--- Disable F10 menu for all players. +-- @param #FOX self +-- @param #boolean switch If true debug mode on. If false/nil debug mode off +-- @return #FOX self +function FOX:SetDisableF10Menu() + + self.menudisabled=true + + return self +end + +--- Set default player setting for missile destruction. +-- @param #FOX self +-- @param #boolean switch If true missiles are destroyed. If false/nil missiles are not destroyed. +-- @return #FOX self +function FOX:SetDefaultMissileDestruction(switch) + + if switch==nil then + self.destroy=false + else + self.destroy=switch + end + + return self +end + +--- Set default player setting for launch alerts. +-- @param #FOX self +-- @param #boolean switch If true launch alerts to players are active. If false/nil no launch alerts are given. +-- @return #FOX self +function FOX:SetDefaultLaunchAlerts(switch) + + if switch==nil then + self.launchalert=false + else + self.launchalert=switch + end + + return self +end + +--- Set default player setting for marking missile launch coordinates +-- @param #FOX self +-- @param #boolean switch If true missile launches are marked. If false/nil marks are disabled. +-- @return #FOX self +function FOX:SetDefaultLaunchMarks(switch) + + if switch==nil then + self.marklaunch=false + else + self.marklaunch=switch + end + + return self +end + + +--- Set debug mode on/off. +-- @param #FOX self +-- @param #boolean switch If true debug mode on. If false/nil debug mode off. +-- @return #FOX self +function FOX:SetDebugOnOff(switch) + + if switch==nil then + self.Debug=false + else + self.Debug=switch + end + + return self +end + +--- Set debug mode on. +-- @param #FOX self +-- @return #FOX self +function FOX:SetDebugOn() + self:SetDebugOnOff(true) + return self +end + +--- Set debug mode off. +-- @param #FOX self +-- @return #FOX self +function FOX:SetDebugOff() + self:SetDebugOff(false) + return self +end + +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Status Functions +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check spawn queue and spawn aircraft if necessary. +-- @param #FOX self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FOX:onafterStatus(From, Event, To) + + -- Get FSM state. + local fsmstate=self:GetState() + + local time=timer.getAbsTime() + local clock=UTILS.SecondsToClock(time) + + -- Status. + self:I(self.lid..string.format("Missile trainer status %s: %s", clock, fsmstate)) + + -- Check missile status. + self:_CheckMissileStatus() + + -- Check player status. + self:_CheckPlayers() + + if fsmstate=="Running" then + self:__Status(-10) + end +end + +--- Check status of players. +-- @param #FOX self +function FOX:_CheckPlayers() + + for playername,_playersettings in pairs(self.players) do + local playersettings=_playersettings --#FOX.PlayerData + + local unitname=playersettings.unitname + local unit=UNIT:FindByName(unitname) + + if unit and unit:IsAlive() then + + local coord=unit:GetCoordinate() + + local issafe=self:_CheckCoordSafe(coord) + + + if issafe then + + ----------------------------- + -- Player INSIDE Safe Zone -- + ----------------------------- + + if not playersettings.inzone then + self:EnterSafeZone(playersettings) + playersettings.inzone=true + end + + else + + ------------------------------ + -- Player OUTSIDE Safe Zone -- + ------------------------------ + + if playersettings.inzone==true then + self:ExitSafeZone(playersettings) + playersettings.inzone=false + end + + end + end + end + +end + +--- Remove missile. +-- @param #FOX self +-- @param #FOX.MissileData missile Missile data. +function FOX:_RemoveMissile(missile) + + if missile then + for i,_missile in pairs(self.missiles) do + local m=_missile --#FOX.MissileData + if missile.missileName==m.missileName then + table.remove(self.missiles, i) + return + end + end + end + +end + +--- Missile status. +-- @param #FOX self +function FOX:_CheckMissileStatus() + + local text="Missiles:" + local inactive={} + for i,_missile in pairs(self.missiles) do + local missile=_missile --#FOX.MissileData + + local targetname="unkown" + if missile.targetUnit then + targetname=missile.targetUnit:GetName() + end + local playername="none" + if missile.targetPlayer then + playername=missile.targetPlayer.name + end + local active=tostring(missile.active) + local mtype=missile.missileType + local dtype=missile.missileType + local range=UTILS.MetersToNM(missile.missileRange) + + if not active then + table.insert(inactive,i) + end + local heading=self:_GetWeapongHeading(missile.weapon) + + text=text..string.format("\n[%d] %s: active=%s, range=%.1f NM, heading=%03d, target=%s, player=%s, missilename=%s", i, mtype, active, range, heading, targetname, playername, missile.missileName) + + end + if #self.missiles==0 then + text=text.." none" + end + self:I(self.lid..text) + + -- Remove inactive missiles. + for i=#self.missiles,1,-1 do + local missile=self.missiles[i] --#FOX.MissileData + if missile and not missile.active then + table.remove(self.missiles, i) + end + end + +end + +--- Check if missile target is protected. +-- @param #FOX self +-- @param Wrapper.Unit#UNIT targetunit Target unit. +-- @return #boolean If true, unit is protected. +function FOX:_IsProtected(targetunit) + + if not self.protectedset then + return false + end + + if targetunit and targetunit:IsAlive() then + + -- Get Group. + local targetgroup=targetunit:GetGroup() + + if targetgroup then + local targetname=targetgroup:GetName() + + for _,_group in pairs(self.protectedset:GetSetObjects()) do + local group=_group --Wrapper.Group#GROUP + + if group then + local groupname=group:GetName() + + -- Target belongs to a protected set. + if targetname==groupname then + return true + end + end + + end + end + end + + return false +end + +--- Missle launch event. +-- @param #FOX self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #FOX.MissileData missile Fired missile +function FOX:onafterMissileLaunch(From, Event, To, missile) + + -- Tracking info and init of last bomb position. + local text=string.format("FOX: Tracking missile %s(%s) - target %s - shooter %s", missile.missileType, missile.missileName, tostring(missile.targetName), missile.shooterName) + self:I(FOX.lid..text) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + + -- Loop over players. + for _,_player in pairs(self.players) do + local player=_player --#FOX.PlayerData + + -- Player position. + local playerUnit=player.unit + + -- Check that player is alive and of the opposite coalition. + if playerUnit and playerUnit:IsAlive() and player.coalition~=missile.shooterCoalition then + + -- Player missile distance. + local distance=playerUnit:GetCoordinate():Get3DDistance(missile.shotCoord) + + -- Player bearing to missile. + local bearing=playerUnit:GetCoordinate():HeadingTo(missile.shotCoord) + + -- Alert that missile has been launched. + if player.launchalert then + + -- Alert directly targeted players or players that are within missile max range. + if (missile.targetPlayer and player.unitname==missile.targetPlayer.unitname) or (distance=self.bigmissilemass + end + + -- If missile is 150 m from target ==> destroy missile if in safe zone. + if destroymissile and self:_CheckCoordSafe(targetCoord) then + + -- Destroy missile. + self:I(self.lid..string.format("Destroying missile %s(%s) fired by %s aimed at %s [player=%s] at distance %.1f m", + missile.missileType, missile.missileName, missile.shooterName, target:GetName(), tostring(missile.targetPlayer~=nil), distance)) + _ordnance:destroy() + + -- Missile is not active any more. + missile.active=false + + -- Debug smoke. + if self.Debug then + missileCoord:SmokeRed() + targetCoord:SmokeGreen() + end + + -- Create event. + self:MissileDestroyed(missile) + + -- Little explosion for the visual effect. + if self.explosionpower>0 and distance>50 and (distShooter==nil or (distShooter and distShooter>50)) then + missileCoord:Explosion(self.explosionpower) + end + + -- Target was a player. + if missile.targetPlayer then + + -- Message to target. + local text=string.format("Destroying missile. %s", self:_DeadText()) + MESSAGE:New(text, 10):ToGroup(target:GetGroup()) + + -- Increase dead counter. + missile.targetPlayer.dead=missile.targetPlayer.dead+1 + end + + -- Terminate timer. + return nil + + else + + -- Time step. + local dt=1.0 + if distance>50000 then + -- > 50 km + dt=self.dt50 --=5.0 + elseif distance>10000 then + -- 10-50 km + dt=self.dt10 --=1.0 + elseif distance>5000 then + -- 5-10 km + dt=self.dt05 --0.5 + elseif distance>1000 then + -- 1-5 km + dt=self.dt01 --0.1 + else + -- < 1 km + dt=self.dt00 --0.01 + end + + -- Check again in dt seconds. + return timer.getTime()+dt + end + + else + + -- Destroy missile. + self:T(self.lid..string.format("Missile %s(%s) fired by %s has no current target. Checking back in 0.1 sec.", missile.missileType, missile.missileName, missile.shooterName)) + return timer.getTime()+0.1 + + -- No target ==> terminate timer. + --return nil + end + + else + + ------------------------------------- + -- Missile does not exist any more -- + ------------------------------------- + + if target then + + -- Get human player. + local player=self:_GetPlayerFromUnit(target) + + -- Check for player and distance < 10 km. + if player and player.unit:IsAlive() then -- and missileCoord and player.unit:GetCoordinate():Get3DDistance(missileCoord)<10*1000 then + local text=string.format("Missile defeated. Well done, %s!", player.name) + MESSAGE:New(text, 10):ToClient(player.client) + + -- Increase defeated counter. + player.defeated=player.defeated+1 + end + + end + + -- Missile is not active any more. + missile.active=false + + --Terminate the timer. + self:T(FOX.lid..string.format("Terminating missile track timer.")) + return nil + + end -- _status check + + end -- end function trackBomb + + -- Weapon is not yet "alife" just yet. Start timer with a little delay. + self:T(FOX.lid..string.format("Tracking of missile starts in 0.0001 seconds.")) + timer.scheduleFunction(trackMissile, missile.weapon, timer.getTime()+0.0001) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Event Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- FOX event handler for event birth. +-- @param #FOX self +-- @param Core.Event#EVENTDATA EventData +function FOX:OnEventBirth(EventData) + self:F3({eventbirth = EventData}) + + -- Nil checks. + if EventData==nil then + self:E(self.lid.."ERROR: EventData=nil in event BIRTH!") + self:E(EventData) + return + end + if EventData.IniUnit==nil then + self:E(self.lid.."ERROR: EventData.IniUnit=nil in event BIRTH!") + self:E(EventData) + return + end + + -- Player unit and name. + local _unitName=EventData.IniUnitName + local playerunit, playername=self:_GetPlayerUnitAndName(_unitName) + + -- Debug info. + self:T(self.lid.."BIRTH: unit = "..tostring(EventData.IniUnitName)) + self:T(self.lid.."BIRTH: group = "..tostring(EventData.IniGroupName)) + self:T(self.lid.."BIRTH: player = "..tostring(playername)) + + -- Check if player entered. + if playerunit and playername then + + local _uid=playerunit:GetID() + local _group=playerunit:GetGroup() + local _callsign=playerunit:GetCallsign() + + -- Debug output. + local text=string.format("Pilot %s, callsign %s entered unit %s of group %s.", playername, _callsign, _unitName, _group:GetName()) + self:T(self.lid..text) + MESSAGE:New(text, 5):ToAllIf(self.Debug) + + -- Add F10 radio menu for player. + if not self.menudisabled then + SCHEDULER:New(nil, self._AddF10Commands, {self,_unitName}, 0.1) + end + + -- Player data. + local playerData={} --#FOX.PlayerData + + -- Player unit, client and callsign. + playerData.unit = playerunit + playerData.unitname = _unitName + playerData.group = _group + playerData.groupname = _group:GetName() + playerData.name = playername + playerData.callsign = playerData.unit:GetCallsign() + playerData.client = CLIENT:FindByName(_unitName, nil, true) + playerData.coalition = _group:GetCoalition() + + playerData.destroy=playerData.destroy or self.destroy + playerData.launchalert=playerData.launchalert or self.launchalert + playerData.marklaunch=playerData.marklaunch or self.marklaunch + + playerData.defeated=playerData.defeated or 0 + playerData.dead=playerData.dead or 0 + + -- Init player data. + self.players[playername]=playerData + + end +end + +--- Get missile target. +-- @param #FOX self +-- @param #FOX.MissileData missile The missile data table. +function FOX:GetMissileTarget(missile) + + local target=nil + local targetName="unknown" + local targetUnit=nil --Wrapper.Unit#UNIT + + if missile.weapon and missile.weapon:isExist() then + + -- Get target of missile. + target=missile.weapon:getTarget() + + -- Get the target unit. Note if if _target is not nil, the unit can sometimes not be found! + if target then + self:T2({missiletarget=target}) + + -- Get target unit. + targetUnit=UNIT:Find(target) + + if targetUnit then + targetName=targetUnit:GetName() + + missile.targetUnit=targetUnit + missile.targetPlayer=self:_GetPlayerFromUnit(missile.targetUnit) + end + + end + end + + -- Missile got new target. + if missile.targetName and missile.targetName~=targetName then + self:I(self.lid..string.format("Missile %s(%s) changed target to %s. Previous target was %s.", missile.missileType, missile.missileName, targetName, missile.targetName)) + end + + -- Set target name. + missile.targetName=targetName + +end + +--- FOX event handler for event shot (when a unit releases a rocket or bomb (but not a fast firing gun). +-- @param #FOX self +-- @param Core.Event#EVENTDATA EventData +function FOX:OnEventShot(EventData) + self:T2({eventshot=EventData}) + + if EventData.Weapon==nil then + return + end + if EventData.IniDCSUnit==nil then + return + end + + -- Weapon data. + local _weapon = EventData.WeaponName + local _target = EventData.Weapon:getTarget() + local _targetName = "unknown" + local _targetUnit = nil --Wrapper.Unit#UNIT + + -- Weapon descriptor. + local desc=EventData.Weapon:getDesc() + self:T2({desc=desc}) + + -- Weapon category: 0=Shell, 1=Missile, 2=Rocket, 3=BOMB + local weaponcategory=desc.category + + -- Missile category: 1=AAM, 2=SAM, 6=OTHER + local missilecategory=desc.missileCategory + + local missilerange=nil + if missilecategory then + missilerange=desc.rangeMaxAltMax + end + + -- Debug info. + self:T2(FOX.lid.."EVENT SHOT: FOX") + self:T2(FOX.lid..string.format("EVENT SHOT: Ini unit = %s", tostring(EventData.IniUnitName))) + self:T2(FOX.lid..string.format("EVENT SHOT: Ini group = %s", tostring(EventData.IniGroupName))) + self:T2(FOX.lid..string.format("EVENT SHOT: Weapon type = %s", tostring(_weapon))) + self:T2(FOX.lid..string.format("EVENT SHOT: Weapon categ = %s", tostring(weaponcategory))) + self:T2(FOX.lid..string.format("EVENT SHOT: Missil categ = %s", tostring(missilecategory))) + self:T2(FOX.lid..string.format("EVENT SHOT: Missil range = %s", tostring(missilerange))) + + + -- Check if fired in launch zone. + if not self:_CheckCoordLaunch(EventData.IniUnit:GetCoordinate()) then + self:T(self.lid.."Missile was not fired in launch zone. No tracking!") + return + end + + -- Track missiles of type AAM=1, SAM=2 or OTHER=6 + local _track = weaponcategory==1 and missilecategory and (missilecategory==1 or missilecategory==2 or missilecategory==6) + + -- Only track missiles + if _track then + + local missile={} --#FOX.MissileData + + missile.active=true + missile.weapon=EventData.weapon + missile.missileType=_weapon + missile.missileRange=missilerange + missile.missileName=EventData.weapon:getName() + missile.shooterUnit=EventData.IniUnit + missile.shooterGroup=EventData.IniGroup + missile.shooterCoalition=EventData.IniUnit:GetCoalition() + missile.shooterName=EventData.IniUnitName + missile.shotTime=timer.getAbsTime() + missile.shotCoord=EventData.IniUnit:GetCoordinate() + missile.fuseDist=desc.fuseDist + missile.explosive=desc.warhead.explosiveMass or desc.warhead.shapedExplosiveMass + missile.targetOrig=missile.targetName + + -- Set missile target name, unit and player. + self:GetMissileTarget(missile) + + self:I(FOX.lid..string.format("EVENT SHOT: Shooter=%s %s(%s) ==> Target=%s, fuse dist=%s, explosive=%s", + tostring(missile.shooterName), tostring(missile.missileType), tostring(missile.missileName), tostring(missile.targetName), tostring(missile.fuseDist), tostring(missile.explosive))) + + -- Only track if target was a player or target is protected. Saw the 9M311 missiles have no target! + if missile.targetPlayer or self:_IsProtected(missile.targetUnit) or missile.targetName=="unknown" then + + -- Add missile table. + table.insert(self.missiles, missile) + + -- Trigger MissileLaunch event. + self:__MissileLaunch(0.1, missile) + + end + + end --if _track + +end + +--- FOX event handler for event hit. +-- @param #FOX self +-- @param Core.Event#EVENTDATA EventData +function FOX:OnEventHit(EventData) + self:T({eventhit = EventData}) + + -- Nil checks. + if EventData.Weapon==nil then + return + end + if EventData.IniUnit==nil then + return + end + if EventData.TgtUnit==nil then + return + end + + local weapon=EventData.Weapon + local weaponname=weapon:getName() + + for i,_missile in pairs(self.missiles) do + local missile=_missile --#FOX.MissileData + if missile.missileName==weaponname then + self:I(self.lid..string.format("WARNING: Missile %s (%s) hit target %s. Missile trainer target was %s.", missile.missileType, missile.missileName, EventData.TgtUnitName, missile.targetName)) + self:I({missile=missile}) + return + end + end + +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- RADIO MENU Functions +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add menu commands for player. +-- @param #FOX self +-- @param #string _unitName Name of player unit. +function FOX:_AddF10Commands(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check for player unit. + if _unit and playername then + + -- Get group and ID. + local group=_unit:GetGroup() + local gid=group:GetID() + + if group and gid then + + if not self.menuadded[gid] then + + -- Enable switch so we don't do this twice. + self.menuadded[gid]=true + + -- Set menu root path. + local _rootPath=nil + if FOX.MenuF10Root then + ------------------------ + -- MISSON LEVEL MENUE -- + ------------------------ + + -- F10/FOX/... + _rootPath=FOX.MenuF10Root + + else + ------------------------ + -- GROUP LEVEL MENUES -- + ------------------------ + + -- Main F10 menu: F10/FOX/ + if FOX.MenuF10[gid]==nil then + FOX.MenuF10[gid]=missionCommands.addSubMenuForGroup(gid, "FOX") + end + + -- F10/FOX/... + _rootPath=FOX.MenuF10[gid] + + end + + + -------------------------------- + -- F10/F FOX/F1 Help + -------------------------------- + --local _helpPath=missionCommands.addSubMenuForGroup(gid, "Help", _rootPath) + -- F10/FOX/F1 Help/ + --missionCommands.addCommandForGroup(gid, "Subtitles On/Off", _helpPath, self._SubtitlesOnOff, self, _unitName) -- F7 + --missionCommands.addCommandForGroup(gid, "Trapsheet On/Off", _helpPath, self._TrapsheetOnOff, self, _unitName) -- F8 + + ------------------------- + -- F10/F FOX/ + ------------------------- + + missionCommands.addCommandForGroup(gid, "Destroy Missiles On/Off", _rootPath, self._ToggleDestroyMissiles, self, _unitName) -- F1 + missionCommands.addCommandForGroup(gid, "Launch Alerts On/Off", _rootPath, self._ToggleLaunchAlert, self, _unitName) -- F2 + missionCommands.addCommandForGroup(gid, "Mark Launch On/Off", _rootPath, self._ToggleLaunchMark, self, _unitName) -- F3 + missionCommands.addCommandForGroup(gid, "My Status", _rootPath, self._MyStatus, self, _unitName) -- F4 + + end + else + self:E(self.lid..string.format("ERROR: Could not find group or group ID in AddF10Menu() function. Unit name: %s.", _unitName)) + end + else + self:E(self.lid..string.format("ERROR: Player unit does not exist in AddF10Menu() function. Unit name: %s.", _unitName)) + end + +end + + +--- Turn player's launch alert on/off. +-- @param #FOX self +-- @param #string _unitname Name of the player unit. +function FOX:_MyStatus(_unitname) + self:F2(_unitname) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Player data. + local playerData=self.players[playername] --#FOX.PlayerData + + if playerData then + + local m,mtext=self:_GetTargetMissiles(playerData.name) + + local text=string.format("Status of player %s:\n", playerData.name) + local safe=self:_CheckCoordSafe(playerData.unit:GetCoordinate()) + + text=text..string.format("Destroy missiles? %s\n", tostring(playerData.destroy)) + text=text..string.format("Launch alert? %s\n", tostring(playerData.launchalert)) + text=text..string.format("Launch marks? %s\n", tostring(playerData.marklaunch)) + text=text..string.format("Am I safe? %s\n", tostring(safe)) + text=text..string.format("Missiles defeated: %d\n", playerData.defeated) + text=text..string.format("Missiles destroyed: %d\n", playerData.dead) + text=text..string.format("Me target: %d\n%s", m, mtext) + + MESSAGE:New(text, 10, nil, true):ToClient(playerData.client) + + end + end +end + +--- Turn player's launch alert on/off. +-- @param #FOX self +-- @param #string playername Name of the player. +-- @return #number Number of missiles targeting the player. +-- @return #string Missile info. +function FOX:_GetTargetMissiles(playername) + + local text="" + local n=0 + for _,_missile in pairs(self.missiles) do + local missile=_missile --#FOX.MissileData + + if missile.targetPlayer and missile.targetPlayer.name==playername then + n=n+1 + text=text..string.format("Type %s: active %s\n", missile.missileType, tostring(missile.active)) + end + + end + + return n,text +end + +--- Turn player's launch alert on/off. +-- @param #FOX self +-- @param #string _unitname Name of the player unit. +function FOX:_ToggleLaunchAlert(_unitname) + self:F2(_unitname) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Player data. + local playerData=self.players[playername] --#FOX.PlayerData + + if playerData then + + -- Invert state. + playerData.launchalert=not playerData.launchalert + + -- Inform player. + local text="" + if playerData.launchalert==true then + text=string.format("%s, missile launch alerts are now ENABLED.", playerData.name) + else + text=string.format("%s, missile launch alerts are now DISABLED.", playerData.name) + end + MESSAGE:New(text, 5):ToClient(playerData.client) + + end + end +end + +--- Turn player's launch marks on/off. +-- @param #FOX self +-- @param #string _unitname Name of the player unit. +function FOX:_ToggleLaunchMark(_unitname) + self:F2(_unitname) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Player data. + local playerData=self.players[playername] --#FOX.PlayerData + + if playerData then + + -- Invert state. + playerData.marklaunch=not playerData.marklaunch + + -- Inform player. + local text="" + if playerData.marklaunch==true then + text=string.format("%s, missile launch marks are now ENABLED.", playerData.name) + else + text=string.format("%s, missile launch marks are now DISABLED.", playerData.name) + end + MESSAGE:New(text, 5):ToClient(playerData.client) + + end + end +end + + +--- Turn destruction of missiles on/off for player. +-- @param #FOX self +-- @param #string _unitname Name of the player unit. +function FOX:_ToggleDestroyMissiles(_unitname) + self:F2(_unitname) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Player data. + local playerData=self.players[playername] --#FOX.PlayerData + + if playerData then + + -- Invert state. + playerData.destroy=not playerData.destroy + + -- Inform player. + local text="" + if playerData.destroy==true then + text=string.format("%s, incoming missiles will be DESTROYED.", playerData.name) + else + text=string.format("%s, incoming missiles will NOT be DESTROYED.", playerData.name) + end + MESSAGE:New(text, 5):ToClient(playerData.client) + + end + end +end + + +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get a random text message in case you die. +-- @param #FOX self +-- @return #string Text in case you die. +function FOX:_DeadText() + + local texts={} + texts[1]="You're dead!" + texts[2]="Meet your maker!" + texts[3]="Time to meet your maker!" + texts[4]="Well, I guess that was it!" + texts[5]="Bye, bye!" + texts[6]="Cheers buddy, was nice knowing you!" + + local r=math.random(#texts) + + return texts[r] +end + + +--- Check if a coordinate lies within a safe training zone. +-- @param #FOX self +-- @param Core.Point#COORDINATE coord Coordinate to check. +-- @return #boolean True if safe. +function FOX:_CheckCoordSafe(coord) + + -- No safe zones defined ==> Everything is safe. + if #self.safezones==0 then + return true + end + + -- Loop over all zones. + for _,_zone in pairs(self.safezones) do + local zone=_zone --Core.Zone#ZONE + local inzone=zone:IsCoordinateInZone(coord) + if inzone then + return true + end + end + + return false +end + +--- Check if a coordinate lies within a launch zone. +-- @param #FOX self +-- @param Core.Point#COORDINATE coord Coordinate to check. +-- @return #boolean True if in launch zone. +function FOX:_CheckCoordLaunch(coord) + + -- No safe zones defined ==> Everything is safe. + if #self.launchzones==0 then + return true + end + + -- Loop over all zones. + for _,_zone in pairs(self.launchzones) do + local zone=_zone --Core.Zone#ZONE + local inzone=zone:IsCoordinateInZone(coord) + if inzone then + return true + end + end + + return false +end + +--- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned. +-- @param #FOX self +-- @param DCS#Weapon weapon The weapon. +-- @return #number Heading of weapon in degrees or -1. +function FOX:_GetWeapongHeading(weapon) + + if weapon and weapon:isExist() then + + local wp=weapon:getPosition() + + local wph = math.atan2(wp.x.z, wp.x.x) + + if wph < 0 then + wph=wph+2*math.pi + end + + wph=math.deg(wph) + + return wph + end + + return -1 +end + +--- Tell player notching headings. +-- @param #FOX self +-- @param #FOX.PlayerData playerData Player data. +-- @param DCS#Weapon weapon The weapon. +function FOX:_SayNotchingHeadings(playerData, weapon) + + if playerData and playerData.unit and playerData.unit:IsAlive() then + + local nr, nl=self:_GetNotchingHeadings(weapon) + + if nr and nl then + local text=string.format("Notching heading %03d° or %03d°", nr, nl) + MESSAGE:New(text, 5, "FOX"):ToClient(playerData.client) + end + + end + +end + +--- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned. +-- @param #FOX self +-- @param DCS#Weapon weapon The weapon. +-- @return #number Notching heading right, i.e. missile heading +90°. +-- @return #number Notching heading left, i.e. missile heading -90°. +function FOX:_GetNotchingHeadings(weapon) + + if weapon then + + local hdg=self:_GetWeapongHeading(weapon) + + local hdg1=hdg+90 + if hdg1>360 then + hdg1=hdg1-360 + end + + local hdg2=hdg-90 + if hdg2<0 then + hdg2=hdg2+360 + end + + return hdg1, hdg2 + end + + return nil, nil +end + +--- Returns the player data from a unit name. +-- @param #FOX self +-- @param #string unitName Name of the unit. +-- @return #FOX.PlayerData Player data. +function FOX:_GetPlayerFromUnitname(unitName) + + for _,_player in pairs(self.players) do + local player=_player --#FOX.PlayerData + + if player.unitname==unitName then + return player + end + end + + return nil +end + +--- Retruns the player data from a unit. +-- @param #FOX self +-- @param Wrapper.Unit#UNIT unit +-- @return #FOX.PlayerData Player data. +function FOX:_GetPlayerFromUnit(unit) + + if unit and unit:IsAlive() then + + -- Name of the unit + local unitname=unit:GetName() + + for _,_player in pairs(self.players) do + local player=_player --#FOX.PlayerData + + if player.unitname==unitname then + return player + end + end + + end + + return nil +end + +--- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned. +-- @param #FOX self +-- @param #string _unitName Name of the player unit. +-- @return Wrapper.Unit#UNIT Unit of player or nil. +-- @return #string Name of the player or nil. +function FOX:_GetPlayerUnitAndName(_unitName) + self:F2(_unitName) + + if _unitName ~= nil then + + -- Get DCS unit from its name. + local DCSunit=Unit.getByName(_unitName) + + if DCSunit then + + -- Get player name if any. + local playername=DCSunit:getPlayerName() + + -- Unit object. + local unit=UNIT:Find(DCSunit) + + -- Debug. + self:T2({DCSunit=DCSunit, unit=unit, playername=playername}) + + -- Check if enverything is there. + if DCSunit and unit and playername then + self:T(self.lid..string.format("Found DCS unit %s with player %s.", tostring(_unitName), tostring(playername))) + return unit, playername + end + + end + + end + + -- Return nil if we could not find a player. + return nil,nil +end + + +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- **Functional** -- Modular, Automatic and Network capable Targeting and Interception System for Air Defenses +-- +-- === +-- +-- **MANTIS** - Moose derived Modular, Automatic and Network capable Targeting and Interception System +-- Controls a network of SAM sites. Use detection to switch on the AA site closest to the enemy +-- Leverage evasiveness from SEAD +-- Leverage attack range setup added by DCS in 11/20 +-- +-- === +-- +-- ## Missions: +-- +-- ### [MANTIS - Modular, Automatic and Network capable Targeting and Interception System](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/MTS%20-%20Mantis/MTS-010%20-%20Basic%20Mantis%20Demo) +-- +-- === +-- +-- ### Author : **applevangelist ** +-- +-- @module Functional.Mantis +-- @image Functional.Mantis.jpg + +-- Date: July 2021 + +------------------------------------------------------------------------- +--- **MANTIS** class, extends Core.Base#BASE +-- @type MANTIS +-- @field #string ClassName +-- @field #string name Name of this Mantis +-- @field #string SAM_Templates_Prefix Prefix to build the #SET_GROUP for SAM sites +-- @field Core.Set#SET_GROUP SAM_Group The SAM #SET_GROUP +-- @field #string EWR_Templates_Prefix Prefix to build the #SET_GROUP for EWR group +-- @field Core.Set#SET_GROUP EWR_Group The EWR #SET_GROUP +-- @field Core.Set#SET_GROUP Adv_EWR_Group The EWR #SET_GROUP used for advanced mode +-- @field #string HQ_Template_CC The ME name of the HQ object +-- @field Wrapper.Group#GROUP HQ_CC The #GROUP object of the HQ +-- @field #table SAM_Table Table of SAM sites +-- @field #string lid Prefix for logging +-- @field Functional.Detection#DETECTION_AREAS Detection The #DETECTION_AREAS object for EWR +-- @field Functional.Detection#DETECTION_AREAS AWACS_Detection The #DETECTION_AREAS object for AWACS +-- @field #boolean debug Switch on extra messages +-- @field #boolean verbose Switch on extra logging +-- @field #number checkradius Radius of the SAM sites +-- @field #number grouping Radius to group detected objects +-- @field #number acceptrange Radius of the EWR detection +-- @field #number detectinterval Interval in seconds for the target detection +-- @field #number engagerange Firing engage range of the SAMs, see [https://wiki.hoggitworld.com/view/DCS_option_engagementRange] +-- @field #boolean autorelocate Relocate HQ and EWR groups in random intervals. Note: You need to select units for this which are *actually mobile* +-- @field #boolean advanced Use 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 +-- @field #number adv_ratio Percentage to use for advanced mode, defaults to 100% +-- @field #number adv_state Advanced mode state tracker +-- @field #boolean advAwacs Boolean switch to use Awacs as a separate detection stream +-- @field #number awacsrange Detection range of an optional Awacs unit +-- @field #boolean UseEmOnOff Decide if we are using Emissions on/off (true) or AlarmState red/green (default) +-- @field Functional.Shorad#SHORAD Shorad SHORAD Object, if available +-- @field #boolean ShoradLink If true, #MANTIS has #SHORAD enabled +-- @field #number ShoradTime Timer in seconds, how long #SHORAD will be active after a detection inside of the defense range +-- @field #number ShoradActDistance Distance of an attacker in meters from a Mantis SAM site, on which Shorad will be switched on. Useful to not give away Shorad sites too early. Default 15km. Should be smaller than checkradius. +-- @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 +-- +-- Simple Class for a more intelligent Air Defense System +-- +-- #MANTIS +-- Moose derived Modular, Automatic and Network capable Targeting and Interception System. +-- Controls a network of SAM sites. Use detection to switch on the AA site closest to the enemy. +-- Leverage evasiveness from @{Functional.Sead#SEAD}. +-- Leverage attack range setup added by DCS in 11/20. +-- +-- Set up your SAM sites in the mission editor. Name the groups with common prefix like "Red SAM". +-- Set up your EWR system in the mission editor. Name the groups with common prefix like "Red EWR". Can be e.g. AWACS or a combination of AWACS and Search Radars like e.g. EWR 1L13 etc. +-- [optional] Set up your HQ. Can be any group, e.g. a command vehicle. +-- +-- # 1. Basic tactical considerations when setting up your SAM sites +-- +-- ## 1.1 Radar systems and AWACS +-- +-- Typically, your setup should consist of EWR (early warning) radars to detect and track targets, accompanied by AWACS if your scenario forsees that. Ensure that your EWR radars have a good coverage of the area you want to track. +-- **Location** is of highest importantance here. Whilst AWACS in DCS has almost the "all seeing eye", EWR don't have that. Choose your location wisely, against a mountain backdrop or inside a valley even the best EWR system +-- doesn't work well. Prefer higher-up locations with a good view; use F7 in-game to check where you actually placed your EWR and have a look around. Apart from the obvious choice, do also consider other radar units +-- for this role, most have "SR" (search radar) or "STR" (search and track radar) in their names, use the encyclopedia to see what they actually do. +-- +-- ## 1.2 SAM sites +-- +-- Typically your SAM should cover all attack ranges. The closer the enemy gets, the more systems you will need to deploy to defend your location. Use a combination of long-range systems like the SA-10/11, midrange like SA-6 and short-range like +-- SA-2 for defense (Patriot, Hawk, Gepard, Blindfire for the blue side). For close-up defense and defense against HARMs or low-flying aircraft, helicopters it is also advisable to deploy SA-15 TOR systems, Shilka, Strela and Tunguska units, as well as manpads (Think Gepard, Avenger, Chaparral, +-- Linebacker, Roland systems for the blue side). If possible, overlap ranges for mutual coverage. +-- +-- ## 1.3 Typical problems +-- +-- Often times, people complain because the detection cannot "see" oncoming targets and/or Mantis switches on too late. Three typial problems here are +-- +-- * bad placement of radar units, +-- * overestimation how far units can "see" and +-- * not taking into account that a SAM site will take (e.g for a SA-6) 30-40 seconds between switching to RED, acquiring the target and firing. +-- +-- An attacker doing 350knots will cover ca 180meters/second or thus more than 6km until the SA-6 fires. Use triggers zones and the ruler in the missione editor to understand distances and zones. Take into account that the ranges given by the circles +-- in the mission editor are absolute maximum ranges; in-game this is rather 50-75% of that depending on the system. Fiddle with placement and options to see what works best for your scenario, and remember **everything in here is in meters**. +-- +-- # 2. Start up your MANTIS with a basic setting +-- +-- `myredmantis = MANTIS:New("myredmantis","Red SAM","Red EWR",nil,"red",false)` +-- `myredmantis:Start()` +-- +-- [optional] Use +-- +-- * `MANTIS:SetEWRGrouping(radius)` +-- * `MANTIS:SetEWRRange(radius)` +-- * `MANTIS:SetSAMRadius(radius)` +-- * `MANTIS:SetDetectInterval(interval)` +-- * `MANTIS:SetAutoRelocate(hq, ewr)` +-- +-- before starting #MANTIS to fine-tune your setup. +-- +-- If you want to use a separate AWACS unit (default detection range: 250km) to support your EWR system, use e.g. the following setup: +-- +-- `mybluemantis = MANTIS:New("bluemantis","Blue SAM","Blue EWR",nil,"blue",false,"Blue Awacs")` +-- `mybluemantis:Start()` +-- +-- # 3. Default settings +-- +-- By default, the following settings are active: +-- +-- * SAM_Templates_Prefix = "Red SAM" - SAM site group names in the mission editor begin with "Red SAM" +-- * EWR_Templates_Prefix = "Red EWR" - EWR group names in the mission editor begin with "Red EWR" - can also be combined with an AWACS unit +-- * checkradius = 25000 (meters) - SAMs will engage enemy flights, if they are within a 25km around each SAM site - `MANTIS:SetSAMRadius(radius)` +-- * grouping = 5000 (meters) - Detection (EWR) will group enemy flights to areas of 5km for tracking - `MANTIS:SetEWRGrouping(radius)` +-- * acceptrange = 80000 (meters) - Detection (EWR) will on consider flights inside a 80km radius - `MANTIS:SetEWRRange(radius)` +-- * detectinterval = 30 (seconds) - MANTIS will decide every 30 seconds which SAM to activate - `MANTIS:SetDetectInterval(interval)` +-- * engagerange = 85 (percent) - SAMs will only fire if flights are inside of a 85% radius of their max firerange - `MANTIS:SetSAMRange(range)` +-- * dynamic = false - Group filtering is set to once, i.e. newly added groups will not be part of the setup by default - `MANTIS:New(name,samprefix,ewrprefix,hq,coaltion,dynamic)` +-- * autorelocate = false - HQ and (mobile) EWR system will not relocate in random intervals between 30mins and 1 hour - `MANTIS:SetAutoRelocate(hq, ewr)` +-- * debug = false - Debugging reports on screen are set to off - `MANTIS:Debug(onoff)` +-- +-- # 4. Advanced Mode +-- +-- Advanced mode will *decrease* reactivity of MANTIS, if HQ and/or EWR network dies. Awacs is counted as one EWR unit. It will 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 )` +-- +-- Use this option if you want to make use of or allow advanced SEAD tactics. +-- +-- # 5. Integrate SHORAD +-- +-- You can also choose to integrate Mantis with @{Functional.Shorad#SHORAD} for protection against HARMs and AGMs. When SHORAD detects a missile fired at one of MANTIS' SAM sites, it will activate SHORAD systems in +-- the given defense checkradius around that SAM site. Create a SHORAD object first, then integrate with MANTIS like so: +-- +-- `local SamSet = SET_GROUP:New():FilterPrefixes("Blue SAM"):FilterCoalitions("blue"):FilterStart()` +-- `myshorad = SHORAD:New("BlueShorad", "Blue SHORAD", SamSet, 22000, 600, "blue")` +-- `-- now set up MANTIS` +-- `mymantis = MANTIS:New("BlueMantis","Blue SAM","Blue EWR",nil,"blue",false,"Blue Awacs")` +-- `mymantis:AddShorad(myshorad,720)` +-- `mymantis:Start()` +-- +-- and (optionally) remove the link later on with +-- +-- `mymantis:RemoveShorad()` +-- +-- @field #MANTIS +MANTIS = { + ClassName = "MANTIS", + name = "mymantis", + SAM_Templates_Prefix = "", + SAM_Group = nil, + EWR_Templates_Prefix = "", + EWR_Group = nil, + Adv_EWR_Group = nil, + HQ_Template_CC = "", + HQ_CC = nil, + SAM_Table = {}, + lid = "", + Detection = nil, + AWACS_Detection = nil, + debug = false, + checkradius = 25000, + grouping = 5000, + acceptrange = 80000, + detectinterval = 30, + engagerange = 75, + autorelocate = false, + advanced = false, + adv_ratio = 100, + adv_state = 0, + AWACS_Prefix = "", + advAwacs = false, + verbose = false, + awacsrange = 250000, + Shorad = nil, + ShoradLink = false, + ShoradTime = 600, + ShoradActDistance = 15000, + UseEmOnOff = false, + TimeStamp = 0, + state2flag = false, + SamStateTracker = {}, + DLink = false, + DLTimeStamp = 0, + Padding = 10, +} + +--- Advanced state enumerator +-- @type MANTIS.AdvancedState +MANTIS.AdvancedState = { + GREEN = 0, + AMBER = 1, + RED = 2, +} + +----------------------------------------------------------------------- +-- MANTIS System +----------------------------------------------------------------------- + +do + --- Function to instantiate a new object of class MANTIS + --@param #MANTIS self + --@param #string name Name of this MANTIS for reporting + --@param #string samprefix Prefixes for the SAM groups from the ME, e.g. all groups starting with "Red Sam..." + --@param #string ewrprefix Prefixes for the EWR groups from the ME, e.g. all groups starting with "Red EWR..." + --@param #string hq Group name of your HQ (optional) + --@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) + --@param #number Padding For #SEAD - Extra number of seconds to add to radar switch-back-on time (optional) + --@return #MANTIS self + --@usage Start up your MANTIS with a basic setting + -- + -- `myredmantis = MANTIS:New("myredmantis","Red SAM","Red EWR",nil,"red",false)` + -- `myredmantis:Start()` + -- + -- [optional] Use + -- + -- * `MANTIS:SetEWRGrouping(radius)` + -- * `MANTIS:SetEWRRange(radius)` + -- * `MANTIS:SetSAMRadius(radius)` + -- * `MANTIS:SetDetectInterval(interval)` + -- * `MANTIS:SetAutoRelocate(hq, ewr)` + -- + -- before starting #MANTIS to fine-tune your setup. + -- + -- If you want to use a separate AWACS unit (default detection range: 250km) to support your EWR system, use e.g. the following setup: + -- + -- `mybluemantis = MANTIS:New("bluemantis","Blue SAM","Blue EWR",nil,"blue",false,"Blue Awacs")` + -- `mybluemantis:Start()` + -- + function MANTIS:New(name,samprefix,ewrprefix,hq,coaltion,dynamic,awacs, EmOnOff, Padding) + + -- DONE: Create some user functions for these + -- DONE: Make HQ useful + -- DONE: Set SAMs to auto if EWR dies + -- DONE: Refresh SAM table in dynamic mode + -- DONE: Treat Awacs separately, since they might be >80km off site + + self.name = name or "mymantis" + self.SAM_Templates_Prefix = samprefix or "Red SAM" + self.EWR_Templates_Prefix = ewrprefix or "Red EWR" + self.HQ_Template_CC = hq or nil + self.Coalition = coaltion or "red" + self.SAM_Table = {} + self.dynamic = dynamic or false + self.checkradius = 25000 + self.grouping = 5000 + self.acceptrange = 80000 + self.detectinterval = 30 + self.engagerange = 75 + self.autorelocate = false + self.autorelocateunits = { HQ = false, EWR = false} + self.advanced = false + self.adv_ratio = 100 + self.adv_state = 0 + self.verbose = false + self.Adv_EWR_Group = nil + self.AWACS_Prefix = awacs or nil + self.awacsrange = 250000 --DONE: 250km, User Function to change + self.Shorad = nil + self.ShoradLink = false + self.ShoradTime = 600 + self.ShoradActDistance = 15000 + 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 + self.Padding = Padding or 10 + + if EmOnOff then + if EmOnOff == false then + self.UseEmOnOff = false + else + self.UseEmOnOff = true + end + end + + if type(awacs) == "string" then + self.advAwacs = true + else + self.advAwacs = false + end + + -- Inherit everything from BASE class. + 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) + + -- Debug trace. + if self.debug then + BASE:TraceOnOff(true) + BASE:TraceClass(self.ClassName) + --BASE:TraceClass("SEAD") + BASE:TraceLevel(1) + end + + if self.dynamic then + -- Set SAM SET_GROUP + self.SAM_Group = SET_GROUP:New():FilterPrefixes(self.SAM_Templates_Prefix):FilterCoalitions(self.Coalition):FilterStart() + -- Set EWR SET_GROUP + self.EWR_Group = SET_GROUP:New():FilterPrefixes({self.SAM_Templates_Prefix,self.EWR_Templates_Prefix}):FilterCoalitions(self.Coalition):FilterStart() + else + -- Set SAM SET_GROUP + self.SAM_Group = SET_GROUP:New():FilterPrefixes(self.SAM_Templates_Prefix):FilterCoalitions(self.Coalition):FilterOnce() + -- Set EWR SET_GROUP + self.EWR_Group = SET_GROUP:New():FilterPrefixes({self.SAM_Templates_Prefix,self.EWR_Templates_Prefix}):FilterCoalitions(self.Coalition):FilterOnce() + end + + -- set up CC + if self.HQ_Template_CC then + self.HQ_CC = GROUP:FindByName(self.HQ_Template_CC) + end + + -- @field #string version + self.version="0.6.2" + self:I(string.format("***** Starting MANTIS Version %s *****", self.version)) + + --- 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 + -- @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 + -- @param #MANTIS self + -- @return #MANTIS self + function MANTIS:_SetSAMTable(table) + self:T(self.lid .. "SetSAMTable") + self.SAM_Table = table + return self + end + + --- Function to set the grouping radius of the detection in meters + -- @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 + end + 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 + return nil + end + end + + --- Function to set separate AWACS detection instance + -- @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) + 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 + self.HQ_CC = GROUP:FindByName(group) + self.HQ_Template_CC = group + else + self.HQ_CC = group + 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 + -- @param #MANTIS self + -- @param #boolean onoff If true, will activate Advanced Mode + -- @param #number ratio [optional] Percentage to use for advanced mode, defaults to 100% + -- @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: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 + self.adv_ratio = ratio + self.advanced = true + self.adv_state = 0 + self.Adv_EWR_Group = SET_GROUP:New():FilterPrefixes(self.EWR_Templates_Prefix):FilterCoalitions(self.Coalition):FilterStart() + 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() + 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" + local m= MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) + 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 + --self:T(self.lid.." HQ is alive!") + return true + else + --self:T(self.lid.." HQ is dead!") + return false + 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:T(text) + local m= MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) + if self.verbose then self:I(text) end + -- start check + if self.advanced then + local EWR_Group = self.Adv_EWR_Group + --local EWR_Set = EWR_Group.Set + local nalive = EWR_Group:CountAlive() + if self.advAwacs then + local awacs = GROUP:FindByName(self.AWACS_Prefix) + if awacs ~= nil then + if awacs:IsAlive() then + nalive = nalive+1 + end + end + end + --self:T(self.lid..string.format(" No of EWR alive is %d", nalive)) + if nalive > 0 then + return true + else + return false + 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:_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() + local HQ_State = self:_CheckHQState() + -- set state + if EWR_State and HQ_State then -- both alive + self.adv_state = 0 --everything is fine + elseif EWR_State or HQ_State then -- one alive + self.adv_state = 1 --slow down level 1 + else -- none alive + self.adv_state = 2 --slow down level 2 + end + -- calculate new detectioninterval + local interval = self.detectinterval -- e.g. 30 + 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 + 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 + + --- Function to set autorelocation for HQ and EWR objects. Note: Units must be actually mobile in DCS! + -- @param #MANTIS self + -- @param #boolean hq If true, will relocate HQ object + -- @param #boolean ewr If true, will relocate EWR objects + function MANTIS:SetAutoRelocate(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}) + end + return self + end + + --- [Internal] Function to execute the relocation + -- @param #MANTIS self + function MANTIS:_RelocateGroups() + 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 self:I(text) end + if self.autorelocate then + -- relocate HQ + 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") + local text = self.lid.." Relocating HQ" + --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 + if self.autorelocateunits.EWR then + -- get EWR Group + 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: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 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 + -- @param #MANTIS self + -- @param #table dectset Table of coordinates of detected items + -- @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: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 targetdistance = samcoordinate:DistanceFromPointVec2(coord) + 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 + end + end + return false, 0 + end + + --- [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:T(self.lid.."Starting Detection") + + -- start detection + local groupset = self.EWR_Group + local grouping = self.grouping or 5000 + local acceptrange = self.acceptrange or 80000 + local interval = self.detectinterval or 60 + + --@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) + --BASE:I( { From, Event, To, DetectedItem }) + local debug = false + if DetectedItem.IsDetected and debug then + local Coordinate = DetectedItem.Coordinate -- Core.Point#COORDINATE + local text = "MANTIS: Detection at "..Coordinate:ToStringLLDMS() + local m = MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) + end + end + return MANTISdetection + end + + --- [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:T(self.lid.."Starting Awacs Detection") + + -- start detection + local group = self.AWACS_Prefix + local groupset = SET_GROUP:New():FilterPrefixes(group):FilterCoalitions(self.Coalition):FilterStart() + local grouping = self.grouping or 5000 + --local acceptrange = self.acceptrange or 80000 + local interval = self.detectinterval or 60 + + --@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) + --BASE:I( { From, Event, To, DetectedItem }) + local debug = false + if DetectedItem.IsDetected and debug then + local Coordinate = DetectedItem.Coordinate -- Core.Point#COORDINATE + local text = "Awacs Detection at "..Coordinate:ToStringLLDMS() + local m = MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) + end + end + return MANTISAwacs + end + + --- [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: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 + local SAM_Tbl = {} -- table of SAM defense zones + local SEAD_Grps = {} -- table of SAM names to make evasive + 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 -- Wrapper.Group#GROUP + -- TODO: add emissions on/off + if self.UseEmOnOff then + group:EnableEmission(false) + --group:SetAIOff() + else + 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() 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 + -- make SAMs evasive + local mysead = SEAD:New( SEAD_Grps, self.Padding ) -- Functional.Sead#SEAD + mysead:SetEngagementRange(engagerange) + self.mysead = mysead + return self + end + + --- [Internal] Function to update SAM table and SEAD state + -- @param #MANTIS self + -- @return #MANTIS self + function MANTIS:_RefreshSAMTable() + self:T(self.lid.."RefreshSAMTable") + -- Requires SEAD 0.2.2 or better + -- get SAM Group + local SAM_SET = self.SAM_Group + local SAM_Grps = SAM_SET.Set --table of objects + local SAM_Tbl = {} -- table of SAM defense zones + local SEAD_Grps = {} -- table of SAM names to make evasive + 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 + group:SetOption(AI.Option.Ground.id.AC_ENGAGEMENT_RANGE_RESTRICTION,engagerange) --engagement will be 75% of firing range + 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 + table.insert( SEAD_Grps, grpname ) + end + end + self.SAM_Table = SAM_Tbl + -- make SAMs evasive + if self.mysead ~= nil then + local mysead = self.mysead + mysead:UpdateSet( SEAD_Grps ) + end + return self + end + + --- Function to link up #MANTIS with a #SHORAD installation + -- @param #MANTIS self + -- @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 + if Shorad:IsInstanceOf("SHORAD") then + self.ShoradLink = ShoradLink + 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 +----------------------------------------------------------------------- + + --- [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:_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 + 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 + self.detectinterval = interval + self.state2flag = false + end + end -- end newstate vs oldstate + return self + end + + --- [Internal] Check DLink state + -- @param #MANTIS self + -- @return #MANTIS self + 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 + 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.advAwacs then + self.AWACS_Detection = self:StartAwacsDetection() + end + 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 +----------------------------------------------------------------------- +--- **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 ** +-- +-- @module Functional.Shorad +-- @image Functional.Shorad.jpg +-- +-- Date: July 2021 + +------------------------------------------------------------------------- +--- **SHORAD** class, extends Core.Base#BASE +-- @type SHORAD +-- @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 #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 +-- @field #string Coalition The coalition of this Shorad +-- @field #number ActiveTimer How long a Shorad stays active after wake-up in seconds +-- @field #table ActiveGroups Table for the timer function +-- @field #string lid The log ID for the dcs.log +-- @field #boolean DefendHarms Default true, intercept incoming HARMS +-- @field #boolean DefendMavs Default true, intercept incoming AG-Missiles +-- @field #number DefenseLowProb Default 70, minimum detection limit +-- @field #number DefenseHighProb Default 90, maximim detection limit +-- @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) +-- +-- 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. +-- Easily integrated with @{Functional.Mantis#MANTIS} to complete the defensive system setup. +-- +-- ## Usage +-- +-- 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. +-- +-- ### 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")` +-- +-- ## Customize options +-- +-- * SHORAD:SwitchDebug(debug) +-- * SHORAD:SwitchHARMDefense(onoff) +-- * SHORAD:SwitchAGMDefense(onoff) +-- * SHORAD:SetDefenseLimits(low,high) +-- * SHORAD:SetActiveTimer(seconds) +-- * SHORAD:SetDefenseRadius(meters) +-- +-- @field #SHORAD +SHORAD = { + ClassName = "SHORAD", + name = "MyShorad", + debug = false, + Prefixes = "", + Radius = 20000, + Groupset = nil, + Samset = nil, + Coalition = nil, + ActiveTimer = 600, --stay on 10 mins + ActiveGroups = {}, + lid = "", + DefendHarms = true, + DefendMavs = true, + DefenseLowProb = 70, + DefenseHighProb = 90, + UseEmOnOff = false, +} + +----------------------------------------------------------------------- +-- SHORAD System +----------------------------------------------------------------------- + +do + -- TODO Complete list? + --- Missile enumerators + -- @field Harms + SHORAD.Harms = { + ["AGM_88"] = "AGM_88", + ["AGM_45"] = "AGM_45", + ["AGM_122"] = "AGM_122", + ["AGM_84"] = "AGM_84", + ["AGM_45"] = "AGM_45", + ["ALARM"] = "ALARM", + ["LD-10"] = "LD-10", + ["X_58"] = "X_58", + ["X_28"] = "X_28", + ["X_25"] = "X_25", + ["X_31"] = "X_31", + ["Kh25"] = "Kh25", + } + + --- TODO complete list? + -- @field Mavs + SHORAD.Mavs = { + ["AGM"] = "AGM", + ["C-701"] = "C-701", + ["Kh25"] = "Kh25", + ["Kh29"] = "Kh29", + ["Kh31"] = "Kh31", + ["Kh66"] = "Kh66", + } + + --- Instantiates a new SHORAD object + -- @param #SHORAD self + -- @param #string Name Name of this SHORAD + -- @param #string ShoradPrefix Filter for the Shorad #SET_GROUP + -- @param Core.Set#SET_GROUP Samset The #SET_GROUP of SAM sites to defend + -- @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" + -- @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" + self.Prefixes = ShoradPrefix or "SAM SHORAD" + self.Radius = Radius or 20000 + self.Coalition = Coalition or "blue" + self.Samset = Samset or GroupSet + self.ActiveTimer = ActiveTimer or 600 + self.ActiveGroups = {} + self.Groupset = GroupSet + 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 = 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}) + local aliveset = set:GetAliveSet() --#table + for _,_group in pairs (aliveset) 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,100 do + math.random() + end + return self + end + + --- Switch debug state on + -- @param #SHORAD self + -- @param #boolean debug Switch debug on (true) or off (false) + function SHORAD:SwitchDebug(onoff) + self:T( { onoff } ) + if onoff then + self:SwitchDebugOn() + else + 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 + function SHORAD:SwitchHARMDefense(onoff) + self:T( { onoff } ) + local onoff = onoff or true + self.DefendHarms = onoff + return self + end + + --- Switch defense for AGMs + -- @param #SHORAD self + -- @param #boolean onoff + function SHORAD:SwitchAGMDefense(onoff) + 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 + -- @param #number high Maximum detection limit integer 1-100 + function SHORAD:SetDefenseLimits(low,high) + self:T( { low, high } ) + local low = low or 70 + local high = high or 90 + if (low < 0) or (low > 100) or (low > high) then + low = 70 + end + if (high < 0) or (high > 100) or (high < low ) then + high = 90 + 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 + 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 + for _,_name in pairs (SHORAD.Harms) do + if string.find(WeaponName,_name,1) then hit = true end + end + 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 + for _,_name in pairs (SHORAD.Mavs) do + if string.find(WeaponName,_name,1) then hit = true end + end + 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 + othercoalition = "neutral" + elseif Coalition == 1 then + othercoalition = "red" + else + othercoalition = "blue" + end + self:T({owncoalition = owncoalition, othercoalition = othercoalition}) + if owncoalition ~= othercoalition then + return true + else + 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 + local returnname = false + for _,_groups in pairs (shoradset) do + local groupname = _groups:GetName() + if string.find(groupname, tgtgrp, 1) then + returnname = true + --_groups:RelocateGroundRandomInRadius(7,100,false,false) -- be a bit evasive + end + end + 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 + local shoradset = shorad:GetSet() --#table + local returnname = false + for _,_groups in pairs (shoradset) do + local groupname = _groups:GetName() + if string.find(groupname, tgtgrp, 1) then + returnname = true + end + 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 + if ActualDetection <= DetectionProb then + IsDetected = true + 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 + -- + -- local SamSet = SET_GROUP:New():FilterPrefixes("Blue SAM"):FilterCoalitions("blue"):FilterStart() + -- myshorad = SHORAD:New("BlueShorad", "Blue SHORAD", SamSet, 22000, 600, "blue") + -- myshorad:SwitchDebug(true) + -- mymantis = MANTIS:New("BlueMantis","Blue SAM","Blue EWR",nil,"blue",false,"Blue Awacs") + -- 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 + local targetvec2 = nil + if targetcat == Object.Category.UNIT then + targetvec2 = GROUP:FindByName(targetgroup):GetVec2() + elseif targetcat == Object.Category.STATIC then + targetvec2 = STATIC:FindByName(targetgroup,false):GetVec2() + else + local samset = self.Samset + local sam = samset:GetRandom() + targetvec2 = sam:GetVec2() + end + local targetzone = ZONE_RADIUS:New("Shorad",targetvec2,Radius) -- create a defense zone to check + local groupset = self.Groupset --Core.Set#SET_GROUP + local shoradset = groupset:GetAliveSet() --#table + -- local function to switch off shorad again + local function SleepShorad(group) + local groupname = group:GetName() + self.ActiveGroups[groupname] = nil + if self.UseEmOnOff then + group:EnableEmission(false) + --group:SetAIOff() + else + group:OptionAlarmStateGreen() + end + local text = string.format("Sleeping SHORAD %s", group:GetName()) + self:T(text) + local m = MESSAGE:New(text,10,"SHORAD"):ToAllIf(self.debug) + end + -- go through set and find the one(s) to activate + for _,_group in pairs (shoradset) do + if _group:IsAnyInZone(targetzone) then + local text = string.format("Waking up SHORAD %s", _group:GetName()) + self:T(text) + local m = MESSAGE:New(text,10,"SHORAD"):ToAllIf(self.debug) + if self.UseEmOnOff then + --_group:SetAIOn() + _group:EnableEmission(true) + end + _group:OptionAlarmStateRed() + local groupname = _group:GetName() + if self.ActiveGroups[groupname] == nil then -- no timer yet for this group + self.ActiveGroups[groupname] = { Timing = ActiveTimer } + local endtime = timer.getTime() + (ActiveTimer * math.random(75,100) / 100 ) -- randomize wakeup a bit + timer.scheduleFunction(SleepShorad, _group, endtime) + 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: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 + local ShootingWeaponName = EventData.WeaponName -- return weapon type + -- get firing coalition + local weaponcoalition = EventData.IniGroup:GetCoalition() + -- get detection probability + if self:_CheckCoalition(weaponcoalition) then --avoid overhead on friendly fire + local IsDetected = self:_ShotIsDetected() + -- convert to text + local DetectedText = "false" + if IsDetected then + DetectedText = "true" + end + local text = string.format("%s Missile Launched = %s | Detected probability state is %s", self.lid, ShootingWeaponName, DetectedText) + self:T( text ) + local m = MESSAGE:New(text,10,"Info"):ToAllIf(self.debug) + -- + if (self:_CheckHarms(ShootingWeaponName) or self:_CheckMavs(ShootingWeaponName)) and IsDetected then + -- get target data + local targetdata = EventData.Weapon:getTarget() -- Identify target + local targetcat = targetdata:getCategory() -- Identify category + self:T(string.format("Target Category (3=STATIC, 1=UNIT)= %s",tostring(targetcat))) + local targetunit = nil + if targetcat == Object.Category.UNIT then -- UNIT + targetunit = UNIT:Find(targetdata) + elseif targetcat == Object.Category.STATIC then -- STATIC + targetunit = STATIC:Find(targetdata) + end + --local targetunitname = Unit.getName(targetdata) -- Unit name + if targetunit and targetunit:IsAlive() then + local targetunitname = targetunit:GetName() + --local targetgroup = Unit.getGroup(Weapon.getTarget(ShootingWeapon)) --targeted group + local targetgroup = nil + local targetgroupname = "none" + if targetcat == Object.Category.UNIT then + targetgroup = targetunit:GetGroup() + targetgroupname = targetgroup:GetName() -- group name + elseif targetcat == Object.Category.STATIC then + targetgroup = targetunit + targetgroupname = targetunitname + end + 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 + --local TargetGroup = EventData.TgtGroup -- Wrapper.Group#GROUP + local shotatus = self:_CheckShotAtShorad(targetgroupname) --#boolean + local shotatsams = self:_CheckShotAtSams(targetgroupname) --#boolean + -- if being shot at, find closest SHORADs to activate + if shotatsams or shotatus then + self:T({shotatsams=shotatsams,shotatus=shotatus}) + self:WakeUpShorad(targetgroupname, self.Radius, self.ActiveTimer, targetcat) + end + end + end + end + end +-- +end +----------------------------------------------------------------------- +-- SHORAD end +-------------------------------------------------------------------------- **Functional** - Autolase targets in the field. +-- +-- === +-- +-- **AUOTLASE** - Autolase targets in the field. +-- +-- === +-- +-- ## Missions: +-- +-- ### [Autolase](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/) +-- +-- === +-- +-- **Main Features:** +-- +-- * Detect and lase contacts automatically +-- * Targets are lased by threat priority order +-- * Use FSM events to link functionality into your scripts +-- * Easy setup +-- +-- === +-- +--- Spot on! +-- +-- === +-- +-- # 1 Autolase concept +-- +-- * Detect and lase contacts automatically +-- * Targets are lased by threat priority order +-- * Use FSM events to link functionality into your scripts +-- * Set laser codes and smoke colors per Recce unit +-- * Easy set-up +-- +-- # 2 Basic usage +-- +-- ## 2.2 Set up a group of Recce Units: +-- +-- local FoxSet = SET_GROUP:New():FilterPrefixes("Recce"):FilterCoalitions("blue"):FilterStart() +-- +-- ## 2.3 (Optional) Set up a group of pilots, this will drive who sees the F10 menu entry: +-- +-- local Pilotset = SET_CLIENT:New():FilterCoalitions("blue"):FilterActive(true):FilterStart() +-- +-- ## 2.4 Set up and start Autolase: +-- +-- local autolaser = AUTOLASE:New(FoxSet,coalition.side.BLUE,"Wolfpack",Pilotset) +-- +-- ## 2.5 Example - Using a fixed laser code and color for a specific Recce unit: +-- +-- local recce = SPAWN:New("Reaper") +-- :InitDelayOff() +-- :OnSpawnGroup( +-- function (group) +-- local unit = group:GetUnit(1) +-- local name = unit:GetName() +-- autolaser:SetRecceLaserCode(name,1688) +-- autolaser:SetRecceSmokeColor(name,SMOKECOLOR.Red) +-- end +-- ) +-- :InitCleanUp(60) +-- :InitLimit(1,0) +-- :SpawnScheduled(30,0.5) +-- +-- ## 2.6 Example - Inform pilots about events: +-- +-- autolaser:SetNotifyPilots(true) -- defaults to true, also shown if debug == true +-- -- Note - message are shown to pilots in the #SET_CLIENT only if using the pilotset option, else to the coalition. +-- +-- +-- ### Author: **applevangelist** +-- @module Functional.Autolase +-- @image Designation.JPG +-- +-- Date: 24 Oct 2021 +-- +--- Class AUTOLASE +-- @type AUTOLASE +-- @field #string ClassName +-- @field #string lid +-- @field #number verbose +-- @field #string alias +-- @field #boolean debug +-- @field #string version +-- @extends Ops.Intel#INTEL + +--- +-- @field #AUTOLASE +AUTOLASE = { + ClassName = "AUTOLASE", + lid = "", + verbose = 0, + alias = "", + debug = false, +} + +--- Laser spot info +-- @type AUTOLASE.LaserSpot +-- @field Core.Spot#SPOT laserspot +-- @field Wrapper.Unit#UNIT lasedunit +-- @field Wrapper.Unit#UNIT lasingunit +-- @field #number lasercode +-- @field #string location +-- @field #number timestamp +-- @field #string unitname +-- @field #string reccename +-- @field #string unittype + +--- AUTOLASE class version. +-- @field #string version +AUTOLASE.version = "0.0.9" + +------------------------------------------------------------------- +-- Begin Functional.Autolase.lua +------------------------------------------------------------------- + +--- Constructor for a new Autolase instance. +-- @param #AUTOLASE self +-- @param Core.Set#SET_GROUP RecceSet Set of detecting and lasing units +-- @param #number Coalition Coalition side. Can also be passed as a string "red", "blue" or "neutral". +-- @param #string Alias (Optional) An alias how this object is called in the logs etc. +-- @param Core.Set#SET_CLIENT PilotSet (Optional) Set of clients for precision bombing, steering menu creation. Leave nil for a coalition-wide F10 entry and display. +-- @return #AUTOLASE self +function AUTOLASE:New(RecceSet, Coalition, Alias, PilotSet) + BASE:T({RecceSet, Coalition, Alias, PilotSet}) + + -- Inherit everything from BASE class. + local self=BASE:Inherit(self, BASE:New()) -- #AUTOLASE + + if Coalition and type(Coalition)=="string" then + if Coalition=="blue" then + self.coalition=coalition.side.BLUE + elseif Coalition=="red" then + self.coalition=coalition.side.RED + elseif Coalition=="neutral" then + self.coalition=coalition.side.NEUTRAL + else + self:E("ERROR: Unknown coalition in AUTOLASE!") + end + end + + -- Set alias. + if Alias then + self.alias=tostring(Alias) + else + self.alias="Lion" + if self.coalition then + if self.coalition==coalition.side.RED then + self.alias="Wolf" + elseif self.coalition==coalition.side.BLUE then + self.alias="Fox" + end + end + end + + -- inherit from INTEL + local self=BASE:Inherit(self, INTEL:New(RecceSet, Coalition, Alias)) -- #AUTOLASE + + self.RecceSet = RecceSet + self.DetectVisual = true + self.DetectOptical = true + self.DetectRadar = true + self.DetectIRST = true + self.DetectRWR = true + self.DetectDLINK = true + self.LaserCodes = UTILS.GenerateLaserCodes() + self.LaseDistance = 5000 + self.LaseDuration = 300 + self.GroupsByThreat = {} + self.UnitsByThreat = {} + self.RecceNames = {} + self.RecceLaserCode = {} + self.RecceSmokeColor = {} + self.RecceUnitNames= {} + self.maxlasing = 4 + self.CurrentLasing = {} + self.lasingindex = 0 + self.deadunitnotes = {} + self.usepilotset = false + self.reporttimeshort = 10 + self.reporttimelong = 30 + self.smoketargets = false + self.smokecolor = SMOKECOLOR.Red + self.notifypilots = true + self.targetsperrecce = {} + self.RecceUnits = {} + self.forcecooldown = true + self.cooldowntime = 60 + self.useSRS = false + self.SRSPath = "" + self.SRSFreq = 251 + self.SRSMod = radio.modulation.AM + + -- Set some string id for output to DCS.log file. + self.lid=string.format("AUTOLASE %s (%s) | ", self.alias, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("*", "Monitor", "*") -- Start FSM + self:AddTransition("*", "Lasing", "*") -- Lasing target + self:AddTransition("*", "TargetLost", "*") -- Lost target + self:AddTransition("*", "TargetDestroyed", "*") -- Target destroyed + self:AddTransition("*", "RecceKIA", "*") -- Recce KIA + self:AddTransition("*", "LaserTimeout", "*") -- Laser timed out + self:AddTransition("*", "Cancel", "*") -- Stop Autolase + + -- Menu Entry + if not PilotSet then + self.Menu = MENU_COALITION_COMMAND:New(self.coalition,"Autolase",nil,self.ShowStatus,self) + else + self.usepilotset = true + self.pilotset = PilotSet + self:HandleEvent(EVENTS.PlayerEnterAircraft) + self:SetPilotMenu() + end + + self:SetClusterAnalysis(false, false) + + self:__Start(2) + self:__Monitor(math.random(5,10)) + + return self + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Monitor". + -- @function [parent=#AUTOLASE] Status + -- @param #AUTOLASE self + + --- Triggers the FSM event "Monitor" after a delay. + -- @function [parent=#AUTOLASE] __Status + -- @param #AUTOLASE self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Cancel". + -- @function [parent=#AUTOLASE] Cancel + -- @param #AUTOLASE self + + --- Triggers the FSM event "Cancel" after a delay. + -- @function [parent=#AUTOLASE] __Cancel + -- @param #AUTOLASE self + -- @param #number delay Delay in seconds. + + --- On After "RecceKIA" event. + -- @function [parent=#AUTOLASE] OnAfterRecceKIA + -- @param #AUTOLASE self + -- @param #string From The from state + -- @param #string Event The event + -- @param #string To The to state + -- @param #string RecceName The lost Recce + + --- On After "TargetDestroyed" event. + -- @function [parent=#AUTOLASE] OnAfterTargetDestroyed + -- @param #AUTOLASE self + -- @param #string From The from state + -- @param #string Event The event + -- @param #string To The to state + -- @param #string UnitName The destroyed unit\'s name + -- @param #string RecceName The Recce name lasing + + --- On After "TargetLost" event. + -- @function [parent=#AUTOLASE] OnAfterTargetLost + -- @param #AUTOLASE self + -- @param #string From The from state + -- @param #string Event The event + -- @param #string To The to state + -- @param #string UnitName The lost unit\'s name + -- @param #string RecceName The Recce name lasing + + --- On After "LaserTimeout" event. + -- @function [parent=#AUTOLASE] OnAfterLaserTimeout + -- @param #AUTOLASE self + -- @param #string From The from state + -- @param #string Event The event + -- @param #string To The to state + -- @param #string UnitName The lost unit\'s name + -- @param #string RecceName The Recce name lasing + + --- On After "Lasing" event. + -- @function [parent=#AUTOLASE] OnAfterLasing + -- @param #AUTOLASE self + -- @param #string From The from state + -- @param #string Event The event + -- @param #string To The to state + -- @param Functional.Autolase#AUTOLASE.LaserSpot LaserSpot The LaserSpot data table + +end + +------------------------------------------------------------------- +-- Helper Functions +------------------------------------------------------------------- + +--- (Internal) Function to set pilot menu. +-- @param #AUTOLASE self +-- @return #AUTOLASE self +function AUTOLASE:SetPilotMenu() + local pilottable = self.pilotset:GetSetObjects() or {} + for _,_unit in pairs (pilottable) do + local Unit = _unit -- Wrapper.Unit#UNIT + if Unit and Unit:IsAlive() then + local Group = Unit:GetGroup() + local lasemenu = MENU_GROUP_COMMAND:New(Group,"Autolase Status",nil,self.ShowStatus,self,Group) + lasemenu:Refresh() + end + end + return self +end + +--- (Internal) Event function for new pilots. +-- @param #AUTOLASE self +-- @param Core.Event#EVENTDATA EventData +-- @return #AUTOLASE self +function AUTOLASE:OnEventPlayerEnterAircraft(EventData) + self:SetPilotMenu() + return self +end + +--- (Internal) Function to get a laser code by recce name +-- @param #AUTOLASE self +-- @param #string RecceName Unit(!) name of the Recce +-- @return #AUTOLASE self +function AUTOLASE:GetLaserCode(RecceName) + local code = 1688 + if self.RecceLaserCode[RecceName] == nil then + code = self.LaserCodes[math.random(#self.LaserCodes)] + self.RecceLaserCode[RecceName] = code + else + code = self.RecceLaserCode[RecceName] + end + return code +end + +--- (Internal) Function to get a smoke color by recce name +-- @param #AUTOLASE self +-- @param #string RecceName Unit(!) name of the Recce +-- @return #AUTOLASE self +function AUTOLASE:GetSmokeColor(RecceName) + local color = self.smokecolor + if self.RecceSmokeColor[RecceName] == nil then + self.RecceSmokeColor[RecceName] = color + else + color = self.RecceLaserCode[RecceName] + end + return color +end + +--- (User) Function enable sending messages via SRS. +-- @param #AUTOLASE self +-- @param #boolean OnOff Switch usage on and off +-- @param #string Path Path to SRS directory, e.g. C:\\Program Files\\DCS-SimpleRadio-Standalon +-- @param #number Frequency Frequency to send, e.g. 243 +-- @param #number Modulation Modulation i.e. radio.modulation.AM or radio.modulation.FM +-- @return #AUTOLASE self +function AUTOLASE:SetUsingSRS(OnOff,Path,Frequency,Modulation) + self.useSRS = OnOff or true + self.SRSPath = Path or "E:\\Program Files\\DCS-SimpleRadio-Standalone" + self.SRSFreq = Frequency or 271 + self.SRSMod = Modulation or radio.modulation.AM + return self +end + +--- (User) Function set max lasing targets +-- @param #AUTOLASE self +-- @param #number Number Max number of targets to lase at once +-- @return #AUTOLASE self +function AUTOLASE:SetMaxLasingTargets(Number) + self.maxlasing = Number or 4 + return self +end + +--- (Internal) Function set notify pilots on events +-- @param #AUTOLASE self +-- @param #boolean OnOff Switch messaging on (true) or off (false) +-- @return #AUTOLASE self +function AUTOLASE:SetNotifyPilots(OnOff) + self.notifypilots = OnOff and true + return self +end + +--- (User) Function to set a specific code to a Recce. +-- @param #AUTOLASE self +-- @param #string RecceName (Unit!) Name of the Recce +-- @param #number Code The lase code +-- @return #AUTOLASE self +function AUTOLASE:SetRecceLaserCode(RecceName, Code) + local code = Code or 1688 + self.RecceLaserCode[RecceName] = code + return self +end + +--- (User) Function to set a specific smoke color for a Recce. +-- @param #AUTOLASE self +-- @param #string RecceName (Unit!) Name of the Recce +-- @param #number Color The color, e.g. SMOKECOLOR.Red, SMOKECOLOR.Green etc +-- @return #AUTOLASE self +function AUTOLASE:SetRecceSmokeColor(RecceName, Color) + local color = Color or self.smokecolor + self.RecceSmokeColor[RecceName] = color + return self +end + +--- (User) Function to force laser cooldown and cool down time +-- @param #AUTOLASE self +-- @param #boolean OnOff Switch cool down on (true) or off (false) - defaults to true +-- @param #number Seconds Number of seconds for cooldown - dafaults to 60 seconds +-- @return #AUTOLASE self +function AUTOLASE:SetLaserCoolDown(OnOff, Seconds) + self.forcecooldown = OnOff and true + self.cooldowntime = Seconds or 60 + return self +end + +--- (User) Function to set message show times. +-- @param #AUTOLASE self +-- @param #number long Longer show time +-- @param #number short Shorter show time +-- @return #AUTOLASE self +function AUTOLASE:SetReportingTimes(long, short) + self.reporttimeshort = short or 10 + self.reporttimelong = long or 30 + return self +end + +--- (User) Function to set lasing distance in meters and duration in seconds +-- @param #AUTOLASE self +-- @param #number Distance (Max) distance for lasing in meters - default 5000 meters +-- @param #number Duration (Max) duration for lasing in seconds - default 300 secs +-- @return #AUTOLASE self +function AUTOLASE:SetLasingParameters(Distance, Duration) + self.LaseDistance = Distance or 5000 + self.LaseDuration = Duration or 300 + return self +end + +--- (User) Function to set smoking of targets. +-- @param #AUTOLASE self +-- @param #boolean OnOff Switch smoking on or off +-- @param #number Color Smokecolor, e.g. SMOKECOLOR.Red +-- @return #AUTOLASE self +function AUTOLASE:SetSmokeTargets(OnOff,Color) + self.smoketargets = OnOff + self.smokecolor = Color or SMOKECOLOR.Red + return self +end + +--- (Internal) Function to calculate line of sight. +-- @param #AUTOLASE self +-- @param Wrapper.Unit#UNIT Unit +-- @return #number LOS Line of sight in meters +function AUTOLASE:GetLosFromUnit(Unit) + local lasedistance = self.LaseDistance + local unitheight = Unit:GetHeight() + local coord = Unit:GetCoordinate() + local landheight = coord:GetLandHeight() + local asl = unitheight - landheight + if asl > 100 then + local absquare = lasedistance^2+asl^2 + lasedistance = math.sqrt(absquare) + end + return lasedistance +end + +--- (Internal) Function to check on lased targets. +-- @param #AUTOLASE self +-- @return #AUTOLASE self +function AUTOLASE:CleanCurrentLasing() + local lasingtable = self.CurrentLasing + local newtable = {} + local newreccecount = {} + local lasing = 0 + + for _ind,_entry in pairs(lasingtable) do + local entry = _entry -- #AUTOLASE.LaserSpot + if not newreccecount[entry.reccename] then + newreccecount[entry.reccename] = 0 + end + end + + for _,_recce in pairs (self.RecceSet:GetSetObjects()) do + local recce = _recce --Wrapper.Group#GROUP + if recce and recce:IsAlive() then + local unit = recce:GetUnit(1) + local name = unit:GetName() + if not self.RecceUnits[name] then + self.RecceUnits[name] = { name=name, unit=unit, cooldown = false, timestamp = timer.getAbsTime() } + end + end + end + + for _ind,_entry in pairs(lasingtable) do + local entry = _entry -- #AUTOLASE.LaserSpot + local valid = 0 + local reccedead = false + local unitdead = false + local lostsight = false + local timeout = false + local Tnow = timer.getAbsTime() + -- check recce dead + local recce = entry.lasingunit + if recce and recce:IsAlive() then + valid = valid + 1 + else + reccedead = true + self:__RecceKIA(2,entry.reccename) + end + -- check entry dead + local unit = entry.lasedunit + if unit and unit:IsAlive() == true then + valid = valid + 1 + else + unitdead = true + if not self.deadunitnotes[entry.unitname] then + self.deadunitnotes[entry.unitname] = true + self:__TargetDestroyed(2,entry.unitname,entry.reccename) + end + end + -- check entry out of sight + if not reccedead and not unitdead then + if self:CanLase(recce,unit) then + valid = valid + 1 + else + lostsight = true + entry.laserspot:LaseOff() + self:__TargetLost(2,entry.unitname,entry.reccename) + end + end + -- check timed out + local timestamp = entry.timestamp + if Tnow - timestamp < self.LaseDuration and not lostsight then + valid = valid + 1 + else + timeout = true + entry.laserspot:LaseOff() + + self.RecceUnits[entry.reccename].cooldown = true + self.RecceUnits[entry.reccename].timestamp = timer.getAbsTime() + + if not lostsight then + self:__LaserTimeout(2,entry.unitname,entry.reccename) + end + end + if valid == 4 then + self.lasingindex = self.lasingindex + 1 + newtable[self.lasingindex] = entry + newreccecount[entry.reccename] = newreccecount[entry.reccename] + 1 + lasing = lasing + 1 + end + end + self.CurrentLasing = newtable + self.targetsperrecce = newreccecount + return lasing +end + +--- (Internal) Function to show status. +-- @param #AUTOLASE self +-- @param Wrapper.Group#GROUP Group (Optional) show to a certain group +-- @return #AUTOLASE self +function AUTOLASE:ShowStatus(Group) + local report = REPORT:New("Autolase") + local reccetable = self.RecceSet:GetSetObjects() + for _,_recce in pairs(reccetable) do + if _recce and _recce:IsAlive() then + local unit = _recce:GetUnit(1) + local name = unit:GetName() + local code = self:GetLaserCode(name) + report:Add(string.format("Recce %s has code %d",name,code)) + end + end + local lines = 0 + for _ind,_entry in pairs(self.CurrentLasing) do + local entry = _entry -- #AUTOLASE.LaserSpot + local reccename = entry.reccename + local typename = entry.unittype + local code = entry.lasercode + local locationstring = entry.location + local text = string.format("%s lasing %s code %d\nat %s",reccename,typename,code,locationstring) + report:Add(text) + lines = lines + 1 + end + if lines == 0 then + report:Add("No targets!") + end + local reporttime = self.reporttimelong + if lines == 0 then reporttime = self.reporttimeshort end + if Group and Group:IsAlive() then + local m = MESSAGE:New(report:Text(),reporttime,"Info"):ToGroup(Group) + else + local m = MESSAGE:New(report:Text(),reporttime,"Info"):ToCoalition(self.coalition) + end + return self +end + +--- (Internal) Function to show messages. +-- @param #AUTOLASE self +-- @param #string Message The message to be sent +-- @param #number Duration Duration in seconds +-- @return #AUTOLASE self +function AUTOLASE:NotifyPilots(Message,Duration) + if self.usepilotset then + local pilotset = self.pilotset:GetSetObjects() --#table + for _,_pilot in pairs(pilotset) do + local pilot = _pilot -- Wrapper.Unit#UNIT + if pilot and pilot:IsAlive() then + local Group = pilot:GetGroup() + local m = MESSAGE:New(Message,Duration,"Autolase"):ToGroup(Group) + end + end + elseif not self.debug then + local m = MESSAGE:New(Message,Duration,"Autolase"):ToCoalition(self.coalition) + else + local m = MESSAGE:New(Message,Duration,"Autolase"):ToAll() + end + if self.debug then self:I(Message) end + return self +end + +--- (User) Send messages via SRS. +-- @param #AUTOLASE self +-- @param #string Message The (short!) message to be sent, e.g. "Lasing target!" +-- @return #AUTOLASE self +-- @usage Step 1 - set up the radio basics **once** with +-- my_autolase:SetUsingSRS(true,"C:\\path\\SRS-Folder",251,radio.modulation.AM) +-- Step 2 - send a message, e.g. +-- function my_autolase:OnAfterLasing(From, Event, To, LaserSpot) +-- my_autolase:NotifyPilotsWithSRS("Reaper lasing new target!") +-- end +function AUTOLASE:NotifyPilotsWithSRS(Message) + if self.useSRS then + -- Create a SOUNDTEXT object. + if self.debug then + BASE:TraceOn() + BASE:TraceClass("SOUNDTEXT") + BASE:TraceClass("MSRS") + end + local path = self.SRSPath or "C:\\Program Files\\DCS-SimpleRadio-Standalone" + local freq = self.SRSFreq or 271 + local mod = self.SRSMod or radio.modulation.AM + local text=SOUNDTEXT:New(Message) + -- MOOSE SRS + local msrs=MSRS:New(path, freq, mod) + -- Text-to speech with default voice after 2 seconds. + msrs:PlaySoundText(text, 2) + end + if self.debug then self:I(Message) end + return self +end + +--- (Internal) Function to check if a unit is already lased. +-- @param #AUTOLASE self +-- @param #string unitname Name of the unit to check +-- @return #boolean outcome True or false +function AUTOLASE:CheckIsLased(unitname) + local outcome = false + for _,_laserspot in pairs(self.CurrentLasing) do + local spot = _laserspot -- #AUTOLASE.LaserSpot + if spot.unitname == unitname then + outcome = true + break + end + end + return outcome +end + +--- (Internal) Function to check if a unit can be lased. +-- @param #AUTOLASE self +-- @param Wrapper.Unit#UNIT Recce The Recce #UNIT +-- @param Wrapper.Unit#UNIT Unit The lased #UNIT +-- @return #boolean outcome True or false +function AUTOLASE:CanLase(Recce,Unit) + local canlase = false + -- cooldown? + local name = Recce:GetName() + local cooldown = self.RecceUnits[name].cooldown and self.forcecooldown + if cooldown then + local Tdiff = timer.getAbsTime() - self.RecceUnits[name].timestamp + if Tdiff < self.cooldowntime then + return false + else + self.RecceUnits[name].cooldown = false + end + end + -- calculate LOS + local reccecoord = Recce:GetCoordinate() + local unitcoord = Unit:GetCoordinate() + local islos = reccecoord:IsLOS(unitcoord,2.5) + -- calculate distance + local distance = math.floor(reccecoord:Get3DDistance(unitcoord)) + local lasedistance = self:GetLosFromUnit(Recce) + if distance <= lasedistance and islos then + canlase = true + end + return canlase +end + +------------------------------------------------------------------- +-- FSM Functions +------------------------------------------------------------------- + +--- (Internal) FSM Function for monitoring +-- @param #AUTOLASE self +-- @param #string From The from state +-- @param #string Event The event +-- @param #string To The to state +-- @return #AUTOLASE self +function AUTOLASE:onbeforeMonitor(From, Event, To) + self:T({From, Event, To}) + -- Check if group has detected any units. + self:UpdateIntel() + return self +end + +--- (Internal) FSM Function for monitoring +-- @param #AUTOLASE self +-- @param #string From The from state +-- @param #string Event The event +-- @param #string To The to state +-- @return #AUTOLASE self +function AUTOLASE:onafterMonitor(From, Event, To) + self:T({From, Event, To}) + + -- Housekeeping + local countlases = self:CleanCurrentLasing() + + self:SetPilotMenu() + + local detecteditems = self.Contacts or {} -- #table of Ops.Intelligence#INTEL.Contact + local groupsbythreat = {} + local report = REPORT:New("Detections") + local lines = 0 + for _,_contact in pairs(detecteditems) do + local contact = _contact -- Ops.Intelligence#INTEL.Contact + local grp = contact.group + local coord = contact.position + local reccename = contact.recce + local reccegrp = UNIT:FindByName(reccename) + local reccecoord = reccegrp:GetCoordinate() + local distance = math.floor(reccecoord:Get3DDistance(coord)) + local text = string.format("%s of %s | Distance %d km | Threatlevel %d",contact.attribute, contact.groupname, math.floor(distance/1000), contact.threatlevel) + report:Add(text) + self:T(text) + if self.debug then self:I(text) end + lines = lines + 1 + -- sort out groups beyond sight + local lasedistance = self:GetLosFromUnit(reccegrp) + if grp:IsGround() and lasedistance >= distance then + table.insert(groupsbythreat,{contact.group,contact.threatlevel}) + self.RecceNames[contact.groupname] = contact.recce + end + end + + self.GroupsByThreat = groupsbythreat + + if self.verbose > 2 and lines > 0 then + local m=MESSAGE:New(report:Text(),self.reporttimeshort,"Autolase"):ToAll() + end + + table.sort(self.GroupsByThreat, function(a,b) + local aNum = a[2] -- Coin value of a + local bNum = b[2] -- Coin value of b + return aNum > bNum -- Return their comparisons, < for ascending, > for descending + end) + + -- build table of Units + local unitsbythreat = {} + for _,_entry in pairs(self.GroupsByThreat) do + local group = _entry[1] -- Wrapper.Group#GROUP + if group and group:IsAlive() then + local units = group:GetUnits() + local reccename = self.RecceNames[group:GetName()] + for _,_unit in pairs(units) do + local unit = _unit -- Wrapper.Unit#UNIT + if unit and unit:IsAlive() then + local threat = unit:GetThreatLevel() + local coord = unit:GetCoordinate() + if threat > 0 then + local unitname = unit:GetName() + table.insert(unitsbythreat,{unit,threat}) + self.RecceUnitNames[unitname] = reccename + end + end + end + end + end + + self.UnitsByThreat = unitsbythreat + + table.sort(self.UnitsByThreat, function(a,b) + local aNum = a[2] -- Coin value of a + local bNum = b[2] -- Coin value of b + return aNum > bNum -- Return their comparisons, < for ascending, > for descending + end) + + local unitreport = REPORT:New("Detected Units") + + local lines = 0 + for _,_entry in pairs(self.UnitsByThreat) do + local threat = _entry[2] + local unit = _entry[1] + local unitname = unit:GetName() + local text = string.format("Unit %s | Threatlevel %d | Detected by %s",unitname,threat,self.RecceUnitNames[unitname]) + unitreport:Add(text) + lines = lines + 1 + self:T(text) + if self.debug then self:I(text) end + end + + if self.verbose > 2 and lines > 0 then + local m=MESSAGE:New(unitreport:Text(),self.reporttimeshort,"Autolase"):ToAll() + end + + for _,_detectingunit in pairs(self.RecceUnits) do + + local reccename = _detectingunit.name + local recce = _detectingunit.unit + local reccecount = self.targetsperrecce[reccename] or 0 + local targets = 0 + for _,_entry in pairs(self.UnitsByThreat) do + local unit = _entry[1] -- Wrapper.Unit#UNIT + local unitname = unit:GetName() + local canlase = self:CanLase(recce,unit) + if targets+reccecount < self.maxlasing and not self:CheckIsLased(unitname) and unit:IsAlive() and canlase then + targets = targets + 1 + local code = self:GetLaserCode(reccename) + local spot = SPOT:New(recce) + spot:LaseOn(unit,code,self.LaseDuration) + local locationstring = unit:GetCoordinate():ToStringLLDDM() + local laserspot = { -- #AUTOLASE.LaserSpot + laserspot = spot, + lasedunit = unit, + lasingunit = recce, + lasercode = code, + location = locationstring, + timestamp = timer.getAbsTime(), + unitname = unitname, + reccename = reccename, + unittype = unit:GetTypeName(), + } + if self.smoketargets then + local coord = unit:GetCoordinate() + local color = self:GetSmokeColor(reccename) + coord:Smoke(color) + end + self.lasingindex = self.lasingindex + 1 + self.CurrentLasing[self.lasingindex] = laserspot + self:__Lasing(2,laserspot) + end + end + end + + self:__Monitor(-30) + return self +end + +--- (Internal) FSM Function onbeforeRecceKIA +-- @param #AUTOLASE self +-- @param #string From The from state +-- @param #string Event The event +-- @param #string To The to state +-- @param #string RecceName The lost Recce +-- @return #AUTOLASE self +function AUTOLASE:onbeforeRecceKIA(From,Event,To,RecceName) + self:T({From, Event, To, RecceName}) + if self.notifypilots or self.debug then + local text = string.format("Recce %s KIA!",RecceName) + self:NotifyPilots(text,self.reporttimeshort) + end + return self +end + +--- (Internal) FSM Function onbeforeTargetDestroyed +-- @param #AUTOLASE self +-- @param #string From The from state +-- @param #string Event The event +-- @param #string To The to state +-- @param #string UnitName The destroyed unit\'s name +-- @param #string RecceName The Recce name lasing +-- @return #AUTOLASE self +function AUTOLASE:onbeforeTargetDestroyed(From,Event,To,UnitName,RecceName) + self:T({From, Event, To, UnitName, RecceName}) + if self.notifypilots or self.debug then + local text = string.format("Unit %s destroyed! Good job!",UnitName) + self:NotifyPilots(text,self.reporttimeshort) + end + return self +end + +--- (Internal) FSM Function onbeforeTargetLost +-- @param #AUTOLASE self +-- @param #string From The from state +-- @param #string Event The event +-- @param #string To The to state +-- @param #string UnitName The lost unit\'s name +-- @param #string RecceName The Recce name lasing +-- @return #AUTOLASE self +function AUTOLASE:onbeforeTargetLost(From,Event,To,UnitName,RecceName) + self:T({From, Event, To, UnitName,RecceName}) + if self.notifypilots or self.debug then + local text = string.format("%s lost sight of unit %s.",RecceName,UnitName) + self:NotifyPilots(text,self.reporttimeshort) + end + return self +end + +--- (Internal) FSM Function onbeforeLaserTimeout +-- @param #AUTOLASE self +-- @param #string From The from state +-- @param #string Event The event +-- @param #string To The to state +-- @param #string UnitName The lost unit\'s name +-- @param #string RecceName The Recce name lasing +-- @return #AUTOLASE self +function AUTOLASE:onbeforeLaserTimeout(From,Event,To,UnitName,RecceName) + self:T({From, Event, To, UnitName,RecceName}) + if self.notifypilots or self.debug then + local text = string.format("%s laser timeout on unit %s.",RecceName,UnitName) + self:NotifyPilots(text,self.reporttimeshort) + end + return self +end + +--- (Internal) FSM Function onbeforeLasing +-- @param #AUTOLASE self +-- @param #string From The from state +-- @param #string Event The event +-- @param #string To The to state +-- @param Functional.Autolase#AUTOLASE.LaserSpot LaserSpot The LaserSpot data table +-- @return #AUTOLASE self +function AUTOLASE:onbeforeLasing(From,Event,To,LaserSpot) + self:T({From, Event, To, LaserSpot.unittype}) + if self.notifypilots or self.debug then + local laserspot = LaserSpot -- #AUTOLASE.LaserSpot + local text = string.format("%s is lasing %s code %d\nat %s",laserspot.reccename,laserspot.unittype,laserspot.lasercode,laserspot.location) + self:NotifyPilots(text,self.reporttimeshort+5) + end + return self +end + +--- (Internal) FSM Function onbeforeCancel +-- @param #AUTOLASE self +-- @param #string From The from state +-- @param #string Event The event +-- @param #string To The to state +-- @return #AUTOLASE self +function AUTOLASE:onbeforeCancel(From,Event,To) + self:UnHandleEvent(EVENTS.PlayerEnterAircraft) + self:__Stop(2) + return self +end + +------------------------------------------------------------------- +-- End Functional.Autolase.lua +------------------------------------------------------------------- +--- **Ops** - Manages aircraft CASE X recoveries for carrier operations (X=I, II, III). +-- +-- The AIRBOSS class manages recoveries of human pilots and AI aircraft on aircraft carriers. +-- +-- **Main Features:** +-- +-- * CASE I, II and III recoveries. +-- * Supports human pilots as well as AI flight groups. +-- * Automatic LSO grading including (optional) live grading while in the groove. +-- * Different skill levels from on-the-fly tips for flight students to *ziplip* for pros. Can be set for each player individually. +-- * Define recovery time windows with individual recovery cases in the same mission. +-- * Option to let the carrier steam into the wind automatically. +-- * Automatic TACAN and ICLS channel setting of carrier. +-- * Separate radio channels for LSO and Marshal transmissions. +-- * Voice over support for LSO and Marshal radio transmissions. +-- * Advanced F10 radio menu including carrier info, weather, radio frequencies, TACAN/ICLS channels, player LSO grades, marking of zones etc. +-- * Recovery tanker and refueling option via integration of @{Ops.RecoveryTanker} class. +-- * Rescue helicopter option via @{Ops.RescueHelo} class. +-- * Combine multiple human players to sections. +-- * Many parameters customizable by convenient user API functions. +-- * Multiple carrier support due to object oriented approach. +-- * Unlimited number of players. +-- * Persistence of player results (optional). LSO grading data is saved to csv file. +-- * Trap sheet (optional). +-- * Finite State Machine (FSM) implementation. +-- +-- **Supported Carriers:** +-- +-- * [USS John C. Stennis](https://en.wikipedia.org/wiki/USS_John_C._Stennis) (CVN-74) +-- * [USS Theodore Roosevelt](https://en.wikipedia.org/wiki/USS_Theodore_Roosevelt_(CVN-71)) (CVN-71) [Super Carrier Module] +-- * [USS Abraham Lincoln](https://en.wikipedia.org/wiki/USS_Abraham_Lincoln_(CVN-72)) (CVN-72) [Super Carrier Module] +-- * [USS George Washington](https://en.wikipedia.org/wiki/USS_George_Washington_(CVN-73)) (CVN-73) [Super Carrier Module] +-- * [USS Harry S. Truman](https://en.wikipedia.org/wiki/USS_Harry_S._Truman) (CVN-75) [Super Carrier Module] +-- * [USS Forrestal](https://en.wikipedia.org/wiki/USS_Forrestal_(CV-59)) (CV-59) [Heatblur Carrier Module] +-- * [USS Tarawa](https://en.wikipedia.org/wiki/USS_Tarawa_(LHA-1)) (LHA-1) [**WIP**] +-- * [USS America](https://en.wikipedia.org/wiki/USS_America_(LHA-6)) (LHA-6) [**WIP**] +-- * [Juan Carlos I](https://en.wikipedia.org/wiki/Spanish_amphibious_assault_ship_Juan_Carlos_I) (L61) [**WIP**] +-- +-- **Supported Aircraft:** +-- +-- * [F/A-18C Hornet Lot 20](https://forums.eagle.ru/forumdisplay.php?f=557) (Player & AI) +-- * [F-14A/B Tomcat](https://forums.eagle.ru/forumdisplay.php?f=395) (Player & AI) +-- * [A-4E Skyhawk Community Mod](https://forums.eagle.ru/showthread.php?t=224989) (Player & AI) +-- * [AV-8B N/A Harrier](https://forums.eagle.ru/forumdisplay.php?f=555) (Player & AI) [**WIP**] +-- * [T-45C Goshawk](https://www.vnao-cvw-7.com/t-45-goshawk) (VNAO)(Player & AI) [**WIP**] +-- * F/A-18C Hornet (AI) +-- * F-14A Tomcat (AI) +-- * E-2D Hawkeye (AI) +-- * S-3B Viking & tanker version (AI) +-- * [C-2A Greyhound](https://forums.eagle.ru/showthread.php?t=255641) (AI) +-- +-- At the moment, optimized parameters are available for the F/A-18C Hornet (Lot 20) and A-4E community mod as aircraft and the USS John C. Stennis as carrier. +-- +-- The AV-8B Harrier, the USS Tarawa, USS America and Juan Carlos I are WIP. The AV-8B harrier and the LHA's and LHD can only be used together, i.e. these ships are the only carriers the harrier is supposed to land on and +-- no other fixed wing aircraft (human or AI controlled) are supposed to land on these ships. Currently only Case I is supported. Case II/III take slightly different steps from the CVN carrier. +-- However, the two Case II/III pattern are very similar so this is not a big drawback. +-- +-- Heatblur's mighty F-14B Tomcat has been added (March 13th 2019) as well. Same goes for the A version. +-- +-- The [DCS Supercarriers](https://forums.eagle.ru/forum/151-dcs-supercarrier/) are also supported. +-- +-- ## Discussion +-- +-- If you have questions or suggestions, please visit the [MOOSE Discord](https://discord.gg/AeYAkHP) #ops-airboss channel. +-- There you also find an example mission and the necessary voice over sound files. Check out the **pinned messages**. +-- +-- ## Example Missions +-- +-- Example missions can be found [here](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/OPS%20-%20Airboss). +-- They contain the latest development Moose.lua file. +-- +-- ## IMPORTANT +-- +-- Some important restrictions (of DCS) you should be aware of: +-- +-- * Each player slot (client) should be in a separate group as DCS does only allow for sending messages to groups and not individual units. +-- * Players are identified by their player name. Hence, ensure that no two player have the same name, e.g. "New Callsign", as this will lead to unexpected results. +-- * The modex (tail number) of an aircraft should **not** be changed dynamically in the mission by a player. Unfortunately, there is no way to get this information via scripting API functions. +-- * The A-4E-C mod needs *easy comms* activated to interact with the F10 radio menu. +-- +-- ## Youtube Videos +-- +-- ### AIRBOSS videos: +-- +-- * [[MOOSE] Airboss - Groove Testing (WIP)](https://www.youtube.com/watch?v=94KHQxxX3UI) +-- * [[MOOSE] Airboss - Groove Test A-4E Community Mod](https://www.youtube.com/watch?v=ZbjD7FHiaHo) +-- * [[MOOSE] Airboss - Groove Test: On-the-fly LSO Grading](https://www.youtube.com/watch?v=Xgs1hwDcPyM) +-- * [[MOOSE] Airboss - Carrier Auto Steam Into Wind](https://www.youtube.com/watch?v=IsU8dYgsp90) +-- * [[MOOSE] Airboss - CASE I Walkthrough in the F/A-18C by TG](https://www.youtube.com/watch?v=o1UrP4Q6PMM) +-- * [[MOOSE] Airboss - New LSO/Marshal Voice Overs by Raynor](https://www.youtube.com/watch?v=_Suo68bRu8k) +-- * [[MOOSE] Airboss - CASE I, "Until We Go Down" featuring the F-14B by Pikes](https://www.youtube.com/watch?v=ojgHDSw3Doc) +-- * [[MOOSE] Airboss - Skipper Menu](https://youtu.be/awnecCxRoNQ) +-- +-- ### Lex explaining Boat Ops: +-- +-- * [( DCS HORNET ) Some boat ops basics VID 1](https://www.youtube.com/watch?v=LvGQS-3AzMc) +-- * [( DCS HORNET ) Some boat ops basics VID 2](https://www.youtube.com/watch?v=bN44wvtRsw0) +-- +-- ### Jabbers Case I and III Recovery Tutorials: +-- +-- * [DCS World - F/A-18 - Case I Carrier Recovery Tutorial](https://www.youtube.com/watch?v=lm-M3VUy-_I) +-- * [DCS World - Case I Recovery Tutorial - Followup](https://www.youtube.com/watch?v=cW5R32Q6xC8) +-- * [DCS World - CASE III Recovery Tutorial](https://www.youtube.com/watch?v=Lnfug5CVAvo) +-- +-- ### Wags DCS Hornet Videos: +-- +-- * [DCS: F/A-18C Hornet - Episode 9: CASE I Carrier Landing](https://www.youtube.com/watch?v=TuigBLhtAH8) +-- * [DCS: F/A-18C Hornet – Episode 16: CASE III Introduction](https://www.youtube.com/watch?v=DvlMHnLjbDQ) +-- * [DCS: F/A-18C Hornet Case I Carrier Landing Training Lesson Recording](https://www.youtube.com/watch?v=D33uM9q4xgA) +-- +-- ### AV-8B Harrier at USS Tarawa +-- +-- * [Harrier Ship Landing Mission with Auto LSO!](https://www.youtube.com/watch?v=lqmVvpunk2c) +-- * [Harrier Practice pattern USS America](https://youtu.be/99NigITYmcI) +-- +-- === +-- +-- ### Author: **funkyfranky** +-- ### Special Thanks To **Bankler** +-- For his great [Recovery Trainer](https://forums.eagle.ru/showthread.php?t=221412) mission and script! +-- His work was the initial inspiration for this class. Also note that this implementation uses some routines for determining the player position in Case I recoveries he developed. +-- Bankler was kind enough to allow me to add this to the class - thanks again! +-- +-- @module Ops.Airboss +-- @image Ops_Airboss.png + +--- AIRBOSS class. +-- @type AIRBOSS +-- @field #string ClassName Name of the class. +-- @field #boolean Debug Debug mode. Messages to all about status. +-- @field #string lid Class id string for output to DCS log file. +-- @field #string theatre The DCS map used in the mission. +-- @field Wrapper.Unit#UNIT carrier Aircraft carrier unit on which we want to practice. +-- @field #string carriertype Type name of aircraft carrier. +-- @field #AIRBOSS.CarrierParameters carrierparam Carrier specific parameters. +-- @field #string alias Alias of the carrier. +-- @field Wrapper.Airbase#AIRBASE airbase Carrier airbase object. +-- @field #table waypoints Waypoint coordinates of carrier. +-- @field #number currentwp Current waypoint, i.e. the one that has been passed last. +-- @field Core.Radio#BEACON beacon Carrier beacon for TACAN and ICLS. +-- @field #boolean TACANon Automatic TACAN is activated. +-- @field #number TACANchannel TACAN channel. +-- @field #string TACANmode TACAN mode, i.e. "X" or "Y". +-- @field #string TACANmorse TACAN morse code, e.g. "STN". +-- @field #boolean ICLSon Automatic ICLS is activated. +-- @field #number ICLSchannel ICLS channel. +-- @field #string ICLSmorse ICLS morse code, e.g. "STN". +-- @field #AIRBOSS.Radio LSORadio Radio for LSO calls. +-- @field #number LSOFreq LSO radio frequency in MHz. +-- @field #string LSOModu LSO radio modulation "AM" or "FM". +-- @field #AIRBOSS.Radio MarshalRadio Radio for carrier calls. +-- @field #number MarshalFreq Marshal radio frequency in MHz. +-- @field #string MarshalModu Marshal radio modulation "AM" or "FM". +-- @field #number TowerFreq Tower radio frequency in MHz. +-- @field Core.Scheduler#SCHEDULER radiotimer Radio queue scheduler. +-- @field Core.Zone#ZONE_UNIT zoneCCA Carrier controlled area (CCA), i.e. a zone of 50 NM radius around the carrier. +-- @field Core.Zone#ZONE_UNIT zoneCCZ Carrier controlled zone (CCZ), i.e. a zone of 5 NM radius around the carrier. +-- @field #table players Table of players. +-- @field #table menuadded Table of units where the F10 radio menu was added. +-- @field #AIRBOSS.Checkpoint BreakEntry Break entry checkpoint. +-- @field #AIRBOSS.Checkpoint BreakEarly Early break checkpoint. +-- @field #AIRBOSS.Checkpoint BreakLate Late break checkpoint. +-- @field #AIRBOSS.Checkpoint Abeam Abeam checkpoint. +-- @field #AIRBOSS.Checkpoint Ninety At the ninety checkpoint. +-- @field #AIRBOSS.Checkpoint Wake Checkpoint right behind the carrier. +-- @field #AIRBOSS.Checkpoint Final Checkpoint when turning to final. +-- @field #AIRBOSS.Checkpoint Groove In the groove checkpoint. +-- @field #AIRBOSS.Checkpoint Platform Case II/III descent at 2000 ft/min at 5000 ft platform. +-- @field #AIRBOSS.Checkpoint DirtyUp Case II/III dirty up and on speed position at 1200 ft and 10-12 NM from the carrier. +-- @field #AIRBOSS.Checkpoint Bullseye Case III intercept glideslope and follow ICLS aka "bullseye". +-- @field #number defaultcase Default recovery case. This is the case used if not specified otherwise. +-- @field #number case Recovery case I, II or III currently in progress. +-- @field #table recoverytimes List of time windows when aircraft are recovered including the recovery case and holding offset. +-- @field #number defaultoffset Default holding pattern update if not specified otherwise. +-- @field #number holdingoffset Offset [degrees] of Case II/III holding pattern. +-- @field #table flights List of all flights in the CCA. +-- @field #table Qmarshal Queue of marshalling aircraft groups. +-- @field #table Qpattern Queue of aircraft groups in the landing pattern. +-- @field #table Qwaiting Queue of aircraft groups waiting outside 10 NM zone for the next free Marshal stack. +-- @field #table Qspinning Queue of aircraft currently spinning. +-- @field #table RQMarshal Radio queue of marshal. +-- @field #number TQMarshal Abs mission time, the last transmission ended. +-- @field #table RQLSO Radio queue of LSO. +-- @field #number TQLSO Abs mission time, the last transmission ended. +-- @field #number Nmaxpattern Max number of aircraft in landing pattern. +-- @field #number Nmaxmarshal Number of max Case I Marshal stacks available. Default 3, i.e. angels 2, 3 and 4. +-- @field #number NmaxSection Number of max section members (excluding the lead itself), i.e. NmaxSection=1 is a section of two. +-- @field #number NmaxStack Number of max flights per stack. Default 2. +-- @field #boolean handleai If true (default), handle AI aircraft. +-- @field Ops.RecoveryTanker#RECOVERYTANKER tanker Recovery tanker flying overhead of carrier. +-- @field DCS#Vec3 Corientation Carrier orientation in space. +-- @field DCS#Vec3 Corientlast Last known carrier orientation. +-- @field Core.Point#COORDINATE Cposition Carrier position. +-- @field #string defaultskill Default player skill @{#AIRBOSS.Difficulty}. +-- @field #boolean adinfinitum If true, carrier patrols ad infinitum, i.e. when reaching its last waypoint it starts at waypoint one again. +-- @field #number magvar Magnetic declination in degrees. +-- @field #number Tcollapse Last time timer.gettime() the stack collapsed. +-- @field #AIRBOSS.Recovery recoverywindow Current or next recovery window opened. +-- @field #boolean usersoundradio Use user sound output instead of radio transmissions. +-- @field #number Tqueue Last time in seconds of timer.getTime() the queue was updated. +-- @field #number dTqueue Time interval in seconds for updating the queues etc. +-- @field #number dTstatus Time interval for call FSM status updates. +-- @field #boolean menumarkzones If false, disables the option to mark zones via smoke or flares. +-- @field #boolean menusmokezones If false, disables the option to mark zones via smoke. +-- @field #table playerscores Table holding all player scores and grades. +-- @field #boolean autosave If true, all player grades are automatically saved to a file on disk. +-- @field #string autosavepath Path where the player grades file is saved on auto save. +-- @field #string autosavefilename File name of the auto player grades save file. Default is auto generated from carrier name/alias. +-- @field #number marshalradius Radius of the Marshal stack zone. +-- @field #boolean airbossnice Airboss is a nice guy. +-- @field #boolean staticweather Mission uses static rather than dynamic weather. +-- @field #number windowcount Running number counting the recovery windows. +-- @field #number LSOdT Time interval in seconds before the LSO will make its next call. +-- @field #string senderac Name of the aircraft acting as sender for broadcasting radio messages from the carrier. DCS shortcoming workaround. +-- @field #string radiorelayLSO Name of the aircraft acting as sender for broadcasting LSO radio messages from the carrier. DCS shortcoming workaround. +-- @field #string radiorelayMSH Name of the aircraft acting as sender for broadcasting Marhsal radio messages from the carrier. DCS shortcoming workaround. +-- @field #boolean turnintowind If true, carrier is currently turning into the wind. +-- @field #boolean detour If true, carrier is currently making a detour from its path along the ME waypoints. +-- @field Core.Point#COORDINATE Creturnto Position to return to after turn into the wind leg is over. +-- @field Core.Set#SET_GROUP squadsetAI AI groups in this set will be handled by the airboss. +-- @field Core.Set#SET_GROUP excludesetAI AI groups in this set will be explicitly excluded from handling by the airboss and not forced into the Marshal pattern. +-- @field #boolean menusingle If true, menu is optimized for a single carrier. +-- @field #number collisiondist Distance up to which collision checks are done. +-- @field #number holdtimestamp Timestamp when the carrier first came to an unexpected hold. +-- @field #number Tmessage Default duration in seconds messages are displayed to players. +-- @field #string soundfolder Folder within the mission (miz) file where airboss sound files are located. +-- @field #string soundfolderLSO Folder withing the mission (miz) file where LSO sound files are stored. +-- @field #string soundfolderMSH Folder withing the mission (miz) file where Marshal sound files are stored. +-- @field #boolean despawnshutdown Despawn group after engine shutdown. +-- @field #number Tbeacon Last time the beacons were refeshed. +-- @field #number dTbeacon Time interval to refresh the beacons. Default 5 minutes. +-- @field #AIRBOSS.LSOCalls LSOCall Radio voice overs of the LSO. +-- @field #AIRBOSS.MarshalCalls MarshalCall Radio voice over of the Marshal/Airboss. +-- @field #AIRBOSS.PilotCalls PilotCall Radio voice over from AI pilots. +-- @field #number lowfuelAI Low fuel threshold for AI groups in percent. +-- @field #boolean emergency If true (default), allow emergency landings, i.e. bypass any pattern and go for final approach. +-- @field #boolean respawnAI If true, respawn AI flights as they enter the CCA to detach and airfields from the mission plan. Default false. +-- @field #boolean turning If true, carrier is currently turning. +-- @field #AIRBOSS.GLE gle Glidesope error thresholds. +-- @field #AIRBOSS.LUE lue Lineup error thresholds. +-- @field #boolean trapsheet If true, players can save their trap sheets. +-- @field #string trappath Path where to save the trap sheets. +-- @field #string trapprefix File prefix for trap sheet files. +-- @field #number initialmaxalt Max altitude in meters to register in the inital zone. +-- @field #boolean welcome If true, display welcome message to player. +-- @field #boolean skipperMenu If true, add skipper menu. +-- @field #number skipperSpeed Speed in knots for manual recovery start. +-- @field #number skipperCase Manual recovery case. +-- @field #boolean skipperUturn U-turn on/off via menu. +-- @field #number skipperOffset Holding offset angle in degrees for Case II/III manual recoveries. +-- @field #number skipperTime Recovery time in min for manual recovery. +-- @extends Core.Fsm#FSM + +--- Be the boss! +-- +-- === +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Main.png) +-- +-- # The AIRBOSS Concept +-- +-- On a carrier, the AIRBOSS is guy who is really in charge - don't mess with him! +-- +-- # Recovery Cases +-- +-- The AIRBOSS class supports all three commonly used recovery cases, i.e. +-- +-- * **CASE I** during daytime and good weather (ceiling > 3000 ft, visibility > 5 NM), +-- * **CASE II** during daytime but poor visibility conditions (ceiling > 1000 ft, visibility > 5NM), +-- * **CASE III** when below Case II conditions and during nighttime (ceiling < 1000 ft, visibility < 5 NM). +-- +-- That being said, this script allows you to use any of the three cases to be used at any time. Or, in other words, *you* need to specify when which case is safe and appropriate. +-- +-- This is a lot of responsibility. *You* are the boss, but *you* need to make the right decisions or things will go terribly wrong! +-- +-- Recovery windows can be set up via the @{#AIRBOSS.AddRecoveryWindow} function as explained below. With this it is possible to seamlessly (within reason!) switch recovery cases in the same mission. +-- +-- ## CASE I +-- +-- As mentioned before, Case I recovery is the standard procedure during daytime and good visibility conditions. +-- +-- ### Holding Pattern +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case1_Holding.png) +-- +-- The graphic depicts a the standard holding pattern during a Case I recovery. Incoming aircraft enter the holding pattern, which is a counter clockwise turn with a +-- diameter of 5 NM, at their assigned altitude. The holding altitude of the first stack is 2000 ft. The interval between stacks is 1000 ft. +-- +-- Once a recovery window opens, the aircraft of the lowest stack commence their landing approach and the rest of the Marshal stack collapses, i.e. aircraft switch from +-- their current stack to the next lower stack. +-- +-- The flight that transitions form the holding pattern to the landing approach, it should leave the Marshal stack at the 3 position and make a left hand turn to the *Initial* +-- position, which is 3 NM astern of the boat. Note that you need to be below 1300 feet to be registered in the initial zone. +-- The altitude can be set via the function @{AIRBOSS.SetInitialMaxAlt}(*altitude*) function. +-- As described below, the initial zone can be smoked or flared via the AIRBOSS F10 Help radio menu. +-- +-- ### Landing Pattern +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case1_Landing.png) +-- +-- Once the aircraft reaches the Initial, the landing pattern begins. The important steps of the pattern are shown in the image above. +-- The AV-8B Harrier pattern is very similar, the only differences are as there is no angled deck there is no wake check. from the ninety you wil fly a straight approach offset 26 ft to port (left) of the tram line. +-- The aim is to arrive abeam the landing spot in a stable hover at 120 ft with forward speed matched to the boat. From there the LSO will call "cleared to land". You then level cross to the tram line at the designated landing spot at land vertcally. +-- +-- +-- ## CASE III +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case3.png) +-- +-- A Case III recovery is conducted during nighttime or when the visibility is below CASE II minima during the day. The holding position and the landing pattern are rather different from a Case I recovery as can be seen in the image above. +-- +-- The first holding zone starts 21 NM astern the carrier at angels 6. The separation between the stacks is 1000 ft just like in Case I. However, the distance to the boat +-- increases by 1 NM with each stack. The general form can be written as D=15+6+(N-1), where D is the distance to the boat in NM and N the number of the stack starting at N=1. +-- +-- Once the aircraft of the lowest stack is allowed to commence to the landing pattern, it starts a descent at 4000 ft/min until it reaches the "*Platform*" at 5000 ft and +-- ~19 NM DME. From there a shallower descent at 2000 ft/min should be performed. At an altitude of 1200 ft the aircraft should level out and "*Dirty Up*" (gear, flaps & hook down). +-- +-- At 3 NM distance to the carrier, the aircraft should intercept the 3.5 degrees glideslope at the "*Bullseye*". From there the pilot should "follow the needles" of the ICLS. +-- +-- ## CASE II +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case2.png) +-- +-- Case II is the common recovery procedure at daytime if visibility conditions are poor. It can be viewed as hybrid between Case I and III. +-- The holding pattern is very similar to that of the Case III recovery with the difference the the radial is the inverse of the BRC instead of the FB. +-- From the holding zone aircraft are follow the Case III path until they reach the Initial position 3 NM astern the boat. From there a standard Case I recovery procedure is +-- in place. +-- +-- Note that the image depicts the case, where the holding zone has an angle offset of 30 degrees with respect to the BRC. This is optional. Commonly used offset angels +-- are 0 (no offset), +-15 or +-30 degrees. The AIRBOSS class supports all these scenarios which are used during Case II and III recoveries. +-- +-- === +-- +-- # The F10 Radio Menu +-- +-- The F10 radio menu can be used to post requests to Marshal but also provides information about the player and carrier status. Additionally, helper functions +-- can be called. +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuMain.png) +-- +-- By default, the script creates a submenu "Airboss" in the "F10 Other ..." menu and each @{#AIRBOSS} carrier gets its own submenu. +-- If you intend to have only one carrier, you can simplify the menu structure using the @{#AIRBOSS.SetMenuSingleCarrier} function, which will create all carrier specific menu entries directly +-- in the "Airboss" submenu. (Needless to say, that if you enable this and define multiple carriers, the menu structure will get completely screwed up.) +-- +-- ## Root Menu +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuRoot.png) +-- +-- The general structure +-- +-- * **F1 Help...** (Help submenu, see below.) +-- * **F2 Kneeboard...** (Kneeboard submenu, see below. Carrier information, weather report, player status.) +-- * **F3 Request Marshal** +-- * **F4 Request Commence** +-- * **F5 Request Refueling** +-- * **F6 Spinning** +-- * **F7 Emergency Landing** +-- * **F8 [Reset My Status]** +-- +-- ### Request Marshal +-- +-- This radio command can be used to request a stack in the holding pattern from Marshal. Necessary conditions are that the flight is inside the Carrier Controlled Area (CCA) +-- (see @{#AIRBOSS.SetCarrierControlledArea}). +-- +-- Marshal will assign an individual stack for each player group depending on the current or next open recovery case window. +-- If multiple players have registered as a section, the section lead will be assigned a stack and is responsible to guide his section to the assigned holding position. +-- +-- ### Request Commence +-- +-- This command can be used to request commencing from the marshal stack to the landing pattern. Necessary condition is that the player is in the lowest marshal stack +-- and that the number of aircraft in the landing pattern is smaller than four (or the number set by the mission designer). +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case1Pattern.png) +-- +-- The image displays the standard Case I Marshal pattern recovery. Pilots are supposed to fly a clockwise circle and descent between the **3** and **1** positions. +-- +-- Commence should be performed at around the **3** position. If the pilot is in the lowest Marshal stack, and flies through this area, he is automatically cleared for the +-- landing pattern. In other words, there is no need for the "Request Commence" radio command. The zone can be marked via smoke or flared using the player's F10 radio menu. +-- +-- A player can also request commencing if he is not registered in a marshal stack yet. If the pattern is free, Marshal will allow him to directly enter the landing pattern. +-- However, this is only possible when the Airboss has a nice day - see @{#AIRBOSS.SetAirbossNiceGuy}. +-- +-- ### Request Refueling +-- +-- If a recovery tanker has been set up via the @{#AIRBOSS.SetRecoveryTanker}, the player can request refueling at any time. If currently in the marshal stack, the stack above will collapse. +-- The player will be informed if the tanker is currently busy or going RTB to refuel itself at its home base. Once the re-fueling is complete, the player has to re-register to the marshal stack. +-- +-- ### Spinning +-- +-- If the pattern is full, players can go into the spinning pattern. This step is only allowed, if the player is in the pattern and his next step +-- is initial, break entry, early/late break. At this point, the player should climb to 1200 ft a fly on the port side of the boat to go back to the initial again. +-- +-- If a player is in the spin pattern, flights in the Marshal queue should hold their altitude and are not allowed into the pattern until the spinning aircraft +-- proceeds. +-- +-- Once the player reaches a point 100 meters behind the boat and at least 1 NM port, his step is set to "Initial" and he can resume the normal pattern approach. +-- +-- If necessary, the player can call "Spinning" again when in the above mentioned steps. +-- +-- ### Emergency Landing +-- +-- Request an emergency landing, i.e. bypass all pattern steps and go directly to the final approach. +-- +-- All section members are supposed to follow. Player (or section lead) is removed from all other queues and automatically added to the landing pattern queue. +-- +-- If this command is called while the player is currently on the carrier, he will be put in the bolter pattern. So the next expected step after take of +-- is the abeam position. This allows for quick landing training exercises without having to go through the whole pattern. +-- +-- The mission designer can forbid this option my setting @{#AIRBOSS.SetEmergencyLandings}(false) in the script. +-- +-- ### [Reset My Status] +-- +-- This will reset the current player status. If player is currently in a marshal stack, he will be removed from the marshal queue and the stack above will collapse. +-- The player needs to re-register later if desired. If player is currently in the landing pattern, he will be removed from the pattern queue. +-- +-- ## Help Menu +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuHelp.png) +-- +-- This menu provides commands to help the player. +-- +-- ### Mark Zones Submenu +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuMarkZones.png) +-- +-- These commands can be used to mark marshal or landing pattern zones. +-- +-- * **Smoke Pattern Zones** Smoke is used to mark the landing pattern zone of the player depending on his recovery case. +-- For Case I this is the initial zone. For Case II/III and three these are the Platform, Arc turn, Dirty Up, Bullseye/Initial zones as well as the approach corridor. +-- * **Flare Pattern Zones** Similar to smoke but uses flares to mark the pattern zones. +-- * **Smoke Marshal Zone** This smokes the surrounding area of the currently assigned Marshal zone of the player. Player has to be registered in Marshal queue. +-- * **Flare Marshal Zone** Similar to smoke but uses flares to mark the Marshal zone. +-- +-- Note that the smoke lasts ~5 minutes but the zones are moving along with the carrier. So after some time, the smoke gives shows you a picture of the past. +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case3_FlarePattern.png) +-- +-- ### Skill Level Submenu +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuSkill.png) +-- +-- The player can choose between three skill or difficulty levels. +-- +-- * **Flight Student**: The player receives tips at certain stages of the pattern, e.g. if he is at the right altitude, speed, etc. +-- * **Naval Aviator**: Less tips are show. Player should be familiar with the procedures and its aircraft parameters. +-- * **TOPGUN Graduate**: Only very few information is provided to the player. This is for the pros. +-- * **Hints On/Off**: Toggle displaying hints. +-- +-- ### My Status +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuMyStatus.png) +-- +-- This command provides information about the current player status. For example, his current step in the pattern. +-- +-- ### Attitude Monitor +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuAttitudeMonitor.png) +-- +-- This command displays the current aircraft attitude of the player aircraft in short intervals as message on the screen. +-- It provides information about current pitch, roll, yaw, orientation of the plane with respect to the carrier's orientation (*Gamma*) etc. +-- +-- If you are in the groove, current lineup and glideslope errors are displayed and you get an on-the-fly LSO grade. +-- +-- ### LSO Radio Check +-- +-- LSO will transmit a short message on his radio frequency. See @{#AIRBOSS.SetLSORadio}. Note that in the A-4E you will not hear the message unless you are in the pattern. +-- +-- ### Marshal Radio Check +-- +-- Marshal will transmit a short message on his radio frequency. See @{#AIRBOSS.SetMarshalRadio}. +-- +-- ### Subtitles On/Off +-- +-- This command toggles the display of radio message subtitles if no radio relay unit is used. By default subtitles are on. +-- Note that subtitles for radio messages which do not have a complete voice over are always displayed. +-- +-- ### Trapsheet On/Off +-- +-- Each player can activated or deactivate the recording of his flight data (AoA, glideslope, lineup, etc.) during his landing approaches. +-- Note that this feature also has to be enabled by the mission designer. +-- +-- ## Kneeboard Menu +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuKneeboard.png) +-- +-- The Kneeboard menu provides information about the carrier, weather and player results. +-- +-- ### Results Submenu +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuResults.png) +-- +-- Here you find your LSO grading results as well as scores of other players. +-- +-- * **Greenie Board** lists average scores of all players obtained during landing approaches. +-- * **My LSO Grades** lists all grades the player has received for his approaches in this mission. +-- * **Last Debrief** shows the detailed debriefing of the player's last approach. +-- +-- ### Carrier Info +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuCarrierInfo.png) +-- +-- Information about the current carrier status is displayed. This includes current BRC, FB, LSO and Marshal frequencies, list of next recovery windows. +-- +-- ### Weather Report +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuWeatherReport.png) +-- +-- Displays information about the current weather at the carrier such as QFE, wind and temperature. +-- +-- For missions using static weather, more information such as cloud base, thickness, precipitation, visibility distance, fog and dust are displayed. +-- If your mission uses dynamic weather, you can disable this output via the @{#AIRBOSS.SetStaticWeather}(**false**) function. +-- +-- ### Set Section +-- +-- With this command, you can define a section of human flights. The player who issues the command becomes the section lead and all other human players +-- within a radius of 100 meters become members of the section. +-- +-- The responsibilities of the section leader are: +-- +-- * To request Marshal. The section members are not allowed to do this and have to follow the lead to his assigned stack. +-- * To lead the right way to the pattern if the flight is allowed to commence. +-- * The lead is also the only one who can request commence if the flight wants to bypass the Marshal stack. +-- +-- Each time the command is issued by the lead, the complete section is set up from scratch. Members which are not inside the 100 m radius any more are +-- removed and/or new members which are now in range are added. +-- +-- If a section member issues this command, it is removed from the section of his lead. All flights which are not yet in another section will become members. +-- +-- The default maximum size of a section is two human players. This can be adjusted by the @{#AIRBOSS.SetMaxSectionSize}(*size*) function. The maximum allowed size +-- is four. +-- +-- ### Marshal Queue +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuMarshalQueue.png) +-- +-- Lists all flights currently in the Marshal queue including their assigned stack, recovery case and Charlie time estimate. +-- By default, the number of available Case I stacks is three, i.e. at angels 2, 3 and 4. Usually, the recovery thanker orbits at angels 6. +-- The number of available stacks can be set by the @{#AIRBOSS.SetMaxMarshalStack} function. +-- +-- The default number of human players per stack is two. This can be set via the @{#AIRBOSS.SetMaxFlightsPerStack} function but has to be between one and four. +-- +-- Due to technical reasons, each AI group always gets its own stack. DCS does not allow to control the AI in a manner that more than one group per stack would make sense unfortunately. +-- +-- ### Pattern Queue +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuPatternQueue.png) +-- +-- Lists all flights currently in the landing pattern queue showing the time since they entered the pattern. +-- By default, a maximum of four flights is allowed to enter the pattern. This can be set via the @{#AIRBOSS.SetMaxLandingPattern} function. +-- +-- ### Waiting Queue +-- +-- Lists all flights currently waiting for a free Case I Marshal stack. Note, stacks are limited only for Case I recovery ops but not for Case II or III. +-- If the carrier is switches recovery ops form Case I to Case II or III, all waiting flights will be assigned a stack. +-- +-- # Landing Signal Officer (LSO) +-- +-- The LSO will first contact you on his radio channel when you are at the the abeam position (Case I) with the phrase "Paddles, contact.". +-- Once you are in the groove the LSO will ask you to "Call the ball." and then acknowledge your ball call by "Roger Ball." +-- +-- During the groove the LSO will give you advice if you deviate from the correct landing path. These advices will be given when you are +-- +-- * too low or too high with respect to the glideslope, +-- * too fast or too slow with respect to the optimal AoA, +-- * too far left or too far right with respect to the lineup of the (angled) runway. +-- +-- ## LSO Grading +-- +-- LSO grading starts when the player enters the groove. The flight path and aircraft attitude is evaluated at certain steps (distances measured from rundown): +-- +-- * **X** At the Start (0.75 NM = 1390 m). +-- * **IM** In the Middle (0.5 NM = 926 m), middle one third of the glideslope. +-- * **IC** In Close (0.25 NM = 463 m), last one third of the glideslope. +-- * **AR** At the Ramp (0.027 NM = 50 m). +-- * **IW** In the Wires (at the landing position). +-- +-- Grading at each step includes the above calls, i.e. +-- +-- * **L**ined **U**p **L**eft or **R**ight: LUL, LUR +-- * Too **H**igh or too **LO**w: H, LO +-- * Too **F**ast or too **SLO**w: F, SLO +-- * **O**ver**S**hoot: OS, only referenced during **X** +-- * **Fly through** glideslope **down** or **up**: \\ , /, advisory only +-- * **D**rift **L**eft or **R**ight:DL, DR, advisory only +-- * **A**ngled **A**pproach: Angled approach (wings level and LUL): AA, advisory only +-- +-- Each grading, x, is subdivided by +-- +-- * (x): parenthesis, indicating "a little" for a minor deviation and +-- * \_x\_: underline, indicating "a lot" for major deviations. +-- +-- The position at the landing event is analyzed and the corresponding trapped wire calculated. If no wire was caught, the LSO will give the bolter call. +-- +-- If a player is significantly off from the ideal parameters from IC to AR, the LSO will wave the player off. Thresholds for wave off are +-- +-- * Line up error > 3.0 degrees left or right and/or +-- * Glideslope error < -1.2 degrees or > 1.8 degrees and/or +-- * AOA depending on aircraft type and only applied if skill level is "TOPGUN graduate". +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_LSOPlatcam.png) +-- +-- Line up and glideslope error thresholds were tested extensively using [VFA-113 Stingers LSO Mod](https://forums.eagle.ru/showthread.php?t=211557), +-- if the aircraft is outside the red box. In the picture above, **blue** numbers denote the line up thresholds while the **blacks** refer to the glideslope. +-- +-- A wave off is called, when the aircraft is outside the red rectangle. The measurement stops already ~50 m before the rundown, since the error in the calculation +-- increases the closer the aircraft gets to the origin/reference point. +-- +-- The optimal glideslope is assumed to be 3.5 degrees leading to a touch down point between the second and third wire. +-- The height of the carrier deck and the exact wire locations are taken into account in the calculations. +-- +-- ## Pattern Waveoff +-- +-- The player's aircraft position is evaluated at certain critical locations in the landing pattern. If the player is far off from the ideal approach, the LSO will +-- issue a pattern wave off. Currently, this is only implemented for Case I recoveries and the Case I part in the Case II recovery, i.e. +-- +-- * Break Entry +-- * Early Break +-- * Late Break +-- * Abeam +-- * Ninety +-- * Wake +-- * Groove +-- +-- At these points it is also checked if a player comes too close to another aircraft ahead of him in the pattern. +-- +-- ## Grading Points +-- +-- Currently grades are given by as follows +-- +-- * 5.0 Points **\_OK\_**: "Okay underline", given only for a perfect pass, i.e. when no deviations at all were observed by the LSO. The unicorn! +-- * 4.0 Points **OK**: "Okay pass" when only minor () deviations happened. +-- * 3.0 Points **(OK)**: "Fair pass", when only "normal" deviations were detected. +-- * 2.0 Points **--**: "No grade", for larger deviations. +-- +-- Furthermore, we have the cases: +-- +-- * 2.5 Points **B**: "Bolder", when the player landed but did not catch a wire. +-- * 2.0 Points **WOP**: "Pattern Wave-Off", when pilot was far away from where he should be in the pattern. +-- * 2.0 Points **OWO**: "Own Wave-Off**, when pilot flies past the deck without touching it. +-- * 1.0 Points **WO**: "Technique Wave-Off": Player got waved off in the final parts of the groove. +-- * 1.0 Points **LIG**: "Long In the Groove", when pilot extents the downwind leg too far and screws up the timing for the following aircraft. +-- * 0.0 Points **CUT**: "Cut pass", when player was waved off but landed anyway. +-- +-- ## Foul Deck Waveoff +-- +-- A foul deck waveoff is called by the LSO if an aircraft is detected within the landing area when an approaching aircraft is at position IM-IC during Case I/II operations, +-- or with an aircraft approaching the 3/4 NM during Case III operations. +-- +-- The approaching aircraft will be notified via LSO radio comms and is supposed to overfly the landing area to enter the Bolter pattern. **This pass is not graded**. +-- +-- === +-- +-- # Scripting +-- +-- Writing a basic script is easy and can be done in two lines. +-- +-- local airbossStennis=AIRBOSS:New("USS Stennis", "Stennis") +-- airbossStennis:Start() +-- +-- The **first line** creates and AIRBOSS object via the @{#AIRBOSS.New}(*carriername*, *alias*) constructor. The first parameter *carriername* is name of the carrier unit as +-- defined in the mission editor. The second parameter *alias* is optional. This name will, e.g., be used for the F10 radio menu entry. If not given, the alias is identical +-- to the *carriername* of the first parameter. +-- +-- This simple script initializes a lot of parameters with default values: +-- +-- * TACAN channel is set to 74X, see @{#AIRBOSS.SetTACAN}, +-- * ICSL channel is set to 1, see @{#AIRBOSS.SetICLS}, +-- * LSO radio is set to 264 MHz FM, see @{#AIRBOSS.SetLSORadio}, +-- * Marshal radio is set to 305 MHz FM, see @{#AIRBOSS.SetMarshalRadio}, +-- * Default recovery case is set to 1, see @{#AIRBOSS.SetRecoveryCase}, +-- * Carrier Controlled Area (CCA) is set to 50 NM, see @{#AIRBOSS.SetCarrierControlledArea}, +-- * Default player skill "Flight Student" (easy), see @{#AIRBOSS.SetDefaultPlayerSkill}, +-- * Once the carrier reaches its final waypoint, it will restart its route, see @{#AIRBOSS.SetPatrolAdInfinitum}. +-- +-- The **second line** starts the AIRBOSS class. If you set options this should happen after the @{#AIRBOSS.New} and before @{#AIRBOSS.Start} command. +-- +-- However, good mission planning involves also planning when aircraft are supposed to be launched or recovered. The definition of *case specific* recovery ops within the same mission is described in +-- the next section. +-- +-- ## Recovery Windows +-- +-- Recovery of aircraft is only allowed during defined time slots. You can define these slots via the @{#AIRBOSS.AddRecoveryWindow}(*start*, *stop*, *case*, *holdingoffset*) function. +-- The parameters are: +-- +-- * *start*: The start time as a string. For example "8:00" for a window opening at 8 am. Or "13:30+1" for half past one on the next day. Default (nil) is ASAP. +-- * *stop*: Time when the window closes as a string. Same format as *start*. Default is 90 minutes after start time. +-- * *case*: The recovery case during that window (1, 2 or 3). Default 1. +-- * *holdingoffset*: Holding offset angle in degrees. Only for Case II or III recoveries. Default 0 deg. Common +-15 deg or +-30 deg. +-- +-- If recovery is closed, AI flights will be send to marshal stacks and orbit there until the next window opens. +-- Players can request marshal via the F10 menu and will also be given a marshal stack. Currently, human players can request commence via the F10 radio regardless of +-- whether a window is open or not and will be allowed to enter the pattern (if not already full). This will probably change in the future. +-- +-- At the moment there is no automatic recovery case set depending on weather or daytime. So it is the AIRBOSS (i.e. you as mission designer) who needs to make that decision. +-- It is probably a good idea to synchronize the timing with the waypoints of the carrier. For example, setting up the waypoints such that the carrier +-- already has turning into the wind, when a recovery window opens. +-- +-- The code for setting up multiple recovery windows could look like this +-- local airbossStennis=AIRBOSS:New("USS Stennis", "Stennis") +-- airbossStennis:AddRecoveryWindow("8:30", "9:30", 1) +-- airbossStennis:AddRecoveryWindow("12:00", "13:15", 2, 15) +-- airbossStennis:AddRecoveryWindow("23:30", "00:30+1", 3, -30) +-- airbossStennis:Start() +-- +-- This will open a Case I recovery window from 8:30 to 9:30. Then a Case II recovery from 12:00 to 13:15, where the holing offset is +15 degrees wrt BRC. +-- Finally, a Case III window opens 23:30 on the day the mission starts and closes 0:30 on the following day. The holding offset is -30 degrees wrt FB. +-- +-- Note that incoming flights will be assigned a holding pattern for the next opening window case if no window is open at the moment. So in the above example, +-- all flights incoming after 13:15 will be assigned to a Case III marshal stack. Therefore, you should make sure that no flights are incoming long before the +-- next window opens or adjust the recovery planning accordingly. +-- +-- The following example shows how you set up a recovery window for the next week: +-- +-- for i=0,7 do +-- airbossStennis:AddRecoveryWindow(string.format("08:05:00+%d", i), string.format("08:50:00+%d", i)) +-- end +-- +-- ### Turning into the Wind +-- +-- For each recovery window, you can define if the carrier should automatically turn into the wind. This is done by passing one or two additional arguments to the @{#AIRBOSS.AddRecoveryWindow} function: +-- +-- airbossStennis:AddRecoveryWindow("8:30", "9:30", 1, nil, true, 20) +-- +-- Setting the fifth parameter to *true* enables the automatic turning into the wind. The sixth parameter (here 20) specifies the speed in knots the carrier will go so that to total wind above the deck +-- corresponds to this wind speed. For example, if the is blowing with 5 knots, the carrier will go 15 knots so that the total velocity adds up to the specified 20 knots for the pilot. +-- +-- The carrier will steam into the wind for as long as the recovery window is open. The distance up to which possible collisions are detected can be set by the @{#AIRBOSS.SetCollisionDistance} function. +-- +-- However, the AIRBOSS scans the type of the surface up to 5 NM in the direction of movement of the carrier. If he detects anything but deep water, he will stop the current course and head back to +-- the point where he initially turned into the wind. +-- +-- The same holds true after the recovery window closes. The carrier will head back to the place where he left its assigned route and resume the path to the next waypoint defined in the mission editor. +-- +-- Note that the carrier will only head into the wind, if the wind direction is different by more than 5° from the current heading of the carrier (the angled runway, if any, fis taken into account here). +-- +-- === +-- +-- # Persistence of Player Results +-- +-- LSO grades of players can be saved to disk and later reloaded when a new mission is started. +-- +-- ## Prerequisites +-- +-- **Important** By default, DCS does not allow for writing data to files. Therefore, one first has to comment out the line "sanitizeModule('io')" and "sanitizeModule('lfs')", i.e. +-- +-- do +-- sanitizeModule('os') +-- --sanitizeModule('io') -- required for saving files +-- --sanitizeModule('lfs') -- optional for setting the default path to your "Saved Games\DCS" folder +-- require = nil +-- loadlib = nil +-- end +-- +-- in the file "MissionScripting.lua", which is located in the subdirectory "Scripts" of your DCS installation root directory. +-- +-- **WARNING** Desanitizing the "io" and "lfs" modules makes your machine or server vulnerable to attacks from the outside! Use this at your own risk. +-- +-- ## Save Results +-- +-- Saving asset data to file is achieved by the @{AIRBOSS.Save}(*path*, *filename*) function. +-- +-- The parameter *path* specifies the path on the file system where the +-- player grades are saved. If you do not specify a path, the file is saved your the DCS installation root directory if the **lfs** module is *not* desanizied or +-- your "Saved Games\\DCS" folder in case you did desanitize the **lfs** module. +-- +-- The parameter *filename* is optional and defines the name of the saved file. By default this is automatically created from the AIRBOSS carrier name/alias, i.e. +-- "Airboss-USS Stennis_LSOgrades.csv", if the alias is "USS Stennis". +-- +-- In the easiest case, you desanitize the **io** and **lfs** modules and just add the line +-- +-- airbossStennis:Save() +-- +-- If you want to specify an explicit path you can do this by +-- +-- airbossStennis:Save("D:\\My Airboss Data\\") +-- +-- This will save all player grades to in "D:\\My Airboss Data\\Airboss-USS Stennis_LSOgrades.csv". +-- +-- ### Automatic Saving +-- +-- The player grades can be saved automatically after each graded player pass via the @{AIRBOSS.SetAutoSave}(*path*, *filename*) function. Again the parameters *path* and *filename* are optional. +-- In the simplest case, you desanitize the **lfs** module and just add +-- +-- airbossStennis:SetAutoSave() +-- +-- Note that the the stats are saved after the *final* grade has been given, i.e. the player has landed on the carrier. After intermediate results such as bolters or waveoffs the stats are not automatically saved. +-- +-- In case you want to specify an explicit path, you can write +-- +-- airbossStennis:SetAutoSave("D:\\My Airboss Data\\") +-- +-- ## Results Output +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_PersistenceResultsTable.png) +-- +-- The results file is stored as comma separated file. The columns are +-- +-- * *Name*: The player name. +-- * *Pass*: A running number counting the passes of the player +-- * *Points Final*: The final points (i.e. when the player has landed). This is the average over all previous bolters or waveoffs, if any. +-- * *Points Pass*: The points of each pass including bolters and waveoffs. +-- * *Grade*: LSO grade. +-- * *Details*: Detailed analysis of deviations within the groove. +-- * *Wire*: Trapped wire, if any. +-- * *Tgroove*: Time in the groove in seconds (not applicable during Case III). +-- * *Case*: The recovery case operations in progress during the pass. +-- * *Wind*: Wind on deck in knots during approach. +-- * *Modex*: Tail number of the player. +-- * *Airframe*: Aircraft type used in the recovery. +-- * *Carrier Type*: Type name of the carrier. +-- * *Carrier Name*: Name/alias of the carrier. +-- * *Theatre*: DCS map. +-- * *Mission Time*: Mission time at the end of the approach. +-- * *Mission Date*: Mission date in yyyy/mm/dd format. +-- * *OS Date*: Real life date from os.date(). Needs **os** to be desanitized. +-- +-- ## Load Results +-- +-- Loading player grades from file is achieved by the @{AIRBOSS.Load}(*path*, *filename*) function. The parameter *path* specifies the path on the file system where the +-- data is loaded from. If you do not specify a path, the file is loaded from your the DCS installation root directory or, if **lfs** was desanitized from you "Saved Games\DCS" directory. +-- The parameter *filename* is optional and defines the name of the file to load. By default this is automatically generated from the AIBOSS carrier name/alias, for example +-- "Airboss-USS Stennis_LSOgrades.csv". +-- +-- Note that the AIRBOSS FSM **must not be started** in order to load the data. In other words, loading should happen **after** the +-- @{#AIRBOSS.New} command is specified in the code but **before** the @{#AIRBOSS.Start} command is given. +-- +-- The easiest was to load player results is +-- +-- airbossStennis:New("USS Stennis") +-- airbossStennis:Load() +-- airbossStennis:SetAutoSave() +-- -- Additional specification of parameters such as recovery windows etc, if required. +-- airbossStennis:Start() +-- +-- This sequence loads all available player grades from the default file and automatically saved them when a player received a (final) grade. Again, if **lfs** was desanitized, the files are save to and loaded +-- from the "Saved Games\DCS" directory. If **lfs** was *not* desanitized, the DCS root installation folder is the default path. +-- +-- # Trap Sheet +-- +-- Important aircraft attitude parameters during the Groove can be saved to file for later analysis. This also requires the **io** and optionally **lfs** modules to be desanitized. +-- +-- In the script you have to add the @{#AIRBOSS.SetTrapSheet}(*path*) function to activate this feature. +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_TrapSheetTable.png) +-- +-- Data the is written to a file in csv format and contains the following information: +-- +-- * *Time*: time in seconds since start. +-- * *Rho*: distance from rundown to player aircraft in NM. +-- * *X*: distance parallel to the carrier in meters. +-- * *Z*: distance perpendicular to the carrier in meters. +-- * *Alt*: altitude of player aircraft in feet. +-- * *AoA*: angle of attack in degrees. +-- * *GSE*: glideslope error in degrees. +-- * *LUE*: lineup error in degrees. +-- * *Vtot*: total velocity of player aircraft in knots. +-- * *Vy*: vertical (descent) velocity in ft/min. +-- * *Gamma*: angle between vector of aircraft nose and vector point in the direction of the carrier runway in degrees. +-- * *Pitch*: pitch angle of player aircraft in degrees. +-- * *Roll*: roll angle of player aircraft in degrees. +-- * *Yaw*: yaw angle of player aircraft in degrees. +-- * *Step*: Step in the groove. +-- * *Grade*: Current LSO grade. +-- * *Points*: Current points for the pass. +-- * *Details*: Detailed grading analysis. +-- +--## Lineup Error +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_TrapSheetLUE.png) +-- +-- The graph displays the lineup error (LUE) as a function of the distance to the carrier. +-- +-- The pilot approaches the carrier from the port side, LUE>0°, at a distance of ~1 NM. +-- At the beginning of the groove (X), he significantly overshoots to the starboard side (LUE<5°). +-- In the middle (IM), he performs good corrections and smoothly reduces the lineup error. +-- Finally, at a distance of ~0.3 NM (IC) he has corrected his lineup with the runway to a reasonable level, |LUE|<0.5°. +-- +-- ## Glideslope Error +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_TrapSheetGLE.png) +-- +-- The graph displays the glideslope error (GSE) as a function of the distance to the carrier. +-- +-- In this case the pilot already enters the groove (X) below the optimal glideslope. He is not able to correct his height in the IM part and +-- stays significantly too low. In close, he performs a harsh correction to gain altitude and ends up even slightly too high (GSE>0.5°). +-- At his point further corrections are necessary. +-- +-- ## Angle of Attack +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_TrapSheetAoA.png) +-- +-- The graph displays the angle of attack (AoA) as a function of the distance to the carrier. +-- +-- The pilot starts off being on speed after the ball call. Then he get way to fast troughout the most part of the groove. He manages to correct +-- this somewhat short before touchdown. +-- +-- === +-- +-- # Sound Files +-- +-- An important aspect of the AIRBOSS is that it uses voice overs for greater immersion. The necessary sound files can be obtained from the +-- MOOSE Discord in the [#ops-airboss](https://discordapp.com/channels/378590350614462464/527363141185830915) channel. Check out the **pinned messages**. +-- +-- However, including sound files into a new mission is tedious as these usually need to be included into the mission **miz** file via (unused) triggers. +-- +-- The default location inside the miz file is "l10n/DEFAULT/". But simply opening the *miz* file with e.g. [7-zip](https://www.7-zip.org/) and copying the files into that folder does not work. +-- The next time the mission is saved, files not included via trigger are automatically removed by DCS. +-- +-- However, if you create a new folder inside the miz file, which contains the sounds, it will not be deleted and can be used. The location of the sound files can be specified +-- via the @{#AIRBOSS.SetSoundfilesFolder}(*folderpath*) function. The parameter *folderpath* defines the location of the sound files folder within the mission *miz* file. +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_SoundfilesFolder.png) +-- +-- For example as +-- +-- airbossStennis:SetSoundfilesFolder("Airboss Soundfiles/") +-- +-- ## Carrier Specific Voice Overs +-- +-- It is possible to use different sound files for different carriers. If you have set up two (or more) AIRBOSS objects at different carriers - say Stennis and Tarawa - each +-- carrier would use the files in the specified directory, e.g. +-- +-- airbossStennis:SetSoundfilesFolder("Airboss Soundfiles Stennis/") +-- airbossTarawa:SetSoundfilesFolder("Airboss Soundfiles Tarawa/") +-- +-- ## Sound Packs +-- +-- The AIRBOSS currently has two different "sound packs" for LSO and three different "sound Packs" for Marshal radios. These contain voice overs by different actors. +-- These can be set by @{#AIRBOSS.SetVoiceOversLSOByRaynor}() and @{#AIRBOSS.SetVoiceOversMarshalByRaynor}(). These are the default settings. +-- The other sound files can be set by @{#AIRBOSS.SetVoiceOversLSOByFF}(), @{#AIRBOSS.SetVoiceOversMarshalByGabriella}() and @{#AIRBOSS.SetVoiceOversMarshalByFF}(). +-- Also combinations can be used, e.g. +-- +-- airbossStennis:SetVoiceOversLSOByFF() +-- airbossStennis:SetVoiceOversMarshalByRaynor() +-- +-- In this example LSO voice overs by FF and Marshal voice overs by Raynor are used. +-- +-- **Note** that this only initializes the correct parameters parameters of sound files, i.e. the duration. The correct files have to be in the directory set by the +-- @{#AIRBOSS.SetSoundfilesFolder}(*folder*) function. +-- +-- ## How To Use Your Own Voice Overs +-- +-- If you have a set of AIRBOSS sound files recorded or got it from elsewhere it is possible to use those instead of the default ones. +-- I recommend to use exactly the same file names as the original sound files have. +-- +-- However, the **timing is critical**! As sometimes sounds are played directly after one another, e.g. by saying the modex but also on other occations, the airboss +-- script has a radio queue implemented (actually two - one for the LSO and one for the Marshal/Airboss radio). +-- By this it is automatically taken care that played messages are not overlapping and played over each other. The disadvantage is, that the script needs to know +-- the exact duration of *each* voice over. For the default sounds this is hard coded in the source code. For your own files, you need to give that bit of information +-- to the script via the @{#AIRBOSS.SetVoiceOver}(**radiocall**, **duration**, **subtitle**, **subduration**, **filename**, **suffix**) function. Only the first two +-- parameters **radiocall** and **duration** are usually important to adjust here. +-- +-- For example, if you want to change the LSO "Call the Ball" and "Roger Ball" calls: +-- +-- airbossStennis:SetVoiceOver(airbossStennis.LSOCall.CALLTHEBALL, 0.6) +-- airbossStennis:SetVoiceOver(airbossStennis.LSOCall.ROGERBALL, 0.7) +-- +-- Again, changing the file name, subtitle, subtitle duration is not required if you name the file exactly like the original one, which is this case would be "LSO-RogerBall.ogg". +-- +-- +-- +-- ## The Radio Dilemma +-- +-- DCS offers two (actually three) ways to send radio messages. Each one has its advantages and disadvantages and it is important to understand the differences. +-- +-- ### Transmission via Command +-- +-- *In principle*, the best way to transmit messages is via the [TransmitMessage](https://wiki.hoggitworld.com/view/DCS_command_transmitMessage) command. +-- This method has the advantage that subtitles can be used and these subtitles are only displayed to the players who dialed in the same radio frequency as +-- used for the transmission. +-- However, this method unfortunately only works if the sending unit is an **aircraft**. Therefore, it is not usable by the AIRBOSS per se as the transmission comes from +-- a naval unit (i.e. the carrier). +-- +-- As a workaround, you can put an aircraft, e.g. a Helicopter on the deck of the carrier or another ship of the strike group. The aircraft should be set to +-- uncontrolled and maybe even to immortal. With the @{#AIRBOSS.SetRadioUnitName}(*unitname*) function you can use this unit as "radio repeater" for both Marshal and LSO +-- radio channels. However, this might lead to interruptions in the transmission if both channels transmit simultaniously. Therefore, it is better to assign a unit for +-- each radio via the @{#AIRBOSS.SetRadioRelayLSO}(unitname) and @{#AIRBOSS.SetRadioRelayMarshal}(unitname) functions. +-- +-- Of course you can also use any other aircraft in the vicinity of the carrier, e.g. a rescue helo or a recovery tanker. It is just important that this +-- unit is and stays close the the boat as the distance from the sender to the receiver is modeled in DCS. So messages from too far away might not reach the players. +-- +-- **Note** that not all radio messages the airboss sends have voice overs. Therefore, if you use a radio relay unit, users should *not* disable the +-- subtitles in the DCS game menu. +-- +-- ### Transmission via Trigger +-- +-- Another way to broadcast messages is via the [radio transmission trigger](https://wiki.hoggitworld.com/view/DCS_func_radioTransmission). This method can be used for all +-- units (land, air, naval). However, messages cannot be subtitled. Therefore, subtitles are displayed to the players via normal textout messages. +-- The disadvantage is that is is impossible to know which players have the right radio frequencies dialed in. Therefore, subtitles of the Marshal radio calls are displayed to all players +-- inside the CCA. Subtitles on the LSO radio frequency are displayed to all players in the pattern. +-- +-- ### Sound to User +-- +-- The third way to play sounds to the user via the [outsound trigger](https://wiki.hoggitworld.com/view/DCS_func_outSound). +-- These sounds are not coming from a radio station and therefore can be heard by players independent of their actual radio frequency setting. +-- The AIRBOSS class uses this method to play sounds to players which are of a more "private" nature - for example when a player has left his assigned altitude +-- in the Marshal stack. Often this is the modex of the player in combination with a textout messaged displayed on screen. +-- +-- If you want to use this method for all radio messages you can enable it via the @{#AIRBOSS.SetUserSoundRadio}() function. This is the analogue of activating easy comms in DCS. +-- +-- Note that this method is used for all players who are in the A-4E community mod as this mod does not have the ability to use radios due to current DCS restrictions. +-- Therefore, A-4E drivers will hear all radio transmissions from the Marshal/Airboss and all LSO messages as soon as their commence the pattern. +-- +-- === +-- +-- # AI Handling +-- +-- The @{#AIRBOSS} class allows to handle incoming AI units and integrate them into the marshal and landing pattern. +-- +-- By default, incoming carrier capable aircraft which are detecting inside the Carrier Controlled Area (CCA) and approach the carrier by more than 5 NM are automatically guided to the holding zone. +-- Each AI group gets its own marshal stack in the holding pattern. Once a recovery window opens, the AI group of the lowest stack is transitioning to the landing pattern +-- and the Marshal stack collapses. +-- +-- If no AI handling is desired, this can be turned off via the @{#AIRBOSS.SetHandleAIOFF} function. +-- +-- In case only specifc AI groups shall be excluded, it can be done by adding the groups to a set, e.g. +-- +-- -- AI groups explicitly excluded from handling by the Airboss +-- local CarrierExcludeSet=SET_GROUP:New():FilterPrefixes("E-2D Wizard Group"):FilterStart() +-- AirbossStennis:SetExcludeAI(CarrierExcludeSet) +-- +-- Similarly, to the @{#AIRBOSS.SetExcludeAI} function, AI groups can be explicitly *included* via the @{#AIRBOSS.SetSquadronAI} function. If this is used, only the *included* groups are handled +-- by the AIRBOSS. +-- +-- ## Keep the Deck Clean +-- +-- Once the AI groups have landed on the carrier, they can be despawned automatically after they shut down their engines. This is achieved by the @{#AIRBOSS.SetDespawnOnEngineShutdown}() function. +-- +-- ## Refueling +-- +-- AI groups in the marshal pattern can be send to refuel at the recovery tanker or if none is defined to the nearest divert airfield. This can be enabled by the @{#AIRBOSS.SetRefuelAI}(*lowfuelthreshold*). +-- The parameter *lowfuelthreshold* is the threshold of fuel in percent. If the fuel drops below this value, the group will go for refueling. If refueling is performed at the recovery tanker, +-- the group will return to the marshal stack when done. The aircraft will not return from the divert airfield however. +-- +-- Note that this feature is not enabled by default as there might be bugs in DCS that prevent a smooth refueling of the AI. Enable at your own risk. +-- +-- ## Respawning - DCS Landing Bug +-- +-- AI groups that enter the CCA are usually guided to Marshal stack. However, due to DCS limitations they might not obey the landing task if they have another airfield as departure and/or destination in +-- their mission task. Therefore, AI groups can be respawned when detected in the CCA. This should clear all other airfields and allow the aircraft to land on the carrier. +-- This is achieved by the @{AIRBOSS.SetRespawnAI}() function. +-- +-- ## Known Issues +-- +-- Dealing with the DCS AI is a big challenge and there is only so much one can do. Please bear this in mind! +-- +-- ### Pattern Updates +-- +-- The holding position of the AI is updated regularly when the carrier has changed its position by more then 2.5 NM or changed its course significantly. +-- The patterns are realized by orbit or racetrack patterns of the DCS scripting API. +-- However, when the position is updated or the marshal stack collapses, it comes to disruptions of the regular orbit because a new waypoint with a new +-- orbit task needs to be created. +-- +-- ### Recovery Cases +-- +-- The AI performs a very realistic Case I recovery. Therefore, we already have a good Case I and II recovery simulation since the final part of Case II is a +-- Case I recovery. However, I don't think the AI can do a proper Case III recovery. If you give the AI the landing command, it is out of our hands and will +-- always go for a Case I in the final pattern part. Maybe this will improve in future DCS version but right now, there is not much we can do about it. +-- +-- === +-- +-- # Finite State Machine (FSM) +-- +-- The AIRBOSS class has a Finite State Machine (FSM) implementation for the carrier. This allows mission designers to hook into certain events and helps +-- simulate complex behaviour easier. +-- +-- FSM events are: +-- +-- * @{#AIRBOSS.Start}: Starts the AIRBOSS FSM. +-- * @{#AIRBOSS.Stop}: Stops the AIRBOSS FSM. +-- * @{#AIRBOSS.Idle}: Carrier is set to idle and not recovering. +-- * @{#AIRBOSS.RecoveryStart}: Starts the recovery ops. +-- * @{#AIRBOSS.RecoveryStop}: Stops the recovery ops. +-- * @{#AIRBOSS.RecoveryPause}: Pauses the recovery ops. +-- * @{#AIRBOSS.RecoveryUnpause}: Unpauses the recovery ops. +-- * @{#AIRBOSS.RecoveryCase}: Sets/switches the recovery case. +-- * @{#AIRBOSS.PassingWaypoint}: Carrier passes a waypoint defined in the mission editor. +-- +-- These events can be used in the user script. When the event is triggered, it is automatically a function OnAfter*Eventname* called. For example +-- +-- --- Carrier just passed waypoint *n*. +-- function AirbossStennis:OnAfterPassingWaypoint(From, Event, To, n) +-- -- Launch green flare. +-- self.carrier:FlareGreen() +-- end +-- +-- In this example, we only launch a green flare every time the carrier passes a waypoint defined in the mission editor. But, of course, you can also use it to add new +-- recovery windows each time a carrier passes a waypoint. Therefore, you can create an "infinite" number of windows easily. +-- +-- === +-- +-- # Examples +-- +-- In this section a few simple examples are given to illustrate the scripting part. +-- +-- ## Simple Case +-- +-- -- Create AIRBOSS object. +-- local AirbossStennis=AIRBOSS:New("USS Stennis") +-- +-- -- Add recovery windows: +-- -- Case I from 9 to 10 am. Carrier will turn into the wind 5 min before window opens and go at a speed so that wind over the deck is 25 knots. +-- local window1=AirbossStennis:AddRecoveryWindow("9:00", "10:00", 1, nil, true, 25) +-- -- Case II with +15 degrees holding offset from 15:00 for 60 min. +-- local window2=AirbossStennis:AddRecoveryWindow("15:00", "16:00", 2, 15) +-- -- Case III with +30 degrees holding offset from 21:00 to 23:30. +-- local window3=AirbossStennis:AddRecoveryWindow("21:00", "23:30", 3, 30) +-- +-- -- Load all saved player grades from your "Saved Games\DCS" folder (if lfs was desanitized). +-- AirbossStennis:Load() +-- +-- -- Automatically save player results to your "Saved Games\DCS" folder each time a player get a final grade from the LSO. +-- AirbossStennis:SetAutoSave() +-- +-- -- Start airboss class. +-- AirbossStennis:Start() +-- +-- === +-- +-- # Debugging +-- +-- In case you have problems, it is always a good idea to have a look at your DCS log file. You find it in your "Saved Games" folder, so for example in +-- C:\Users\\Saved Games\DCS\Logs\dcs.log +-- All output concerning the @{#AIRBOSS} class should have the string "AIRBOSS" in the corresponding line. +-- Searching for lines that contain the string "error" or "nil" can also give you a hint what's wrong. +-- +-- The verbosity of the output can be increased by adding the following lines to your script: +-- +-- BASE:TraceOnOff(true) +-- BASE:TraceLevel(1) +-- BASE:TraceClass("AIRBOSS") +-- +-- To get even more output you can increase the trace level to 2 or even 3, c.f. @{Core.Base#BASE} for more details. +-- +-- ### Debug Mode +-- +-- You have the option to enable the debug mode for this class via the @{#AIRBOSS.SetDebugModeON} function. +-- If enabled, status and debug text messages will be displayed on the screen. Also informative marks on the F10 map are created. +-- +-- @field #AIRBOSS +AIRBOSS = { + ClassName = "AIRBOSS", + Debug = false, + lid = nil, + theatre = nil, + carrier = nil, + carriertype = nil, + carrierparam = {}, + alias = nil, + airbase = nil, + waypoints = {}, + currentwp = nil, + beacon = nil, + TACANon = nil, + TACANchannel = nil, + TACANmode = nil, + TACANmorse = nil, + ICLSon = nil, + ICLSchannel = nil, + ICLSmorse = nil, + LSORadio = nil, + LSOFreq = nil, + LSOModu = nil, + MarshalRadio = nil, + MarshalFreq = nil, + MarshalModu = nil, + TowerFreq = nil, + radiotimer = nil, + zoneCCA = nil, + zoneCCZ = nil, + players = {}, + menuadded = {}, + BreakEntry = {}, + BreakEarly = {}, + BreakLate = {}, + Abeam = {}, + Ninety = {}, + Wake = {}, + Final = {}, + Groove = {}, + Platform = {}, + DirtyUp = {}, + Bullseye = {}, + defaultcase = nil, + case = nil, + defaultoffset = nil, + holdingoffset = nil, + recoverytimes = {}, + flights = {}, + Qpattern = {}, + Qmarshal = {}, + Qwaiting = {}, + Qspinning = {}, + RQMarshal = {}, + RQLSO = {}, + TQMarshal = 0, + TQLSO = 0, + Nmaxpattern = nil, + Nmaxmarshal = nil, + NmaxSection = nil, + NmaxStack = nil, + handleai = nil, + tanker = nil, + Corientation = nil, + Corientlast = nil, + Cposition = nil, + defaultskill = nil, + adinfinitum = nil, + magvar = nil, + Tcollapse = nil, + recoverywindow = nil, + usersoundradio = nil, + Tqueue = nil, + dTqueue = nil, + dTstatus = nil, + menumarkzones = nil, + menusmokezones = nil, + playerscores = nil, + autosave = nil, + autosavefile = nil, + autosavepath = nil, + marshalradius = nil, + airbossnice = nil, + staticweather = nil, + windowcount = 0, + LSOdT = nil, + senderac = nil, + radiorelayLSO = nil, + radiorelayMSH = nil, + turnintowind = nil, + detour = nil, + squadsetAI = nil, + excludesetAI = nil, + menusingle = nil, + collisiondist = nil, + holdtimestamp = nil, + Tmessage = nil, + soundfolder = nil, + soundfolderLSO = nil, + soundfolderMSH = nil, + despawnshutdown= nil, + dTbeacon = nil, + Tbeacon = nil, + LSOCall = nil, + MarshalCall = nil, + lowfuelAI = nil, + emergency = nil, + respawnAI = nil, + gle = {}, + lue = {}, + trapsheet = nil, + trappath = nil, + trapprefix = nil, + initialmaxalt = nil, + welcome = nil, + skipperMenu = nil, + skipperSpeed = nil, + skipperTime = nil, + skipperOffset = nil, + skipperUturn = nil, +} + +--- Aircraft types capable of landing on carrier (human+AI). +-- @type AIRBOSS.AircraftCarrier +-- @field #string AV8B AV-8B Night Harrier. Works only with the USS Tarawa, USS America and Juan Carlos I. +-- @field #string A4EC A-4E Community mod. +-- @field #string HORNET F/A-18C Lot 20 Hornet by Eagle Dynamics. +-- @field #string F14A F-14A by Heatblur. +-- @field #string F14B F-14B by Heatblur. +-- @field #string F14A_AI F-14A Tomcat (AI). +-- @field #string FA18C F/A-18C Hornet (AI). +-- @field #string S3B Lockheed S-3B Viking. +-- @field #string S3BTANKER Lockheed S-3B Viking tanker. +-- @field #string E2D Grumman E-2D Hawkeye AWACS. +-- @field #string C2A Grumman C-2A Greyhound from Military Aircraft Mod. +-- @field #string T45C T-45C by VNAO +AIRBOSS.AircraftCarrier={ + AV8B="AV8BNA", + HORNET="FA-18C_hornet", + A4EC="A-4E-C", + F14A="F-14A-135-GR", + F14B="F-14B", + F14A_AI="F-14A", + FA18C="F/A-18C", + T45C="T-45", + S3B="S-3B", + S3BTANKER="S-3B Tanker", + E2D="E-2C", + C2A="C2A_Greyhound", +} + +--- Carrier types. +-- @type AIRBOSS.CarrierType +-- @field #string ROOSEVELT USS Theodore Roosevelt (CVN-71) [Super Carrier Module] +-- @field #string LINCOLN USS Abraham Lincoln (CVN-72) [Super Carrier Module] +-- @field #string WASHINGTON USS George Washington (CVN-73) [Super Carrier Module] +-- @field #string STENNIS USS John C. Stennis (CVN-74) +-- @field #string TRUMAN USS Harry S. Truman (CVN-75) [Super Carrier Module] +-- @field #string FORRESTAL USS Forrestal (CV-59) [Heatblur Carrier Module] +-- @field #string VINSON USS Carl Vinson (CVN-70) [Obsolete] +-- @field #string TARAWA USS Tarawa (LHA-1) +-- @field #string AMERICA USS America (LHA-6) +-- @field #string JCARLOS Juan Carlos I (L61) +-- @field #string KUZNETSOV Admiral Kuznetsov (CV 1143.5) +AIRBOSS.CarrierType={ + ROOSEVELT="CVN_71", + LINCOLN="CVN_72", + WASHINGTON="CVN_73", + TRUMAN="CVN_75", + STENNIS="Stennis", + FORRESTAL="Forrestal", + VINSON="VINSON", + TARAWA="LHA_Tarawa", + AMERICA="USS America LHA-6", + JCARLOS="L61", + KUZNETSOV="KUZNECOW", +} + +--- Carrier specific parameters. +-- @type AIRBOSS.CarrierParameters +-- @field #number rwyangle Runway angle in degrees. for carriers with angled deck. For USS Stennis -9 degrees. +-- @field #number sterndist Distance in meters from carrier position to stern of carrier. For USS Stennis -150 meters. +-- @field #number deckheight Height of deck in meters. For USS Stennis ~63 ft = 19 meters. +-- @field #number wire1 Distance in meters from carrier position to first wire. +-- @field #number wire2 Distance in meters from carrier position to second wire. +-- @field #number wire3 Distance in meters from carrier position to third wire. +-- @field #number wire4 Distance in meters from carrier position to fourth wire. +-- @field #number rwylength Length of the landing runway in meters. +-- @field #number rwywidth Width of the landing runway in meters. +-- @field #number totlength Total length of carrier. +-- @field #number totwidthstarboard Total with of the carrier from stern position to starboard side (asymmetric carriers). +-- @field #number totwidthport Total with of the carrier from stern position to port side (asymmetric carriers). + +--- Aircraft specific Angle of Attack (AoA) (or alpha) parameters. +-- @type AIRBOSS.AircraftAoA +-- @field #number OnSpeedMin Minimum on speed AoA. Values below are fast +-- @field #number OnSpeedMax Maximum on speed AoA. Values above are slow. +-- @field #number OnSpeed Optimal on-speed AoA. +-- @field #number Fast Fast AoA threshold. Smaller means faster. +-- @field #number Slow Slow AoA threshold. Larger means slower. +-- @field #number FAST Really fast AoA threshold. +-- @field #number SLOW Really slow AoA threshold. + +--- Glideslope error thresholds in degrees. +-- @type AIRBOSS.GLE +-- @field #number _max Max _OK_ value. Default 0.4 deg. +-- @field #number _min Min _OK_ value. Default -0.3 deg. +-- @field #number High (H) threshold. Default 0.8 deg. +-- @field #number Low (L) threshold. Default -0.6 deg. +-- @field #number HIGH H threshold. Default 1.5 deg. +-- @field #number LOW L threshold. Default -0.9 deg. + +--- Lineup error thresholds in degrees. +-- @type AIRBOSS.LUE +-- @field #number _max Max _OK_ value. Default 0.5 deg. +-- @field #number _min Min _OK_ value. Default -0.5 deg. +-- @field #number Left (LUR) threshold. Default -1.0 deg. +-- @field #number Right (LUL) threshold. Default 1.0 deg. +-- @field #number LeftMed threshold for AA/OS measuring. Default -2.0 deg. +-- @field #number RightMed threshold for AA/OS measuring. Default 2.0 deg. +-- @field #number LEFT LUR threshold. Default -3.0 deg. +-- @field #number RIGHT LUL threshold. Default 3.0 deg. + + +--- Pattern steps. +-- @type AIRBOSS.PatternStep +-- @field #string UNDEFINED "Undefined". +-- @field #string REFUELING "Refueling". +-- @field #string SPINNING "Spinning". +-- @field #string COMMENCING "Commencing". +-- @field #string HOLDING "Holding". +-- @field #string WAITING "Waiting for free Marshal stack". +-- @field #string PLATFORM "Platform". +-- @field #string ARCIN "Arc Turn In". +-- @field #string ARCOUT "Arc Turn Out". +-- @field #string DIRTYUP "Dirty Up". +-- @field #string BULLSEYE "Bullseye". +-- @field #string INITIAL "Initial". +-- @field #string BREAKENTRY "Break Entry". +-- @field #string EARLYBREAK "Early Break". +-- @field #string LATEBREAK "Late Break". +-- @field #string ABEAM "Abeam". +-- @field #string NINETY "Ninety". +-- @field #string WAKE "Wake". +-- @field #string FINAL "Final". +-- @field #string GROOVE_XX "Groove X". +-- @field #string GROOVE_IM "Groove In the Middle". +-- @field #string GROOVE_IC "Groove In Close". +-- @field #string GROOVE_AR "Groove At the Ramp". +-- @field #string GROOVE_AL "Groove Abeam Landing Spot". +-- @field #string GROOVE_LC "Groove Level Cross". +-- @field #string GROOVE_IW "Groove In the Wires". +-- @field #string BOLTER "Bolter Pattern". +-- @field #string EMERGENCY "Emergency Landing". +-- @field #string DEBRIEF "Debrief". +AIRBOSS.PatternStep={ + UNDEFINED="Undefined", + REFUELING="Refueling", + SPINNING="Spinning", + COMMENCING="Commencing", + HOLDING="Holding", + WAITING="Waiting for free Marshal stack", + PLATFORM="Platform", + ARCIN="Arc Turn In", + ARCOUT="Arc Turn Out", + DIRTYUP="Dirty Up", + BULLSEYE="Bullseye", + INITIAL="Initial", + BREAKENTRY="Break Entry", + EARLYBREAK="Early Break", + LATEBREAK="Late Break", + ABEAM="Abeam", + NINETY="Ninety", + WAKE="Wake", + FINAL="Turn Final", + GROOVE_XX="Groove X", + GROOVE_IM="Groove In the Middle", + GROOVE_IC="Groove In Close", + GROOVE_AR="Groove At the Ramp", + GROOVE_IW="Groove In the Wires", + GROOVE_AL="Groove Abeam Landing Spot", + GROOVE_LC="Groove Level Cross", + BOLTER="Bolter Pattern", + EMERGENCY="Emergency Landing", + DEBRIEF="Debrief", +} + +--- Groove position. +-- @type AIRBOSS.GroovePos +-- @field #string X0 "X0": Entering the groove. +-- @field #string XX "XX": At the start, i.e. 3/4 from the run down. +-- @field #string IM "IM": In the middle. +-- @field #string IC "IC": In close. +-- @field #string AR "AR": At the ramp. +-- @field #string AL "AL": Abeam landing position (V/STOL). +-- @field #string LC "LC": Level crossing (V/STOL). +-- @field #string IW "IW": In the wires. +AIRBOSS.GroovePos={ + X0="X0", + XX="XX", + IM="IM", + IC="IC", + AR="AR", + AL="AL", + LC="LC", + IW="IW", +} + +--- Radio. +-- @type AIRBOSS.Radio +-- @field #number frequency Frequency in Hz. +-- @field #number modulation Band modulation. +-- @field #string alias Radio alias. + +--- Radio sound file and subtitle. +-- @type AIRBOSS.RadioCall +-- @field #string file Sound file name without suffix. +-- @field #string suffix File suffix/extension, e.g. "ogg". +-- @field #boolean loud Loud version of sound file available. +-- @field #string subtitle Subtitle displayed during transmission. +-- @field #number duration Duration of the sound in seconds. This is also the duration the subtitle is displayed. +-- @field #number subduration Duration in seconds the subtitle is displayed. +-- @field #string modexsender Onboard number of the sender (optional). +-- @field #string modexreceiver Onboard number of the receiver (optional). +-- @field #string sender Sender of the message (optional). Default radio alias. + +--- Pilot radio calls. +-- type AIRBOSS.PilotCalls +-- @field #AIRBOSS.RadioCall N0 "Zero" call. +-- @field #AIRBOSS.RadioCall N1 "One" call. +-- @field #AIRBOSS.RadioCall N2 "Two" call. +-- @field #AIRBOSS.RadioCall N3 "Three" call. +-- @field #AIRBOSS.RadioCall N4 "Four" call. +-- @field #AIRBOSS.RadioCall N5 "Five" call. +-- @field #AIRBOSS.RadioCall N6 "Six" call. +-- @field #AIRBOSS.RadioCall N7 "Seven" call. +-- @field #AIRBOSS.RadioCall N8 "Eight" call. +-- @field #AIRBOSS.RadioCall N9 "Nine" call. +-- @field #AIRBOSS.RadioCall POINT "Point" call. +-- @field #AIRBOSS.RadioCall BALL "Ball" call. +-- @field #AIRBOSS.RadioCall HARRIER "Harrier" call. +-- @field #AIRBOSS.RadioCall HAWKEYE "Hawkeye" call. +-- @field #AIRBOSS.RadioCall HORNET "Hornet" call. +-- @field #AIRBOSS.RadioCall SKYHAWK "Skyhawk" call. +-- @field #AIRBOSS.RadioCall TOMCAT "Tomcat" call. +-- @field #AIRBOSS.RadioCall VIKING "Viking" call. +-- @field #AIRBOSS.RadioCall BINGOFUEL "Bingo Fuel" call. +-- @field #AIRBOSS.RadioCall GASATDIVERT "Going for gas at the divert field" call. +-- @field #AIRBOSS.RadioCall GASATTANKER "Going for gas at the recovery tanker" call. + +--- LSO radio calls. +-- @type AIRBOSS.LSOCalls +-- @field #AIRBOSS.RadioCall BOLTER "Bolter, Bolter" call. +-- @field #AIRBOSS.RadioCall CALLTHEBALL "Call the Ball" call. +-- @field #AIRBOSS.RadioCall CHECK "CHECK" call. +-- @field #AIRBOSS.RadioCall CLEAREDTOLAND "Cleared to land" call. +-- @field #AIRBOSS.RadioCall COMELEFT "Come left" call. +-- @field #AIRBOSS.RadioCall DEPARTANDREENTER "Depart and re-enter" call. +-- @field #AIRBOSS.RadioCall EXPECTHEAVYWAVEOFF "Expect heavy wavoff" call. +-- @field #AIRBOSS.RadioCall EXPECTSPOT75 "Expect spot 7.5" call. +-- @field #AIRBOSS.RadioCall EXPECTSPOT5 "Expect spot 5" call. +-- @field #AIRBOSS.RadioCall FAST "You're fast" call. +-- @field #AIRBOSS.RadioCall FOULDECK "Foul Deck" call. +-- @field #AIRBOSS.RadioCall HIGH "You're high" call. +-- @field #AIRBOSS.RadioCall IDLE "Idle" call. +-- @field #AIRBOSS.RadioCall LONGINGROOVE "You're long in the groove" call. +-- @field #AIRBOSS.RadioCall LOW "You're low" call. +-- @field #AIRBOSS.RadioCall N0 "Zero" call. +-- @field #AIRBOSS.RadioCall N1 "One" call. +-- @field #AIRBOSS.RadioCall N2 "Two" call. +-- @field #AIRBOSS.RadioCall N3 "Three" call. +-- @field #AIRBOSS.RadioCall N4 "Four" call. +-- @field #AIRBOSS.RadioCall N5 "Five" call. +-- @field #AIRBOSS.RadioCall N6 "Six" call. +-- @field #AIRBOSS.RadioCall N7 "Seven" call. +-- @field #AIRBOSS.RadioCall N8 "Eight" call. +-- @field #AIRBOSS.RadioCall N9 "Nine" call. +-- @field #AIRBOSS.RadioCall PADDLESCONTACT "Paddles, contact" call. +-- @field #AIRBOSS.RadioCall POWER "Power" call. +-- @field #AIRBOSS.RadioCall RADIOCHECK "Paddles, radio check" call. +-- @field #AIRBOSS.RadioCall RIGHTFORLINEUP "Right for line up" call. +-- @field #AIRBOSS.RadioCall ROGERBALL "Roger ball" call. +-- @field #AIRBOSS.RadioCall SLOW "You're slow" call. +-- @field #AIRBOSS.RadioCall STABILIZED "Stabilized" call. +-- @field #AIRBOSS.RadioCall WAVEOFF "Wave off" call. +-- @field #AIRBOSS.RadioCall WELCOMEABOARD "Welcome aboard" call. +-- @field #AIRBOSS.RadioCall CLICK Radio end transmission click sound. +-- @field #AIRBOSS.RadioCall NOISE Static noise sound. +-- @field #AIRBOSS.RadioCall SPINIT "Spin it" call. + +--- Marshal radio calls. +-- @type AIRBOSS.MarshalCalls +-- @field #AIRBOSS.RadioCall AFFIRMATIVE "Affirmative" call. +-- @field #AIRBOSS.RadioCall ALTIMETER "Altimeter" call. +-- @field #AIRBOSS.RadioCall BRC "BRC" call. +-- @field #AIRBOSS.RadioCall CARRIERTURNTOHEADING "Turn to heading" call. +-- @field #AIRBOSS.RadioCall CASE "Case" call. +-- @field #AIRBOSS.RadioCall CHARLIETIME "Charlie Time" call. +-- @field #AIRBOSS.RadioCall CLEAREDFORRECOVERY "You're cleared for case" call. +-- @field #AIRBOSS.RadioCall DECKCLOSED "Deck closed" sound. +-- @field #AIRBOSS.RadioCall DEGREES "Degrees" call. +-- @field #AIRBOSS.RadioCall EXPECTED "Expected" call. +-- @field #AIRBOSS.RadioCall FLYNEEDLES "Fly your needles" call. +-- @field #AIRBOSS.RadioCall HOLDATANGELS "Hold at angels" call. +-- @field #AIRBOSS.RadioCall HOURS "Hours" sound. +-- @field #AIRBOSS.RadioCall MARSHALRADIAL "Marshal radial" call. +-- @field #AIRBOSS.RadioCall N0 "Zero" call. +-- @field #AIRBOSS.RadioCall N1 "One" call. +-- @field #AIRBOSS.RadioCall N2 "Two" call. +-- @field #AIRBOSS.RadioCall N3 "Three" call. +-- @field #AIRBOSS.RadioCall N4 "Four" call. +-- @field #AIRBOSS.RadioCall N5 "Five" call. +-- @field #AIRBOSS.RadioCall N6 "Six" call. +-- @field #AIRBOSS.RadioCall N7 "Seven" call. +-- @field #AIRBOSS.RadioCall N8 "Eight" call. +-- @field #AIRBOSS.RadioCall N9 "Nine" call. +-- @field #AIRBOSS.RadioCall NEGATIVE "Negative" sound. +-- @field #AIRBOSS.RadioCall NEWFB "New final bearing" call. +-- @field #AIRBOSS.RadioCall OBS "Obs" call. +-- @field #AIRBOSS.RadioCall POINT "Point" call. +-- @field #AIRBOSS.RadioCall RADIOCHECK "Radio check" call. +-- @field #AIRBOSS.RadioCall RECOVERY "Recovery" call. +-- @field #AIRBOSS.RadioCall RECOVERYOPSSTOPPED "Recovery ops stopped" sound. +-- @field #AIRBOSS.RadioCall RECOVERYPAUSEDNOTICE "Recovery paused until further notice" call. +-- @field #AIRBOSS.RadioCall RECOVERYPAUSEDRESUMED "Recovery paused and will be resumed at" call. +-- @field #AIRBOSS.RadioCall RESUMERECOVERY "Resuming aircraft recovery" call. +-- @field #AIRBOSS.RadioCall REPORTSEEME "Report see me" call. +-- @field #AIRBOSS.RadioCall ROGER "Roger" call. +-- @field #AIRBOSS.RadioCall SAYNEEDLES "Say needles" call. +-- @field #AIRBOSS.RadioCall STACKFULL "Marshal stack is currently full. Hold outside 10 NM zone and wait for further instructions" call. +-- @field #AIRBOSS.RadioCall STARTINGRECOVERY "Starting aircraft recovery" call. +-- @field #AIRBOSS.RadioCall CLICK Radio end transmission click sound. +-- @field #AIRBOSS.RadioCall NOISE Static noise sound. + + +--- Difficulty level. +-- @type AIRBOSS.Difficulty +-- @field #string EASY Flight Student. Shows tips and hints in important phases of the approach. +-- @field #string NORMAL Naval aviator. Moderate number of hints but not really zip lip. +-- @field #string HARD TOPGUN graduate. For people who know what they are doing. Nearly *ziplip*. +AIRBOSS.Difficulty={ + EASY="Flight Student", + NORMAL="Naval Aviator", + HARD="TOPGUN Graduate", +} + +--- Recovery window parameters. +-- @type AIRBOSS.Recovery +-- @field #number START Start of recovery in seconds of abs mission time. +-- @field #number STOP End of recovery in seconds of abs mission time. +-- @field #number CASE Recovery case (1-3) of that time slot. +-- @field #number OFFSET Angle offset of the holding pattern in degrees. Usually 0, +-15, or +-30 degrees. +-- @field #boolean OPEN Recovery window is currently open. +-- @field #boolean OVER Recovery window is over and closed. +-- @field #boolean WIND Carrier will turn into the wind. +-- @field #number SPEED The speed in knots the carrier has during the recovery. +-- @field #boolean UTURN If true, carrier makes a U-turn to the point it came from before resuming its route to the next waypoint. +-- @field #number ID Recovery window ID. + +--- Groove data. +-- @type AIRBOSS.GrooveData +-- @field #number Step Current step. +-- @field #number Time Time in seconds. +-- @field #number Rho Distance in meters. +-- @field #number X Distance in meters. +-- @field #number Z Distance in meters. +-- @field #number AoA Angle of Attack. +-- @field #number Alt Altitude in meters. +-- @field #number GSE Glideslope error in degrees. +-- @field #number LUE Lineup error in degrees. +-- @field #number Pitch Pitch angle in degrees. +-- @field #number Roll Roll angle in degrees. +-- @field #number Yaw Yaw angle in degrees. +-- @field #number Vel Total velocity in m/s. +-- @field #number Vy Vertical velocity in m/s. +-- @field #number Gamma Relative heading player to carrier's runway. 0=parallel, +-90=perpendicular. +-- @field #string Grade LSO grade. +-- @field #number GradePoints LSO grade points +-- @field #string GradeDetail LSO grade details. +-- @field #string FlyThrough Fly through up "/" or fly through down "\\". + +--- LSO grade data. +-- @type AIRBOSS.LSOgrade +-- @field #string grade LSO grade, i.e. _OK_, OK, (OK), --, CUT +-- @field #number points Points received. +-- @field #number finalscore Points received after player has finally landed. This is the average over all incomplete passes (bolter, waveoff) before. +-- @field #string details Detailed flight analysis. +-- @field #number wire Wire caught. +-- @field #number Tgroove Time in the groove in seconds. +-- @field #number case Recovery case. +-- @field #string wind Wind speed on deck in knots. +-- @field #string modex Onboard number. +-- @field #string airframe Aircraft type name of player. +-- @field #string carriertype Carrier type name. +-- @field #string carriername Carrier name/alias. +-- @field #string theatre DCS map. +-- @field #string mitime Mission time in hh:mm:ss+d format +-- @field #string midate Mission date in yyyy/mm/dd format. +-- @field #string osdate Real live date. Needs **os** to be desanitized. + +--- Checkpoint parameters triggering the next step in the pattern. +-- @type AIRBOSS.Checkpoint +-- @field #string name Name of checkpoint. +-- @field #number Xmin Minimum allowed longitual distance to carrier. +-- @field #number Xmax Maximum allowed longitual distance to carrier. +-- @field #number Zmin Minimum allowed latitudal distance to carrier. +-- @field #number Zmax Maximum allowed latitudal distance to carrier. +-- @field #number LimitXmin Latitudal threshold for triggering the next step if XXmax. +-- @field #number LimitZmin Latitudal threshold for triggering the next step if ZZmax. + +--- Parameters of a flight group. +-- @type AIRBOSS.FlightGroup +-- @field Wrapper.Group#GROUP group Flight group. +-- @field #string groupname Name of the group. +-- @field #number nunits Number of units in group. +-- @field #number dist0 Distance to carrier in meters when the group was first detected inside the CCA. +-- @field #number time Timestamp in seconds of timer.getAbsTime() of the last important event, e.g. added to the queue. +-- @field #number flag Flag value describing the current stack. +-- @field #boolean ai If true, flight is purly AI. +-- @field #string actype Aircraft type name. +-- @field #table onboardnumbers Onboard numbers of aircraft in the group. +-- @field #string onboard Onboard number of player or first unit in group. +-- @field #number case Recovery case of flight. +-- @field #string seclead Name of section lead. +-- @field #table section Other human flight groups belonging to this flight. This flight is the lead. +-- @field #boolean holding If true, flight is in holding zone. +-- @field #boolean ballcall If true, flight called the ball in the groove. +-- @field #table elements Flight group elements. +-- @field #number Tcharlie Charlie (abs) time in seconds. +-- @field #string name Player name or name of first AI unit. +-- @field #boolean refueling Flight is refueling. + +--- Parameters of an element in a flight group. +-- @type AIRBOSS.FlightElement +-- @field Wrapper.Unit#UNIT unit Aircraft unit. +-- @field #string unitname Name of the unit. +-- @field #boolean ai If true, AI sits inside. If false, human player is flying. +-- @field #string onboard Onboard number of the aircraft. +-- @field #boolean ballcall If true, flight called the ball in the groove. +-- @field #boolean recovered If true, element was successfully recovered. + +--- Player data table holding all important parameters of each player. +-- @type AIRBOSS.PlayerData +-- @field Wrapper.Unit#UNIT unit Aircraft of the player. +-- @field #string unitname Name of the unit. +-- @field Wrapper.Client#CLIENT client Client object of player. +-- @field #string callsign Callsign of player. +-- @field #string difficulty Difficulty level. +-- @field #string step Current/next pattern step. +-- @field #boolean warning Set true once the player got a warning. +-- @field #number passes Number of passes. +-- @field #boolean attitudemonitor If true, display aircraft attitude and other parameters constantly. +-- @field #table debrief Debrief analysis of the current step of this pass. +-- @field #table lastdebrief Debrief of player performance of last completed pass. +-- @field #boolean landed If true, player landed or attempted to land. +-- @field #boolean boltered If true, player boltered. +-- @field #boolean waveoff If true, player was waved off during final approach. +-- @field #boolean wop If true, player was waved off during the pattern. +-- @field #boolean lig If true, player was long in the groove. +-- @field #boolean owo If true, own waveoff by player. +-- @field #boolean wofd If true, player was waved off because of a foul deck. +-- @field #number Tlso Last time the LSO gave an advice. +-- @field #number Tgroove Time in the groove in seconds. +-- @field #number TIG0 Time in groove start timer.getTime(). +-- @field #number wire Wire caught by player when trapped. +-- @field #AIRBOSS.GroovePos groove Data table at each position in the groove. Elements are of type @{#AIRBOSS.GrooveData}. +-- @field #table points Points of passes until finally landed. +-- @field #number finalscore Final score if points are averaged over multiple passes. +-- @field #boolean valid If true, player made a valid approach. Is set true on start of Groove X. +-- @field #boolean subtitles If true, display subtitles of radio messages. +-- @field #boolean showhints If true, show step hints. +-- @field #table trapsheet Groove data table recorded every 0.5 seconds. +-- @field #boolean trapon If true, save trap sheets. +-- @field #string debriefschedulerID Debrief scheduler ID. +-- @extends #AIRBOSS.FlightGroup + +--- Main group level radio menu: F10 Other/Airboss. +-- @field #table MenuF10 +AIRBOSS.MenuF10={} + +--- Airboss mission level F10 root menu. +-- @field #table MenuF10Root +AIRBOSS.MenuF10Root=nil + +--- Airboss class version. +-- @field #string version +AIRBOSS.version="1.2.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- DONE: Handle tanker and AWACS. Put them into pattern. +-- TODO: Handle cases where AI crashes on carrier deck ==> Clean up deck. +-- TODO: Player eject and crash debrief "gradings". +-- TODO: PWO during case 2/3. +-- TODO: PWO when player comes too close to other flight. +-- DONE: Spin pattern. Add radio menu entry. Not sure what to add though?! +-- DONE: Despawn AI after engine shutdown option. +-- DONE: What happens when section lead or member dies? +-- DONE: Do not remove recovered elements but only set switch. Remove only groups which are completely recovered. +-- DONE: Option to filter AI groups for recovery. +-- DONE: Rework radio messages. Better control over player board numbers. +-- DONE: Case I & II/III zone so that player gets into pattern automatically. Case I 3 position on the circle. Case II/III when the player enters the approach corridor maybe? +-- DONE: Add static weather information. +-- DONE: Allow up to two flights per Case I marshal stack. +-- DONE: Add max stack for Case I and define waiting queue outside CCZ. +-- DONE: Maybe do an additional step at the initial (Case II) or bullseye (Case III) and register player in case he missed some steps. +-- DONE: Subtitles off options on player level. +-- DONE: Persistence of results. +-- DONE: Foul deck waveoff. +-- DONE: Get Charlie time estimate function. +-- DONE: Average player grades until landing. +-- DONE: Check player heading at zones, e.g. initial. +-- DONE: Fix bug that player leaves the approach zone if he boltered or was waved off during Case II or III. NOTE: Partly due to increasing approach zone size. +-- DONE: Fix bug that player gets an altitude warning if stack collapses. NOTE: Would not work if two stacks Case I and II/III are used. +-- DONE: Improve radio messages. Maybe usersound for messages which are only meant for players? +-- DONE: Add voice over fly needs and welcome aboard. +-- DONE: Improve trapped wire calculation. +-- DONE: Carrier zone with dimensions of carrier. to check if landing happened on deck. +-- DONE: Carrier runway zone for fould deck check. +-- DONE: More Hints for Case II/III. +-- DONE: Set magnetic declination function. +-- DONE: First send AI to marshal and then allow them into the landing pattern ==> task function when reaching the waypoint. +-- DONE: Extract (static) weather from mission for cloud cover etc. +-- DONE: Check distance to players during approach. +-- DONE: Option to turn AI handling off. +-- DONE: Add user functions. +-- DONE: Update AI holding pattern wrt to moving carrier. +-- DONE: Generalize parameters for other carriers. +-- DONE: Generalize parameters for other aircraft. +-- DONE: Add radio check (LSO, AIRBOSS) to F10 radio menu. +-- DONE: Right pattern step after bolter/wo/patternWO? Guess so. +-- DONE: Set case II and III times (via recovery time). +-- DONE: Get correct wire when trapped. DONE but might need further tweaking. +-- DONE: Add radio transmission queue for LSO and airboss. +-- DONE: CASE II. +-- DONE: CASE III. +-- NOPE: Strike group with helo bringing cargo etc. Not yet. +-- DONE: Handle crash event. Delete A/C from queue, send rescue helo. +-- DONE: Get fuel state in pounds. (working for the hornet, did not check others) +-- DONE: Add aircraft numbers in queue to carrier info F10 radio output. +-- DONE: Monitor holding of players/AI in zoneHolding. +-- DONE: Transmission via radio. +-- DONE: Get board numbers. +-- DONE: Get an _OK_ pass if long in groove. Possible other pattern wave offs as well?! +-- DONE: Add scoring to radio menu. +-- DONE: Optimized debrief. +-- DONE: Add automatic grading. +-- DONE: Fix radio menu. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new AIRBOSS class object for a specific aircraft carrier unit. +-- @param #AIRBOSS self +-- @param carriername Name of the aircraft carrier unit as defined in the mission editor. +-- @param alias (Optional) Alias for the carrier. This will be used for radio messages and the F10 radius menu. Default is the carrier name as defined in the mission editor. +-- @return #AIRBOSS self or nil if carrier unit does not exist. +function AIRBOSS:New(carriername, alias) + + -- Inherit everthing from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #AIRBOSS + + -- Debug. + self:F2({carriername=carriername, alias=alias}) + + -- Set carrier unit. + self.carrier=UNIT:FindByName(carriername) + + -- Check if carrier unit exists. + if self.carrier==nil then + -- Error message. + local text=string.format("ERROR: Carrier unit %s could not be found! Make sure this UNIT is defined in the mission editor and check the spelling of the unit name carefully.", carriername) + MESSAGE:New(text, 120):ToAll() + self:E(text) + return nil + end + + -- Set some string id for output to DCS.log file. + self.lid=string.format("AIRBOSS %s | ", carriername) + + -- Current map. + self.theatre=env.mission.theatre + self:T2(self.lid..string.format("Theatre = %s.", tostring(self.theatre))) + + -- Get carrier type. + self.carriertype=self.carrier:GetTypeName() + + -- Set alias. + self.alias=alias or carriername + + -- Set carrier airbase object. + self.airbase=AIRBASE:FindByName(carriername) + + -- Create carrier beacon. + self.beacon=BEACON:New(self.carrier) + + -- Set Tower Frequency of carrier. + self:_GetTowerFrequency() + + -- Init player scores table. + self.playerscores={} + + -- Initialize ME waypoints. + self:_InitWaypoints() + + -- Current waypoint. + self.currentwp=1 + + -- Patrol route. + self:_PatrolRoute() + + ------------- + --- Defaults: + ------------- + + -- Set up Airboss radio. + self:SetMarshalRadio() + + -- Set up LSO radio. + self:SetLSORadio() + + -- Set LSO call interval. Default 4 sec. + self:SetLSOCallInterval() + + -- Radio scheduler. + self.radiotimer=SCHEDULER:New() + + -- Set magnetic declination. + self:SetMagneticDeclination() + + -- Set ICSL to channel 1. + self:SetICLS() + + -- Set TACAN to channel 74X. + self:SetTACAN() + + -- Becons are reactivated very 5 min. + self:SetBeaconRefresh() + + -- Set max aircraft in landing pattern. Default 4. + self:SetMaxLandingPattern() + + -- Set max Case I Marshal stacks. Default 3. + self:SetMaxMarshalStacks() + + -- Set max section members. Default 2. + self:SetMaxSectionSize() + + -- Set max flights per stack. Default is 2. + self:SetMaxFlightsPerStack() + + -- Set AI handling On. + self:SetHandleAION() + + -- Airboss is a nice guy. + self:SetAirbossNiceGuy() + + -- Allow emergency landings. + self:SetEmergencyLandings() + + -- No despawn after engine shutdown by default. + self:SetDespawnOnEngineShutdown(false) + + -- No respawning of AI groups when entering the CCA. + self:SetRespawnAI(false) + + -- Mission uses static weather by default. + self:SetStaticWeather() + + -- Default recovery case. This sets self.defaultcase and self.case. Default Case I. + self:SetRecoveryCase() + + -- Set time the turn starts before the window opens. + self:SetRecoveryTurnTime() + + -- Set holding offset to 0 degrees. This set self.defaultoffset and self.holdingoffset. + self:SetHoldingOffsetAngle() + + -- Set Marshal stack radius. Default 2.75 NM, which gives a diameter of 5.5 NM. + self:SetMarshalRadius() + + -- Set max alt at initial. Default 1300 ft. + self:SetInitialMaxAlt() + + -- Default player skill EASY. + self:SetDefaultPlayerSkill(AIRBOSS.Difficulty.EASY) + + -- Default glideslope error thresholds. + self:SetGlideslopeErrorThresholds() + + -- Default lineup error thresholds. + self:SetLineupErrorThresholds() + + -- CCA 50 NM radius zone around the carrier. + self:SetCarrierControlledArea() + + -- CCZ 5 NM radius zone around the carrier. + self:SetCarrierControlledZone() + + -- Carrier patrols its waypoints until the end of time. + self:SetPatrolAdInfinitum(true) + + -- Collision check distance. Default 5 NM. + self:SetCollisionDistance() + + -- Set update time intervals. + self:SetQueueUpdateTime() + self:SetStatusUpdateTime() + self:SetDefaultMessageDuration() + + -- Menu options. + self:SetMenuMarkZones() + self:SetMenuSmokeZones() + self:SetMenuSingleCarrier(false) + + -- Welcome players. + self:SetWelcomePlayers(true) + + -- Coordinates + self.landingcoord=COORDINATE:New(0,0,0) --Core.Point#COORDINATE + self.sterncoord=COORDINATE:New(0, 0, 0) --Core.Point#COORDINATE + self.landingspotcoord=COORDINATE:New(0,0,0) --Core.Point#COORDINATE + + -- Init carrier parameters. + if self.carriertype==AIRBOSS.CarrierType.STENNIS then + self:_InitStennis() + elseif self.carriertype==AIRBOSS.CarrierType.ROOSEVELT then + self:_InitNimitz() + elseif self.carriertype==AIRBOSS.CarrierType.LINCOLN then + self:_InitNimitz() + elseif self.carriertype==AIRBOSS.CarrierType.WASHINGTON then + self:_InitNimitz() + elseif self.carriertype==AIRBOSS.CarrierType.TRUMAN then + self:_InitNimitz() + elseif self.carriertype==AIRBOSS.CarrierType.FORRESTAL then + self:_InitForrestal() + elseif self.carriertype==AIRBOSS.CarrierType.VINSON then + -- TODO: Carl Vinson parameters. + self:_InitStennis() + elseif self.carriertype==AIRBOSS.CarrierType.TARAWA then + -- Tarawa parameters. + self:_InitTarawa() + elseif self.carriertype==AIRBOSS.CarrierType.AMERICA then + -- Use America parameters. + self:_InitAmerica() + elseif self.carriertype==AIRBOSS.CarrierType.JCARLOS then + -- Use Juan Carlos parameters. + self:_InitJcarlos() + elseif self.carriertype==AIRBOSS.CarrierType.KUZNETSOV then + -- Kusnetsov parameters - maybe... + self:_InitStennis() + else + self:E(self.lid..string.format("ERROR: Unknown carrier type %s!", tostring(self.carriertype))) + return nil + end + + -- Init voice over files. + self:_InitVoiceOvers() + + ------------------- + -- Debug Section -- + ------------------- + + -- Debug trace. + if false then + self.Debug=true + BASE:TraceOnOff(true) + BASE:TraceClass(self.ClassName) + BASE:TraceLevel(3) + --self.dTstatus=0.1 + end + + -- Smoke zones. + if false then + local case=3 + self.holdingoffset=30 + self:_GetZoneGroove():SmokeZone(SMOKECOLOR.Red, 5) + self:_GetZoneLineup():SmokeZone(SMOKECOLOR.Green, 5) + self:_GetZoneBullseye(case):SmokeZone(SMOKECOLOR.White, 45) + self:_GetZoneDirtyUp(case):SmokeZone(SMOKECOLOR.Orange, 45) + self:_GetZoneArcIn(case):SmokeZone(SMOKECOLOR.Blue, 45) + self:_GetZoneArcOut(case):SmokeZone(SMOKECOLOR.Blue, 45) + self:_GetZonePlatform(case):SmokeZone(SMOKECOLOR.Blue, 45) + self:_GetZoneCorridor(case):SmokeZone(SMOKECOLOR.Green, 45) + self:_GetZoneHolding(case, 1):SmokeZone(SMOKECOLOR.White, 45) + self:_GetZoneHolding(case, 2):SmokeZone(SMOKECOLOR.White, 45) + self:_GetZoneInitial(case):SmokeZone(SMOKECOLOR.Orange, 45) + self:_GetZoneCommence(case, 1):SmokeZone(SMOKECOLOR.Red, 45) + self:_GetZoneCommence(case, 2):SmokeZone(SMOKECOLOR.Red, 45) + self:_GetZoneAbeamLandingSpot():SmokeZone(SMOKECOLOR.Red, 5) + self:_GetZoneLandingSpot():SmokeZone(SMOKECOLOR.Red, 5) + end + + -- Carrier parameter debug tests. + if false then + -- Stern coordinate. + local FB=self:GetFinalBearing(false) + local hdg=self:GetHeading(false) + + -- Stern pos. + local stern=self:_GetSternCoord() + + -- Bow pos. + local bow=stern:Translate(self.carrierparam.totlength, hdg, true) + + -- End of rwy. + local rwy=stern:Translate(self.carrierparam.rwylength, FB, true) + + --- Flare points and zones. + local function flareme() + + -- Carrier pos. + self:GetCoordinate():FlareYellow() + + -- Stern + stern:FlareYellow() + + -- Bow + bow:FlareYellow() + + -- Runway half width = 10 m. + local r1=stern:Translate(self.carrierparam.rwywidth*0.5, FB+90, true) + local r2=stern:Translate(self.carrierparam.rwywidth*0.5, FB-90, true) + --r1:FlareWhite() + --r2:FlareWhite() + + -- End of runway. + rwy:FlareRed() + + -- Right 30 meters from stern. + local cR=stern:Translate(self.carrierparam.totwidthstarboard, hdg+90, true) + --cR:FlareYellow() + + -- Left 40 meters from stern. + local cL=stern:Translate(self.carrierparam.totwidthport, hdg-90, true) + --cL:FlareYellow() + + + -- Carrier specific. + if self.carrier:GetTypeName()~=AIRBOSS.CarrierType.TARAWA or self.carrier:GetTypeName()~=AIRBOSS.CarrierType.AMERICA or self.carrier:GetTypeName()~=AIRBOSS.CarrierType.JCARLOS then + + -- Flare wires. + local w1=stern:Translate(self.carrierparam.wire1, FB, true) + local w2=stern:Translate(self.carrierparam.wire2, FB, true) + local w3=stern:Translate(self.carrierparam.wire3, FB, true) + local w4=stern:Translate(self.carrierparam.wire4, FB, true) + w1:FlareWhite() + w2:FlareYellow() + w3:FlareWhite() + w4:FlareYellow() + + else + + -- Abeam landing spot zone. + local ALSPT=self:_GetZoneAbeamLandingSpot() + ALSPT:FlareZone(FLARECOLOR.Red, 5, nil, UTILS.FeetToMeters(120)) + + -- Primary landing spot zone. + local LSPT=self:_GetZoneLandingSpot() + LSPT:FlareZone(FLARECOLOR.Green, 5, nil, self.carrierparam.deckheight) + + -- Landing spot coordinate. + local PLSC=self:_GetLandingSpotCoordinate() + PLSC:FlareWhite() + end + + -- Flare carrier and landing runway. + local cbox=self:_GetZoneCarrierBox() + local rbox=self:_GetZoneRunwayBox() + cbox:FlareZone(FLARECOLOR.Green, 5, nil, self.carrierparam.deckheight) + rbox:FlareZone(FLARECOLOR.White, 5, nil, self.carrierparam.deckheight) + end + + -- Flare points every 3 seconds for 3 minutes. + SCHEDULER:New(nil, flareme, {}, 1, 3, nil, 180) + end + + ----------------------- + --- FSM Transitions --- + ----------------------- + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Load", "Stopped") -- Load player scores from file. + self:AddTransition("Stopped", "Start", "Idle") -- Start AIRBOSS script. + self:AddTransition("*", "Idle", "Idle") -- Carrier is idling. + self:AddTransition("Idle", "RecoveryStart", "Recovering") -- Start recovering aircraft. + self:AddTransition("Recovering", "RecoveryStop", "Idle") -- Stop recovering aircraft. + self:AddTransition("Recovering", "RecoveryPause", "Paused") -- Pause recovering aircraft. + self:AddTransition("Paused", "RecoveryUnpause", "Recovering") -- Unpause recovering aircraft. + self:AddTransition("*", "Status", "*") -- Update status of players and queues. + self:AddTransition("*", "RecoveryCase", "*") -- Switch to another case recovery. + self:AddTransition("*", "PassingWaypoint", "*") -- Carrier is passing a waypoint. + self:AddTransition("*", "LSOGrade", "*") -- LSO grade. + self:AddTransition("*", "Marshal", "*") -- A flight was send into the marshal stack. + self:AddTransition("*", "Save", "*") -- Save player scores to file. + self:AddTransition("*", "Stop", "Stopped") -- Stop AIRBOSS FMS. + + + --- Triggers the FSM event "Start" that starts the airboss. Initializes parameters and starts event handlers. + -- @function [parent=#AIRBOSS] Start + -- @param #AIRBOSS self + + --- Triggers the FSM event "Start" that starts the airboss after a delay. Initializes parameters and starts event handlers. + -- @function [parent=#AIRBOSS] __Start + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + + --- On after "Start" user function. Called when the AIRBOSS FSM is started. + -- @function [parent=#AIRBOSS] OnAfterStart + -- @param #AIRBOSS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Idle" that puts the carrier into state "Idle" where no recoveries are carried out. + -- @function [parent=#AIRBOSS] Idle + -- @param #AIRBOSS self + + --- Triggers the FSM delayed event "Idle" that puts the carrier into state "Idle" where no recoveries are carried out. + -- @function [parent=#AIRBOSS] __Idle + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "RecoveryStart" that starts the recovery of aircraft. Marshalling aircraft are send to the landing pattern. + -- @function [parent=#AIRBOSS] RecoveryStart + -- @param #AIRBOSS self + -- @param #number Case Recovery case (1, 2 or 3) that is started. + -- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. + + --- Triggers the FSM delayed event "RecoveryStart" that starts the recovery of aircraft. Marshalling aircraft are send to the landing pattern. + -- @function [parent=#AIRBOSS] __RecoveryStart + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + -- @param #number Case Recovery case (1, 2 or 3) that is started. + -- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. + + --- On after "RecoveryStart" user function. Called when recovery of aircraft is started and carrier switches to state "Recovering". + -- @function [parent=#AIRBOSS] OnAfterRecoveryStart + -- @param #AIRBOSS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #number Case The recovery case (1, 2 or 3) to start. + -- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. + + + --- Triggers the FSM event "RecoveryStop" that stops the recovery of aircraft. + -- @function [parent=#AIRBOSS] RecoveryStop + -- @param #AIRBOSS self + + --- Triggers the FSM delayed event "RecoveryStop" that stops the recovery of aircraft. + -- @function [parent=#AIRBOSS] __RecoveryStop + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + + --- On after "RecoveryStop" user function. Called when recovery of aircraft is stopped. + -- @function [parent=#AIRBOSS] OnAfterRecoveryStop + -- @param #AIRBOSS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "RecoveryPause" that pauses the recovery of aircraft. + -- @function [parent=#AIRBOSS] RecoveryPause + -- @param #AIRBOSS self + -- @param #number duration Duration of pause in seconds. After that recovery is automatically resumed. + + --- Triggers the FSM delayed event "RecoveryPause" that pauses the recovery of aircraft. + -- @function [parent=#AIRBOSS] __RecoveryPause + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + -- @param #number duration Duration of pause in seconds. After that recovery is automatically resumed. + + --- Triggers the FSM event "RecoveryUnpause" that resumes the recovery of aircraft if it was paused. + -- @function [parent=#AIRBOSS] RecoveryUnpause + -- @param #AIRBOSS self + + --- Triggers the FSM delayed event "RecoveryUnpause" that resumes the recovery of aircraft if it was paused. + -- @function [parent=#AIRBOSS] __RecoveryUnpause + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "RecoveryCase" that switches the aircraft recovery case. + -- @function [parent=#AIRBOSS] RecoveryCase + -- @param #AIRBOSS self + -- @param #number Case The new recovery case (1, 2 or 3). + -- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. + + --- Triggers the delayed FSM event "RecoveryCase" that sets the used aircraft recovery case. + -- @function [parent=#AIRBOSS] __RecoveryCase + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + -- @param #number Case The new recovery case (1, 2 or 3). + -- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. + + + --- Triggers the FSM event "PassingWaypoint". Called when the carrier passes a waypoint. + -- @function [parent=#AIRBOSS] PassingWaypoint + -- @param #AIRBOSS self + -- @param #number waypoint Number of waypoint. + + --- Triggers the FSM delayed event "PassingWaypoint". Called when the carrier passes a waypoint. + -- @function [parent=#AIRBOSS] __PassingWaypoint + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + -- @param #number Case Recovery case (1, 2 or 3) that is started. + -- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. + + --- On after "PassingWaypoint" user function. Called when the carrier passes a waypoint of its route. + -- @function [parent=#AIRBOSS] OnAfterPassingWaypoint + -- @param #AIRBOSS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #number waypoint Number of waypoint. + + + --- Triggers the FSM event "Save" that saved the player scores to a file. + -- @function [parent=#AIRBOSS] Save + -- @param #AIRBOSS self + -- @param #string path Path where the file is saved. Default is the DCS installation root directory or your "Saved Games\DCS" folder if lfs was desanitized. + -- @param #string filename (Optional) File name. Default is AIRBOSS-*ALIAS*_LSOgrades.csv. + + --- Triggers the FSM delayed event "Save" that saved the player scores to a file. + -- @function [parent=#AIRBOSS] __Save + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + -- @param #string path Path where the file is saved. Default is the DCS installation root directory or your "Saved Games\DCS" folder if lfs was desanitized. + -- @param #string filename (Optional) File name. Default is AIRBOSS-*ALIAS*_LSOgrades.csv. + + --- On after "Save" event user function. Called when the player scores are saved to disk. + -- @function [parent=#AIRBOSS] OnAfterSave + -- @param #AIRBOSS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string path Path where the file is saved. Default is the DCS installation root directory or your "Saved Games\DCS" folder if lfs was desanitized. + -- @param #string filename (Optional) File name. Default is AIRBOSS-*ALIAS*_LSOgrades.csv. + + + --- Triggers the FSM event "Load" that loads the player scores from a file. AIRBOSS FSM must **not** be started at this point. + -- @function [parent=#AIRBOSS] Load + -- @param #AIRBOSS self + -- @param #string path Path where the file is located. Default is the DCS installation root directory. + -- @param #string filename (Optional) File name. Default is AIRBOSS-_LSOgrades.csv. + + --- Triggers the FSM delayed event "Load" that loads the player scores from a file. AIRBOSS FSM must **not** be started at this point. + -- @function [parent=#AIRBOSS] __Load + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + -- @param #string path Path where the file is located. Default is the DCS installation root directory or your "Saved Games\DCS" folder if lfs was desanitized. + -- @param #string filename (Optional) File name. Default is AIRBOSS-*ALIAS*_LSOgrades.csv. + + --- On after "Load" event user function. Called when the player scores are loaded from disk. + -- @function [parent=#AIRBOSS] OnAfterLoad + -- @param #AIRBOSS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string path Path where the file is located. Default is the DCS installation root directory or your "Saved Games\DCS" folder if lfs was desanitized. + -- @param #string filename (Optional) File name. Default is AIRBOSS-*ALIAS*_LSOgrades.csv. + + + --- Triggers the FSM event "LSOGrade". Called when the LSO grades a player + -- @function [parent=#AIRBOSS] LSOGrade + -- @param #AIRBOSS self + -- @param #AIRBOSS.PlayerData playerData Player Data. + -- @param #AIRBOSS.LSOgrade grade LSO grade. + + --- Triggers the FSM event "LSOGrade". Delayed called when the LSO grades a player. + -- @function [parent=#AIRBOSS] __LSOGrade + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + -- @param #AIRBOSS.PlayerData playerData Player Data. + -- @param #AIRBOSS.LSOgrade grade LSO grade. + + --- On after "LSOGrade" user function. Called when the carrier passes a waypoint of its route. + -- @function [parent=#AIRBOSS] OnAfterLSOGrade + -- @param #AIRBOSS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #AIRBOSS.PlayerData playerData Player Data. + -- @param #AIRBOSS.LSOgrade grade LSO grade. + + + --- Triggers the FSM event "Marshal". Called when a flight is send to the Marshal stack. + -- @function [parent=#AIRBOSS] Marshal + -- @param #AIRBOSS self + -- @param #AIRBOSS.FlightGroup flight The flight group data. + + --- Triggers the FSM event "Marshal". Delayed call when a flight is send to the Marshal stack. + -- @function [parent=#AIRBOSS] __Marshal + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + -- @param #AIRBOSS.FlightGroup flight The flight group data. + + --- On after "Marshal" user function. Called when a flight is send to the Marshal stack. + -- @function [parent=#AIRBOSS] OnAfterMarshal + -- @param #AIRBOSS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #AIRBOSS.FlightGroup flight The flight group data. + + + --- Triggers the FSM event "Stop" that stops the airboss. Event handlers are stopped. + -- @function [parent=#AIRBOSS] Stop + -- @param #AIRBOSS self + + --- Triggers the FSM event "Stop" that stops the airboss after a delay. Event handlers are stopped. + -- @function [parent=#AIRBOSS] __Stop + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- USER API Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set welcome messages for players. +-- @param #AIRBOSS self +-- @param #boolean switch If true, display welcome message to player. +-- @return #AIRBOSS self +function AIRBOSS:SetWelcomePlayers(switch) + + self.welcome=switch + + return self +end + + +--- Set carrier controlled area (CCA). +-- This is a large zone around the carrier, which is constantly updated wrt the carrier position. +-- @param #AIRBOSS self +-- @param #number radius Radius of zone in nautical miles (NM). Default 50 NM. +-- @return #AIRBOSS self +function AIRBOSS:SetCarrierControlledArea(radius) + + radius=UTILS.NMToMeters(radius or 50) + + self.zoneCCA=ZONE_UNIT:New("Carrier Controlled Area", self.carrier, radius) + + return self +end + +--- Set carrier controlled zone (CCZ). +-- This is a small zone (usually 5 NM radius) around the carrier, which is constantly updated wrt the carrier position. +-- @param #AIRBOSS self +-- @param #number radius Radius of zone in nautical miles (NM). Default 5 NM. +-- @return #AIRBOSS self +function AIRBOSS:SetCarrierControlledZone(radius) + + radius=UTILS.NMToMeters(radius or 5) + + self.zoneCCZ=ZONE_UNIT:New("Carrier Controlled Zone", self.carrier, radius) + + return self +end + +--- Set distance up to which water ahead is scanned for collisions. +-- @param #AIRBOSS self +-- @param #number dist Distance in NM. Default 5 NM. +-- @return #AIRBOSS self +function AIRBOSS:SetCollisionDistance(distance) + self.collisiondist=UTILS.NMToMeters(distance or 5) + return self +end + +--- Set the default recovery case. +-- @param #AIRBOSS self +-- @param #number case Case of recovery. Either 1, 2 or 3. Default 1. +-- @return #AIRBOSS self +function AIRBOSS:SetRecoveryCase(case) + + -- Set default case or 1. + self.defaultcase=case or 1 + + -- Current case init. + self.case=self.defaultcase + + return self +end + +--- Set holding pattern offset from final bearing for Case II/III recoveries. +-- Usually, this is +-15 or +-30 degrees. You should not use and offset angle >= 90 degrees, because this will cause a devision by zero in some of the equations used to calculate the approach corridor. +-- So best stick to the defaults up to 30 degrees. +-- @param #AIRBOSS self +-- @param #number offset Offset angle in degrees. Default 0. +-- @return #AIRBOSS self +function AIRBOSS:SetHoldingOffsetAngle(offset) + + -- Set default angle or 0. + self.defaultoffset=offset or 0 + + -- Current offset init. + self.holdingoffset=self.defaultoffset + + return self +end + +--- Enable F10 menu to manually start recoveries. +-- @param #AIRBOSS self +-- @param #number duration Default duration of the recovery in minutes. Default 30 min. +-- @param #number windondeck Default wind on deck in knots. Default 25 knots. +-- @param #boolean uturn U-turn after recovery window closes on=true or off=false/nil. Default off. +-- @param #number offset Relative Marshal radial in degrees for Case II/III recoveries. Default 30°. +-- @return #AIRBOSS self +function AIRBOSS:SetMenuRecovery(duration, windondeck, uturn, offset) + + self.skipperMenu=true + self.skipperTime=duration or 30 + self.skipperSpeed=windondeck or 25 + self.skipperOffset=offset or 30 + + if uturn then + self.skipperUturn=true + else + self.skipperUturn=false + end + + return self +end + +--- Add aircraft recovery time window and recovery case. +-- @param #AIRBOSS self +-- @param #string starttime Start time, e.g. "8:00" for eight o'clock. Default now. +-- @param #string stoptime Stop time, e.g. "9:00" for nine o'clock. Default 90 minutes after start time. +-- @param #number case Recovery case for that time slot. Number between one and three. +-- @param #number holdingoffset Only for CASE II/III: Angle in degrees the holding pattern is offset. +-- @param #boolean turnintowind If true, carrier will turn into the wind 5 minutes before the recovery window opens. +-- @param #number speed Speed in knots during turn into wind leg. +-- @param #boolean uturn If true (or nil), carrier wil perform a U-turn and go back to where it came from before resuming its route to the next waypoint. If false, it will go directly to the next waypoint. +-- @return #AIRBOSS.Recovery Recovery window. +function AIRBOSS:AddRecoveryWindow(starttime, stoptime, case, holdingoffset, turnintowind, speed, uturn) + + -- Absolute mission time in seconds. + local Tnow=timer.getAbsTime() + + if starttime and type(starttime)=="number" then + starttime=UTILS.SecondsToClock(Tnow+starttime) + end + + if stoptime and type(stoptime)=="number" then + stoptime=UTILS.SecondsToClock(Tnow+stoptime) + end + + + -- Input or now. + starttime=starttime or UTILS.SecondsToClock(Tnow) + + -- Set start time. + local Tstart=UTILS.ClockToSeconds(starttime) + + -- Set stop time. + local Tstop=stoptime and UTILS.ClockToSeconds(stoptime) or Tstart+90*60 + + -- Consistancy check for timing. + if Tstart>Tstop then + self:E(string.format("ERROR: Recovery stop time %s lies before recovery start time %s! Recovery window rejected.", UTILS.SecondsToClock(Tstart), UTILS.SecondsToClock(Tstop))) + return self + end + if Tstop<=Tnow then + self:I(string.format("WARNING: Recovery stop time %s already over. Tnow=%s! Recovery window rejected.", UTILS.SecondsToClock(Tstop), UTILS.SecondsToClock(Tnow))) + return self + end + + -- Case or default value. + case=case or self.defaultcase + + -- Holding offset or default value. + holdingoffset=holdingoffset or self.defaultoffset + + -- Offset zero for case I. + if case==1 then + holdingoffset=0 + end + + -- Increase counter. + self.windowcount=self.windowcount+1 + + -- Recovery window. + local recovery={} --#AIRBOSS.Recovery + recovery.START=Tstart + recovery.STOP=Tstop + recovery.CASE=case + recovery.OFFSET=holdingoffset + recovery.OPEN=false + recovery.OVER=false + recovery.WIND=turnintowind + recovery.SPEED=speed or 20 + recovery.ID=self.windowcount + + if uturn==nil or uturn==true then + recovery.UTURN=true + else + recovery.UTURN=false + end + + -- Add to table + table.insert(self.recoverytimes, recovery) + + return recovery +end + +--- Define a set of AI groups that are handled by the airboss. +-- @param #AIRBOSS self +-- @param Core.Set#SET_GROUP setgroup The set of AI groups which are handled by the airboss. +-- @return #AIRBOSS self +function AIRBOSS:SetSquadronAI(setgroup) + self.squadsetAI=setgroup + return self +end + +--- Define a set of AI groups that excluded from AI handling. Members of this set will be left allone by the airboss and not forced into the Marshal pattern. +-- @param #AIRBOSS self +-- @param Core.Set#SET_GROUP setgroup The set of AI groups which are excluded. +-- @return #AIRBOSS self +function AIRBOSS:SetExcludeAI(setgroup) + self.excludesetAI=setgroup + return self +end + +--- Add a group to the exclude set. If no set exists, it is created. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group The group to be excluded. +-- @return #AIRBOSS self +function AIRBOSS:AddExcludeAI(group) + + self.excludesetAI=self.excludesetAI or SET_GROUP:New() + + self.excludesetAI:AddGroup(group) + + return self +end + +--- Close currently running recovery window and stop recovery ops. Recovery window is deleted. +-- @param #AIRBOSS self +-- @param #number delay (Optional) Delay in seconds before the window is deleted. +function AIRBOSS:CloseCurrentRecoveryWindow(delay) + + if delay and delay>0 then + --SCHEDULER:New(nil, self.CloseCurrentRecoveryWindow, {self}, delay) + self:ScheduleOnce(delay, self.CloseCurrentRecoveryWindow, self) + else + if self:IsRecovering() and self.recoverywindow and self.recoverywindow.OPEN then + self:RecoveryStop() + self.recoverywindow.OPEN=false + self.recoverywindow.OVER=true + self:DeleteRecoveryWindow(self.recoverywindow) + end + end +end + +--- Delete all recovery windows. +-- @param #AIRBOSS self +-- @param #number delay (Optional) Delay in seconds before the windows are deleted. +-- @return #AIRBOSS self +function AIRBOSS:DeleteAllRecoveryWindows(delay) + + -- Loop over all recovery windows. + for _,recovery in pairs(self.recoverytimes) do + self:I(self.lid..string.format("Deleting recovery window ID %s", tostring(recovery.ID))) + self:DeleteRecoveryWindow(recovery, delay) + end + + return self +end + +--- Return the recovery window of the given ID. +-- @param #AIRBOSS self +-- @param #number id The ID of the recovery window. +-- @return #AIRBOSS.Recovery Recovery window with the right ID or nil if no such window exists. +function AIRBOSS:GetRecoveryWindowByID(id) + if id then + for _,_window in pairs(self.recoverytimes) do + local window=_window --#AIRBOSS.Recovery + if window and window.ID==id then + return window + end + end + end + return nil +end + +--- Delete a recovery window. If the window is currently open, it is closed and the recovery stopped. +-- @param #AIRBOSS self +-- @param #AIRBOSS.Recovery window Recovery window. +-- @param #number delay Delay in seconds, before the window is deleted. +function AIRBOSS:DeleteRecoveryWindow(window, delay) + + if delay and delay>0 then + -- Delayed call. + --SCHEDULER:New(nil, self.DeleteRecoveryWindow, {self, window}, delay) + self:ScheduleOnce(delay, self.DeleteRecoveryWindow, self, window) + else + + for i,_recovery in pairs(self.recoverytimes) do + local recovery=_recovery --#AIRBOSS.Recovery + + if window and window.ID==recovery.ID then + if window.OPEN then + -- Window is currently open. + self:RecoveryStop() + else + table.remove(self.recoverytimes, i) + end + + end + end + end +end + +--- Set time before carrier turns and recovery window opens. +-- @param #AIRBOSS self +-- @param #number interval Time interval in seconds. Default 300 sec. +-- @return #AIRBOSS self +function AIRBOSS:SetRecoveryTurnTime(interval) + self.dTturn=interval or 300 + return self +end + +--- Set multiplayer environment wire correction. +-- @param #AIRBOSS self +-- @param #number Dcorr Correction distance in meters. Default 12 m. +-- @return #AIRBOSS self +function AIRBOSS:SetMPWireCorrection(Dcorr) + self.mpWireCorrection=Dcorr or 12 + return self +end + +--- Set time interval for updating queues and other stuff. +-- @param #AIRBOSS self +-- @param #number interval Time interval in seconds. Default 30 sec. +-- @return #AIRBOSS self +function AIRBOSS:SetQueueUpdateTime(interval) + self.dTqueue=interval or 30 + return self +end + +--- Set time interval between LSO calls. Optimal time in the groove is ~16 seconds. So the default of 4 seconds gives around 3-4 correction calls in the groove. +-- @param #AIRBOSS self +-- @param #number interval Time interval in seconds between LSO calls. Default 4 sec. +-- @return #AIRBOSS self +function AIRBOSS:SetLSOCallInterval(timeinterval) + self.LSOdT=timeinterval or 4 + return self +end + +--- Airboss is a rather nice guy and not strictly following the rules. Fore example, he does allow you into the landing pattern if you are not coming from the Marshal stack. +-- @param #AIRBOSS self +-- @param #boolean switch If true or nil, Airboss bends the rules a bit. +-- @return #AIRBOSS self +function AIRBOSS:SetAirbossNiceGuy(switch) + if switch==true or switch==nil then + self.airbossnice=true + else + self.airbossnice=false + end + return self +end + +--- Allow emergency landings, i.e. bypassing any pattern and go directly to final approach. +-- @param #AIRBOSS self +-- @param #boolean switch If true or nil, emergency landings are okay. +-- @return #AIRBOSS self +function AIRBOSS:SetEmergencyLandings(switch) + if switch==true or switch==nil then + self.emergency=true + else + self.emergency=false + end + return self +end + + +--- Despawn AI groups after they they shut down their engines. +-- @param #AIRBOSS self +-- @param #boolean switch If true or nil, AI groups are despawned. +-- @return #AIRBOSS self +function AIRBOSS:SetDespawnOnEngineShutdown(switch) + if switch==true or switch==nil then + self.despawnshutdown=true + else + self.despawnshutdown=false + end + return self +end + +--- Respawn AI groups once they reach the CCA. Clears any attached airbases and allows making them land on the carrier via script. +-- @param #AIRBOSS self +-- @param #boolean switch If true or nil, AI groups are respawned. +-- @return #AIRBOSS self +function AIRBOSS:SetRespawnAI(switch) + if switch==true or switch==nil then + self.respawnAI=true + else + self.respawnAI=false + end + return self +end + +--- Give AI aircraft the refueling task if a recovery tanker is present or send them to the nearest divert airfield. +-- @param #AIRBOSS self +-- @param #number lowfuelthreshold Low fuel threshold in percent. AI will go refueling if their fuel level drops below this value. Default 10 %. +-- @return #AIRBOSS self +function AIRBOSS:SetRefuelAI(lowfuelthreshold) + self.lowfuelAI=lowfuelthreshold or 10 + return self +end + +--- Set max alitude to register flights in the initial zone. Aircraft above this altitude will not be registerered. +-- @param #AIRBOSS self +-- @param #number altitude Max alitude in feet. Default 1300 ft. +-- @return #AIRBOSS self +function AIRBOSS:SetInitialMaxAlt(altitude) + self.initialmaxalt=UTILS.FeetToMeters(altitude or 1300) + return self +end + + +--- Set folder where the airboss sound files are located **within you mission (miz) file**. +-- The default path is "l10n/DEFAULT/" but sound files simply copied there will be removed by DCS the next time you save the mission. +-- However, if you create a new folder inside the miz file, which contains the sounds, it will not be deleted and can be used. +-- @param #AIRBOSS self +-- @param #string folderpath The path to the sound files, e.g. "Airboss Soundfiles/". +-- @return #AIRBOSS self +function AIRBOSS:SetSoundfilesFolder(folderpath) + + -- Check that it ends with / + if folderpath then + local lastchar=string.sub(folderpath, -1) + if lastchar~="/" then + folderpath=folderpath.."/" + end + end + + -- Folderpath. + self.soundfolder=folderpath + + -- Info message. + self:I(self.lid..string.format("Setting sound files folder to: %s", self.soundfolder)) + + return self +end + +--- Set time interval for updating player status and other things. +-- @param #AIRBOSS self +-- @param #number interval Time interval in seconds. Default 0.5 sec. +-- @return #AIRBOSS self +function AIRBOSS:SetStatusUpdateTime(interval) + self.dTstatus=interval or 0.5 + return self +end + +--- Set duration how long messages are displayed to players. +-- @param #AIRBOSS self +-- @param #number duration Duration in seconds. Default 10 sec. +-- @return #AIRBOSS self +function AIRBOSS:SetDefaultMessageDuration(duration) + self.Tmessage=duration or 10 + return self +end + + +--- Set glideslope error thresholds. +-- @param #AIRBOSS self +-- @param #number _max +-- @param #number _min +-- @param #number High +-- @param #number HIGH +-- @param #number Low +-- @param #number LOW +-- @return #AIRBOSS self +function AIRBOSS:SetGlideslopeErrorThresholds(_max,_min, High, HIGH, Low, LOW) + self.gle._max=_max or 0.4 + self.gle.High=High or 0.8 + self.gle.HIGH=HIGH or 1.5 + self.gle._min=_min or -0.3 + self.gle.Low=Low or -0.6 + self.gle.LOW=LOW or -0.9 + return self +end + +--- Set lineup error thresholds. +-- @param #AIRBOSS self +-- @param #number _max +-- @param #number _min +-- @param #number Left +-- @param #number LeftMed +-- @param #number LEFT +-- @param #number Right +-- @param #number RightMed +-- @param #number RIGHT +-- @return #AIRBOSS self +function AIRBOSS:SetLineupErrorThresholds(_max,_min, Left, LeftMed, LEFT, Right, RightMed, RIGHT) + self.lue._max=_max or 0.5 + self.lue._min=_min or -0.5 + self.lue.Left=Left or -1.0 + self.lue.LeftMed=LeftMed or -2.0 + self.lue.LEFT=LEFT or -3.0 + self.lue.Right=Right or 1.0 + self.lue.RightMed=RightMed or 2.0 + self.lue.RIGHT=RIGHT or 3.0 + return self +end + +--- Set Case I Marshal radius. This is the radius of the valid zone around "the post" aircraft are supposed to be holding in the Case I Marshal stack. +-- The post is 2.5 NM port of the carrier. +-- @param #AIRBOSS self +-- @param #number Radius in NM. Default 2.8 NM, which gives a diameter of 5.6 NM. +-- @return #AIRBOSS self +function AIRBOSS:SetMarshalRadius(radius) + self.marshalradius=UTILS.NMToMeters(radius or 2.8) + return self +end + +--- Optimized F10 radio menu for a single carrier. The menu entries will be stored directly under F10 Other/Airboss/ and not F10 Other/Airboss/"Carrier Alias"/. +-- **WARNING**: If you use this with two airboss objects/carriers, the radio menu will be screwed up! +-- @param #AIRBOSS self +-- @param #boolean switch If true or nil single menu is enabled. If false, menu is for multiple carriers in the mission. +-- @return #AIRBOSS self +function AIRBOSS:SetMenuSingleCarrier(switch) + if switch==true or switch==nil then + self.menusingle=true + else + self.menusingle=false + end + return self +end + +--- Enable or disable F10 radio menu for marking zones via smoke or flares. +-- @param #AIRBOSS self +-- @param #boolean switch If true or nil, menu is enabled. If false, menu is not available to players. +-- @return #AIRBOSS self +function AIRBOSS:SetMenuMarkZones(switch) + if switch==nil or switch==true then + self.menumarkzones=true + else + self.menumarkzones=false + end + return self +end + +--- Enable or disable F10 radio menu for marking zones via smoke. +-- @param #AIRBOSS self +-- @param #boolean switch If true or nil, menu is enabled. If false, menu is not available to players. +-- @return #AIRBOSS self +function AIRBOSS:SetMenuSmokeZones(switch) + if switch==nil or switch==true then + self.menusmokezones=true + else + self.menusmokezones=false + end + return self +end + +--- Enable saving of player's trap sheets and specify an optional directory path. +-- @param #AIRBOSS self +-- @param #string path (Optional) Path where to save the trap sheets. +-- @param #string prefix (Optional) Prefix for trap sheet files. File name will be saved as *prefix_aircrafttype-0001.csv*, *prefix_aircrafttype-0002.csv*, etc. +-- @return #AIRBOSS self +function AIRBOSS:SetTrapSheet(path, prefix) + if io then + self.trapsheet=true + self.trappath=path + self.trapprefix=prefix + else + self:E(self.lid.."ERROR: io is not desanitized. Cannot save trap sheet.") + end + return self +end + +--- Specify weather the mission has set static or dynamic weather. +-- @param #AIRBOSS self +-- @param #boolean switch If true or nil, mission uses static weather. If false, dynamic weather is used in this mission. +-- @return #AIRBOSS self +function AIRBOSS:SetStaticWeather(switch) + if switch==nil or switch==true then + self.staticweather=true + else + self.staticweather=false + end + return self +end + + +--- Disable automatic TACAN activation +-- @param #AIRBOSS self +-- @return #AIRBOSS self +function AIRBOSS:SetTACANoff() + self.TACANon=false + return self +end + +--- Set TACAN channel of carrier. +-- @param #AIRBOSS self +-- @param #number channel TACAN channel. Default 74. +-- @param #string mode TACAN mode, i.e. "X" or "Y". Default "X". +-- @param #string morsecode Morse code identifier. Three letters, e.g. "STN". +-- @return #AIRBOSS self +function AIRBOSS:SetTACAN(channel, mode, morsecode) + + self.TACANchannel=channel or 74 + self.TACANmode=mode or "X" + self.TACANmorse=morsecode or "STN" + self.TACANon=true + + return self +end + +--- Disable automatic ICLS activation. +-- @param #AIRBOSS self +-- @return #AIRBOSS self +function AIRBOSS:SetICLSoff() + self.ICLSon=false + return self +end + +--- Set ICLS channel of carrier. +-- @param #AIRBOSS self +-- @param #number channel ICLS channel. Default 1. +-- @param #string morsecode Morse code identifier. Three letters, e.g. "STN". Default "STN". +-- @return #AIRBOSS self +function AIRBOSS:SetICLS(channel, morsecode) + + self.ICLSchannel=channel or 1 + self.ICLSmorse=morsecode or "STN" + self.ICLSon=true + + return self +end + + +--- Set beacon (TACAN/ICLS) time refresh interfal in case the beacons die. +-- @param #AIRBOSS self +-- @param #number interval Time interval in seconds. Default 1200 sec = 20 min. +-- @return #AIRBOSS self +function AIRBOSS:SetBeaconRefresh(interval) + self.dTbeacon=interval or 20*60 + return self +end + + +--- Set LSO radio frequency and modulation. Default frequency is 264 MHz AM. +-- @param #AIRBOSS self +-- @param #number frequency Frequency in MHz. Default 264 MHz. +-- @param #string modulation Modulation, i.e. "AM" (default) or "FM". +-- @return #AIRBOSS self +function AIRBOSS:SetLSORadio(frequency, modulation) + + self.LSOFreq=(frequency or 264) + modulation=modulation or "AM" + + if modulation=="FM" then + self.LSOModu=radio.modulation.FM + else + self.LSOModu=radio.modulation.AM + end + + self.LSORadio={} --#AIRBOSS.Radio + self.LSORadio.frequency=self.LSOFreq + self.LSORadio.modulation=self.LSOModu + self.LSORadio.alias="LSO" + + return self +end + +--- Set carrier radio frequency and modulation. Default frequency is 305 MHz AM. +-- @param #AIRBOSS self +-- @param #number frequency Frequency in MHz. Default 305 MHz. +-- @param #string modulation Modulation, i.e. "AM" (default) or "FM". +-- @return #AIRBOSS self +function AIRBOSS:SetMarshalRadio(frequency, modulation) + + self.MarshalFreq=frequency or 305 + modulation=modulation or "AM" + + if modulation=="FM" then + self.MarshalModu=radio.modulation.FM + else + self.MarshalModu=radio.modulation.AM + end + + self.MarshalRadio={} --#AIRBOSS.Radio + self.MarshalRadio.frequency=self.MarshalFreq + self.MarshalRadio.modulation=self.MarshalModu + self.MarshalRadio.alias="MARSHAL" + + return self +end + +--- Set unit name for sending radio messages. +-- @param #AIRBOSS self +-- @param #string unitname Name of the unit. +-- @return #AIRBOSS self +function AIRBOSS:SetRadioUnitName(unitname) + self.senderac=unitname + return self +end + +--- Set unit acting as radio relay for the LSO radio. +-- @param #AIRBOSS self +-- @param #string unitname Name of the unit. +-- @return #AIRBOSS self +function AIRBOSS:SetRadioRelayLSO(unitname) + self.radiorelayLSO=unitname + return self +end + +--- Set unit acting as radio relay for the Marshal radio. +-- @param #AIRBOSS self +-- @param #string unitname Name of the unit. +-- @return #AIRBOSS self +function AIRBOSS:SetRadioRelayMarshal(unitname) + self.radiorelayMSH=unitname + return self +end + + +--- Use user sound output instead of radio transmission for messages. Might be handy if radio transmissions are broken. +-- @param #AIRBOSS self +-- @return #AIRBOSS self +function AIRBOSS:SetUserSoundRadio() + self.usersoundradio=true + return self +end + +--- Test LSO radio sounds. +-- @param #AIRBOSS self +-- @param #number delay Delay in seconds be sound check starts. +-- @return #AIRBOSS self +function AIRBOSS:SoundCheckLSO(delay) + + if delay and delay>0 then + -- Delayed call. + --SCHEDULER:New(nil, AIRBOSS.SoundCheckLSO, {self}, delay) + self:ScheduleOnce(delay, AIRBOSS.SoundCheckLSO, self) + else + + + local text="Playing LSO sound files:" + + for _name,_call in pairs(self.LSOCall) do + local call=_call --#AIRBOSS.RadioCall + + -- Debug text. + text=text..string.format("\nFile=%s.%s, duration=%.2f sec, loud=%s, subtitle=\"%s\".", call.file, call.suffix, call.duration, tostring(call.loud), call.subtitle) + + -- Radio transmission to queue. + self:RadioTransmission(self.LSORadio, call, false) + + -- Also play the loud version. + if call.loud then + self:RadioTransmission(self.LSORadio, call, true) + end + end + + -- Debug message. + self:I(self.lid..text) + + end +end + +--- Test Marshal radio sounds. +-- @param #AIRBOSS self +-- @param #number delay Delay in seconds be sound check starts. +-- @return #AIRBOSS self +function AIRBOSS:SoundCheckMarshal(delay) + + if delay and delay>0 then + -- Delayed call. + --SCHEDULER:New(nil, AIRBOSS.SoundCheckMarshal, {self}, delay) + self:ScheduleOnce(delay, AIRBOSS.SoundCheckMarshal, self) + else + + + local text="Playing Marshal sound files:" + + for _name,_call in pairs(self.MarshalCall) do + local call=_call --#AIRBOSS.RadioCall + + -- Debug text. + text=text..string.format("\nFile=%s.%s, duration=%.2f sec, loud=%s, subtitle=\"%s\".", call.file, call.suffix, call.duration, tostring(call.loud), call.subtitle) + + -- Radio transmission to queue. + self:RadioTransmission(self.MarshalRadio, call, false) + + -- Also play the loud version. + if call.loud then + self:RadioTransmission(self.MarshalRadio, call, true) + end + end + + -- Debug message. + self:I(self.lid..text) + + end +end + +--- Set number of aircraft units, which can be in the landing pattern before the pattern is full. +-- @param #AIRBOSS self +-- @param #number nmax Max number. Default 4. Minimum is 1, maximum is 6. +-- @return #AIRBOSS self +function AIRBOSS:SetMaxLandingPattern(nmax) + nmax=nmax or 4 + nmax=math.max(nmax,1) + nmax=math.min(nmax,6) + self.Nmaxpattern=nmax + return self +end + +--- Set number available Case I Marshal stacks. If Marshal stacks are full, flights requesting Marshal will be told to hold outside 10 NM zone until a stack becomes available again. +-- Marshal stacks for Case II/III are unlimited. +-- @param #AIRBOSS self +-- @param #number nmax Max number of stacks available to players and AI flights. Default 3, i.e. angels 2, 3, 4. Minimum is 1. +-- @return #AIRBOSS self +function AIRBOSS:SetMaxMarshalStacks(nmax) + self.Nmaxmarshal=nmax or 3 + self.Nmaxmarshal=math.max(self.Nmaxmarshal, 1) + return self +end + +--- Set max number of section members. Minimum is one, i.e. the section lead itself. Maximum number is four. Default is two, i.e. the lead and one other human flight. +-- @param #AIRBOSS self +-- @param #number nmax Number of max allowed members including the lead itself. For example, Nmax=2 means a section lead plus one member. +-- @return #AIRBOSS self +function AIRBOSS:SetMaxSectionSize(nmax) + nmax=nmax or 2 + nmax=math.max(nmax,1) + nmax=math.min(nmax,4) + self.NmaxSection=nmax-1 -- We substract one because internally the section lead is not counted! + return self +end + +--- Set max number of flights per stack. All members of a section count as one "flight". +-- @param #AIRBOSS self +-- @param #number nmax Number of max allowed flights per stack. Default is two. Minimum is one, maximum is 4. +-- @return #AIRBOSS self +function AIRBOSS:SetMaxFlightsPerStack(nmax) + nmax=nmax or 2 + nmax=math.max(nmax,1) + nmax=math.min(nmax,4) + self.NmaxStack=nmax + return self +end + + +--- Handle AI aircraft. +-- @param #AIRBOSS self +-- @return #AIRBOSS self +function AIRBOSS:SetHandleAION() + self.handleai=true + return self +end + +--- Do not handle AI aircraft. +-- @param #AIRBOSS self +-- @return #AIRBOSS self +function AIRBOSS:SetHandleAIOFF() + self.handleai=false + return self +end + + +--- Define recovery tanker associated with the carrier. +-- @param #AIRBOSS self +-- @param Ops.RecoveryTanker#RECOVERYTANKER recoverytanker Recovery tanker object. +-- @return #AIRBOSS self +function AIRBOSS:SetRecoveryTanker(recoverytanker) + self.tanker=recoverytanker + return self +end + +--- Define an AWACS associated with the carrier. +-- @param #AIRBOSS self +-- @param Ops.RecoveryTanker#RECOVERYTANKER awacs AWACS (recovery tanker) object. +-- @return #AIRBOSS self +function AIRBOSS:SetAWACS(awacs) + self.awacs=awacs + return self +end + +--- Set default player skill. New players will be initialized with this skill. +-- +-- * "Flight Student" = @{#AIRBOSS.Difficulty.Easy} +-- * "Naval Aviator" = @{#AIRBOSS.Difficulty.Normal} +-- * "TOPGUN Graduate" = @{#AIRBOSS.Difficulty.Hard} +-- @param #AIRBOSS self +-- @param #string skill Player skill. Default "Naval Aviator". +-- @return #AIRBOSS self +function AIRBOSS:SetDefaultPlayerSkill(skill) + + -- Set skill or normal. + self.defaultskill=skill or AIRBOSS.Difficulty.NORMAL + + -- Check that defualt skill is valid. + local gotit=false + for _,_skill in pairs(AIRBOSS.Difficulty) do + if _skill==self.defaultskill then + gotit=true + end + end + + -- If invalid user input, fall back to normal. + if not gotit then + self.defaultskill=AIRBOSS.Difficulty.NORMAL + self:E(self.lid..string.format("ERROR: Invalid default skill = %s. Resetting to Naval Aviator.", tostring(skill))) + end + + return self +end + +--- Enable auto save of player results each time a player is *finally* graded. *Finally* means after the player landed on the carrier! After intermediate passes (bolter or waveoff) the stats are *not* saved. +-- @param #AIRBOSS self +-- @param #string path Path where to save the asset data file. Default is the DCS root installation directory or your "Saved Games\\DCS" folder if lfs was desanitized. +-- @param #string filename File name. Default is generated automatically from airboss carrier name/alias. +-- @return #AIRBOSS self +function AIRBOSS:SetAutoSave(path, filename) + self.autosave=true + self.autosavepath=path + self.autosavefile=filename + return self +end + +--- Activate debug mode. Display debug messages on screen. +-- @param #AIRBOSS self +-- @return #AIRBOSS self +function AIRBOSS:SetDebugModeON() + self.Debug=true + return self +end + +--- Carrier patrols ad inifintum. If the last waypoint is reached, it will go to waypoint one and repeat its route. +-- @param #AIRBOSS self +-- @param #boolean switch If true or nil, patrol until the end of time. If false, go along the waypoints once and stop. +-- @return #AIRBOSS self +function AIRBOSS:SetPatrolAdInfinitum(switch) + if switch==false then + self.adinfinitum=false + else + self.adinfinitum=true + end + return self +end + +--- Set the magnetic declination (or variation). By default this is set to the standard declination of the map. +-- @param #AIRBOSS self +-- @param #number declination Declination in degrees or nil for default declination of the map. +-- @return #AIRBOSS self +function AIRBOSS:SetMagneticDeclination(declination) + self.magvar=declination or UTILS.GetMagneticDeclination() + return self +end + +--- Deactivate debug mode. This is also the default setting. +-- @param #AIRBOSS self +-- @return #AIRBOSS self +function AIRBOSS:SetDebugModeOFF() + self.Debug=false + return self +end + +--- Get next time the carrier will start recovering aircraft. +-- @param #AIRBOSS self +-- @param #boolean InSeconds If true, abs. mission time seconds is returned. Default is a clock #string. +-- @return #string Clock start (or start time in abs. seconds). +-- @return #string Clock stop (or stop time in abs. seconds). +function AIRBOSS:GetNextRecoveryTime(InSeconds) + if self.recoverywindow then + if InSeconds then + return self.recoverywindow.START, self.recoverywindow.STOP + else + return UTILS.SecondsToClock(self.recoverywindow.START), UTILS.SecondsToClock(self.recoverywindow.STOP) + end + else + if InSeconds then + return -1, -1 + else + return "?", "?" + end + end +end + +--- Check if carrier is recovering aircraft. +-- @param #AIRBOSS self +-- @return #boolean If true, time slot for recovery is open. +function AIRBOSS:IsRecovering() + return self:is("Recovering") +end + +--- Check if carrier is idle, i.e. no operations are carried out. +-- @param #AIRBOSS self +-- @return #boolean If true, carrier is in idle state. +function AIRBOSS:IsIdle() + return self:is("Idle") +end + +--- Check if recovery of aircraft is paused. +-- @param #AIRBOSS self +-- @return #boolean If true, recovery is paused +function AIRBOSS:IsPaused() + return self:is("Paused") +end + +--- Activate TACAN and ICLS beacons. +-- @param #AIRBOSS self +function AIRBOSS:_ActivateBeacons() + self:T(self.lid..string.format("Activating Beacons (TACAN=%s, ICLS=%s)", tostring(self.TACANon), tostring(self.ICLSon))) + + -- Activate TACAN. + if self.TACANon then + self:I(self.lid..string.format("Activating TACAN Channel %d%s (%s)", self.TACANchannel, self.TACANmode, self.TACANmorse)) + self.beacon:ActivateTACAN(self.TACANchannel, self.TACANmode, self.TACANmorse, true) + end + + -- Activate ICLS. + if self.ICLSon then + self:I(self.lid..string.format("Activating ICLS Channel %d (%s)", self.ICLSchannel, self.ICLSmorse)) + self.beacon:ActivateICLS(self.ICLSchannel, self.ICLSmorse) + end + + -- Set time stamp. + self.Tbeacon=timer.getTime() +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM event functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after Start event. Starts the AIRBOSS. Adds event handlers and schedules status updates of requests and queue. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AIRBOSS:onafterStart(From, Event, To) + + -- Events are handled my MOOSE. + self:I(self.lid..string.format("Starting AIRBOSS v%s for carrier unit %s of type %s on map %s", AIRBOSS.version, self.carrier:GetName(), self.carriertype, self.theatre)) + + -- Activate TACAN and ICLS if desired. + self:_ActivateBeacons() + + -- Schedule radio queue checks. + --self.RQLid=self.radiotimer:Schedule(nil, AIRBOSS._CheckRadioQueue, {self, self.RQLSO, "LSO"}, 1, 0.1) + --self.RQMid=self.radiotimer:Schedule(nil, AIRBOSS._CheckRadioQueue, {self, self.RQMarshal, "MARSHAL"}, 1, 0.1) + + --self:I("FF: starting timer.scheduleFunction") + --timer.scheduleFunction(AIRBOSS._CheckRadioQueueT, {airboss=self, radioqueue=self.RQLSO, name="LSO"}, timer.getTime()+1) + --timer.scheduleFunction(AIRBOSS._CheckRadioQueueT, {airboss=self, radioqueue=self.RQMarshal, name="MARSHAL"}, timer.getTime()+1) + + -- Initial carrier position and orientation. + self.Cposition=self:GetCoordinate() + self.Corientation=self.carrier:GetOrientationX() + self.Corientlast=self.Corientation + self.Tpupdate=timer.getTime() + + -- Check if no recovery window is set. DISABLED! + if #self.recoverytimes==0 and false then + + -- Open window in 15 minutes for 3 hours. + local Topen=timer.getAbsTime()+15*60 + local Tclose=Topen+3*60*60 + + -- Add window. + self:AddRecoveryWindow(UTILS.SecondsToClock(Topen), UTILS.SecondsToClock(Tclose)) + end + + -- Check Recovery time.s + self:_CheckRecoveryTimes() + + -- Time stamp for checking queues. We substract 60 seconds so the routine is called right after status is called the first time. + self.Tqueue=timer.getTime()-60 + + -- Handle events. + self:HandleEvent(EVENTS.Birth) + self:HandleEvent(EVENTS.Land) + self:HandleEvent(EVENTS.EngineShutdown) + self:HandleEvent(EVENTS.Takeoff) + self:HandleEvent(EVENTS.Crash) + self:HandleEvent(EVENTS.Ejection) + self:HandleEvent(EVENTS.PlayerLeaveUnit, self._PlayerLeft) + self:HandleEvent(EVENTS.MissionEnd) + self:HandleEvent(EVENTS.RemoveUnit) + + --self.StatusScheduler=SCHEDULER:New(self) + --self.StatusScheduler:Schedule(self, self._Status, {}, 1, 0.5) + + self.StatusTimer=TIMER:New(self._Status, self):Start(2, 0.5) + + -- Start status check in 1 second. + self:__Status(1) +end + +--- On after Status event. Checks for new flights, updates queue and checks player status. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AIRBOSS:onafterStatus(From, Event, To) + + -- Get current time. + local time=timer.getTime() + + -- Update marshal and pattern queue every 30 seconds. + if time-self.Tqueue>self.dTqueue then + + -- Get time. + local clock=UTILS.SecondsToClock(timer.getAbsTime()) + local eta=UTILS.SecondsToClock(self:_GetETAatNextWP()) + + -- Current heading and position of the carrier. + local hdg=self:GetHeading() + local pos=self:GetCoordinate() + local speed=self.carrier:GetVelocityKNOTS() + + -- Check water is ahead. + local collision=false --self:_CheckCollisionCoord(pos:Translate(self.collisiondist, hdg)) + + local holdtime=0 + if self.holdtimestamp then + holdtime=timer.getTime()-self.holdtimestamp + end + + -- Check if carrier is stationary. + local NextWP=self:_GetNextWaypoint() + local ExpectedSpeed=UTILS.MpsToKnots(NextWP:GetVelocity()) + if speed<0.5 and ExpectedSpeed>0 and not (self.detour or self.turnintowind) then + if not self.holdtimestamp then + self:E(self.lid..string.format("Carrier came to an unexpected standstill. Trying to re-route in 3 min. Speed=%.1f knots, expected=%.1f knots", speed, ExpectedSpeed)) + self.holdtimestamp=timer.getTime() + else + if holdtime>3*60 then + local coord=self:GetCoordinate():Translate(500, hdg+10) + --coord:MarkToAll("Re-route after standstill.") + self:CarrierResumeRoute(coord) + self.holdtimestamp=nil + end + end + end + + -- Debug info. + local text=string.format("Time %s - Status %s (case=%d) - Speed=%.1f kts - Heading=%d - WP=%d - ETA=%s - Turning=%s - Collision Warning=%s - Detour=%s - Turn Into Wind=%s - Holdtime=%d sec", + clock, self:GetState(), self.case, speed, hdg, self.currentwp, eta, tostring(self.turning), tostring(collision), tostring(self.detour), tostring(self.turnintowind), holdtime) + self:T(self.lid..text) + + -- Players online: + text="Players:" + local i=0 + for _name,_player in pairs(self.players) do + i=i+1 + local player=_player --#AIRBOSS.FlightGroup + text=text..string.format("\n%d.) %s: Step=%s, Unit=%s, Airframe=%s", i, tostring(player.name), tostring(player.step), tostring(player.unitname), tostring(player.actype)) + end + if i==0 then + text=text.." none" + end + self:I(self.lid..text) + + -- Check for collision. + if collision then + + -- We are currently turning into the wind. + if self.turnintowind then + + -- Carrier resumes its initial route. This disables turnintowind switch. + self:CarrierResumeRoute(self.Creturnto) + + -- Since current window would stay open, we disable the WIND switch. + if self:IsRecovering() and self.recoverywindow and self.recoverywindow.WIND then + -- Disable turn into the wind for this window so that we do not do this all over again. + self.recoverywindow.WIND=false + end + + end + + end + + + -- Check recovery times and start/stop recovery mode if necessary. + self:_CheckRecoveryTimes() + + -- Remove dead/zombie flight groups. Player leaving the server whilst in pattern etc. + --self:_RemoveDeadFlightGroups() + + -- Scan carrier zone for new aircraft. + self:_ScanCarrierZone() + + -- Check marshal and pattern queues. + self:_CheckQueue() + + -- Check if carrier is currently turning. + self:_CheckCarrierTurning() + + -- Check if marshal pattern of AI needs an update. + self:_CheckPatternUpdate() + + -- Time stamp. + self.Tqueue=time + end + + -- (Re-)activate TACAN and ICLS channels. + if time-self.Tbeacon>self.dTbeacon then + self:_ActivateBeacons() + end + + -- Call status every ~0.5 seconds. + self:__Status(-30) + +end + +--- Check AI status. Pattern queue AI in the groove? Marshal queue AI arrived in holding zone? +-- @param #AIRBOSS self +function AIRBOSS:_Status() + + -- Check player status. + self:_CheckPlayerStatus() + + -- Check AI landing pattern status + self:_CheckAIStatus() + +end + +--- Check AI status. Pattern queue AI in the groove? Marshal queue AI arrived in holding zone? +-- @param #AIRBOSS self +function AIRBOSS:_CheckAIStatus() + + -- Loop over all flights in Marshal stack. + for _,_flight in pairs(self.Qmarshal) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Only AI! + if flight.ai then + + -- Get fuel amount in %. + local fuel=flight.group:GetFuelMin()*100 + + -- Debug text. + local text=string.format("Group %s fuel=%.1f %%", flight.groupname, fuel) + self:T3(self.lid..text) + + -- Check if flight is low on fuel and not yet refueling. + if self.lowfuelAI and fuel=recovery.START then + -- Start time has passed. + + if time0 then + + -- Extend recovery time. 5 min per flight. + local extmin=5*npattern + recovery.STOP=recovery.STOP+extmin*60 + + local text=string.format("We still got flights in the pattern.\nRecovery time prolonged by %d minutes.\nNow get your act together and no more bolters!", extmin) + self:MessageToPattern(text, "AIRBOSS", "99", 10, false, nil) + + else + + -- Set carrier to idle. + self:RecoveryStop() + state="closing now" + + -- Closed. + recovery.OPEN=false + + -- Window just closed. + recovery.OVER=true + + end + else + + -- Carrier is already idle. + state="closed" + end + + end + + else + -- This recovery is in the future. + state="in the future" + + -- This is the next to come as we sorted by start time. + if nextwindow==nil then + nextwindow=recovery + state="next in line" + end + end + + -- Debug text. + text=text..string.format("\n- Start=%s Stop=%s Case=%d Offset=%d Open=%s Closed=%s Status=\"%s\"", Cstart, Cstop, recovery.CASE, recovery.OFFSET, tostring(recovery.OPEN), tostring(recovery.OVER), state) + end + + -- Debug output. + self:T(self.lid..text) + + -- Current recovery window. + self.recoverywindow=nil + + + if self:IsIdle() then + ----------------------------------------------------------------------------------------------------------------- + -- Carrier is idle: We need to make sure that incoming flights get the correct recovery info of the next window. + ----------------------------------------------------------------------------------------------------------------- + + -- Check if there is a next windows defined. + if nextwindow then + + -- Set case and offset of the next window. + self:RecoveryCase(nextwindow.CASE, nextwindow.OFFSET) + + -- Check if time is less than 5 minutes. + if nextwindow.WIND and nextwindow.START-time 5° different from the current heading. + local hdg=self:GetHeading() + local wind=self:GetHeadingIntoWind() + local delta=self:_GetDeltaHeading(hdg, wind) + local uturn=delta>5 + + -- Check if wind is actually blowing (0.1 m/s = 0.36 km/h = 0.2 knots) + local _,vwind=self:GetWind() + if vwind<0.1 then + uturn=false + end + + -- U-turn disabled by user input. + if not nextwindow.UTURN then + uturn=false + end + + --Debug info + self:T(self.lid..string.format("Heading=%03d°, Wind=%03d° %.1f kts, Delta=%03d° ==> U-turn=%s", hdg, wind,UTILS.MpsToKnots(vwind), delta, tostring(uturn))) + + -- Time into the wind 1 day or if longer recovery time + the 5 min early. + local t=math.max(nextwindow.STOP-nextwindow.START+self.dTturn, 60*60*24) + + -- Recovery wind on deck in knots. + local v=UTILS.KnotsToMps(nextwindow.SPEED) + + -- Check that we do not go above max possible speed. + local vmax=self.carrier:GetSpeedMax()/3.6 -- convert to m/s + v=math.min(v,vmax) + + -- Route carrier into the wind. Sets self.turnintowind=true + self:CarrierTurnIntoWind(t, v, uturn) + + end + + -- Set current recovery window. + self.recoverywindow=nextwindow + + else + -- No next window. Set default values. + self:RecoveryCase() + end + + else + ------------------------------------------------------------------------------------- + -- Carrier is recovering: We set the recovery window to the current one or next one. + ------------------------------------------------------------------------------------- + + if currwindow then + self.recoverywindow=currwindow + else + self.recoverywindow=nextwindow + end + end + + self:T2({"FF", recoverywindow=self.recoverywindow}) +end + +--- Get section lead of a flight. +--@param #AIRBOSS self +--@param #AIRBOSS.FlightGroup flight +--@return #AIRBOSS.FlightGroup The leader of the section. Could be the flight itself. +--@return #boolean If true, flight is lead. +function AIRBOSS:_GetFlightLead(flight) + + if flight.name~=flight.seclead then + -- Section lead of flight. + local lead=self.players[flight.seclead] + return lead,false + else + -- Flight without section or section lead. + return flight,true + end + +end + +--- On before "RecoveryCase" event. Check if case or holding offset did change. If not transition is denied. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number Case The recovery case (1, 2 or 3) to switch to. +-- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. +function AIRBOSS:onbeforeRecoveryCase(From, Event, To, Case, Offset) + + -- Input or default value. + Case=Case or self.defaultcase + + -- Input or default value + Offset=Offset or self.defaultoffset + + if Case==self.case and Offset==self.holdingoffset then + return false + end + + return true +end + +--- On after "RecoveryCase" event. Sets new aircraft recovery case. Updates +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number Case The recovery case (1, 2 or 3) to switch to. +-- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. +function AIRBOSS:onafterRecoveryCase(From, Event, To, Case, Offset) + + -- Input or default value. + Case=Case or self.defaultcase + + -- Input or default value + Offset=Offset or self.defaultoffset + + -- Debug output. + local text=string.format("Switching recovery case %d ==> %d", self.case, Case) + if Case>1 then + text=text..string.format(" Holding offset angle %d degrees.", Offset) + end + MESSAGE:New(text, 20, self.alias):ToAllIf(self.Debug) + self:T(self.lid..text) + + -- Set new recovery case. + self.case=Case + + -- Set holding offset. + self.holdingoffset=Offset + + -- Update case of all flights not in Marshal or Pattern queue. + for _,_flight in pairs(self.flights) do + local flight=_flight --#AIRBOSS.FlightGroup + if not (self:_InQueue(self.Qmarshal, flight.group) or self:_InQueue(self.Qpattern, flight.group)) then + + -- Also not for section members. These are not in the marshal or pattern queue if the lead is. + if flight.name~=flight.seclead then + local lead=self.players[flight.seclead] + + if lead and not (self:_InQueue(self.Qmarshal, lead.group) or self:_InQueue(self.Qpattern, lead.group)) then + -- This is section member and the lead is not in the Marshal or Pattern queue. + flight.case=self.case + end + + else + + -- This is a flight without section or the section lead. + flight.case=self.case + + end + + end + end +end + +--- On after "RecoveryStart" event. Recovery of aircraft is started and carrier switches to state "Recovering". +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number Case The recovery case (1, 2 or 3) to start. +-- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. +function AIRBOSS:onafterRecoveryStart(From, Event, To, Case, Offset) + + -- Input or default value. + Case=Case or self.defaultcase + + -- Input or default value. + Offset=Offset or self.defaultoffset + + -- Radio message: "99, starting aircraft recovery case X ops. (Marshal radial XYZ degrees)" + self:_MarshalCallRecoveryStart(Case) + + -- Switch to case. + self:RecoveryCase(Case, Offset) +end + +--- On after "RecoveryStop" event. Recovery of aircraft is stopped and carrier switches to state "Idle". Running recovery window is deleted. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AIRBOSS:onafterRecoveryStop(From, Event, To) + -- Debug output. + self:T(self.lid..string.format("Stopping aircraft recovery.")) + + -- Recovery ops stopped message. + self:_MarshalCallRecoveryStopped(self.case) + + -- If carrier is currently heading into the wind, we resume the original route. + if self.turnintowind then + + -- Coordinate to return to. + local coord=self.Creturnto + + -- No U-turn. + if self.recoverywindow and self.recoverywindow.UTURN==false then + coord=nil + end + + -- Carrier resumes route. + self:CarrierResumeRoute(coord) + end + + -- Delete current recovery window if open. + if self.recoverywindow and self.recoverywindow.OPEN==true then + self.recoverywindow.OPEN=false + self.recoverywindow.OVER=true + self:DeleteRecoveryWindow(self.recoverywindow) + end + + -- Check recovery windows. This sets self.recoverywindow to the next window. + self:_CheckRecoveryTimes() +end + + +--- On after "RecoveryPause" event. Recovery of aircraft is paused. Marshal queue stays intact. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number duration Duration of pause in seconds. After that recovery is resumed automatically. +function AIRBOSS:onafterRecoveryPause(From, Event, To, duration) + -- Debug output. + self:T(self.lid..string.format("Pausing aircraft recovery.")) + + -- Message text + + if duration then + + -- Auto resume. + self:__RecoveryUnpause(duration) + + -- Time to resume. + local clock=UTILS.SecondsToClock(timer.getAbsTime()+duration) + + -- Marshal call: "99, aircraft recovery paused and will be resume at XX:YY." + self:_MarshalCallRecoveryPausedResumedAt(clock) + else + + local text=string.format("aircraft recovery is paused until further notice.") + + -- Marshal call: "99, aircraft recovery paused until further notice." + self:_MarshalCallRecoveryPausedNotice() + + end + +end + +--- On after "RecoveryUnpause" event. Recovery of aircraft is resumed. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AIRBOSS:onafterRecoveryUnpause(From, Event, To) + -- Debug output. + self:T(self.lid..string.format("Unpausing aircraft recovery.")) + + -- Resume recovery. + self:_MarshalCallResumeRecovery() + +end + +--- On after "PassingWaypoint" event. Carrier has just passed a waypoint +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number n Number of waypoint that was passed. +function AIRBOSS:onafterPassingWaypoint(From, Event, To, n) + -- Debug output. + self:I(self.lid..string.format("Carrier passed waypoint %d.", n)) +end + +--- On after "Idle" event. Carrier goes to state "Idle". +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AIRBOSS:onafterIdle(From, Event, To) + -- Debug output. + self:T(self.lid..string.format("Carrier goes to idle.")) +end + +--- On after Stop event. Unhandle events. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AIRBOSS:onafterStop(From, Event, To) + self:I(self.lid..string.format("Stopping airboss script.")) + + -- Unhandle events. + self:UnHandleEvent(EVENTS.Birth) + self:UnHandleEvent(EVENTS.Land) + self:UnHandleEvent(EVENTS.EngineShutdown) + self:UnHandleEvent(EVENTS.Takeoff) + self:UnHandleEvent(EVENTS.Crash) + self:UnHandleEvent(EVENTS.Ejection) + self:UnHandleEvent(EVENTS.PlayerLeaveUnit) + self:UnHandleEvent(EVENTS.MissionEnd) + + self.CallScheduler:Clear() +end + +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Parameter initialization +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Init parameters for USS Stennis carrier. +-- @param #AIRBOSS self +function AIRBOSS:_InitStennis() + + -- Carrier Parameters. + self.carrierparam.sterndist =-153 + self.carrierparam.deckheight = 19.06 + + -- Total size of the carrier (approx as rectangle). + self.carrierparam.totlength=310 -- Wiki says 332.8 meters overall length. + self.carrierparam.totwidthport=40 -- Wiki says 76.8 meters overall beam. + self.carrierparam.totwidthstarboard=30 + + -- Landing runway. + self.carrierparam.rwyangle = -9.1359 + self.carrierparam.rwylength = 225 + self.carrierparam.rwywidth = 20 + + -- Wires. + self.carrierparam.wire1 = 46 -- Distance from stern to first wire. + self.carrierparam.wire2 = 46+12 + self.carrierparam.wire3 = 46+24 + self.carrierparam.wire4 = 46+35 -- Last wire is strangely one meter closer. + + + -- Platform at 5k. Reduce descent rate to 2000 ft/min to 1200 dirty up level flight. + self.Platform.name="Platform 5k" + self.Platform.Xmin=-UTILS.NMToMeters(22) -- Not more than 22 NM behind the boat. Last check was at 21 NM. + self.Platform.Xmax =nil + self.Platform.Zmin=-UTILS.NMToMeters(30) -- Not more than 30 NM port of boat. + self.Platform.Zmax= UTILS.NMToMeters(30) -- Not more than 30 NM starboard of boat. + self.Platform.LimitXmin=nil -- Limits via zone + self.Platform.LimitXmax=nil + self.Platform.LimitZmin=nil + self.Platform.LimitZmax=nil + + -- Level out at 1200 ft and dirty up. + self.DirtyUp.name="Dirty Up" + self.DirtyUp.Xmin=-UTILS.NMToMeters(21) -- Not more than 21 NM behind the boat. + self.DirtyUp.Xmax= nil + self.DirtyUp.Zmin=-UTILS.NMToMeters(30) -- Not more than 30 NM port of boat. + self.DirtyUp.Zmax= UTILS.NMToMeters(30) -- Not more than 30 NM starboard of boat. + self.DirtyUp.LimitXmin=nil -- Limits via zone + self.DirtyUp.LimitXmax=nil + self.DirtyUp.LimitZmin=nil + self.DirtyUp.LimitZmax=nil + + -- Intercept glide slope and follow bullseye. + self.Bullseye.name="Bullseye" + self.Bullseye.Xmin=-UTILS.NMToMeters(11) -- Not more than 11 NM behind the boat. Last check was at 10 NM. + self.Bullseye.Xmax= nil + self.Bullseye.Zmin=-UTILS.NMToMeters(30) -- Not more than 30 NM port. + self.Bullseye.Zmax= UTILS.NMToMeters(30) -- Not more than 30 NM starboard. + self.Bullseye.LimitXmin=nil -- Limits via zone. + self.Bullseye.LimitXmax=nil + self.Bullseye.LimitZmin=nil + self.Bullseye.LimitZmax=nil + + -- Break entry. + self.BreakEntry.name="Break Entry" + self.BreakEntry.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. Check for initial is at 3 NM with a radius of 500 m and 100 m starboard. + self.BreakEntry.Xmax= nil + self.BreakEntry.Zmin=-UTILS.NMToMeters(0.5) -- Not more than 0.5 NM port of boat. + self.BreakEntry.Zmax= UTILS.NMToMeters(1.5) -- Not more than 1.5 NM starboard. + self.BreakEntry.LimitXmin=0 -- Check and next step when at carrier and starboard of carrier. + self.BreakEntry.LimitXmax=nil + self.BreakEntry.LimitZmin=nil + self.BreakEntry.LimitZmax=nil + + -- Early break. + self.BreakEarly.name="Early Break" + self.BreakEarly.Xmin=-UTILS.NMToMeters(1) -- Not more than 1 NM behind the boat. Last check was at 0. + self.BreakEarly.Xmax= UTILS.NMToMeters(5) -- Not more than 5 NM in front of the boat. Enough for late breaks? + self.BreakEarly.Zmin=-UTILS.NMToMeters(2) -- Not more than 2 NM port. + self.BreakEarly.Zmax= UTILS.NMToMeters(1) -- Not more than 1 NM starboard. + self.BreakEarly.LimitXmin= 0 -- Check and next step 0.2 NM port and in front of boat. + self.BreakEarly.LimitXmax= nil + self.BreakEarly.LimitZmin=-UTILS.NMToMeters(0.2) -- -370 m port + self.BreakEarly.LimitZmax= nil + + -- Late break. + self.BreakLate.name="Late Break" + self.BreakLate.Xmin=-UTILS.NMToMeters(1) -- Not more than 1 NM behind the boat. Last check was at 0. + self.BreakLate.Xmax= UTILS.NMToMeters(5) -- Not more than 5 NM in front of the boat. Enough for late breaks? + self.BreakLate.Zmin=-UTILS.NMToMeters(2) -- Not more than 2 NM port. + self.BreakLate.Zmax= UTILS.NMToMeters(1) -- Not more than 1 NM starboard. + self.BreakLate.LimitXmin= 0 -- Check and next step 0.8 NM port and in front of boat. + self.BreakLate.LimitXmax= nil + self.BreakLate.LimitZmin=-UTILS.NMToMeters(0.8) -- -1470 m port + self.BreakLate.LimitZmax= nil + + -- Abeam position. + self.Abeam.name="Abeam Position" + self.Abeam.Xmin=-UTILS.NMToMeters(5) -- Not more then 5 NM astern of boat. Should be LIG call anyway. + self.Abeam.Xmax= UTILS.NMToMeters(5) -- Not more then 5 NM ahead of boat. + self.Abeam.Zmin=-UTILS.NMToMeters(2) -- Not more than 2 NM port. + self.Abeam.Zmax= 500 -- Not more than 500 m starboard. Must be port! + self.Abeam.LimitXmin=-200 -- Check and next step 200 meters behind the ship. + self.Abeam.LimitXmax= nil + self.Abeam.LimitZmin= nil + self.Abeam.LimitZmax= nil + + -- At the Ninety. + self.Ninety.name="Ninety" + self.Ninety.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. LIG check anyway. + self.Ninety.Xmax= 0 -- Must be behind the boat. + self.Ninety.Zmin=-UTILS.NMToMeters(2) -- Not more than 2 NM port of boat. + self.Ninety.Zmax= nil + self.Ninety.LimitXmin=nil + self.Ninety.LimitXmax=nil + self.Ninety.LimitZmin=nil + self.Ninety.LimitZmax=-UTILS.NMToMeters(0.6) -- Check and next step when 0.6 NM port. + + -- At the Wake. + self.Wake.name="Wake" + self.Wake.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. + self.Wake.Xmax= 0 -- Must be behind the boat. + self.Wake.Zmin=-2000 -- Not more than 2 km port of boat. + self.Wake.Zmax= nil + self.Wake.LimitXmin=nil + self.Wake.LimitXmax=nil + self.Wake.LimitZmin=0 -- Check and next step when directly behind the boat. + self.Wake.LimitZmax=nil + + -- Turn to final. + self.Final.name="Final" + self.Final.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. + self.Final.Xmax= 0 -- Must be behind the boat. + self.Final.Zmin=-2000 -- Not more than 2 km port. + self.Final.Zmax= nil + self.Final.LimitXmin=nil -- No limits. Check is carried out differently. + self.Final.LimitXmax=nil + self.Final.LimitZmin=nil + self.Final.LimitZmax=nil + + -- In the Groove. + self.Groove.name="Groove" + self.Groove.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. + self.Groove.Xmax= nil + self.Groove.Zmin=-UTILS.NMToMeters(2) -- Not more than 2 NM port + self.Groove.Zmax= UTILS.NMToMeters(2) -- Not more than 2 NM starboard. + self.Groove.LimitXmin=nil -- No limits. Check is carried out differently. + self.Groove.LimitXmax=nil + self.Groove.LimitZmin=nil + self.Groove.LimitZmax=nil + +end + +--- Init parameters for Nimitz class super carriers. +-- @param #AIRBOSS self +function AIRBOSS:_InitNimitz() + + -- Init Stennis as default. + self:_InitStennis() + + -- Carrier Parameters. + self.carrierparam.sterndist =-164 + self.carrierparam.deckheight = 20.1494 --DCS World OpenBeta\CoreMods\tech\USS_Nimitz\Database\USS_CVN_7X.lua + + -- Total size of the carrier (approx as rectangle). + self.carrierparam.totlength=332.8 -- Wiki says 332.8 meters overall length. + self.carrierparam.totwidthport=45 -- Wiki says 76.8 meters overall beam. + self.carrierparam.totwidthstarboard=35 + + -- Landing runway. + self.carrierparam.rwyangle = -9.1359 --DCS World OpenBeta\CoreMods\tech\USS_Nimitz\scripts\USS_Nimitz_RunwaysAndRoutes.lua + self.carrierparam.rwylength = 250 + self.carrierparam.rwywidth = 25 + + -- Wires. + self.carrierparam.wire1 = 55 -- Distance from stern to first wire. + self.carrierparam.wire2 = 67 + self.carrierparam.wire3 = 79 + self.carrierparam.wire4 = 92 + +end + +--- Init parameters for Forrestal class super carriers. +-- @param #AIRBOSS self +function AIRBOSS:_InitForrestal() + + -- Init Nimitz as default. + self:_InitNimitz() + + -- Carrier Parameters. + self.carrierparam.sterndist =-135.5 + self.carrierparam.deckheight = 20 --20.1494 --DCS World OpenBeta\CoreMods\tech\USS_Nimitz\Database\USS_CVN_7X.lua + + -- Total size of the carrier (approx as rectangle). + self.carrierparam.totlength=315 -- Wiki says 325 meters overall length. + self.carrierparam.totwidthport=45 -- Wiki says 73 meters overall beam. + self.carrierparam.totwidthstarboard=35 + + -- Landing runway. + self.carrierparam.rwyangle = -9.1359 --DCS World OpenBeta\CoreMods\tech\USS_Nimitz\scripts\USS_Nimitz_RunwaysAndRoutes.lua + self.carrierparam.rwylength = 212 + self.carrierparam.rwywidth = 25 + + -- Wires. + self.carrierparam.wire1 = 44 -- Distance from stern to first wire. Original from Frank - 42 + self.carrierparam.wire2 = 54 --51.5 + self.carrierparam.wire3 = 64 --62 + self.carrierparam.wire4 = 74 --72.5 + +end + +--- Init parameters for LHA-1 Tarawa carrier. +-- @param #AIRBOSS self +function AIRBOSS:_InitTarawa() + + -- Init Stennis as default. + self:_InitStennis() + + -- Carrier Parameters. + self.carrierparam.sterndist =-125 + self.carrierparam.deckheight = 21 --69 ft + + -- Total size of the carrier (approx as rectangle). + self.carrierparam.totlength=245 + self.carrierparam.totwidthport=10 + self.carrierparam.totwidthstarboard=25 + + -- Landing runway. + self.carrierparam.rwyangle = 0 + self.carrierparam.rwylength = 225 + self.carrierparam.rwywidth = 15 + + -- Wires. + self.carrierparam.wire1=nil + self.carrierparam.wire2=nil + self.carrierparam.wire3=nil + self.carrierparam.wire4=nil + + -- Late break. + self.BreakLate.name="Late Break" + self.BreakLate.Xmin=-UTILS.NMToMeters(1) -- Not more than 1 NM behind the boat. Last check was at 0. + self.BreakLate.Xmax= UTILS.NMToMeters(5) -- Not more than 5 NM in front of the boat. Enough for late breaks? + self.BreakLate.Zmin=-UTILS.NMToMeters(1.6) -- Not more than 1.6 NM port. + self.BreakLate.Zmax= UTILS.NMToMeters(1) -- Not more than 1 NM starboard. + self.BreakLate.LimitXmin= 0 -- Check and next step 0.8 NM port and in front of boat. + self.BreakLate.LimitXmax= nil + self.BreakLate.LimitZmin=-UTILS.NMToMeters(0.5) -- 926 m port, closer than the stennis as abeam is 0.8-1.0 rather than 1.2 + self.BreakLate.LimitZmax= nil + +end + +--- Init parameters for LHA-6 America carrier. +-- @param #AIRBOSS self +function AIRBOSS:_InitAmerica() + + -- Init Stennis as default. + self:_InitStennis() + + -- Carrier Parameters. + self.carrierparam.sterndist =-125 + self.carrierparam.deckheight = 20 --67 ft + + -- Total size of the carrier (approx as rectangle). + self.carrierparam.totlength=257 + self.carrierparam.totwidthport=11 + self.carrierparam.totwidthstarboard=25 + + -- Landing runway. + self.carrierparam.rwyangle = 0 + self.carrierparam.rwylength = 240 + self.carrierparam.rwywidth = 15 + + -- Wires. + self.carrierparam.wire1=nil + self.carrierparam.wire2=nil + self.carrierparam.wire3=nil + self.carrierparam.wire4=nil + + -- Late break. + self.BreakLate.name="Late Break" + self.BreakLate.Xmin=-UTILS.NMToMeters(1) -- Not more than 1 NM behind the boat. Last check was at 0. + self.BreakLate.Xmax= UTILS.NMToMeters(5) -- Not more than 5 NM in front of the boat. Enough for late breaks? + self.BreakLate.Zmin=-UTILS.NMToMeters(1.6) -- Not more than 1.6 NM port. + self.BreakLate.Zmax= UTILS.NMToMeters(1) -- Not more than 1 NM starboard. + self.BreakLate.LimitXmin= 0 -- Check and next step 0.8 NM port and in front of boat. + self.BreakLate.LimitXmax= nil + self.BreakLate.LimitZmin=-UTILS.NMToMeters(0.5) -- 926 m port, closer than the stennis as abeam is 0.8-1.0 rather than 1.2 + self.BreakLate.LimitZmax= nil + +end + +--- Init parameters for L61 Juan Carlos carrier. +-- @param #AIRBOSS self +function AIRBOSS:_InitJcarlos() + + -- Init Stennis as default. + self:_InitStennis() + + -- Carrier Parameters. + self.carrierparam.sterndist =-125 + self.carrierparam.deckheight = 20 --67 ft + + -- Total size of the carrier (approx as rectangle). + self.carrierparam.totlength=231 + self.carrierparam.totwidthport=10 + self.carrierparam.totwidthstarboard=22 + + -- Landing runway. + self.carrierparam.rwyangle = 0 + self.carrierparam.rwylength = 202 + self.carrierparam.rwywidth = 14 + + -- Wires. + self.carrierparam.wire1=nil + self.carrierparam.wire2=nil + self.carrierparam.wire3=nil + self.carrierparam.wire4=nil + + -- Late break. + self.BreakLate.name="Late Break" + self.BreakLate.Xmin=-UTILS.NMToMeters(1) -- Not more than 1 NM behind the boat. Last check was at 0. + self.BreakLate.Xmax= UTILS.NMToMeters(5) -- Not more than 5 NM in front of the boat. Enough for late breaks? + self.BreakLate.Zmin=-UTILS.NMToMeters(1.6) -- Not more than 1.6 NM port. + self.BreakLate.Zmax= UTILS.NMToMeters(1) -- Not more than 1 NM starboard. + self.BreakLate.LimitXmin= 0 -- Check and next step 0.8 NM port and in front of boat. + self.BreakLate.LimitXmax= nil + self.BreakLate.LimitZmin=-UTILS.NMToMeters(0.5) -- 926 m port, closer than the stennis as abeam is 0.8-1.0 rather than 1.2 + self.BreakLate.LimitZmax= nil + +end +--- Init parameters for Marshal Voice overs *Gabriella* by HighwaymanEd. +-- @param #AIRBOSS self +-- @param #string mizfolder (Optional) Folder within miz file where the sound files are located. +function AIRBOSS:SetVoiceOversMarshalByGabriella(mizfolder) + + -- Set sound files folder. + if mizfolder then + local lastchar=string.sub(mizfolder, -1) + if lastchar~="/" then + mizfolder=mizfolder.."/" + end + self.soundfolderMSH=mizfolder + else + -- Default is the general folder. + self.soundfolderMSH=self.soundfolder + end + + -- Report for duty. + self:I(self.lid..string.format("Marshal Gabriella reporting for duty! Soundfolder=%s", tostring(self.soundfolderMSH))) + + self.MarshalCall.AFFIRMATIVE.duration=0.65 + self.MarshalCall.ALTIMETER.duration=0.60 + self.MarshalCall.BRC.duration=0.67 + self.MarshalCall.CARRIERTURNTOHEADING.duration=1.62 + self.MarshalCall.CASE.duration=0.30 + self.MarshalCall.CHARLIETIME.duration=0.77 + self.MarshalCall.CLEAREDFORRECOVERY.duration=0.93 + self.MarshalCall.DECKCLOSED.duration=0.73 + self.MarshalCall.DEGREES.duration=0.48 + self.MarshalCall.EXPECTED.duration=0.50 + self.MarshalCall.FLYNEEDLES.duration=0.89 + self.MarshalCall.HOLDATANGELS.duration=0.81 + self.MarshalCall.HOURS.duration=0.41 + self.MarshalCall.MARSHALRADIAL.duration=0.95 + self.MarshalCall.N0.duration=0.41 + self.MarshalCall.N1.duration=0.30 + self.MarshalCall.N2.duration=0.34 + self.MarshalCall.N3.duration=0.31 + self.MarshalCall.N4.duration=0.34 + self.MarshalCall.N5.duration=0.30 + self.MarshalCall.N6.duration=0.33 + self.MarshalCall.N7.duration=0.38 + self.MarshalCall.N8.duration=0.35 + self.MarshalCall.N9.duration=0.35 + self.MarshalCall.NEGATIVE.duration=0.60 + self.MarshalCall.NEWFB.duration=0.95 + self.MarshalCall.OPS.duration=0.23 + self.MarshalCall.POINT.duration=0.38 + self.MarshalCall.RADIOCHECK.duration=1.27 + self.MarshalCall.RECOVERY.duration=0.60 + self.MarshalCall.RECOVERYOPSSTOPPED.duration=1.25 + self.MarshalCall.RECOVERYPAUSEDNOTICE.duration=2.55 + self.MarshalCall.RECOVERYPAUSEDRESUMED.duration=2.55 + self.MarshalCall.REPORTSEEME.duration=0.87 + self.MarshalCall.RESUMERECOVERY.duration=1.55 + self.MarshalCall.ROGER.duration=0.50 + self.MarshalCall.SAYNEEDLES.duration=0.82 + self.MarshalCall.STACKFULL.duration=5.70 + self.MarshalCall.STARTINGRECOVERY.duration=1.61 + +end + + + +--- Init parameters for Marshal Voice overs by *Raynor*. +-- @param #AIRBOSS self +-- @param #string mizfolder (Optional) Folder within miz file where the sound files are located. +function AIRBOSS:SetVoiceOversMarshalByRaynor(mizfolder) + + -- Set sound files folder. + if mizfolder then + local lastchar=string.sub(mizfolder, -1) + if lastchar~="/" then + mizfolder=mizfolder.."/" + end + self.soundfolderMSH=mizfolder + else + -- Default is the general folder. + self.soundfolderMSH=self.soundfolder + end + + -- Report for duty. + self:I(self.lid..string.format("Marshal Raynor reporting for duty! Soundfolder=%s", tostring(self.soundfolderMSH))) + + self.MarshalCall.AFFIRMATIVE.duration=0.70 + self.MarshalCall.ALTIMETER.duration=0.60 + self.MarshalCall.BRC.duration=0.60 + self.MarshalCall.CARRIERTURNTOHEADING.duration=1.87 + self.MarshalCall.CASE.duration=0.60 + self.MarshalCall.CHARLIETIME.duration=0.81 + self.MarshalCall.CLEAREDFORRECOVERY.duration=1.21 + self.MarshalCall.DECKCLOSED.duration=0.86 + self.MarshalCall.DEGREES.duration=0.55 + self.MarshalCall.EXPECTED.duration=0.61 + self.MarshalCall.FLYNEEDLES.duration=0.90 + self.MarshalCall.HOLDATANGELS.duration=0.91 + self.MarshalCall.HOURS.duration=0.54 + self.MarshalCall.MARSHALRADIAL.duration=0.80 + self.MarshalCall.N0.duration=0.38 + self.MarshalCall.N1.duration=0.30 + self.MarshalCall.N2.duration=0.30 + self.MarshalCall.N3.duration=0.30 + self.MarshalCall.N4.duration=0.32 + self.MarshalCall.N5.duration=0.41 + self.MarshalCall.N6.duration=0.48 + self.MarshalCall.N7.duration=0.51 + self.MarshalCall.N8.duration=0.38 + self.MarshalCall.N9.duration=0.34 + self.MarshalCall.NEGATIVE.duration=0.60 + self.MarshalCall.NEWFB.duration=1.10 + self.MarshalCall.OPS.duration=0.46 + self.MarshalCall.POINT.duration=0.21 + self.MarshalCall.RADIOCHECK.duration=0.95 + self.MarshalCall.RECOVERY.duration=0.63 + self.MarshalCall.RECOVERYOPSSTOPPED.duration=1.36 + self.MarshalCall.RECOVERYPAUSEDNOTICE.duration=2.8 -- Strangely the file is actually a shorter ~2.4 sec. + self.MarshalCall.RECOVERYPAUSEDRESUMED.duration=2.75 + self.MarshalCall.REPORTSEEME.duration=1.06 --0.96 + self.MarshalCall.RESUMERECOVERY.duration=1.41 + self.MarshalCall.ROGER.duration=0.41 + self.MarshalCall.SAYNEEDLES.duration=0.79 + self.MarshalCall.STACKFULL.duration=4.70 + self.MarshalCall.STARTINGRECOVERY.duration=2.06 + +end + +--- Set parameters for LSO Voice overs by *Raynor*. +-- @param #AIRBOSS self +-- @param #string mizfolder (Optional) Folder within miz file where the sound files are located. +function AIRBOSS:SetVoiceOversLSOByRaynor(mizfolder) + + -- Set sound files folder. + if mizfolder then + local lastchar=string.sub(mizfolder, -1) + if lastchar~="/" then + mizfolder=mizfolder.."/" + end + self.soundfolderLSO=mizfolder + else + -- Default is the general folder. + self.soundfolderLSO=self.soundfolder + end + + -- Report for duty. + self:I(self.lid..string.format("LSO Raynor reporting for duty! Soundfolder=%s", tostring(self.soundfolderLSO))) + + self.LSOCall.BOLTER.duration=0.75 + self.LSOCall.CALLTHEBALL.duration=0.625 + self.LSOCall.CHECK.duration=0.40 + self.LSOCall.CLEAREDTOLAND.duration=0.85 + self.LSOCall.COMELEFT.duration=0.60 + self.LSOCall.DEPARTANDREENTER.duration=1.10 + self.LSOCall.EXPECTHEAVYWAVEOFF.duration=1.30 + self.LSOCall.EXPECTSPOT75.duration=1.85 + self.LSOCall.EXPECTSPOT5.duration=1.3 + self.LSOCall.FAST.duration=0.75 + self.LSOCall.FOULDECK.duration=0.75 + self.LSOCall.HIGH.duration=0.65 + self.LSOCall.IDLE.duration=0.40 + self.LSOCall.LONGINGROOVE.duration=1.25 + self.LSOCall.LOW.duration=0.60 + self.LSOCall.N0.duration=0.38 + self.LSOCall.N1.duration=0.30 + self.LSOCall.N2.duration=0.30 + self.LSOCall.N3.duration=0.30 + self.LSOCall.N4.duration=0.32 + self.LSOCall.N5.duration=0.41 + self.LSOCall.N6.duration=0.48 + self.LSOCall.N7.duration=0.51 + self.LSOCall.N8.duration=0.38 + self.LSOCall.N9.duration=0.34 + self.LSOCall.PADDLESCONTACT.duration=0.91 + self.LSOCall.POWER.duration=0.45 + self.LSOCall.RADIOCHECK.duration=0.90 + self.LSOCall.RIGHTFORLINEUP.duration=0.70 + self.LSOCall.ROGERBALL.duration=0.72 + self.LSOCall.SLOW.duration=0.63 + --self.LSOCall.SLOW.duration=0.59 --TODO + self.LSOCall.STABILIZED.duration=0.75 + self.LSOCall.WAVEOFF.duration=0.55 + self.LSOCall.WELCOMEABOARD.duration=0.80 +end + + + +--- Set parameters for LSO Voice overs by *funkyfranky*. +-- @param #AIRBOSS self +-- @param #string mizfolder (Optional) Folder within miz file where the sound files are located. +function AIRBOSS:SetVoiceOversLSOByFF(mizfolder) + + -- Set sound files folder. + if mizfolder then + local lastchar=string.sub(mizfolder, -1) + if lastchar~="/" then + mizfolder=mizfolder.."/" + end + self.soundfolderLSO=mizfolder + else + -- Default is the general folder. + self.soundfolderLSO=self.soundfolder + end + + -- Report for duty. + self:I(self.lid..string.format("LSO FF reporting for duty! Soundfolder=%s", tostring(self.soundfolderLSO))) + + self.LSOCall.BOLTER.duration=0.75 + self.LSOCall.CALLTHEBALL.duration=0.60 + self.LSOCall.CHECK.duration=0.45 + self.LSOCall.CLEAREDTOLAND.duration=1.00 + self.LSOCall.COMELEFT.duration=0.60 + self.LSOCall.DEPARTANDREENTER.duration=1.10 + self.LSOCall.EXPECTHEAVYWAVEOFF.duration=1.20 + self.LSOCall.EXPECTSPOT75.duration=2.00 + self.LSOCall.EXPECTSPOT5.duration=1.3 + self.LSOCall.FAST.duration=0.70 + self.LSOCall.FOULDECK.duration=0.62 + self.LSOCall.HIGH.duration=0.65 + self.LSOCall.IDLE.duration=0.45 + self.LSOCall.LONGINGROOVE.duration=1.20 + self.LSOCall.LOW.duration=0.50 + self.LSOCall.N0.duration=0.40 + self.LSOCall.N1.duration=0.25 + self.LSOCall.N2.duration=0.37 + self.LSOCall.N3.duration=0.37 + self.LSOCall.N4.duration=0.39 + self.LSOCall.N5.duration=0.39 + self.LSOCall.N6.duration=0.40 + self.LSOCall.N7.duration=0.40 + self.LSOCall.N8.duration=0.37 + self.LSOCall.N9.duration=0.40 + self.LSOCall.PADDLESCONTACT.duration=1.00 + self.LSOCall.POWER.duration=0.50 + self.LSOCall.RADIOCHECK.duration=1.10 + self.LSOCall.RIGHTFORLINEUP.duration=0.80 + self.LSOCall.ROGERBALL.duration=1.00 + self.LSOCall.SLOW.duration=0.65 + self.LSOCall.SLOW.duration=0.59 + self.LSOCall.STABILIZED.duration=0.90 + self.LSOCall.WAVEOFF.duration=0.60 + self.LSOCall.WELCOMEABOARD.duration=1.00 +end + +--- Intit parameters for Marshal Voice overs by *funkyfranky*. +-- @param #AIRBOSS self +-- @param #string mizfolder (Optional) Folder within miz file where the sound files are located. +function AIRBOSS:SetVoiceOversMarshalByFF(mizfolder) + + -- Set sound files folder. + if mizfolder then + local lastchar=string.sub(mizfolder, -1) + if lastchar~="/" then + mizfolder=mizfolder.."/" + end + self.soundfolderMSH=mizfolder + else + -- Default is the general folder. + self.soundfolderMSH=self.soundfolder + end + + -- Report for duty. + self:I(self.lid..string.format("Marshal FF reporting for duty! Soundfolder=%s", tostring(self.soundfolderMSH))) + + self.MarshalCall.AFFIRMATIVE.duration=0.90 + self.MarshalCall.ALTIMETER.duration=0.85 + self.MarshalCall.BRC.duration=0.80 + self.MarshalCall.CARRIERTURNTOHEADING.duration=2.48 + self.MarshalCall.CASE.duration=0.40 + self.MarshalCall.CHARLIETIME.duration=0.90 + self.MarshalCall.CLEAREDFORRECOVERY.duration=1.25 + self.MarshalCall.DECKCLOSED.duration=1.10 + self.MarshalCall.DEGREES.duration=0.60 + self.MarshalCall.EXPECTED.duration=0.55 + self.MarshalCall.FLYNEEDLES.duration=0.90 + self.MarshalCall.HOLDATANGELS.duration=1.10 + self.MarshalCall.HOURS.duration=0.60 + self.MarshalCall.MARSHALRADIAL.duration=1.10 + self.MarshalCall.N0.duration=0.40 + self.MarshalCall.N1.duration=0.25 + self.MarshalCall.N2.duration=0.37 + self.MarshalCall.N3.duration=0.37 + self.MarshalCall.N4.duration=0.39 + self.MarshalCall.N5.duration=0.39 + self.MarshalCall.N6.duration=0.40 + self.MarshalCall.N7.duration=0.40 + self.MarshalCall.N8.duration=0.37 + self.MarshalCall.N9.duration=0.40 + self.MarshalCall.NEGATIVE.duration=0.80 + self.MarshalCall.NEWFB.duration=1.35 + self.MarshalCall.OPS.duration=0.48 + self.MarshalCall.POINT.duration=0.33 + self.MarshalCall.RADIOCHECK.duration=1.20 + self.MarshalCall.RECOVERY.duration=0.70 + self.MarshalCall.RECOVERYOPSSTOPPED.duration=1.65 + self.MarshalCall.RECOVERYPAUSEDNOTICE.duration=2.9 -- Strangely the file is actually a shorter ~2.4 sec. + self.MarshalCall.RECOVERYPAUSEDRESUMED.duration=3.40 + self.MarshalCall.REPORTSEEME.duration=0.95 + self.MarshalCall.RESUMERECOVERY.duration=1.75 + self.MarshalCall.ROGER.duration=0.53 + self.MarshalCall.SAYNEEDLES.duration=0.90 + self.MarshalCall.STACKFULL.duration=6.35 + self.MarshalCall.STARTINGRECOVERY.duration=2.65 + +end + +--- Init voice over radio transmission call. +-- @param #AIRBOSS self +function AIRBOSS:_InitVoiceOvers() + + --------------- + -- LSO Radio -- + --------------- + + -- LSO Radio Calls. + self.LSOCall={ + BOLTER={ + file="LSO-BolterBolter", + suffix="ogg", + loud=false, + subtitle="Bolter, Bolter", + duration=0.75, + subduration=5, + }, + CALLTHEBALL={ + file="LSO-CallTheBall", + suffix="ogg", + loud=false, + subtitle="Call the ball", + duration=0.6, + subduration=2, + }, + CHECK={ + file="LSO-Check", + suffix="ogg", + loud=false, + subtitle="Check", + duration=0.45, + subduration=2.5, + }, + CLEAREDTOLAND={ + file="LSO-ClearedToLand", + suffix="ogg", + loud=false, + subtitle="Cleared to land", + duration=1.0, + subduration=5, + }, + COMELEFT={ + file="LSO-ComeLeft", + suffix="ogg", + loud=true, + subtitle="Come left", + duration=0.60, + subduration=1, + }, + RADIOCHECK={ + file="LSO-RadioCheck", + suffix="ogg", + loud=false, + subtitle="Paddles, radio check", + duration=1.1, + subduration=5, + }, + RIGHTFORLINEUP={ + file="LSO-RightForLineup", + suffix="ogg", + loud=true, + subtitle="Right for line up", + duration=0.80, + subduration=1, + }, + HIGH={ + file="LSO-High", + suffix="ogg", + loud=true, + subtitle="You're high", + duration=0.65, + subduration=1, + }, + LOW={ + file="LSO-Low", + suffix="ogg", + loud=true, + subtitle="You're low", + duration=0.50, + subduration=1, + }, + POWER={ + file="LSO-Power", + suffix="ogg", + loud=true, + subtitle="Power", + duration=0.50, --0.45 was too short + subduration=1, + }, + SLOW={ + file="LSO-Slow", + suffix="ogg", + loud=true, + subtitle="You're slow", + duration=0.65, + subduration=1, + }, + FAST={ + file="LSO-Fast", + suffix="ogg", + loud=true, + subtitle="You're fast", + duration=0.70, + subduration=1, + }, + ROGERBALL={ + file="LSO-RogerBall", + suffix="ogg", + loud=false, + subtitle="Roger ball", + duration=1.00, + subduration=2, + }, + WAVEOFF={ + file="LSO-WaveOff", + suffix="ogg", + loud=false, + subtitle="Wave off", + duration=0.6, + subduration=5, + }, + LONGINGROOVE={ + file="LSO-LongInTheGroove", + suffix="ogg", + loud=false, + subtitle="You're long in the groove", + duration=1.2, + subduration=5, + }, + FOULDECK={ + file="LSO-FoulDeck", + suffix="ogg", + loud=false, + subtitle="Foul deck", + duration=0.62, + subduration=5, + }, + DEPARTANDREENTER={ + file="LSO-DepartAndReenter", + suffix="ogg", + loud=false, + subtitle="Depart and re-enter", + duration=1.1, + subduration=5, + }, + PADDLESCONTACT={ + file="LSO-PaddlesContact", + suffix="ogg", + loud=false, + subtitle="Paddles, contact", + duration=1.0, + subduration=5, + }, + WELCOMEABOARD={ + file="LSO-WelcomeAboard", + suffix="ogg", + loud=false, + subtitle="Welcome aboard", + duration=1.0, + subduration=5, + }, + EXPECTHEAVYWAVEOFF={ + file="LSO-ExpectHeavyWaveoff", + suffix="ogg", + loud=false, + subtitle="Expect heavy waveoff", + duration=1.2, + subduration=5, + }, + EXPECTSPOT75={ + file="LSO-ExpectSpot75", + suffix="ogg", + loud=false, + subtitle="Expect spot 7.5", + duration=2.0, + subduration=5, + }, + EXPECTSPOT5={ + file="LSO-ExpectSpot5", + suffix="ogg", + loud=false, + subtitle="Expect spot 5", + duration=1.3, + subduration=5, + }, + STABILIZED={ + file="LSO-Stabilized", + suffix="ogg", + loud=false, + subtitle="Stabilized", + duration=0.9, + subduration=5, + }, + IDLE={ + file="LSO-Idle", + suffix="ogg", + loud=false, + subtitle="Idle", + duration=0.45, + subduration=5, + }, + N0={ + file="LSO-N0", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + N1={ + file="LSO-N1", + suffix="ogg", + loud=false, + subtitle="", + duration=0.25, + }, + N2={ + file="LSO-N2", + suffix="ogg", + loud=false, + subtitle="", + duration=0.37, + }, + N3={ + file="LSO-N3", + suffix="ogg", + loud=false, + subtitle="", + duration=0.37, + }, + N4={ + file="LSO-N4", + suffix="ogg", + loud=false, + subtitle="", + duration=0.39, + }, + N5={ + file="LSO-N5", + suffix="ogg", + loud=false, + subtitle="", + duration=0.39, + }, + N6={ + file="LSO-N6", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + N7={ + file="LSO-N7", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + N8={ + file="LSO-N8", + suffix="ogg", + loud=false, + subtitle="", + duration=0.37, + }, + N9={ + file="LSO-N9", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + CLICK={ + file="AIRBOSS-RadioClick", + suffix="ogg", + loud=false, + subtitle="", + duration=0.35, + }, + NOISE={ + file="AIRBOSS-Noise", + suffix="ogg", + loud=false, + subtitle="", + duration=3.6, + }, + SPINIT={ + file="AIRBOSS-SpinIt", + suffix="ogg", + loud=false, + subtitle="", + duration=0.73, + subduration=5, + }, + } + + ----------------- + -- Pilot Calls -- + ----------------- + + -- Pilot Radio Calls. + self.PilotCall={ + N0={ + file="PILOT-N0", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + N1={ + file="PILOT-N1", + suffix="ogg", + loud=false, + subtitle="", + duration=0.25, + }, + N2={ + file="PILOT-N2", + suffix="ogg", + loud=false, + subtitle="", + duration=0.37, + }, + N3={ + file="PILOT-N3", + suffix="ogg", + loud=false, + subtitle="", + duration=0.37, + }, + N4={ + file="PILOT-N4", + suffix="ogg", + loud=false, + subtitle="", + duration=0.39, + }, + N5={ + file="PILOT-N5", + suffix="ogg", + loud=false, + subtitle="", + duration=0.39, + }, + N6={ + file="PILOT-N6", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + N7={ + file="PILOT-N7", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + N8={ + file="PILOT-N8", + suffix="ogg", + loud=false, + subtitle="", + duration=0.37, + }, + N9={ + file="PILOT-N9", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + POINT={ + file="PILOT-Point", + suffix="ogg", + loud=false, + subtitle="", + duration=0.33, + }, + SKYHAWK={ + file="PILOT-Skyhawk", + suffix="ogg", + loud=false, + subtitle="", + duration=0.95, + subduration=5, + }, + HARRIER={ + file="PILOT-Harrier", + suffix="ogg", + loud=false, + subtitle="", + duration=0.58, + subduration=5, + }, + HAWKEYE={ + file="PILOT-Hawkeye", + suffix="ogg", + loud=false, + subtitle="", + duration=0.63, + subduration=5, + }, + TOMCAT={ + file="PILOT-Tomcat", + suffix="ogg", + loud=false, + subtitle="", + duration=0.66, + subduration=5, + }, + HORNET={ + file="PILOT-Hornet", + suffix="ogg", + loud=false, + subtitle="", + duration=0.56, + subduration=5, + }, + VIKING={ + file="PILOT-Viking", + suffix="ogg", + loud=false, + subtitle="", + duration=0.61, + subduration=5, + }, + BALL={ + file="PILOT-Ball", + suffix="ogg", + loud=false, + subtitle="", + duration=0.50, + subduration=5, + }, + BINGOFUEL={ + file="PILOT-BingoFuel", + suffix="ogg", + loud=false, + subtitle="", + duration=0.80, + }, + GASATDIVERT={ + file="PILOT-GasAtDivert", + suffix="ogg", + loud=false, + subtitle="", + duration=1.80, + }, + GASATTANKER={ + file="PILOT-GasAtTanker", + suffix="ogg", + loud=false, + subtitle="", + duration=1.95, + }, + } + + ------------------- + -- MARSHAL Radio -- + ------------------- + + -- MARSHAL Radio Calls. + self.MarshalCall={ + AFFIRMATIVE={ + file="MARSHAL-Affirmative", + suffix="ogg", + loud=false, + subtitle="", + duration=0.90, + }, + ALTIMETER={ + file="MARSHAL-Altimeter", + suffix="ogg", + loud=false, + subtitle="", + duration=0.85, + }, + BRC={ + file="MARSHAL-BRC", + suffix="ogg", + loud=false, + subtitle="", + duration=0.80, + }, + CARRIERTURNTOHEADING={ + file="MARSHAL-CarrierTurnToHeading", + suffix="ogg", + loud=false, + subtitle="", + duration=2.48, + subduration=5, + }, + CASE={ + file="MARSHAL-Case", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + CHARLIETIME={ + file="MARSHAL-CharlieTime", + suffix="ogg", + loud=false, + subtitle="", + duration=0.90, + }, + CLEAREDFORRECOVERY={ + file="MARSHAL-ClearedForRecovery", + suffix="ogg", + loud=false, + subtitle="", + duration=1.25, + }, + DECKCLOSED={ + file="MARSHAL-DeckClosed", + suffix="ogg", + loud=false, + subtitle="", + duration=1.10, + subduration=5, + }, + DEGREES={ + file="MARSHAL-Degrees", + suffix="ogg", + loud=false, + subtitle="", + duration=0.60, + }, + EXPECTED={ + file="MARSHAL-Expected", + suffix="ogg", + loud=false, + subtitle="", + duration=0.55, + }, + FLYNEEDLES={ + file="MARSHAL-FlyYourNeedles", + suffix="ogg", + loud=false, + subtitle="Fly your needles", + duration=0.9, + subduration=5, + }, + HOLDATANGELS={ + file="MARSHAL-HoldAtAngels", + suffix="ogg", + loud=false, + subtitle="", + duration=1.10, + }, + HOURS={ + file="MARSHAL-Hours", + suffix="ogg", + loud=false, + subtitle="", + duration=0.60, + subduration=5, + }, + MARSHALRADIAL={ + file="MARSHAL-MarshalRadial", + suffix="ogg", + loud=false, + subtitle="", + duration=1.10, + }, + N0={ + file="MARSHAL-N0", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + N1={ + file="MARSHAL-N1", + suffix="ogg", + loud=false, + subtitle="", + duration=0.25, + }, + N2={ + file="MARSHAL-N2", + suffix="ogg", + loud=false, + subtitle="", + duration=0.37, + }, + N3={ + file="MARSHAL-N3", + suffix="ogg", + loud=false, + subtitle="", + duration=0.37, + }, + N4={ + file="MARSHAL-N4", + suffix="ogg", + loud=false, + subtitle="", + duration=0.39, + }, + N5={ + file="MARSHAL-N5", + suffix="ogg", + loud=false, + subtitle="", + duration=0.39, + }, + N6={ + file="MARSHAL-N6", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + N7={ + file="MARSHAL-N7", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + N8={ + file="MARSHAL-N8", + suffix="ogg", + loud=false, + subtitle="", + duration=0.37, + }, + N9={ + file="MARSHAL-N9", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + NEGATIVE={ + file="MARSHAL-Negative", + suffix="ogg", + loud=false, + subtitle="", + duration=0.80, + subduration=5, + }, + NEWFB={ + file="MARSHAL-NewFB", + suffix="ogg", + loud=false, + subtitle="", + duration=1.35, + }, + OPS={ + file="MARSHAL-Ops", + suffix="ogg", + loud=false, + subtitle="", + duration=0.48, + }, + POINT={ + file="MARSHAL-Point", + suffix="ogg", + loud=false, + subtitle="", + duration=0.33, + }, + RADIOCHECK={ + file="MARSHAL-RadioCheck", + suffix="ogg", + loud=false, + subtitle="Radio check", + duration=1.20, + subduration=5, + }, + RECOVERY={ + file="MARSHAL-Recovery", + suffix="ogg", + loud=false, + subtitle="", + duration=0.70, + subduration=5, + }, + RECOVERYOPSSTOPPED={ + file="MARSHAL-RecoveryOpsStopped", + suffix="ogg", + loud=false, + subtitle="", + duration=1.65, + subduration=5, + }, + RECOVERYPAUSEDNOTICE={ + file="MARSHAL-RecoveryPausedNotice", + suffix="ogg", + loud=false, + subtitle="aircraft recovery paused until further notice", + duration=2.90, + subduration=5, + }, + RECOVERYPAUSEDRESUMED={ + file="MARSHAL-RecoveryPausedResumed", + suffix="ogg", + loud=false, + subtitle="", + duration=3.40, + subduration=5, + }, + REPORTSEEME={ + file="MARSHAL-ReportSeeMe", + suffix="ogg", + loud=false, + subtitle="", + duration=0.95, + }, + RESUMERECOVERY={ + file="MARSHAL-ResumeRecovery", + suffix="ogg", + loud=false, + subtitle="resuming aircraft recovery", + duration=1.75, + subduraction=5, + }, + ROGER={ + file="MARSHAL-Roger", + suffix="ogg", + loud=false, + subtitle="", + duration=0.53, + subduration=5, + }, + SAYNEEDLES={ + file="MARSHAL-SayNeedles", + suffix="ogg", + loud=false, + subtitle="Say needles", + duration=0.90, + subduration=5, + }, + STACKFULL={ + file="MARSHAL-StackFull", + suffix="ogg", + loud=false, + subtitle="Marshal Stack is currently full. Hold outside 10 NM zone and wait for further instructions", + duration=6.35, + subduration=10, + }, + STARTINGRECOVERY={ + file="MARSHAL-StartingRecovery", + suffix="ogg", + loud=false, + subtitle="", + duration=2.65, + subduration=5, + }, + CLICK={ + file="AIRBOSS-RadioClick", + suffix="ogg", + loud=false, + subtitle="", + duration=0.35, + }, + NOISE={ + file="AIRBOSS-Noise", + suffix="ogg", + loud=false, + subtitle="", + duration=3.6, + }, + } + + -- Default timings by Raynor + self:SetVoiceOversLSOByRaynor() + self:SetVoiceOversMarshalByRaynor() + +end + +--- Init voice over radio transmission call. +-- @param #AIRBOSS self +-- @param #AIRBOSS.RadioCall radiocall LSO or Marshal radio call object. +-- @param #number duration Duration of the voice over in seconds. +-- @param #string subtitle (Optional) Subtitle to be displayed along with voice over. +-- @param #number subduration (Optional) Duration how long the subtitle is displayed. +-- @param #string filename (Optional) Name of the voice over sound file. +-- @param #string suffix (Optional) Extention of file. Default ".ogg". +function AIRBOSS:SetVoiceOver(radiocall, duration, subtitle, subduration, filename, suffix) + radiocall.duration=duration + radiocall.subtitle=subtitle or radiocall.subtitle + radiocall.file=filename + radiocall.suffix=suffix or ".ogg" +end + +--- Get optimal aircraft AoA parameters.. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @return #AIRBOSS.AircraftAoA AoA parameters for the given aircraft type. +function AIRBOSS:_GetAircraftAoA(playerData) + + -- Get AC type. + local hornet=playerData.actype==AIRBOSS.AircraftCarrier.HORNET + local goshawk=playerData.actype==AIRBOSS.AircraftCarrier.T45C + local skyhawk=playerData.actype==AIRBOSS.AircraftCarrier.A4EC + local harrier=playerData.actype==AIRBOSS.AircraftCarrier.AV8B + local tomcat=playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B + + -- Table with AoA values. + local aoa={} -- #AIRBOSS.AircraftAoA + + if hornet then + -- F/A-18C Hornet parameters. + aoa.SLOW = 9.8 + aoa.Slow = 9.3 + aoa.OnSpeedMax = 8.8 + aoa.OnSpeed = 8.1 + aoa.OnSpeedMin = 7.4 + aoa.Fast = 6.9 + aoa.FAST = 6.3 + elseif tomcat then + -- F-14A/B Tomcat parameters (taken from NATOPS). Converted from units 0-30 to degrees. + -- Currently assuming a linear relationship with 0=-10 degrees and 30=+40 degrees as stated in NATOPS. + aoa.SLOW = self:_AoAUnit2Deg(playerData, 17.0) --18.33 --17.0 units + aoa.Slow = self:_AoAUnit2Deg(playerData, 16.0) --16.67 --16.0 units + aoa.OnSpeedMax = self:_AoAUnit2Deg(playerData, 15.5) --15.83 --15.5 units + aoa.OnSpeed = self:_AoAUnit2Deg(playerData, 15.0) --15.0 --15.0 units + aoa.OnSpeedMin = self:_AoAUnit2Deg(playerData, 14.5) --14.17 --14.5 units + aoa.Fast = self:_AoAUnit2Deg(playerData, 14.0) --13.33 --14.0 units + aoa.FAST = self:_AoAUnit2Deg(playerData, 13.0) --11.67 --13.0 units + elseif goshawk then + -- T-45C Goshawk parameters. + aoa.SLOW = 8.00 --19 + aoa.Slow = 7.75 --18 + aoa.OnSpeedMax = 7.25 --17.5 + aoa.OnSpeed = 7.00 --17 + aoa.OnSpeedMin = 6.75 --16.5 + aoa.Fast = 6.25 --16 + aoa.FAST = 6.00 --15 + elseif skyhawk then + -- A-4E-C Skyhawk parameters from https://forums.eagle.ru/showpost.php?p=3703467&postcount=390 + -- Note that these are arbitrary UNITS and not degrees. We need a conversion formula! + -- Github repo suggests they simply use a factor of two to get from degrees to units. + aoa.SLOW = 9.50 --=19.0/2 + aoa.Slow = 9.25 --=18.5/2 + aoa.OnSpeedMax = 9.00 --=18.0/2 + aoa.OnSpeed = 8.75 --=17.5/2 8.1 + aoa.OnSpeedMin = 8.50 --=17.0/2 + aoa.Fast = 8.25 --=17.5/2 + aoa.FAST = 8.00 --=16.5/2 + elseif harrier then + -- AV-8B Harrier parameters. Tuning done on the Fast AoA to allow for abeam and ninety at Nozzles 60 - 73. + aoa.SLOW = 14.0 + aoa.Slow = 13.0 + aoa.OnSpeedMax = 12.0 + aoa.OnSpeed = 11.0 + aoa.OnSpeedMin = 10.0 + aoa.Fast = 8.0 + aoa.FAST = 7.5 + end + + return aoa +end + +--- Convert AoA from arbitrary units to degrees. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #number aoaunits AoA in arbitrary units. +-- @return #number AoA in degrees. +function AIRBOSS:_AoAUnit2Deg(playerData, aoaunits) + + -- Init. + local degrees=aoaunits + + -- Check aircraft type of player. + if playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B then + + ------------- + -- F-14A/B -- + ------------- + + -- NATOPS: + -- unit=0 ==> alpha=-10 degrees. + -- unit=30 ==> alpha=+40 degrees. + + -- Assuming a linear relationship between these to points of the graph. + -- However: AoA=15 Units ==> 15 degrees, which is too much. + degrees=-10+50/30*aoaunits + + -- HB Facebook page https://www.facebook.com/heatblur/photos/a.683612385159716/754368278084126 + -- AoA=15 Units <==> AoA=10.359 degrees. + degrees=0.918*aoaunits-3.411 + + elseif playerData.actype==AIRBOSS.AircraftCarrier.A4EC then + + ---------- + -- A-4E -- + ---------- + + -- A-4E-C source code suggests a simple factor of 1/2 for conversion. + degrees=0.5*aoaunits + + end + + return degrees +end + +--- Convert AoA from degrees to arbitrary units. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #number degrees AoA in degrees. +-- @return #number AoA in arbitrary units. +function AIRBOSS:_AoADeg2Units(playerData, degrees) + + -- Init. + local aoaunits=degrees + + -- Check aircraft type of player. + if playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B then + + ------------- + -- F-14A/B -- + ------------- + + -- NATOPS: + -- unit=0 ==> alpha=-10 degrees. + -- unit=30 ==> alpha=+40 degrees. + + -- Assuming a linear relationship between these to points of the graph. + aoaunits=(degrees+10)*30/50 + + -- HB Facebook page https://www.facebook.com/heatblur/photos/a.683612385159716/754368278084126 + -- AoA=15 Units <==> AoA=10.359 degrees. + aoaunits=1.089*degrees+3.715 + + elseif playerData.actype==AIRBOSS.AircraftCarrier.A4EC then + + ---------- + -- A-4E -- + ---------- + + -- A-4E source code suggests a simple factor of two as conversion. + aoaunits=2*degrees + + end + + return aoaunits +end + +--- Get optimal aircraft flight parameters at checkpoint. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #string step Pattern step. +-- @return #number Altitude in meters or nil. +-- @return #number Angle of Attack or nil. +-- @return #number Distance to carrier in meters or nil. +-- @return #number Speed in m/s or nil. +function AIRBOSS:_GetAircraftParameters(playerData, step) + + -- Get parameters depended on step. + step=step or playerData.step + + -- Get AC type. + local hornet=playerData.actype==AIRBOSS.AircraftCarrier.HORNET + local skyhawk=playerData.actype==AIRBOSS.AircraftCarrier.A4EC + local tomcat=playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B + local harrier=playerData.actype==AIRBOSS.AircraftCarrier.AV8B + + -- Return values. + local alt + local aoa + local dist + local speed + + -- Aircraft specific AoA. + local aoaac=self:_GetAircraftAoA(playerData) + + if step==AIRBOSS.PatternStep.PLATFORM then + + alt=UTILS.FeetToMeters(5000) + + --dist=UTILS.NMToMeters(20) + + speed=UTILS.KnotsToMps(250) + + elseif step==AIRBOSS.PatternStep.ARCIN then + + if tomcat then + speed=UTILS.KnotsToMps(150) + else + speed=UTILS.KnotsToMps(250) + end + + elseif step==AIRBOSS.PatternStep.ARCOUT then + + if tomcat then + speed=UTILS.KnotsToMps(150) + else + speed=UTILS.KnotsToMps(250) + end + + elseif step==AIRBOSS.PatternStep.DIRTYUP then + + alt=UTILS.FeetToMeters(1200) + + --speed=UTILS.KnotsToMps(250) + + elseif step==AIRBOSS.PatternStep.BULLSEYE then + + alt=UTILS.FeetToMeters(1200) + + dist=-UTILS.NMToMeters(3) + + aoa=aoaac.OnSpeed + + elseif step==AIRBOSS.PatternStep.INITIAL then + + if hornet or tomcat or harrier then + alt=UTILS.FeetToMeters(800) + speed=UTILS.KnotsToMps(350) + elseif skyhawk then + alt=UTILS.FeetToMeters(600) + speed=UTILS.KnotsToMps(250) + elseif goshawk then + alt=UTILS.FeetToMeters(800) + speed=UTILS.KnotsToMps(300) + end + + elseif step==AIRBOSS.PatternStep.BREAKENTRY then + + if hornet or tomcat or harrier then + alt=UTILS.FeetToMeters(800) + speed=UTILS.KnotsToMps(350) + elseif skyhawk then + alt=UTILS.FeetToMeters(600) + speed=UTILS.KnotsToMps(250) + elseif goshawk then + alt=UTILS.FeetToMeters(800) + speed=UTILS.KnotsToMps(300) + end + + elseif step==AIRBOSS.PatternStep.EARLYBREAK then + + if hornet or tomcat or harrier or goshawk then + alt=UTILS.FeetToMeters(800) + elseif skyhawk then + alt=UTILS.FeetToMeters(600) + end + + elseif step==AIRBOSS.PatternStep.LATEBREAK then + + if hornet or tomcat or harrier or goshawk then + alt=UTILS.FeetToMeters(800) + elseif skyhawk then + alt=UTILS.FeetToMeters(600) + end + + elseif step==AIRBOSS.PatternStep.ABEAM then + + if hornet or tomcat or harrier or goshawk then + alt=UTILS.FeetToMeters(600) + elseif skyhawk then + alt=UTILS.FeetToMeters(500) + end + + aoa=aoaac.OnSpeed + + if harrier then + -- 0.8 to 1.0 NM + dist=UTILS.NMToMeters(0.9) + else + dist=UTILS.NMToMeters(1.2) + end + + if goshawk then + -- 0.9 to 1.1 NM per natops ch.4 page 48 + dist=UTILS.NMToMeters(0.9) + else + dist=UTILS.NMToMeters(1.1) + end + + elseif step==AIRBOSS.PatternStep.NINETY then + + if hornet or tomcat then + alt=UTILS.FeetToMeters(500) + elseif goshawk then + alt=UTILS.FeetToMeters(450) + elseif skyhawk then + alt=UTILS.FeetToMeters(500) + elseif harrier then + alt=UTILS.FeetToMeters(425) + end + + aoa=aoaac.OnSpeed + + elseif step==AIRBOSS.PatternStep.WAKE then + + if hornet or goshawk then + alt=UTILS.FeetToMeters(370) + elseif tomcat then + alt=UTILS.FeetToMeters(430) -- Tomcat should be a bit higher as it intercepts the GS a bit higher. + elseif skyhawk then + alt=UTILS.FeetToMeters(370) --? + end + -- Harrier wont get into wake pos. Runway is not angled and it stays port. + + aoa=aoaac.OnSpeed + + elseif step==AIRBOSS.PatternStep.FINAL then + + if hornet or goshawk then + alt=UTILS.FeetToMeters(300) + elseif tomcat then + alt=UTILS.FeetToMeters(360) + elseif skyhawk then + alt=UTILS.FeetToMeters(300) --? + elseif harrier then + -- 300-325 ft + alt=UTILS.FeetToMeters(300)-- Need to verify + end + + aoa=aoaac.OnSpeed + + end + + return alt, aoa, dist, speed +end + +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- QUEUE Functions +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get next marshal flight which is ready to enter the landing pattern. +-- @param #AIRBOSS self +-- @return #AIRBOSS.FlightGroup Marshal flight next in line and ready to enter the pattern. Or nil if no flight is ready. +function AIRBOSS:_GetNextMarshalFight() + + -- Loop over all marshal flights. + for _,_flight in pairs(self.Qmarshal) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Current stack. + local stack=flight.flag + + -- Total marshal time in seconds. + local Tmarshal=timer.getAbsTime()-flight.time + + -- Min time in marshal stack. + local TmarshalMin=2*60 --Two minutes for human players. + if flight.ai then + TmarshalMin=3*60 -- Three minutes for AI. + end + + -- Check if conditions are right. + if flight.holding~=nil and Tmarshal>=TmarshalMin then + if flight.case==1 and stack==1 or flight.case>1 then + if flight.ai then + -- Return AI flight. + return flight + else + -- Check for human player if they are already commencing. + if flight.step~=AIRBOSS.PatternStep.COMMENCING then + return flight + end + end + end + end + end + + return nil +end + +--- Check marshal and pattern queues. +-- @param #AIRBOSS self +function AIRBOSS:_CheckQueue() + + -- Print queues. + if self.Debug then + self:_PrintQueue(self.flights, "All Flights") + end + self:_PrintQueue(self.Qmarshal, "Marshal") + self:_PrintQueue(self.Qpattern, "Pattern") + self:_PrintQueue(self.Qwaiting, "Waiting") + self:_PrintQueue(self.Qspinning, "Spinning") + + -- If flights are waiting outside 10 NM zone and carrier switches from Case I to Case II/III, they should be added to the Marshal stack as now there is no stack limit any more. + if self.case>1 then + for _,_flight in pairs(self.Qwaiting) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Remove flight from waiting queue. + local removed=self:_RemoveFlightFromQueue(self.Qwaiting, flight) + + if removed then + + -- Get free stack + local stack=self:_GetFreeStack(flight.ai) + + -- Debug info. + self:T(self.lid..string.format("Moving flight %s onboard %s from Waiting queue to Case %d Marshal stack %d", flight.groupname, flight.onboard, self.case, stack)) + + -- Send flight to marshal stack. + if flight.ai then + self:_MarshalAI(flight, stack) + else + self:_MarshalPlayer(flight, stack) + end + + -- Break the loop so that only one flight per 30 seconds is removed. + break + end + + end + end + + -- Check if carrier is currently in recovery mode. + if not self:IsRecovering() then + + ----------------------------- + -- Switching Recovery Case -- + ----------------------------- + + -- Loop over all flights currently in the marshal queue. + for _,_flight in pairs(self.Qmarshal) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- TODO: In principle this should be done/necessary only if case 1-->2/3 or 2/3-->1, right? + -- When recovery switches from 2->3 or 3-->2 nothing changes in the marshal stack. + + -- Check if a change of stack is necessary. + if (flight.case==1 and self.case>1) or (flight.case>1 and self.case==1) then + + -- Remove flight from marshal queue. + local removed=self:_RemoveFlightFromQueue(self.Qmarshal, flight) + + if removed then + + -- Get free stack + local stack=self:_GetFreeStack(flight.ai) + + -- Debug output. + self:T(self.lid..string.format("Moving flight %s onboard %s from Marshal Case %d ==> %d Marshal stack %d", flight.groupname, flight.onboard, flight.case, self.case, stack)) + + -- Send flight to marshal queue. + if flight.ai then + self:_MarshalAI(flight, stack) + else + self:_MarshalPlayer(flight, stack) + end + + -- Break the loop so that only one flight per 30 seconds is removed. No spam of messages, no conflict with the loop over queue entries. + break + + elseif flight.case~=self.case then + + -- This should handle 2-->3 or 3-->2 + flight.case=self.case + + end + + end + end + + -- Not recovering ==> skip the rest! + return + end + + -- Get number of airborne aircraft units(!) currently in pattern. + local _,npattern=self:_GetQueueInfo(self.Qpattern) + + -- Get number of aircraft units spinning. + local _,nspinning=self:_GetQueueInfo(self.Qspinning) + + -- Get next marshal flight. + local marshalflight=self:_GetNextMarshalFight() + + -- Check if there are flights waiting in the Marshal stack and if the pattern is free. No one should be spinning. + if marshalflight and npattern0 then + + -- Last flight group send to pattern. + local patternflight=self.Qpattern[#self.Qpattern] --#AIRBOSS.FlightGroup + + -- Recovery case of pattern flight. + pcase=patternflight.case + + -- Number of airborne aircraft in this group. Count includes section members. + local npunits=self:_GetFlightUnits(patternflight, false) + + -- Get time in pattern. + Tpattern=timer.getAbsTime()-patternflight.time + self:T(self.lid..string.format("Pattern time of last group %s = %d seconds. # of units=%d.", patternflight.groupname, Tpattern, npunits)) + end + + -- Min time in pattern before next aircraft is allowed. + local TpatternMin + if pcase==1 then + TpatternMin=2*60*npunits --45*npunits -- 45 seconds interval per plane! + else + TpatternMin=2*60*npunits --120*npunits -- 120 seconds interval per plane! + end + + -- Check interval to last pattern flight. + if Tpattern>TpatternMin then + self:T(self.lid..string.format("Sending marshal flight %s to pattern.", marshalflight.groupname)) + self:_ClearForLanding(marshalflight) + end + + end +end + + +--- Clear flight for landing. AI are removed from Marshal queue and the Marshal stack is collapsed. +-- If next in line is an AI flight, this is done. If human player is next, we wait for "Commence" via F10 radio menu command. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight Flight to go to pattern. +function AIRBOSS:_ClearForLanding(flight) + + -- Check if flight is AI or human. If AI, we collapse the stack and commence. If human, we suggest to commence. + if flight.ai then + + -- Collapse stack and send AI to pattern. + self:_RemoveFlightFromMarshalQueue(flight, false) + self:_LandAI(flight) + + -- Cleared for Case X recovery. + self:_MarshalCallClearedForRecovery(flight.onboard, flight.case) + + else + + -- Cleared for Case X recovery. + if flight.step~=AIRBOSS.PatternStep.COMMENCING then + self:_MarshalCallClearedForRecovery(flight.onboard, flight.case) + flight.time=timer.getAbsTime() + end + + -- Set step to commencing. This will trigger the zone check until the player is in the right place. + self:_SetPlayerStep(flight, AIRBOSS.PatternStep.COMMENCING, 3) + + end + +end + +--- Set player step. Any warning is erased and next step hint shown. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #string step Next step. +-- @param #number delay (Optional) Set set after a delay in seconds. +function AIRBOSS:_SetPlayerStep(playerData, step, delay) + + if delay and delay>0 then + -- Delayed call. + --SCHEDULER:New(nil, self._SetPlayerStep, {self, playerData, step}, delay) + self:ScheduleOnce(delay, self._SetPlayerStep, self, playerData, step) + else + + -- Check if player still exists after possible delay. + if playerData then + + -- Set player step. + playerData.step=step + + -- Erase warning. + playerData.warning=nil + + -- Next step hint. + self:_StepHint(playerData) + end + + end + +end + +--- Scan carrier zone for (new) units. +-- @param #AIRBOSS self +function AIRBOSS:_ScanCarrierZone() + + -- Carrier position. + local coord=self:GetCoordinate() + + -- Scan radius = radius of the CCA. + local RCCZ=self.zoneCCA:GetRadius() + + -- Debug info. + self:T(self.lid..string.format("Scanning Carrier Controlled Area. Radius=%.1f NM.", UTILS.MetersToNM(RCCZ))) + + -- Scan units in carrier zone. + local _,_,_,unitscan=coord:ScanObjects(RCCZ, true, false, false) + + + -- Make a table with all groups currently in the CCA zone. + local insideCCA={} + for _,_unit in pairs(unitscan) do + local unit=_unit --Wrapper.Unit#UNIT + + -- Necessary conditions to be met: + local airborne=unit:IsAir() --and unit:InAir() + local inzone=unit:IsInZone(self.zoneCCA) + local friendly=self:GetCoalition()==unit:GetCoalition() + local carrierac=self:_IsCarrierAircraft(unit) + + -- Check if this an aircraft and that it is airborne and closing in. + if airborne and inzone and friendly and carrierac then + + local group=unit:GetGroup() + local groupname=group:GetName() + + if insideCCA[groupname]==nil then + insideCCA[groupname]=group + end + + end + end + + -- Find new flights that are inside CCA. + for groupname,_group in pairs(insideCCA) do + local group=_group --Wrapper.Group#GROUP + + -- Get flight group if possible. + local knownflight=self:_GetFlightFromGroupInQueue(group, self.flights) + + -- Get aircraft type name. + local actype=group:GetTypeName() + + -- Create a new flight group + if knownflight then + + -- Check if flight is AI and if we want to handle it at all. + if knownflight.ai and knownflight.flag==-100 and self.handleai then + + local putintomarshal=false + + -- Get flight group. + local flight=_DATABASE:GetFlightGroup(groupname) + + if flight and flight:IsInbound() and flight.destbase:GetName()==self.carrier:GetName() then + if flight.ishelo then + else + putintomarshal=true + end + flight.airboss=self + end + + + + -- Send AI flight to marshal stack. + if putintomarshal then + + -- Get the next free stack for current recovery case. + local stack=self:_GetFreeStack(knownflight.ai) + + -- Repawn. + local respawn=self.respawnAI + + if stack then + + -- Send AI to marshal stack. We respawn the group to clean possible departure and destination airbases. + self:_MarshalAI(knownflight, stack, respawn) + + else + + -- Send AI to orbit outside 10 NM zone and wait until the next Marshal stack is available. + if not self:_InQueue(self.Qwaiting, knownflight.group) then + self:_WaitAI(knownflight, respawn) -- Group is respawned to clear any attached airfields. + end + + end + + -- Break the loop to not have all flights at once! Spams the message screen. + break + + end -- Closed in or tanker/AWACS + + end + + else + + -- Unknown new AI flight. Create a new flight group. + if not self:_IsHuman(group) then + self:_CreateFlightGroup(group) + end + + end + + end + + -- Find flights that are not in CCA. + local remove={} + for _,_flight in pairs(self.flights) do + local flight=_flight --#AIRBOSS.FlightGroup + if insideCCA[flight.groupname]==nil then + -- Do not remove flights in marshal pattern. At least for case 2 & 3. If zone is set small, they might be outside in the holding pattern. + if flight.ai and not (self:_InQueue(self.Qmarshal, flight.group) or self:_InQueue(self.Qpattern, flight.group)) then + table.insert(remove, flight) + end + end + end + + -- Remove flight groups outside CCA. + for _,flight in pairs(remove) do + self:_RemoveFlightFromQueue(self.flights, flight) + end + +end + +--- Tell player to wait outside the 10 NM zone until a Marshal stack is available. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +function AIRBOSS:_WaitPlayer(playerData) + + -- Check if flight is known to the airboss already. + if playerData then + + -- Number of waiting flights + local nwaiting=#self.Qwaiting + + -- Radio message: Stack is full. + self:_MarshalCallStackFull(playerData.onboard, nwaiting) + + -- Add player flight to waiting queue. + table.insert(self.Qwaiting, playerData) + + -- Set time stamp. + playerData.time=timer.getAbsTime() + + -- Set step to waiting. + playerData.step=AIRBOSS.PatternStep.WAITING + playerData.warning=nil + + -- Set all flights in section to waiting. + for _,_flight in pairs(playerData.section) do + local flight=_flight --#AIRBOSS.PlayerData + flight.step=AIRBOSS.PatternStep.WAITING + flight.time=timer.getAbsTime() + flight.warning=nil + end + + end + +end + + +--- Orbit at a specified position at a specified altitude with a specified speed. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #number stack The Marshal stack the player gets. +function AIRBOSS:_MarshalPlayer(playerData, stack) + + -- Check if flight is known to the airboss already. + if playerData then + + -- Add group to marshal stack. + self:_AddMarshalGroup(playerData, stack) + + -- Set step to holding. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.HOLDING) + + -- Holding switch to nil until player arrives in the holding zone. + playerData.holding=nil + + -- Set same stack for all flights in section. + for _,_flight in pairs(playerData.section) do + local flight=_flight --#AIRBOSS.PlayerData + + -- XXX: Inform player? Should be done by lead via radio? + + -- Set step. + self:_SetPlayerStep(flight, AIRBOSS.PatternStep.HOLDING) + + -- Holding to nil, until arrived. + flight.holding=nil + + -- Set case to that of lead. + flight.case=playerData.case + + -- Set stack flag. + flight.flag=stack + + -- Trigger Marshal event. + self:Marshal(flight) + end + + else + self:E(self.lid.."ERROR: Could not add player to Marshal stack! playerData=nil") + end + +end + +--- Command AI flight to orbit outside the 10 NM zone and wait for a free Marshal stack. +-- If the flight is not already holding in the Marshal stack, it is guided there first. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight Flight group. +-- @param #boolean respawn If true respawn the group. Otherwise reset the mission task with new waypoints. +function AIRBOSS:_WaitAI(flight, respawn) + + -- Set flag to something other than -100 and <0 + flight.flag=-99 + + -- Add AI flight to waiting queue. + table.insert(self.Qwaiting, flight) + + -- Flight group name. + local group=flight.group + local groupname=flight.groupname + + -- Aircraft speed 274 knots TAS ~= 250 KIAS when orbiting the pattern. (Orbit expects m/s.) + local speedOrbitMps=UTILS.KnotsToMps(274) + + -- Orbit speed in km/h for waypoints. + local speedOrbitKmh=UTILS.KnotsToKmph(274) + + -- Aircraft speed 400 knots when transiting to holding zone. (Waypoint expects km/h.) + local speedTransit=UTILS.KnotsToKmph(370) + + -- Carrier coordinate + local cv=self:GetCoordinate() + + -- Coordinate of flight group + local fc=group:GetCoordinate() + + -- Carrier heading + local hdg=self:GetHeading(false) + + -- Heading from carrier to flight group + local hdgto=cv:HeadingTo(fc) + + -- Holding alitude between angels 6 and 10 (random). + local angels=math.random(6,10) + local altitude=UTILS.FeetToMeters(angels*1000) + + -- Point outsize 10 NM zone of the carrier. + local p0=cv:Translate(UTILS.NMToMeters(11), hdgto):Translate(UTILS.NMToMeters(5), hdg):SetAltitude(altitude) + + -- Waypoints array to be filled depending on case etc. + local wp={} + + -- Current position. Always good for as the first waypoint. + wp[1]=group:GetCoordinate():WaypointAirTurningPoint(nil, speedTransit, {}, "Current Position") + + -- Set orbit task. + local taskorbit=group:TaskOrbit(p0, altitude, speedOrbitMps) + + -- Orbit at waypoint. + wp[#wp+1]=p0:WaypointAirTurningPoint(nil, speedOrbitKmh, {taskorbit}, string.format("Waiting Orbit at Angels %d", angels)) + + -- Debug markers. + if self.Debug then + p0:MarkToAll(string.format("Waiting Orbit of flight %s at Angels %s", groupname, angels)) + end + + if respawn then + + -- This should clear the landing waypoints. + -- Note: This resets the weapons and the fuel state. But not the units fortunately. + + -- Get group template. + local Template=group:GetTemplate() + + -- Set route points. + Template.route.points=wp + + -- Respawn the group. + group=group:Respawn(Template, true) + + end + + -- Reinit waypoints. + group:WayPointInitialize(wp) + + -- Route group. + group:Route(wp, 1) + +end + +--- Command AI flight to orbit at a specified position at a specified altitude with a specified speed. If flight is not in the Marshal queue yet, it is added. This fixes the recovery case. +-- If the flight is not already holding in the Marshal stack, it is guided there first. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight Flight group. +-- @param #number nstack Stack number of group. Can also be the current stack if AI position needs to be updated wrt to changed carrier position. +-- @param #boolean respawn If true, respawn the flight otherwise update mission task with new waypoints. +function AIRBOSS:_MarshalAI(flight, nstack, respawn) + self:F2({flight=flight, nstack=nstack, respawn=respawn}) + + -- Nil check. + if flight==nil or flight.group==nil then + self:E(self.lid.."ERROR: flight or flight.group is nil.") + return + end + + -- Nil check. + if flight.group:GetCoordinate()==nil then + self:E(self.lid.."ERROR: cannot get coordinate of flight group.") + return + end + + -- Check if flight is already in Marshal queue. + if not self:_InQueue(self.Qmarshal,flight.group) then + -- Add group to marshal stack queue. + self:_AddMarshalGroup(flight, nstack) + end + + -- Explode unit for testing. Worked! + --local u1=flight.group:GetUnit(1) --Wrapper.Unit#UNIT + --u1:Explode(500, 10) + + -- Recovery case. + local case=flight.case + + -- Get old/current stack. + local ostack=flight.flag + + -- Flight group name. + local group=flight.group + local groupname=flight.groupname + + -- Set new stack. + flight.flag=nstack + + -- Current carrier position. + local Carrier=self:GetCoordinate() + + -- Carrier heading. + local hdg=self:GetHeading() + + -- Aircraft speed 274 knots TAS ~= 250 KIAS when orbiting the pattern. (Orbit expects m/s.) + local speedOrbitMps=UTILS.KnotsToMps(274) + + -- Orbit speed in km/h for waypoints. + local speedOrbitKmh=UTILS.KnotsToKmph(274) + + -- Aircraft speed 400 knots when transiting to holding zone. (Waypoint expects km/h.) + local speedTransit=UTILS.KnotsToKmph(370) + + local altitude + local p0 --Core.Point#COORDINATE + local p1 --Core.Point#COORDINATE + local p2 --Core.Point#COORDINATE + + -- Get altitude and positions. + altitude, p1, p2=self:_GetMarshalAltitude(nstack, case) + + -- Waypoints array to be filled depending on case etc. + local wp={} + + -- If flight has not arrived in the holding zone, we guide it there. + if not flight.holding then + + ---------------------- + -- Route to Holding -- + ---------------------- + + -- Debug info. + self:T(self.lid..string.format("Guiding AI flight %s to marshal stack %d-->%d.", groupname, ostack, nstack)) + + -- Current position. Always good for as the first waypoint. + wp[1]=group:GetCoordinate():WaypointAirTurningPoint(nil, speedTransit, {}, "Current Position") + + -- Task function when arriving at the holding zone. This will set flight.holding=true. + local TaskArrivedHolding=flight.group:TaskFunction("AIRBOSS._ReachedHoldingZone", self, flight) + + -- Select case. + if case==1 then + + -- Initial point 7 NM and a bit port of carrier. + local pE=Carrier:Translate(UTILS.NMToMeters(7), hdg-30):SetAltitude(altitude) + + -- Entry point 5 NM port and slightly astern the boat. + p0=Carrier:Translate(UTILS.NMToMeters(5), hdg-135):SetAltitude(altitude) + + -- Waypoint ahead of carrier's holding zone. + wp[#wp+1]=pE:WaypointAirTurningPoint(nil, speedTransit, {TaskArrivedHolding}, "Entering Case I Marshal Pattern") + + else + + -- Get correct radial depending on recovery case including offset. + local radial=self:GetRadial(case, false, true) + + -- Point in the middle of the race track and a 5 NM more port perpendicular. + p0=p2:Translate(UTILS.NMToMeters(5), radial+90):Translate(UTILS.NMToMeters(5), radial, true) + + -- Entering Case II/III marshal pattern waypoint. + wp[#wp+1]=p0:WaypointAirTurningPoint(nil, speedTransit, {TaskArrivedHolding}, "Entering Case II/III Marshal Pattern") + + end + + else + + ------------------------ + -- In Marshal Pattern -- + ------------------------ + + -- Debug info. + self:T(self.lid..string.format("Updating AI flight %s at marshal stack %d-->%d.", groupname, ostack, nstack)) + + -- Current position. Speed expected in km/h. + wp[1]=group:GetCoordinate():WaypointAirTurningPoint(nil, speedOrbitKmh, {}, "Current Position") + + -- Create new waypoint 0.2 Nm ahead of current positon. + p0=group:GetCoordinate():Translate(UTILS.NMToMeters(0.2), group:GetHeading(), true) + + end + + -- Set orbit task. + local taskorbit=group:TaskOrbit(p1, altitude, speedOrbitMps, p2) + + -- Orbit at waypoint. + wp[#wp+1]=p0:WaypointAirTurningPoint(nil, speedOrbitKmh, {taskorbit}, string.format("Marshal Orbit Stack %d", nstack)) + + -- Debug markers. + if self.Debug then + p0:MarkToAll("WP P0 "..groupname) + p1:MarkToAll("RT P1 "..groupname) + p2:MarkToAll("RT P2 "..groupname) + end + + if respawn then + + -- This should clear the landing waypoints. + -- Note: This resets the weapons and the fuel state. But not the units fortunately. + + -- Get group template. + local Template=group:GetTemplate() + + -- Set route points. + Template.route.points=wp + + -- Respawn the group. + flight.group=group:Respawn(Template, true) + + end + + -- Reinit waypoints. + flight.group:WayPointInitialize(wp) + + -- Route group. + flight.group:Route(wp, 1) + + -- Trigger Marshal event. + self:Marshal(flight) + +end + +--- Tell AI to refuel. Either at the recovery tanker or at the nearest divert airfield. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight Flight group. +function AIRBOSS:_RefuelAI(flight) + + -- Waypoints array. + local wp={} + + -- Current speed. + local CurrentSpeed=flight.group:GetVelocityKMH() + + -- Current positon. + wp[#wp+1]=flight.group:GetCoordinate():WaypointAirTurningPoint(nil, CurrentSpeed, {}, "Current position") + + -- Check if aircraft can be refueled. + -- TODO: This should also depend on the tanker type AC. + local refuelac=false + local actype=flight.group:GetTypeName() + if actype==AIRBOSS.AircraftCarrier.AV8B or + actype==AIRBOSS.AircraftCarrier.F14A or + actype==AIRBOSS.AircraftCarrier.F14B or + actype==AIRBOSS.AircraftCarrier.F14A_AI or + actype==AIRBOSS.AircraftCarrier.HORNET or + actype==AIRBOSS.AircraftCarrier.FA18C or + actype==AIRBOSS.AircraftCarrier.S3B or + actype==AIRBOSS.AircraftCarrier.S3BTANKER then + refuelac=true + end + + -- Message. + local text="" + + -- Refuel or divert? + if self.tanker and refuelac then + + -- Current Tanker position. + local tankerpos=self.tanker.tanker:GetCoordinate() + + -- Task refueling. + local TaskRefuel=flight.group:TaskRefueling() + + -- Task to go back to Marshal. + local TaskMarshal=flight.group:TaskFunction("AIRBOSS._TaskFunctionMarshalAI", self, flight) + + -- Waypoint with tasks. + wp[#wp+1]=tankerpos:WaypointAirTurningPoint(nil, CurrentSpeed, {TaskRefuel, TaskMarshal}, "Refueling") + + -- Marshal Message. + self:_MarshalCallGasAtTanker(flight.onboard) + + else + + ------------------------------ + -- Guide AI to divert field -- + ------------------------------ + + -- Closest Airfield of the coaliton. + local divertfield=self:GetCoordinate():GetClosestAirbase(Airbase.Category.AIRDROME, self:GetCoalition()) + + -- Handle case where there is no divert field of the own coalition and try neutral instead. + if divertfield==nil then + divertfield=self:GetCoordinate():GetClosestAirbase(Airbase.Category.AIRDROME, 0) + end + + if divertfield then + + -- Coordinate. + local divertcoord=divertfield:GetCoordinate() + + -- Landing waypoint. + wp[#wp+1]=divertcoord:WaypointAirLanding(UTILS.KnotsToKmph(300), divertfield, {}, "Divert Field") + + -- Marshal Message. + self:_MarshalCallGasAtDivert(flight.onboard, divertfield:GetName()) + + -- Respawn! + + -- Get group template. + local Template=flight.group:GetTemplate() + + -- Set route points. + Template.route.points=wp + + -- Respawn the group. + flight.group=flight.group:Respawn(Template, true) + + else + -- Set flight to refueling so this is not called again. + self:E(self.lid..string.format("WARNING: No recovery tanker or divert field available for group %s.", flight.groupname)) + flight.refueling=true + return + end + + end + + -- Reinit waypoints. + flight.group:WayPointInitialize(wp) + + -- Route group. + flight.group:Route(wp, 1) + + -- Set refueling switch. + flight.refueling=true + +end + +--- Tell AI to land on the carrier. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight Flight group. +function AIRBOSS:_LandAI(flight) + + -- Debug info. + self:T(self.lid..string.format("Landing AI flight %s.", flight.groupname)) + + -- NOTE: Looks like the AI needs to approach at the "correct" speed. If they are too fast, they fly an unnecessary circle to bleed of speed first. + -- Unfortunately, the correct speed depends on the aircraft type! + + -- Aircraft speed when flying the pattern. + local Speed=UTILS.KnotsToKmph(200) + + if flight.actype==AIRBOSS.AircraftCarrier.HORNET or flight.actype==AIRBOSS.AircraftCarrier.FA18C then + Speed=UTILS.KnotsToKmph(200) + elseif flight.actype==AIRBOSS.AircraftCarrier.E2D then + Speed=UTILS.KnotsToKmph(150) + elseif flight.actype==AIRBOSS.AircraftCarrier.F14A_AI or flight.actype==AIRBOSS.AircraftCarrier.F14A or flight.actype==AIRBOSS.AircraftCarrier.F14B then + Speed=UTILS.KnotsToKmph(175) + elseif flight.actype==AIRBOSS.AircraftCarrier.S3B or flight.actype==AIRBOSS.AircraftCarrier.S3BTANKER then + Speed=UTILS.KnotsToKmph(140) + end + + -- Carrier position. + local Carrier=self:GetCoordinate() + + -- Carrier heading. + local hdg=self:GetHeading() + + -- Waypoints array. + local wp={} + + local CurrentSpeed=flight.group:GetVelocityKMH() + + -- Current positon. + wp[#wp+1]=flight.group:GetCoordinate():WaypointAirTurningPoint(nil, CurrentSpeed, {}, "Current position") + + -- Altitude 800 ft. Looks like this works best. + local alt=UTILS.FeetToMeters(800) + + -- Landing waypoint 5 NM behind carrier at 2000 ft = 610 meters ASL. + wp[#wp+1]=Carrier:Translate(UTILS.NMToMeters(4), hdg-160):SetAltitude(alt):WaypointAirLanding(Speed, self.airbase, nil, "Landing") + --wp[#wp+1]=Carrier:Translate(UTILS.NMToMeters(4), hdg-160):SetAltitude(alt):WaypointAirLandingReFu(Speed, self.airbase, nil, "Landing") + + --wp[#wp+1]=self:GetCoordinate():Translate(UTILS.NMToMeters(3), hdg-160):SetAltitude(alt):WaypointAirTurningPoint(nil,Speed, {}, "Before Initial") ---WaypointAirLanding(Speed, self.airbase, nil, "Landing") + --wp[#wp+1]=self:GetCoordinate():WaypointAirLanding(Speed, self.airbase, nil, "Landing") + + -- Reinit waypoints. + flight.group:WayPointInitialize(wp) + + -- Route group. + flight.group:Route(wp, 0) +end + +--- Get marshal altitude and two positions of a counter-clockwise race track pattern. +-- @param #AIRBOSS self +-- @param #number stack Assigned stack number. Counting starts at one, i.e. stack=1 is the first stack. +-- @param #number case Recovery case. Default is self.case. +-- @return #number Holding altitude in meters. +-- @return Core.Point#COORDINATE First race track coordinate. +-- @return Core.Point#COORDINATE Second race track coordinate. +function AIRBOSS:_GetMarshalAltitude(stack, case) + + -- Stack <= 0. + if stack<=0 then + return 0,nil,nil + end + + -- Recovery case. + case=case or self.case + + -- Carrier position. + local Carrier=self:GetCoordinate() + + -- Altitude of first stack. Depends on recovery case. + local angels0 + local Dist + local p1=nil --Core.Point#COORDINATE + local p2=nil --Core.Point#COORDINATE + + -- Stack number. + local nstack=stack-1 + + if case==1 then + + -- CASE I: Holding at 2000 ft on a circular pattern port of the carrier. Interval +1000 ft for next stack. + angels0=2 + + -- Get true heading of carrier. + local hdg=self.carrier:GetHeading() + + -- For CCW pattern: First point astern, second ahead of the carrier. + + -- First point over carrier. + p1=Carrier + + -- Second point 1.5 NM ahead. + p2=Carrier:Translate(UTILS.NMToMeters(1.5), hdg) + + -- Tarawa,LHA,LHD Delta patterns. + if self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS then + + -- Pattern is directly overhead the carrier. + p1=Carrier:Translate(UTILS.NMToMeters(1.0), hdg+90) + p2=p1:Translate(2.5, hdg) + + end + + else + + -- CASE II/III: Holding at 6000 ft on a racetrack pattern astern the carrier. + angels0=6 + + -- Distance: d=n*angels0+15 NM, so first stack is at 15+6=21 NM + Dist=UTILS.NMToMeters(nstack+angels0+15) + + -- Get correct radial depending on recovery case including offset. + local radial=self:GetRadial(case, false, true) + + -- For CCW pattern: p1 further astern than p2. + + -- Length of the race track pattern. + local l=UTILS.NMToMeters(10) + + -- First point of race track pattern. + p1=Carrier:Translate(Dist+l, radial) + + -- Second point. + p2=Carrier:Translate(Dist, radial) + + end + + -- Pattern altitude. + local altitude=UTILS.FeetToMeters((nstack+angels0)*1000) + + -- Set altitude of coordinate. + p1:SetAltitude(altitude, true) + p2:SetAltitude(altitude, true) + + return altitude, p1, p2 +end + +--- Calculate an estimate of the charlie time of the player based on how many other aircraft are in the marshal or pattern queue before him. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flightgroup Flight data. +-- @return #number Charlie (abs) time in seconds. Or nil, if stack<0 or no recovery window will open. +function AIRBOSS:_GetCharlieTime(flightgroup) + + -- Get current stack of player. + local stack=flightgroup.flag + + -- Flight is not in marshal stack. + if stack<=0 then + return nil + end + + -- Current abs time. + local Tnow=timer.getAbsTime() + + -- Time the player has to spend in marshal stack until all lower stacks are emptied. + local Tcharlie=0 + + local Trecovery=0 + if self.recoverywindow then + -- Time in seconds until the next recovery starts or 0 if window is already open. + Trecovery=math.max(self.recoverywindow.START-Tnow, 0) + else + -- Set ~7 min if no future recovery window is defined. Otherwise radio call function crashes. + Trecovery=7*60 + end + + -- Loop over flights currently in the marshal queue. + for _,_flight in pairs(self.Qmarshal) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Stack of marshal flight. + local mstack=flight.flag + + -- Time to get to the marshal stack if not holding already. + local Tarrive=0 + + -- Minimum holding time per stack. + local Tholding=3*60 + + if stack>0 and mstack>0 and mstack<=stack then + + -- Check if flight is already holding or just on its way. + if flight.holding==nil then + -- Flight is on its way to the marshal stack. + + -- Coordinate of the holding zone. + local holdingzone=self:_GetZoneHolding(flight.case, 1):GetCoordinate() + + -- Distance to holding zone. + local d0=holdingzone:Get2DDistance(flight.group:GetCoordinate()) + + -- Current velocity. + local v0=flight.group:GetVelocityMPS() + + -- Time to get to the carrier. + Tarrive=d0/v0 + + self:T3(self.lid..string.format("Tarrive=%.1f seconds, Clock %s", Tarrive, UTILS.SecondsToClock(Tnow+Tarrive))) + + else + -- Flight is already holding. + + -- Next in line. + if mstack==1 then + + -- Current holding time. flight.time stamp should be when entering holding or last time the stack collapsed. + local tholding=timer.getAbsTime()-flight.time + + -- Deduce current holding time. Ensure that is >=0. + Tholding=math.max(3*60-tholding, 0) + end + + end + + -- This is the approx time needed to get to the pattern. If we are already there, it is the time until the recovery window opens or 0 if it is already open. + local Tmin=math.max(Tarrive, Trecovery) + + -- Charlie time + 2 min holding in stack 1. + Tcharlie=math.max(Tmin, Tcharlie)+Tholding + end + + end + + -- Convert to abs time. + Tcharlie=Tcharlie+Tnow + + -- Debug info. + local text=string.format("Charlie time for flight %s (%s) %s", flightgroup.onboard, flightgroup.groupname, UTILS.SecondsToClock(Tcharlie)) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) + + return Tcharlie +end + +--- Add a flight group to the Marshal queue at a specific stack. Flight is informed via message. This fixes the recovery case to the current case ops in progress self.case). +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight Flight group. +-- @param #number stack Marshal stack. This (re-)sets the flag value. +function AIRBOSS:_AddMarshalGroup(flight, stack) + + -- Set flag value. This corresponds to the stack number which starts at 1. + flight.flag=stack + + -- Set recovery case. + flight.case=self.case + + -- Add to marshal queue. + table.insert(self.Qmarshal, flight) + + -- Pressure. + local P=UTILS.hPa2inHg(self:GetCoordinate():GetPressure()) + + -- Stack altitude. + --local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack, flight.case)) + local alt=self:_GetMarshalAltitude(stack, flight.case) + + -- Current BRC. + local brc=self:GetBRC() + + -- If the carrier is supposed to turn into the wind, we take the wind coordinate. + if self.recoverywindow and self.recoverywindow.WIND then + brc=self:GetBRCintoWind() + end + + -- Get charlie time estimate. + flight.Tcharlie=self:_GetCharlieTime(flight) + + -- Convert to clock string. + local Ccharlie=UTILS.SecondsToClock(flight.Tcharlie) + + -- Combined marshal call. + self:_MarshalCallArrived(flight.onboard, flight.case, brc, alt, Ccharlie, P) + + -- Hint about TACAN bearing. + if self.TACANon and (not flight.ai) and flight.difficulty==AIRBOSS.Difficulty.EASY then + -- Get inverse magnetic radial potential offset. + local radial=self:GetRadial(flight.case, true, true, true) + if flight.case==1 then + -- For case 1 we want the BRC but above routine return FB. + radial=self:GetBRC() + end + local text=string.format("Select TACAN %03d°, channel %d%s (%s)", radial, self.TACANchannel,self.TACANmode, self.TACANmorse) + self:MessageToPlayer(flight, text, nil, "") + end + +end + +--- Collapse marshal stack. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight Flight that left the marshal stack. +-- @param #boolean nopattern If true, flight does not go to pattern. +function AIRBOSS:_CollapseMarshalStack(flight, nopattern) + self:F2({flight=flight, nopattern=nopattern}) + + -- Recovery case of flight. + local case=flight.case + + -- Stack of flight. + local stack=flight.flag + + -- Check that stack > 0. + if stack<=0 then + self:E(self.lid..string.format("ERROR: Flight %s is has stack value %d<0. Cannot collapse stack!", flight.groupname, stack)) + return + end + + -- Memorize time when stack collapsed. Should better depend on case but for now we assume there are no two different stacks Case I or II/III. + self.Tcollapse=timer.getTime() + + -- Decrease flag values of all flight groups in marshal stack. + for _,_flight in pairs(self.Qmarshal) do + local mflight=_flight --#AIRBOSS.PlayerData + + -- Only collapse stack of which the flight left. CASE II/III stacks are not collapsed. + if (case==1 and mflight.case==1) then --or (case>1 and mflight.case>1) then + + -- Get current flag/stack value. + local mstack=mflight.flag + + -- Only collapse stacks above the new pattern flight. + if mstack>stack then + + -- TODO: Is this now right as we allow more flights per stack? + -- Question is, does the stack collapse if the lower stack is completely empty or do aircraft descent if just one flight leaves. + -- For now, assuming that the stack must be completely empty before the next higher AC are allowed to descent. + local newstack=self:_GetFreeStack(mflight.ai, mflight.case, true) + + -- Free stack has to be below. + if newstack and newstack %d.", mflight.groupname, mflight.case, mstack, newstack)) + + if mflight.ai then + + -- Command AI to decrease stack. Flag is set in the routine. + self:_MarshalAI(mflight, newstack) + + else + + -- Decrease stack/flag. Human player needs to take care himself. + mflight.flag=newstack + + -- Angels of new stack. + local angels=self:_GetAngels(self:_GetMarshalAltitude(newstack, case)) + + -- Inform players. + if mflight.difficulty~=AIRBOSS.Difficulty.HARD then + + -- Send message to all non-pros that they can descent. + local text=string.format("descent to stack at Angels %d.", angels) + self:MessageToPlayer(mflight, text, "MARSHAL") + + end + + -- Set time stamp. + mflight.time=timer.getAbsTime() + + -- Loop over section members. + for _,_sec in pairs(mflight.section) do + local sec=_sec --#AIRBOSS.PlayerData + + -- Also decrease flag for section members of flight. + sec.flag=newstack + + -- Set new time stamp. + sec.time=timer.getAbsTime() + + -- Inform section member. + if sec.difficulty~=AIRBOSS.Difficulty.HARD then + local text=string.format("descent to stack at Angels %d.", angels) + self:MessageToPlayer(sec, text, "MARSHAL") + end + + end + + end + + end + end + end + end + + + if nopattern then + + -- Debug message. + self:T(self.lid..string.format("Flight %s is leaving stack but not going to pattern.", flight.groupname)) + + else + + -- Debug message. + local Tmarshal=UTILS.SecondsToClock(timer.getAbsTime()-flight.time) + self:T(self.lid..string.format("Flight %s is leaving marshal after %s and going pattern.", flight.groupname, Tmarshal)) + + -- Add flight to pattern queue. + self:_AddFlightToPatternQueue(flight) + + end + + -- Set flag to -1 (-1 is rather arbitrary but it should not be positive or -100 or -42). + flight.flag=-1 + + -- New time stamp for time in pattern. + flight.time=timer.getAbsTime() + +end + +--- Get next free Marshal stack. Depending on AI/human and recovery case. +-- @param #AIRBOSS self +-- @param #boolean ai If true, get a free stack for an AI flight group. +-- @param #number case Recovery case. Default current (self) case in progress. +-- @param #boolean empty Return lowest stack that is completely empty. +-- @return #number Lowest free stack available for the given case or nil if all Case I stacks are taken. +function AIRBOSS:_GetFreeStack(ai, case, empty) + + -- Recovery case. + case=case or self.case + + if case==1 then + return self:_GetFreeStack_Old(ai, case, empty) + end + + -- Max number of stacks available. + local nmaxstacks=100 + if case==1 then + nmaxstacks=self.Nmaxmarshal + end + + -- Assume up to two (human) flights per stack. All are free. + local stack={} + for i=1,nmaxstacks do + stack[i]=self.NmaxStack -- Number of human flights per stack. + end + + local nmax=1 + + -- Loop over all flights in marshal stack. + for _,_flight in pairs(self.Qmarshal) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Check that the case is right. + if flight.case==case then + + -- Get stack of flight. + local n=flight.flag + + if n>nmax then + nmax=n + end + + if n>0 then + if flight.ai or flight.case>1 then + stack[n]=0 -- AI get one stack on their own. Also CASE II/III get one stack each. + else + stack[n]=stack[n]-1 + end + else + self:E(string.format("ERROR: Flight %s in marshal stack has stack value <= 0. Stack value is %d.", flight.groupname, n)) + end + + end + end + + local nfree=nil + if stack[nmax]==0 then + -- Max occupied stack is completely full! + if case==1 then + if nmax>=nmaxstacks then + -- Already all Case I stacks are occupied ==> wait outside 10 NM zone. + nfree=nil + else + -- Return next free stack. + nfree=nmax+1 + end + else + -- Case II/III return next stack + nfree=nmax+1 + end + + elseif stack[nmax]==self.NmaxStack then + -- Max occupied stack is completely empty! This should happen only when there is no other flight in the marshal queue. + self:E(self.lid..string.format("ERROR: Max occupied stack is empty. Should not happen! Nmax=%d, stack[nmax]=%d", nmax, stack[nmax])) + nfree=nmax + else + -- Max occupied stack is partly full. + if ai or empty or case>1 then + nfree=nmax+1 + else + nfree=nmax + end + + end + + self:I(self.lid..string.format("Returning free stack %s", tostring(nfree))) + return nfree +end + +--- Get next free Marshal stack. Depending on AI/human and recovery case. +-- @param #AIRBOSS self +-- @param #boolean ai If true, get a free stack for an AI flight group. +-- @param #number case Recovery case. Default current (self) case in progress. +-- @param #boolean empty Return lowest stack that is completely empty. +-- @return #number Lowest free stack available for the given case or nil if all Case I stacks are taken. +function AIRBOSS:_GetFreeStack_Old(ai, case, empty) + + -- Recovery case. + case=case or self.case + + -- Max number of stacks available. + local nmaxstacks=100 + if case==1 then + nmaxstacks=self.Nmaxmarshal + end + + -- Assume up to two (human) flights per stack. All are free. + local stack={} + for i=1,nmaxstacks do + stack[i]=self.NmaxStack -- Number of human flights per stack. + end + + -- Loop over all flights in marshal stack. + for _,_flight in pairs(self.Qmarshal) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Check that the case is right. + if flight.case==case then + + -- Get stack of flight. + local n=flight.flag + + if n>0 then + if flight.ai or flight.case>1 then + stack[n]=0 -- AI get one stack on their own. Also CASE II/III get one stack each. + else + stack[n]=stack[n]-1 + end + else + self:E(string.format("ERROR: Flight %s in marshal stack has stack value <= 0. Stack value is %d.", flight.groupname, n)) + end + + end + end + + -- Loop over stacks and check which one has a place left. + local nfree=nil + for i=1,nmaxstacks do + self:T2(self.lid..string.format("FF Stack[%d]=%d", i, stack[i])) + if ai or empty or case>1 then + -- AI need the whole stack. + if stack[i]==self.NmaxStack then + nfree=i + return i + end + else + -- Human players only need one free spot. + if stack[i]>0 then + nfree=i + return i + end + end + end + + return nfree +end + +--- Get number of (airborne) units in a flight. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight The flight group. +-- @param #boolean onground If true, include units on the ground. By default only airborne units are counted. +-- @return #number Number of units in flight including section members. +-- @return #number Number of units in flight excluding section members. +-- @return #number Number of section members. +function AIRBOSS:_GetFlightUnits(flight, onground) + + -- Default is only airborne. + local inair=true + if onground==true then + inair=false + end + + --- Count units of a group which are alive and in the air. + local function countunits(_group, inair) + local group=_group --Wrapper.Group#GROUP + local units=group:GetUnits() + local n=0 + if units then + for _,_unit in pairs(units) do + local unit=_unit --Wrapper.Unit#UNIT + if unit and unit:IsAlive() then + if inair then + -- Only count units in air. + if unit:InAir() then + self:T2(self.lid..string.format("Unit %s is in AIR", unit:GetName())) + n=n+1 + end + else + -- Count units in air or on the ground. + n=n+1 + end + end + end + end + return n + end + + + -- Count units of the group itself (alive units in air). + local nunits=countunits(flight.group, inair) + + -- Count section members. + local nsection=0 + for _,sec in pairs(flight.section) do + local secflight=sec --#AIRBOSS.PlayerData + -- Count alive units in air. + nsection=nsection+countunits(secflight.group, inair) + end + + return nunits+nsection, nunits, nsection +end + +--- Get number of groups and units in queue, which are alive and airborne. In units we count the section members as well. +-- @param #AIRBOSS self +-- @param #table queue The queue. Can be self.flights, self.Qmarshal or self.Qpattern. +-- @param #number case (Optional) Only count flights, which are in a specific recovery case. Note that you can use case=23 for flights that are either in Case II or III. By default all groups/units regardless of case are counted. +-- @return #number Total number of flight groups in queue. +-- @return #number Total number of aircraft in queue since each flight group can contain multiple aircraft. +function AIRBOSS:_GetQueueInfo(queue, case) + + local ngroup=0 + local Nunits=0 + + -- Loop over flight groups. + for _,_flight in pairs(queue) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Check if a specific case was requested. + if case then + + ------------------------------------------------------------------------ + -- Only count specific case with special 23 = CASE II and III combined. + ------------------------------------------------------------------------ + + if (flight.case==case) or (case==23 and (flight.case==2 or flight.case==3)) then + + -- Number of total units, units in flight and section members ALIVE and AIRBORNE. + local ntot,nunits,nsection=self:_GetFlightUnits(flight) + + -- Add up total unit number. + Nunits=Nunits+ntot + + -- Increase group count. + if ntot>0 then + ngroup=ngroup+1 + end + + end + + else + + --------------------------------------------------------------------------- + -- No specific case requested. Count all groups & units in selected queue. + --------------------------------------------------------------------------- + + -- Number of total units, units in flight and section members ALIVE and AIRBORNE. + local ntot,nunits,nsection=self:_GetFlightUnits(flight) + + -- Add up total unit number. + Nunits=Nunits+ntot + + -- Increase group count. + if ntot>0 then + ngroup=ngroup+1 + end + + end + + end + + return ngroup, Nunits +end + +--- Print holding queue. +-- @param #AIRBOSS self +-- @param #table queue Queue to print. +-- @param #string name Queue name. +function AIRBOSS:_PrintQueue(queue, name) + + --local nqueue=#queue + local Nqueue, nqueue=self:_GetQueueInfo(queue) + + local text=string.format("%s Queue N=%d (#%d), n=%d:", name, Nqueue, #queue, nqueue) + if #queue==0 then + text=text.." empty." + else + for i,_flight in pairs(queue) do + local flight=_flight --#AIRBOSS.FlightGroup + + local clock=UTILS.SecondsToClock(timer.getAbsTime()-flight.time) + local case=flight.case + local stack=flight.flag + local fuel=flight.group:GetFuelMin()*100 + local ai=tostring(flight.ai) + local lead=flight.seclead + local Nsec=#flight.section + local actype=self:_GetACNickname(flight.actype) + local onboard=flight.onboard + local holding=tostring(flight.holding) + + -- Airborne units. + local _, nunits, nsec=self:_GetFlightUnits(flight, false) + + -- Text. + text=text..string.format("\n[%d] %s*%d (%s): lead=%s (%d/%d), onboard=%s, flag=%d, case=%d, time=%s, fuel=%d, ai=%s, holding=%s", + i, flight.groupname, nunits, actype, lead, nsec, Nsec, onboard, stack, case, clock, fuel, ai, holding) + if stack>0 then + local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack, case)) + text=text..string.format(" stackalt=%d ft", alt) + end + for j,_element in pairs(flight.elements) do + local element=_element --#AIRBOSS.FlightElement + text=text..string.format("\n (%d) %s (%s): ai=%s, ballcall=%s, recovered=%s", + j, element.onboard, element.unitname, tostring(element.ai), tostring(element.ballcall), tostring(element.recovered)) + end + end + end + self:T(self.lid..text) +end + +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FLIGHT & PLAYER functions +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new flight group. Usually when a flight appears in the CCA. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group Aircraft group. +-- @return #AIRBOSS.FlightGroup Flight group. +function AIRBOSS:_CreateFlightGroup(group) + + -- Debug info. + self:T(self.lid..string.format("Creating new flight for group %s of aircraft type %s.", group:GetName(), group:GetTypeName())) + + -- New flight. + local flight={} --#AIRBOSS.FlightGroup + + -- Check if not already in flights + if not self:_InQueue(self.flights, group) then + + -- Flight group name + local groupname=group:GetName() + local human, playername=self:_IsHuman(group) + + -- Queue table item. + flight.group=group + flight.groupname=group:GetName() + flight.nunits=#group:GetUnits() + flight.time=timer.getAbsTime() + flight.dist0=group:GetCoordinate():Get2DDistance(self:GetCoordinate()) + flight.flag=-100 + flight.ai=not human + flight.actype=group:GetTypeName() + flight.onboardnumbers=self:_GetOnboardNumbers(group) + flight.seclead=flight.group:GetUnit(1):GetName() -- Sec lead is first unitname of group but player name for players. + flight.section={} + flight.ballcall=false + flight.refueling=false + flight.holding=nil + flight.name=flight.group:GetUnit(1):GetName() --Will be overwritten in _Newplayer with player name if human player in the group. + + -- Note, this should be re-set elsewhere! + flight.case=self.case + + -- Flight elements. + local text=string.format("Flight elements of group %s:", flight.groupname) + flight.elements={} + local units=group:GetUnits() + for i,_unit in pairs(units) do + local unit=_unit --Wrapper.Unit#UNIT + local element={} --#AIRBOSS.FlightElement + element.unit=unit + element.unitname=unit:GetName() + element.onboard=flight.onboardnumbers[element.unitname] + element.ballcall=false + element.ai=not self:_IsHumanUnit(unit) + element.recovered=nil + text=text..string.format("\n[%d] %s onboard #%s, AI=%s", i, element.unitname, tostring(element.onboard), tostring(element.ai)) + table.insert(flight.elements, element) + end + self:T(self.lid..text) + + -- Onboard + if flight.ai then + local onboard=flight.onboardnumbers[flight.seclead] + flight.onboard=onboard + else + flight.onboard=self:_GetOnboardNumberPlayer(group) + end + + -- Add to known flights. + table.insert(self.flights, flight) + + else + self:E(self.lid..string.format("ERROR: Flight group %s already exists in self.flights!", group:GetName())) + return nil + end + + return flight +end + + +--- Initialize player data after birth event of player unit. +-- @param #AIRBOSS self +-- @param #string unitname Name of the player unit. +-- @return #AIRBOSS.PlayerData Player data. +function AIRBOSS:_NewPlayer(unitname) + + -- Get player unit and name. + local playerunit, playername=self:_GetPlayerUnitAndName(unitname) + + if playerunit and playername then + + -- Get group. + local group=playerunit:GetGroup() + + -- Player data. + local playerData --#AIRBOSS.PlayerData + + -- Create a flight group for the player. + playerData=self:_CreateFlightGroup(group) + + -- Nil check. + if playerData then + + -- Player unit, client and callsign. + playerData.unit = playerunit + playerData.unitname = unitname + playerData.name = playername + playerData.callsign = playerData.unit:GetCallsign() + playerData.client = CLIENT:FindByName(unitname, nil, true) + playerData.seclead = playername + + -- Number of passes done by player in this slot. + playerData.passes=0 --playerData.passes or 0 + + -- Messages for player. + playerData.messages={} + + -- Debriefing tables. + playerData.lastdebrief=playerData.lastdebrief or {} + + -- Attitude monitor. + playerData.attitudemonitor=false + + -- Trap sheet save. + if playerData.trapon==nil then + playerData.trapon=self.trapsheet + end + + -- Set difficulty level. + playerData.difficulty=playerData.difficulty or self.defaultskill + + -- Subtitles of player. + if playerData.subtitles==nil then + playerData.subtitles=true + end + + -- Show step hints. + if playerData.showhints==nil then + if playerData.difficulty==AIRBOSS.Difficulty.HARD then + playerData.showhints=false + else + playerData.showhints=true + end + end + + -- Points rewarded. + playerData.points={} + + -- Init stuff for this round. + playerData=self:_InitPlayer(playerData) + + -- Init player data. + self.players[playername]=playerData + + -- Init player grades table if necessary. + self.playerscores[playername]=self.playerscores[playername] or {} + + -- Welcome player message. + if self.welcome then + self:MessageToPlayer(playerData, string.format("Welcome, %s %s!", playerData.difficulty, playerData.name), string.format("AIRBOSS %s", self.alias), "", 5) + end + + end + + -- Return player data table. + return playerData + end + + return nil +end + +--- Initialize player data by (re-)setting parmeters to initial values. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #string step (Optional) New player step. Default UNDEFINED. +-- @return #AIRBOSS.PlayerData Initialized player data. +function AIRBOSS:_InitPlayer(playerData, step) + self:T(self.lid..string.format("Initializing player data for %s callsign %s.", playerData.name, playerData.callsign)) + + playerData.step=step or AIRBOSS.PatternStep.UNDEFINED + playerData.groove={} + playerData.debrief={} + playerData.trapsheet={} + playerData.warning=nil + playerData.holding=nil + playerData.refueling=false + playerData.valid=false + playerData.lig=false + playerData.wop=false + playerData.waveoff=false + playerData.wofd=false + playerData.owo=false + playerData.boltered=false + playerData.landed=false + playerData.Tlso=timer.getTime() + playerData.Tgroove=nil + playerData.TIG0=nil + playerData.wire=nil + playerData.flag=-100 + playerData.debriefschedulerID=nil + + -- Set us up on final if group name contains "Groove". But only for the first pass. + if playerData.group:GetName():match("Groove") and playerData.passes==0 then + self:MessageToPlayer(playerData, "Group name contains \"Groove\". Happy groove testing.") + playerData.attitudemonitor=true + playerData.step=AIRBOSS.PatternStep.FINAL + self:_AddFlightToPatternQueue(playerData) + self.dTstatus=0.1 + end + + return playerData +end + + +--- Get flight from group in a queue. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group Group that will be removed from queue. +-- @param #table queue The queue from which the group will be removed. +-- @return #AIRBOSS.FlightGroup Flight group or nil. +-- @return #number Queue index or nil. +function AIRBOSS:_GetFlightFromGroupInQueue(group, queue) + + if group then + + -- Group name + local name=group:GetName() + + -- Loop over all flight groups in queue + for i,_flight in pairs(queue) do + local flight=_flight --#AIRBOSS.FlightGroup + + if flight.groupname==name then + return flight, i + end + end + + self:T2(self.lid..string.format("WARNING: Flight group %s could not be found in queue.", name)) + end + + self:T2(self.lid..string.format("WARNING: Flight group could not be found in queue. Group is nil!")) + return nil, nil +end + +--- Get element in flight. +-- @param #AIRBOSS self +-- @param #string unitname Name of the unit. +-- @return #AIRBOSS.FlightElement Element of the flight or nil. +-- @return #number Element index or nil. +-- @return #AIRBOSS.FlightGroup The Flight group or nil +function AIRBOSS:_GetFlightElement(unitname) + + -- Get the unit. + local unit=UNIT:FindByName(unitname) + + -- Check if unit exists. + if unit then + + -- Get flight element from all flights. + local flight=self:_GetFlightFromGroupInQueue(unit:GetGroup(), self.flights) + + -- Check if fight exists. + if flight then + + -- Loop over all elements in flight group. + for i,_element in pairs(flight.elements) do + local element=_element --#AIRBOSS.FlightElement + + if element.unit:GetName()==unitname then + return element, i, flight + end + end + + self:T2(self.lid..string.format("WARNING: Flight element %s could not be found in flight group.", unitname, flight.groupname)) + end + end + + return nil, nil, nil +end + +--- Get element in flight. +-- @param #AIRBOSS self +-- @param #string unitname Name of the unit. +-- @return #boolean If true, element could be removed or nil otherwise. +function AIRBOSS:_RemoveFlightElement(unitname) + + -- Get table index. + local element,idx, flight=self:_GetFlightElement(unitname) + + if idx then + table.remove(flight.elements, idx) + return true + else + self:T("WARNING: Flight element could not be removed from flight group. Index=nil!") + return nil + end +end + +--- Check if a group is in a queue. +-- @param #AIRBOSS self +-- @param #table queue The queue to check. +-- @param Wrapper.Group#GROUP group The group to be checked. +-- @return #boolean If true, group is in the queue. False otherwise. +function AIRBOSS:_InQueue(queue, group) + local name=group:GetName() + for _,_flight in pairs(queue) do + local flight=_flight --#AIRBOSS.FlightGroup + if name==flight.groupname then + return true + end + end + return false +end + +--- Remove dead flight groups from all queues. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group Aircraft group. +-- @return #AIRBOSS.FlightGroup Flight group. +function AIRBOSS:_RemoveDeadFlightGroups() + + -- Remove dead flights from all flights table. + for i=#self.flight,1,-1 do + local flight=self.flights[i] --#AIRBOSS.FlightGroup + if not flight.group:IsAlive() then + self:T(string.format("Removing dead flight group %s from ALL flights table.", flight.groupname)) + table.remove(self.flights, i) + end + end + + -- Remove dead flights from Marhal queue table. + for i=#self.Qmarshal,1,-1 do + local flight=self.Qmarshal[i] --#AIRBOSS.FlightGroup + if not flight.group:IsAlive() then + self:T(string.format("Removing dead flight group %s from Marshal Queue table.", flight.groupname)) + table.remove(self.Qmarshal, i) + end + end + + -- Remove dead flights from Pattern queue table. + for i=#self.Qpattern,1,-1 do + local flight=self.Qpattern[i] --#AIRBOSS.FlightGroup + if not flight.group:IsAlive() then + self:T(string.format("Removing dead flight group %s from Pattern Queue table.", flight.groupname)) + table.remove(self.Qpattern, i) + end + end + +end + +--- Get the lead flight group of a flight group. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight Flight group to check. +-- @return #AIRBOSS.FlightGroup Flight group of the leader or flight itself if no other leader. +function AIRBOSS:_GetLeadFlight(flight) + + -- Init. + local lead=flight + + -- Only human players can be section leads of other players. + if flight.name~=flight.seclead then + lead=self.players[flight.seclead] + end + + return lead +end + +--- Check if all elements of a flight were recovered. This also checks potential section members. +-- If so, flight is removed from the queue. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight Flight group to check. +-- @return #boolean If true, all elements landed. +function AIRBOSS:_CheckSectionRecovered(flight) + + -- Nil check. + if flight==nil then + return true + end + + -- Get the lead flight first, so that we can also check all section members. + local lead=self:_GetLeadFlight(flight) + + -- Check all elements of the lead flight group. + for _,_element in pairs(lead.elements) do + local element=_element --#AIRBOSS.FlightElement + if not element.recovered then + return false + end + end + + -- Now check all section members, if any. + for _,_section in pairs(lead.section) do + local sectionmember=_section --#AIRBOSS.FlightGroup + + -- Check all elements of the secmember flight group. + for _,_element in pairs(sectionmember.elements) do + local element=_element --#AIRBOSS.FlightElement + if not element.recovered then + return false + end + end + end + + -- Remove lead flight from pattern queue. It is this flight who is added to the queue. + self:_RemoveFlightFromQueue(self.Qpattern, lead) + + -- Just for now, check if it is in other queues as well. + if self:_InQueue(self.Qmarshal, lead.group) then + self:E(self.lid..string.format("ERROR: lead flight group %s should not be in marshal queue", lead.groupname)) + self:_RemoveFlightFromMarshalQueue(lead, true) + end + -- Just for now, check if it is in other queues as well. + if self:_InQueue(self.Qwaiting, lead.group) then + self:E(self.lid..string.format("ERROR: lead flight group %s should not be in pattern queue", lead.groupname)) + self:_RemoveFlightFromQueue(self.Qwaiting, lead) + end + + return true +end + +--- Add flight to pattern queue and set recoverd to false for all elements of the flight and its section members. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup Flight group of element. +function AIRBOSS:_AddFlightToPatternQueue(flight) + + -- Add flight to table. + table.insert(self.Qpattern, flight) + + -- Set flag to -1 (-1 is rather arbitrary but it should not be positive or -100 or -42). + flight.flag=-1 + -- New time stamp for time in pattern. + flight.time=timer.getAbsTime() + + -- Init recovered switch. + flight.recovered=false + for _,elem in pairs(flight.elements) do + elem.recoverd=false + end + + -- Set recovered for all section members. + for _,sec in pairs(flight.section) do + -- Set flag and timestamp for section members + sec.flag=-1 + sec.time=timer.getAbsTime() + for _,elem in pairs(sec.elements) do + elem.recoverd=false + end + end +end + +--- Sets flag recovered=true for a flight element, which was successfully recovered (landed). +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit The aircraft unit that was recovered. +-- @return #AIRBOSS.FlightGroup Flight group of element. +function AIRBOSS:_RecoveredElement(unit) + + -- Get element of flight. + local element, idx, flight=self:_GetFlightElement(unit:GetName()) --#AIRBOSS.FlightElement + + -- Nil check. Could be if a helo landed or something else we dont know! + if element then + element.recovered=true + end + + return flight +end + +--- Remove a flight group from the Marshal queue. Marshal stack is collapsed, too, if flight was in the queue. Waiting flights are send to marshal. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight Flight group that will be removed from queue. +-- @param #boolean nopattern If true, flight is NOT going to landing pattern. +-- @return #boolean True, flight was removed or false otherwise. +-- @return #number Table index of the flight in the Marshal queue. +function AIRBOSS:_RemoveFlightFromMarshalQueue(flight, nopattern) + + -- Remove flight from marshal queue if it is in. + local removed, idx=self:_RemoveFlightFromQueue(self.Qmarshal, flight) + + -- Collapse marshal stack if flight was removed. + if removed then + + -- Flight is not holding any more. + flight.holding=nil + + -- Collapse marshal stack if flight was removed. + self:_CollapseMarshalStack(flight, nopattern) + + -- Stacks are only limited for Case I. + if flight.case==1 and #self.Qwaiting>0 then + + -- Next flight in line waiting. + local nextflight=self.Qwaiting[1] --#AIRBOSS.FlightGroup + + -- Get free stack. + local freestack=self:_GetFreeStack(nextflight.ai) + + -- Send next flight to marshal stack. + if nextflight.ai then + + -- Send AI to Marshal Stack. + self:_MarshalAI(nextflight, freestack) + + else + + -- Send player to Marshal stack. + self:_MarshalPlayer(nextflight, freestack) + + end + + -- Remove flight from waiting queue. + self:_RemoveFlightFromQueue(self.Qwaiting, nextflight) + + end + end + + return removed, idx +end + +--- Remove a flight group from a queue. +-- @param #AIRBOSS self +-- @param #table queue The queue from which the group will be removed. +-- @param #AIRBOSS.FlightGroup flight Flight group that will be removed from queue. +-- @return #boolean True, flight was in Queue and removed. False otherwise. +-- @return #number Table index of removed queue element or nil. +function AIRBOSS:_RemoveFlightFromQueue(queue, flight) + + -- Loop over all flights in group. + for i,_flight in pairs(queue) do + local qflight=_flight --#AIRBOSS.FlightGroup + + -- Check for name. + if qflight.groupname==flight.groupname then + self:T(self.lid..string.format("Removing flight group %s from queue.", flight.groupname)) + table.remove(queue, i) + return true, i + end + end + + return false, nil +end + +--- Remove a unit and its element from a flight group (e.g. when landed) and update all queues if the whole flight group is gone. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit The unit to be removed. +function AIRBOSS:_RemoveUnitFromFlight(unit) + + -- Check if unit exists. + if unit and unit:IsInstanceOf("UNIT") then + + -- Get group. + local group=unit:GetGroup() + + -- Check if group exists. + if group then + + -- Get flight. + local flight=self:_GetFlightFromGroupInQueue(group, self.flights) + + -- Check if flight exists. + if flight then + + -- Remove element from flight group. + local removed=self:_RemoveFlightElement(unit:GetName()) + + if removed then + + -- Get number of units (excluding section members). For AI only those that are still in air as we assume once they landed, they are out of the game. + local _,nunits=self:_GetFlightUnits(flight, not flight.ai) + + -- Number of flight elements still left. + local nelements=#flight.elements + + -- Debug info. + self:T(self.lid..string.format("Removed unit %s: nunits=%d, nelements=%d", unit:GetName(), nunits, nelements)) + + -- Check if no units are left. + if nunits==0 or nelements==0 then + -- Remove flight from all queues. + self:_RemoveFlight(flight) + end + + end + end + end + end + +end + +--- Remove a flight, which is a member of a section, from this section. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight The flight to be removed from the section +function AIRBOSS:_RemoveFlightFromSection(flight) + + -- First check if player is not the lead. + if flight.name~=flight.seclead then + + -- Remove this flight group from the section of the leader. + local lead=self.players[flight.seclead] --#AIRBOSS.FlightGroup + if lead then + for i,sec in pairs(lead.section) do + local sectionmember=sec --#AIRBOSS.FlightGroup + if sectionmember.name==flight.name then + table.remove(lead.section, i) + break + end + end + end + end + +end + +--- Update section if a flight is removed. +-- If removed flight is member of a section, he is removed for the leaders section. +-- If removed flight is the section lead, we try to find a new leader. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight The flight to be removed. +function AIRBOSS:_UpdateFlightSection(flight) + + -- Check if this player is the leader of a section. + if flight.seclead==flight.name then + + -------------------- + -- Section Leader -- + -------------------- + + -- This player is the leader ==> We need a new one. + if #flight.section>=1 then + + -- New leader. + local newlead=flight.section[1] --#AIRBOSS.FlightGroup + newlead.seclead=newlead.name + + -- Adjust new section members. + for i=2,#flight.section do + local member=flight.section[i] --#AIRBOSS.FlightGroup + + -- Add remaining members new leaders table. + table.insert(newlead.section, member) + + -- Set new section lead of member. + member.seclead=newlead.name + end + + end + + -- Flight section empty + flight.section={} + + else + + -------------------- + -- Section Member -- + -------------------- + + -- Remove flight from its leaders section. + self:_RemoveFlightFromSection(flight) + + end + +end + +--- Remove a flight from Marshal, Pattern and Waiting queues. If flight is in Marhal queue, the above stack is collapsed. +-- Also set player step to undefined if applicable or remove human flight if option *completely* is true. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData flight The flight to be removed. +-- @param #boolean completely If true, also remove human flight from all flights table. +function AIRBOSS:_RemoveFlight(flight, completely) + self:F(self.lid.. string.format("Removing flight %s, ai=%s completely=%s.", tostring(flight.groupname), tostring(flight.ai), tostring(completely))) + + -- Remove flight from all queues. + self:_RemoveFlightFromMarshalQueue(flight, true) + self:_RemoveFlightFromQueue(self.Qpattern, flight) + self:_RemoveFlightFromQueue(self.Qwaiting, flight) + self:_RemoveFlightFromQueue(self.Qspinning, flight) + + -- Check if player or AI + if flight.ai then + + -- Remove AI flight completely. Pure AI flights have no sections and cannot be members. + self:_RemoveFlightFromQueue(self.flights, flight) + + else + + -- Remove all grades until a final grade is reached. + local grades=self.playerscores[flight.name] + if grades and #grades>0 then + while #grades>0 and grades[#grades].finalscore==nil do + table.remove(grades, #grades) + end + end + + -- Check if flight should be completely removed, e.g. after the player died or simply left the slot. + if completely then + + -- Update flight section. Remove flight from section or find new section leader if flight was the lead. + self:_UpdateFlightSection(flight) + + -- Remove completely. + self:_RemoveFlightFromQueue(self.flights, flight) + + -- Remove player from players table. + local playerdata=self.players[flight.name] + if playerdata then + self:I(self.lid..string.format("Removing player %s completely.", flight.name)) + self.players[flight.name]=nil + end + + -- Remove flight. + flight=nil + + else + + -- Set player step to undefined. + self:_SetPlayerStep(flight, AIRBOSS.PatternStep.UNDEFINED) + + -- Also set this for the section members as they are in the same boat. + for _,sectionmember in pairs(flight.section) do + self:_SetPlayerStep(sectionmember, AIRBOSS.PatternStep.UNDEFINED) + -- Also remove section member in case they are in the spinning queue. + self:_RemoveFlightFromQueue(self.Qspinning, sectionmember) + end + + -- What if flight is member of a section. His status is now undefined. Should he be removed from the section? + -- I think yes, if he pulls the trigger. + self:_RemoveFlightFromSection(flight) + + end + end + +end + +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Player Status +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check current player status. +-- @param #AIRBOSS self +function AIRBOSS:_CheckPlayerStatus() + + -- Loop over all players. + for _playerName,_playerData in pairs(self.players) do + local playerData=_playerData --#AIRBOSS.PlayerData + + if playerData then + + -- Player unit. + local unit=playerData.unit + + -- Check if unit is alive. + if unit and unit:IsAlive() then + + -- Check if player is in carrier controlled area (zone with R=50 NM around the carrier). + -- TODO: This might cause problems if the CCA is set to be very small! + if unit:IsInZone(self.zoneCCA) then + + -- Display aircraft attitude and other parameters as message text. + if playerData.attitudemonitor then + self:_AttitudeMonitor(playerData) + end + + -- Check distance to other flights. + self:_CheckPlayerPatternDistance(playerData) + + -- Foul deck check. + self:_CheckFoulDeck(playerData) + + -- Check current step. + if playerData.step==AIRBOSS.PatternStep.UNDEFINED then + + -- Status undefined. + --local time=timer.getAbsTime() + --local clock=UTILS.SecondsToClock(time) + --self:T3(string.format("Player status undefined. Waiting for next step. Time %s", clock)) + + elseif playerData.step==AIRBOSS.PatternStep.REFUELING then + + -- Nothing to do here at the moment. + + elseif playerData.step==AIRBOSS.PatternStep.SPINNING then + + -- Player is spinning. + self:_Spinning(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.HOLDING then + + -- CASE I/II/III: In holding pattern. + self:_Holding(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.WAITING then + + -- CASE I: Waiting outside 10 NM zone for next free Marshal stack. + self:_Waiting(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.COMMENCING then + + -- CASE I/II/III: New approach. + self:_Commencing(playerData, true) + + elseif playerData.step==AIRBOSS.PatternStep.BOLTER then + + -- CASE I/II/III: Bolter pattern. + self:_BolterPattern(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.PLATFORM then + + -- CASE II/III: Player has reached 5k "Platform". + self:_Platform(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.ARCIN then + + -- Case II/III if offset. + self:_ArcInTurn(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.ARCOUT then + + -- Case II/III if offset. + self:_ArcOutTurn(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.DIRTYUP then + + -- CASE III: Player has descended to 1200 ft and is going level from now on. + self:_DirtyUp(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.BULLSEYE then + + -- CASE III: Player has intercepted the glide slope and should follow "Bullseye" (ICLS). + self:_Bullseye(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.INITIAL then + + -- CASE I/II: Player is at the initial position entering the landing pattern. + self:_Initial(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.BREAKENTRY then + + -- CASE I/II: Break entry. + self:_BreakEntry(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.EARLYBREAK then + + -- CASE I/II: Early break. + self:_Break(playerData, AIRBOSS.PatternStep.EARLYBREAK) + + elseif playerData.step==AIRBOSS.PatternStep.LATEBREAK then + + -- CASE I/II: Late break. + self:_Break(playerData, AIRBOSS.PatternStep.LATEBREAK) + + elseif playerData.step==AIRBOSS.PatternStep.ABEAM then + + -- CASE I/II: Abeam position. + self:_Abeam(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.NINETY then + + -- CASE:I/II: Check long down wind leg. + self:_CheckForLongDownwind(playerData) + + -- At the ninety. + self:_Ninety(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.WAKE then + + -- CASE I/II: In the wake. + self:_Wake(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.EMERGENCY then + + -- Emergency landing. Player pos is not checked. + self:_Final(playerData, true) + + elseif playerData.step==AIRBOSS.PatternStep.FINAL then + + -- CASE I/II: Turn to final and enter the groove. + self:_Final(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.GROOVE_XX or + playerData.step==AIRBOSS.PatternStep.GROOVE_IM or + playerData.step==AIRBOSS.PatternStep.GROOVE_IC or + playerData.step==AIRBOSS.PatternStep.GROOVE_AR or + playerData.step==AIRBOSS.PatternStep.GROOVE_AL or + playerData.step==AIRBOSS.PatternStep.GROOVE_LC or + playerData.step==AIRBOSS.PatternStep.GROOVE_IW then + + -- CASE I/II: In the groove. + self:_Groove(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.DEBRIEF then + + -- Debriefing in 5 seconds. + --SCHEDULER:New(nil, self._Debrief, {self, playerData}, 5) + playerData.debriefschedulerID=self:ScheduleOnce(5, self._Debrief, self, playerData) + + -- Undefined status. + playerData.step=AIRBOSS.PatternStep.UNDEFINED + + else + + -- Error, unknown step! + self:E(self.lid..string.format("ERROR: Unknown player step %s. Please report!", tostring(playerData.step))) + + end + + -- Check if player missed a step during Case II/III and allow him to enter the landing pattern. + self:_CheckMissedStepOnEntry(playerData) + + else + self:T2(self.lid.."WARNING: Player unit not inside the CCA!") + end + + else + -- Unit not alive. + self:T(self.lid.."WARNING: Player unit is not alive!") + end + end + end + +end + + +--- Checks if a player is in the pattern queue and has missed a step in Case II/III approach. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +function AIRBOSS:_CheckMissedStepOnEntry(playerData) + + -- Conditions to be met: Case II/III, in pattern queue, flag!=42 (will be set to 42 at the end if player missed a step). + local rightcase=playerData.case>1 + local rightqueue=self:_InQueue(self.Qpattern, playerData.group) + local rightflag=playerData.flag~=-42 + + -- Steps that the player could have missed during Case II/III. + local step=playerData.step + local missedstep=step==AIRBOSS.PatternStep.PLATFORM or step==AIRBOSS.PatternStep.ARCIN or step==AIRBOSS.PatternStep.ARCOUT or step==AIRBOSS.PatternStep.DIRTYUP + + -- Check if player is about to enter the initial or bullseye zones and maybe has missed a step in the pattern. + if rightcase and rightqueue and rightflag then + + -- Get right zone. + local zone=nil + if playerData.case==2 and missedstep then + + zone=self:_GetZoneInitial(playerData.case) + + elseif playerData.case==3 and missedstep then + + zone=self:_GetZoneBullseye(playerData.case) + + end + + -- Zone only exists if player is not at the initial or bullseye step. + if zone then + + -- Check if player is in initial or bullseye zone. + local inzone=playerData.unit:IsInZone(zone) + + -- Relative heading to carrier direction. + local relheading=self:_GetRelativeHeading(playerData.unit, false) + + -- Check if player is in zone and flying roughly in the right direction. + if inzone and math.abs(relheading)<60 then + + -- Player is in one of the initial zones short before the landing pattern. + local text=string.format("you missed an important step in the pattern!\nYour next step would have been %s.", playerData.step) + self:MessageToPlayer(playerData, text, "AIRBOSS", nil, 5) + + if playerData.case==2 then + -- Set next step to initial. + playerData.step=AIRBOSS.PatternStep.INITIAL + elseif playerData.case==3 then + -- Set next step to bullseye. + playerData.step=AIRBOSS.PatternStep.BULLSEYE + end + + -- Set flag value to -42. This is the value to ensure that this routine is not called again! + playerData.flag=-42 + end + end + end +end + +--- Set time in the groove for player. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +function AIRBOSS:_SetTimeInGroove(playerData) + + -- Set time in the groove + if playerData.TIG0 then + playerData.Tgroove=timer.getTime()-playerData.TIG0 + else + playerData.Tgroove=999 + end + +end + +--- Get time in the groove of player. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @return #number Player's time in groove in seconds. +function AIRBOSS:_GetTimeInGroove(playerData) + + local Tgroove=999 + + -- Get time in the groove. + if playerData.TIG0 then + Tgroove=timer.getTime()-playerData.TIG0 + end + + return Tgroove +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- EVENT functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Airboss event handler for event birth. +-- @param #AIRBOSS self +-- @param Core.Event#EVENTDATA EventData +function AIRBOSS:OnEventBirth(EventData) + self:F3({eventbirth = EventData}) + + -- Nil checks. + if EventData==nil then + self:E(self.lid.."ERROR: EventData=nil in event BIRTH!") + self:E(EventData) + return + end + if EventData.IniUnit==nil then + self:E(self.lid.."ERROR: EventData.IniUnit=nil in event BIRTH!") + self:E(EventData) + return + end + + local _unitName=EventData.IniUnitName + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + + self:T(self.lid.."BIRTH: unit = "..tostring(EventData.IniUnitName)) + self:T(self.lid.."BIRTH: group = "..tostring(EventData.IniGroupName)) + self:T(self.lid.."BIRTH: player = "..tostring(_playername)) + + if _unit and _playername then + + local _uid=_unit:GetID() + local _group=_unit:GetGroup() + local _callsign=_unit:GetCallsign() + + -- Debug output. + local text=string.format("Pilot %s, callsign %s entered unit %s of group %s.", _playername, _callsign, _unitName, _group:GetName()) + self:T(self.lid..text) + MESSAGE:New(text, 5):ToAllIf(self.Debug) + + -- Check if aircraft type the player occupies is carrier capable. + local rightaircraft=self:_IsCarrierAircraft(_unit) + if rightaircraft==false then + local text=string.format("Player aircraft type %s not supported by AIRBOSS class.", _unit:GetTypeName()) + MESSAGE:New(text, 30):ToAllIf(self.Debug) + self:T2(self.lid..text) + return + end + + -- Check that coalition of the carrier and aircraft match. + if self:GetCoalition()~=_unit:GetCoalition() then + local text=string.format("Player entered aircraft of other coalition.") + MESSAGE:New(text, 30):ToAllIf(self.Debug) + self:T(self.lid..text) + return + end + + -- Add Menu commands. + self:_AddF10Commands(_unitName) + + -- Delaying the new player for a second, because AI units of the flight would not be registered correctly. + --SCHEDULER:New(nil, self._NewPlayer, {self, _unitName}, 1) + self:ScheduleOnce(1, self._NewPlayer, self, _unitName) + + end +end + +--- Airboss event handler for event land. +-- @param #AIRBOSS self +-- @param Core.Event#EVENTDATA EventData +function AIRBOSS:OnEventLand(EventData) + self:F3({eventland = EventData}) + + -- Nil checks. + if EventData==nil then + self:E(self.lid.."ERROR: EventData=nil in event LAND!") + self:E(EventData) + return + end + if EventData.IniUnit==nil then + self:E(self.lid.."ERROR: EventData.IniUnit=nil in event LAND!") + self:E(EventData) + return + end + + -- Get unit name that landed. + local _unitName=EventData.IniUnitName + + -- Check if this was a player. + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + + -- Debug output. + self:T(self.lid.."LAND: unit = "..tostring(EventData.IniUnitName)) + self:T(self.lid.."LAND: group = "..tostring(EventData.IniGroupName)) + self:T(self.lid.."LAND: player = "..tostring(_playername)) + + -- This would be the closest airbase. + local airbase=EventData.Place + + -- Nil check for airbase. Crashed as player gave me no airbase. + if airbase==nil then + return + end + + -- Get airbase name. + local airbasename=tostring(airbase:GetName()) + + -- Check if aircraft landed on the right airbase. + if airbasename==self.airbase:GetName() then + + -- Stern coordinate at the rundown. + local stern=self:_GetSternCoord() + + -- Polygon zone close around the carrier. + local zoneCarrier=self:_GetZoneCarrierBox() + + -- Check if player or AI landed. + if _unit and _playername then + + ------------------------- + -- Human Player landed -- + ------------------------- + + -- Get info. + local _uid=_unit:GetID() + local _group=_unit:GetGroup() + local _callsign=_unit:GetCallsign() + + -- Debug output. + local text=string.format("Player %s, callsign %s unit %s (ID=%d) of group %s landed at airbase %s", _playername, _callsign, _unitName, _uid, _group:GetName(), airbasename) + self:T(self.lid..text) + MESSAGE:New(text, 5, "DEBUG"):ToAllIf(self.Debug) + + -- Player data. + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + -- Check if playerData is okay. + if playerData==nil then + self:E(self.lid..string.format("ERROR: playerData nil in landing event. unit=%s player=%s", tostring(_unitName), tostring(_playername))) + return + end + + -- Check that player landed on the carrier. + if _unit:IsInZone(zoneCarrier) then + + -- Check if this was a valid approach. + if not playerData.valid then + -- Player missed at least one step in the pattern. + local text=string.format("you missed at least one important step in the pattern!\nYour next step would have been %s.\nThis pass is INVALID.", playerData.step) + self:MessageToPlayer(playerData, text, "AIRBOSS", nil, 30, true, 5) + + -- Clear queues just in case. + self:_RemoveFlightFromMarshalQueue(playerData, true) + self:_RemoveFlightFromQueue(self.Qpattern, playerData) + self:_RemoveFlightFromQueue(self.Qwaiting, playerData) + self:_RemoveFlightFromQueue(self.Qspinning, playerData) + + -- Reinitialize player data. + self:_InitPlayer(playerData) + + return + end + + -- Check if player already landed. We dont need a second time. + if playerData.landed then + + self:E(self.lid..string.format("Player %s just landed a second time.", _playername)) + + else + + -- We did land. + playerData.landed=true + + -- Switch attitude monitor off if on. + playerData.attitudemonitor=false + + -- Coordinate at landing event. + local coord=playerData.unit:GetCoordinate() + + -- Get distances relative to + local X,Z,rho,phi=self:_GetDistances(_unit) + + -- Landing distance wrt to stern position. + local dist=coord:Get2DDistance(stern) + + -- Debug mark of player landing coord. + if self.Debug and false then + -- Debug mark of player landing coord. + local lp=coord:MarkToAll("Landing coord.") + coord:SmokeGreen() + end + + -- Set time in the groove of player. + self:_SetTimeInGroove(playerData) + + -- Debug text. + local text=string.format("Player %s AC type %s landed at dist=%.1f m. Tgroove=%.1f sec.", playerData.name, playerData.actype, dist, self:_GetTimeInGroove(playerData)) + text=text..string.format(" X=%.1f m, Z=%.1f m, rho=%.1f m.", X, Z, rho) + self:T(self.lid..text) + + -- Check carrier type. + if self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS then + + -- Power "Idle". + self:RadioTransmission(self.LSORadio, self.LSOCall.IDLE, false, 1, nil, true) + + -- Next step debrief. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.DEBRIEF) + + else + + -- Next step undefined until we know more. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.UNDEFINED) + + -- Call trapped function in 1 second to make sure we did not bolter. + --SCHEDULER:New(nil, self._Trapped, {self, playerData}, 1) + self:ScheduleOnce(1, self._Trapped, self, playerData) + + end + + end + + else + -- Handle case where player did not land on the carrier. + -- Well, I guess, he leaves the slot or ejects. Both should be handled. + if playerData then + self:E(self.lid..string.format("Player %s did not land in carrier box zone. Maybe in the water near the carrier?", playerData.name)) + end + end + + else + + -------------------- + -- AI unit landed -- + -------------------- + + if self.carriertype~=AIRBOSS.CarrierType.TARAWA or self.carriertype~=AIRBOSS.CarrierType.AMERICA or self.carriertype~=AIRBOSS.CarrierType.JCARLOS then + + -- Coordinate at landing event + local coord=EventData.IniUnit:GetCoordinate() + + -- Debug mark of player landing coord. + local dist=coord:Get2DDistance(self:GetCoordinate()) + + -- Get wire + local wire=self:_GetWire(coord, 0) + + -- Aircraft type. + local _type=EventData.IniUnit:GetTypeName() + + -- Debug text. + local text=string.format("AI unit %s of type %s landed at dist=%.1f m. Trapped wire=%d.", _unitName, _type, dist, wire) + self:T(self.lid..text) + + end + + -- AI always lands ==> remove unit from flight group and queues. + local flight=self:_RecoveredElement(EventData.IniUnit) + + -- Check if all were recovered. If so update pattern queue. + self:_CheckSectionRecovered(flight) + + end + end +end + +--- Airboss event handler for event that a unit shuts down its engines. +-- @param #AIRBOSS self +-- @param Core.Event#EVENTDATA EventData +function AIRBOSS:OnEventEngineShutdown(EventData) + self:F3({eventengineshutdown=EventData}) + + -- Nil checks. + if EventData==nil then + self:E(self.lid.."ERROR: EventData=nil in event ENGINESHUTDOWN!") + self:E(EventData) + return + end + if EventData.IniUnit==nil then + self:E(self.lid.."ERROR: EventData.IniUnit=nil in event ENGINESHUTDOWN!") + self:E(EventData) + return + end + + + local _unitName=EventData.IniUnitName + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + + self:T3(self.lid.."ENGINESHUTDOWN: unit = "..tostring(EventData.IniUnitName)) + self:T3(self.lid.."ENGINESHUTDOWN: group = "..tostring(EventData.IniGroupName)) + self:T3(self.lid.."ENGINESHUTDOWN: player = "..tostring(_playername)) + + if _unit and _playername then + + -- Debug message. + self:T(self.lid..string.format("Player %s shut down its engines!",_playername)) + + else + + -- Debug message. + self:T(self.lid..string.format("AI unit %s shut down its engines!", _unitName)) + + -- Get flight. + local flight=self:_GetFlightFromGroupInQueue(EventData.IniGroup, self.flights) + + -- Only AI flights. + if flight and flight.ai then + + -- Check if all elements were recovered. + local recovered=self:_CheckSectionRecovered(flight) + + -- Despawn group and completely remove flight. + if recovered then + self:T(self.lid..string.format("AI group %s completely recovered. Despawning group after engine shutdown event as requested in 5 seconds.", tostring(EventData.IniGroupName))) + + -- Remove flight. + self:_RemoveFlight(flight) + + -- Check if this is a tanker or AWACS associated with the carrier. + local istanker=self.tanker and self.tanker.tanker:GetName()==EventData.IniGroupName + local isawacs=self.awacs and self.awacs.tanker:GetName()==EventData.IniGroupName + + -- Destroy group if desired. Recovery tankers have their own logic for despawning. + if self.despawnshutdown and not (istanker or isawacs) then + EventData.IniGroup:Destroy(nil, 5) + end + + end + + end + end +end + +--- Airboss event handler for event that a unit takes off. +-- @param #AIRBOSS self +-- @param Core.Event#EVENTDATA EventData +function AIRBOSS:OnEventTakeoff(EventData) + self:F3({eventtakeoff=EventData}) + + -- Nil checks. + if EventData==nil then + self:E(self.lid.."ERROR: EventData=nil in event TAKEOFF!") + self:E(EventData) + return + end + if EventData.IniUnit==nil then + self:E(self.lid.."ERROR: EventData.IniUnit=nil in event TAKEOFF!") + self:E(EventData) + return + end + + + local _unitName=EventData.IniUnitName + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + + self:T3(self.lid.."TAKEOFF: unit = "..tostring(EventData.IniUnitName)) + self:T3(self.lid.."TAKEOFF: group = "..tostring(EventData.IniGroupName)) + self:T3(self.lid.."TAKEOFF: player = "..tostring(_playername)) + + -- Airbase. + local airbase=EventData.Place + + -- Airbase name. + local airbasename="unknown" + if airbase then + airbasename=airbase:GetName() + end + + -- Check right airbase. + if airbasename==self.airbase:GetName() then + + if _unit and _playername then + + -- Debug message. + self:T(self.lid..string.format("Player %s took off at %s!",_playername, airbasename)) + + else + + -- Debug message. + self:T2(self.lid..string.format("AI unit %s took off at %s!", _unitName, airbasename)) + + -- Get flight. + local flight=self:_GetFlightFromGroupInQueue(EventData.IniGroup, self.flights) + + if flight then + + -- Set ballcall and recoverd status. + for _,elem in pairs(flight.elements) do + local element=elem --#AIRBOSS.FlightElement + element.ballcall=false + element.recovered=nil + end + end + end + + end +end + +--- Airboss event handler for event crash. +-- @param #AIRBOSS self +-- @param Core.Event#EVENTDATA EventData +function AIRBOSS:OnEventCrash(EventData) + self:F3({eventcrash = EventData}) + + -- Nil checks. + if EventData==nil then + self:E(self.lid.."ERROR: EventData=nil in event CRASH!") + self:E(EventData) + return + end + if EventData.IniUnit==nil then + self:E(self.lid.."ERROR: EventData.IniUnit=nil in event CRASH!") + self:E(EventData) + return + end + + + local _unitName=EventData.IniUnitName + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + + self:T3(self.lid.."CRASH: unit = "..tostring(EventData.IniUnitName)) + self:T3(self.lid.."CRASH: group = "..tostring(EventData.IniGroupName)) + self:T3(self.lid.."CARSH: player = "..tostring(_playername)) + + if _unit and _playername then + -- Debug message. + self:T(self.lid..string.format("Player %s crashed!",_playername)) + + -- Get player flight. + local flight=self.players[_playername] + + -- Remove flight completely from all queues and collapse marshal if necessary. + -- This also updates the section, if any and removes any unfinished gradings of the player. + if flight then + self:_RemoveFlight(flight, true) + end + + else + -- Debug message. + self:T2(self.lid..string.format("AI unit %s crashed!", EventData.IniUnitName)) + + -- Remove unit from flight and queues. + self:_RemoveUnitFromFlight(EventData.IniUnit) + end + +end + +--- Airboss event handler for event Ejection. +-- @param #AIRBOSS self +-- @param Core.Event#EVENTDATA EventData +function AIRBOSS:OnEventEjection(EventData) + self:F3({eventland = EventData}) + + -- Nil checks. + if EventData==nil then + self:E(self.lid.."ERROR: EventData=nil in event EJECTION!") + self:E(EventData) + return + end + if EventData.IniUnit==nil then + self:E(self.lid.."ERROR: EventData.IniUnit=nil in event EJECTION!") + self:E(EventData) + return + end + + + local _unitName=EventData.IniUnitName + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + + self:T3(self.lid.."EJECT: unit = "..tostring(EventData.IniUnitName)) + self:T3(self.lid.."EJECT: group = "..tostring(EventData.IniGroupName)) + self:T3(self.lid.."EJECT: player = "..tostring(_playername)) + + if _unit and _playername then + self:T(self.lid..string.format("Player %s ejected!",_playername)) + + -- Get player flight. + local flight=self.players[_playername] + + -- Remove flight completely from all queues and collapse marshal if necessary. + if flight then + self:_RemoveFlight(flight, true) + end + + else + -- Debug message. + self:T(self.lid..string.format("AI unit %s ejected!", EventData.IniUnitName)) + + -- Remove element/unit from flight group and from all queues if no elements alive. + self:_RemoveUnitFromFlight(EventData.IniUnit) + + -- What could happen is, that another element has landed (recovered) already and this one crashes. + -- This would mean that the flight would not be deleted from the queue ==> Check if section recovered. + local flight=self:_GetFlightFromGroupInQueue(EventData.IniGroup, self.flights) + self:_CheckSectionRecovered(flight) + end + +end + +--- Airboss event handler for event REMOVEUNIT. +-- @param #AIRBOSS self +-- @param Core.Event#EVENTDATA EventData +function AIRBOSS:OnEventRemoveUnit(EventData) + self:F3({eventland = EventData}) + + -- Nil checks. + if EventData==nil then + self:E(self.lid.."ERROR: EventData=nil in event REMOVEUNIT!") + self:E(EventData) + return + end + if EventData.IniUnit==nil then + self:E(self.lid.."ERROR: EventData.IniUnit=nil in event REMOVEUNIT!") + self:E(EventData) + return + end + + + local _unitName=EventData.IniUnitName + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + + self:T3(self.lid.."EJECT: unit = "..tostring(EventData.IniUnitName)) + self:T3(self.lid.."EJECT: group = "..tostring(EventData.IniGroupName)) + self:T3(self.lid.."EJECT: player = "..tostring(_playername)) + + if _unit and _playername then + self:T(self.lid..string.format("Player %s removed!",_playername)) + + -- Get player flight. + local flight=self.players[_playername] + + -- Remove flight completely from all queues and collapse marshal if necessary. + if flight then + self:_RemoveFlight(flight, true) + end + + else + -- Debug message. + self:T(self.lid..string.format("AI unit %s removed!", EventData.IniUnitName)) + + -- Remove element/unit from flight group and from all queues if no elements alive. + self:_RemoveUnitFromFlight(EventData.IniUnit) + + -- What could happen is, that another element has landed (recovered) already and this one crashes. + -- This would mean that the flight would not be deleted from the queue ==> Check if section recovered. + local flight=self:_GetFlightFromGroupInQueue(EventData.IniGroup, self.flights) + self:_CheckSectionRecovered(flight) + end + +end + +--- Airboss event handler for event player leave unit. +-- @param #AIRBOSS self +-- @param Core.Event#EVENTDATA EventData +--function AIRBOSS:OnEventPlayerLeaveUnit(EventData) +function AIRBOSS:_PlayerLeft(EventData) + self:F3({eventleave=EventData}) + + -- Nil checks. + if EventData==nil then + self:E(self.lid.."ERROR: EventData=nil in event PLAYERLEFTUNIT!") + self:E(EventData) + return + end + if EventData.IniUnit==nil then + self:E(self.lid.."ERROR: EventData.IniUnit=nil in event PLAYERLEFTUNIT!") + self:E(EventData) + return + end + + + local _unitName=EventData.IniUnitName + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + + self:T3(self.lid.."PLAYERLEAVEUNIT: unit = "..tostring(EventData.IniUnitName)) + self:T3(self.lid.."PLAYERLEAVEUNIT: group = "..tostring(EventData.IniGroupName)) + self:T3(self.lid.."PLAYERLEAVEUNIT: player = "..tostring(_playername)) + + if _unit and _playername then + + -- Debug info. + self:T(self.lid..string.format("Player %s left unit %s!",_playername, _unitName)) + + -- Get player flight. + local flight=self.players[_playername] + + -- Remove flight completely from all queues and collapse marshal if necessary. + if flight then + self:_RemoveFlight(flight, true) + end + + end + +end + +--- Airboss event function handling the mission end event. +-- Handles the case when the mission is ended. +-- @param #AIRBOSS self +-- @param Core.Event#EVENTDATA EventData Event data. +function AIRBOSS:OnEventMissionEnd(EventData) + self:T3(self.lid.."Mission Ended") +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- PATTERN functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Spinning +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +function AIRBOSS:_Spinning(playerData) + + -- Early break. + local SpinIt={} + SpinIt.name="Spinning" + SpinIt.Xmin=-UTILS.NMToMeters(6) -- Not more than 5 NM behind the boat. + SpinIt.Xmax= UTILS.NMToMeters(5) -- Not more than 5 NM in front of the boat. + SpinIt.Zmin=-UTILS.NMToMeters(6) -- Not more than 5 NM port. + SpinIt.Zmax= UTILS.NMToMeters(2) -- Not more than 3 NM starboard. + SpinIt.LimitXmin=-100 -- 100 meters behind the boat + SpinIt.LimitXmax=nil + SpinIt.LimitZmin=-UTILS.NMToMeters(1) -- 1 NM port + SpinIt.LimitZmax=nil + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local X, Z, rho, phi=self:_GetDistances(playerData.unit) + + -- Check if we are in front of the boat (diffX > 0). + if self:_CheckLimits(X, Z, SpinIt) then + + -- Player is "de-spinned". Should go to initial again. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.INITIAL) + + -- Remove player from spinning queue. + self:_RemoveFlightFromQueue(self.Qspinning, playerData) + + end + +end + +--- Waiting outside 10 NM zone for free Marshal stack. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +function AIRBOSS:_Waiting(playerData) + + -- Create 10 NM zone around the carrier. + local radius=UTILS.NMToMeters(10) + local zone=ZONE_RADIUS:New("Carrier 10 NM Zone", self.carrier:GetVec2(), radius) + + -- Check if player is inside 10 NM radius of the carrier. + local inzone=playerData.unit:IsInZone(zone) + + -- Time player is waiting. + local Twaiting=timer.getAbsTime()-playerData.time + + -- Warning if player is inside the zone. + if inzone and Twaiting>3*60 and not playerData.warning then + local text=string.format("You are supposed to wait outside the 10 NM zone.") + self:MessageToPlayer(playerData, text, "AIRBOSS") + playerData.warning=true + end + + -- Reset warning. + if inzone==false and playerData.warning==true then + playerData.warning=nil + end + +end + +--- Holding. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +function AIRBOSS:_Holding(playerData) + + -- Player unit and flight. + local unit=playerData.unit + + -- Current stack. + local stack=playerData.flag + + -- Check for reported error. + if stack<=0 then + local text=string.format("ERROR: player %s in step %s is holding but has stack=%s (<=0)", playerData.name, playerData.step, tostring(stack)) + self:E(self.lid..text) + end + + --------------------------- + -- Holding Pattern Check -- + --------------------------- + + -- Pattern altitude. + local patternalt=self:_GetMarshalAltitude(stack, playerData.case) + + -- Player altitude. + local playeralt=unit:GetAltitude() + + -- Get holding zone of player. + local zoneHolding=self:_GetZoneHolding(playerData.case, stack) + + -- Nil check. + if zoneHolding==nil then + self:E(self.lid.."ERROR: zoneHolding is nil!") + self:E({playerData=playerData}) + return + end + + -- Check if player is in holding zone. + local inholdingzone=unit:IsInZone(zoneHolding) + + -- Altitude difference between player and assigned stack. + local altdiff=playeralt-patternalt + + -- Acceptable altitude depending on player skill. + local altgood=UTILS.FeetToMeters(500) + if playerData.difficulty==AIRBOSS.Difficulty.HARD then + -- Pros can be expected to be within +-200 ft. + altgood=UTILS.FeetToMeters(200) + elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then + -- Normal guys should be within +-350 ft. + altgood=UTILS.FeetToMeters(350) + elseif playerData.difficulty==AIRBOSS.Difficulty.EASY then + -- Students should be within +-500 ft. + altgood=UTILS.FeetToMeters(500) + end + + -- When back to good altitude = 50%. + local altback=altgood*0.5 + + -- Check if stack just collapsed and give the player one minute to change the altitude. + local justcollapsed=false + if self.Tcollapse then + -- Time since last stack change. + local dT=timer.getTime()-self.Tcollapse + + -- TODO: check if this works. + --local dT=timer.getAbsTime()-playerData.time + + -- Check if less then 90 seconds. + if dT<=90 then + justcollapsed=true + end + end + + -- Check if altitude is acceptable. + local goodalt=math.abs(altdiff)altgood then + + -- Issue warning for being too high. + if not playerData.warning then + text=text..string.format("You left your assigned altitude. Descent to angels %d.", angels) + playerData.warning=true + end + + elseif altdiff<-altgood then + + -- Issue warning for being too low. + if not playerData.warning then + text=text..string.format("You left your assigned altitude. Climb to angels %d.", angels) + playerData.warning=true + end + + end + + end + + -- Back to assigned altitude. + if playerData.warning and math.abs(altdiff)<=altback then + text=text..string.format("Altitude is looking good again.") + playerData.warning=nil + end + + elseif playerData.holding==false then + + -- Player left holding zone + if inholdingzone then + -- Player is back in the holding zone. + text=text..string.format("You are back in the holding zone. Now stay there!") + playerData.holding=true + else + -- Player is still outside the holding zone. + self:T3("Player still outside the holding zone. What are you doing man?!") + end + + elseif playerData.holding==nil then + -- Player did not entered the holding zone yet. + + if inholdingzone then + + -- Player arrived in holding zone. + playerData.holding=true + + -- Inform player. + text=text..string.format("You arrived at the holding zone.") + + -- Feedback on altitude. + if goodalt then + text=text..string.format(" Altitude is good.") + else + if altdiff<0 then + text=text..string.format(" But you're too low.") + else + text=text..string.format(" But you're too high.") + end + text=text..string.format("\nCurrently assigned altitude is %d ft.", UTILS.MetersToFeet(patternalt)) + playerData.warning=true + end + + else + -- Player did not yet arrive in holding zone. + self:T3("Waiting for player to arrive in the holding zone.") + end + + end + + -- Send message. + if playerData.showhints then + self:MessageToPlayer(playerData, text, "MARSHAL") + end + +end + + +--- Commence approach. This step initializes the player data. Section members are also set to commence. Next step depends on recovery case: +-- +-- * Case 1: Initial +-- * Case 2/3: Platform +-- +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #boolean zonecheck If true, zone is checked before player is released. +function AIRBOSS:_Commencing(playerData, zonecheck) + + -- Check for auto commence + if zonecheck then + + -- Get auto commence zone. + local zoneCommence=self:_GetZoneCommence(playerData.case, playerData.flag) + + -- Check if unit is in the zone. + local inzone=playerData.unit:IsInZone(zoneCommence) + + -- Skip the rest if not in the zone yet. + if not inzone then + + -- Friendly reminder. + if timer.getAbsTime()-playerData.time>180 then + self:_MarshalCallClearedForRecovery(playerData.onboard, playerData.case) + playerData.time=timer.getAbsTime() + end + + -- Skip the rest. + return + end + + end + + -- Remove flight from Marshal queue. If flight was in queue, stack is collapsed and flight added to the pattern queue. + self:_RemoveFlightFromMarshalQueue(playerData) + + -- Initialize player data for new approach. + self:_InitPlayer(playerData) + + -- Commencing message to player only. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + + -- Text + local text="" + + -- Positive response. + if playerData.case==1 then + text=text.."Proceed to initial." + else + text=text.."Descent to platform." + if playerData.difficulty==AIRBOSS.Difficulty.EASY and playerData.showhints then + text=text.." VSI 4000 ft/min until you reach 5000 ft." + end + end + + -- Message to player. + self:MessageToPlayer(playerData, text, "MARSHAL") + end + + -- Next step: depends on case recovery. + local nextstep + if playerData.case==1 then + -- CASE I: Player has to fly to the initial which is 3 NM DME astern of the boat. + nextstep=AIRBOSS.PatternStep.INITIAL + else + -- CASE II/III: Player has to start the descent at 4000 ft/min to the platform at 5k ft. + nextstep=AIRBOSS.PatternStep.PLATFORM + end + + -- Next step hint. + self:_SetPlayerStep(playerData, nextstep) + + -- Commence section members as well but dont check the zone. + for i,_flight in pairs(playerData.section) do + local flight=_flight --#AIRBOSS.PlayerData + self:_Commencing(flight, false) + end + +end + +--- Start pattern when player enters the initial zone in case I/II recoveries. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @return #boolean True if player is in the initial zone. +function AIRBOSS:_Initial(playerData) + + -- Check if player is in initial zone and entering the CASE I pattern. + local inzone=playerData.unit:IsInZone(self:_GetZoneInitial(playerData.case)) + + -- Relative heading to carrier direction. + local relheading=self:_GetRelativeHeading(playerData.unit, false) + + -- Alitude of player in feet. + local altitude=playerData.unit:GetAltitude() + + -- Check if player is in zone and flying roughly in the right direction. + if inzone and math.abs(relheading)<60 and altitude<=self.initialmaxalt then + + -- Send message for normal and easy difficulty. + if playerData.showhints then + + -- Inform player. + local hint=string.format("Initial") + + -- Hook down for students. + if playerData.difficulty==AIRBOSS.Difficulty.EASY and playerData.actype~=AIRBOSS.AircraftCarrier.AV8B then + if playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B then + hint=hint.." - Hook down, SAS on, Wing Sweep 68°!" + else + hint=hint.." - Hook down!" + end + end + + self:MessageToPlayer(playerData, hint, "MARSHAL") + end + + -- Next step: Break entry. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.BREAKENTRY) + + return true + end + + return false +end + +--- Check if player is in CASE II/III approach corridor. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_CheckCorridor(playerData) + + -- Check if player is in valid zone + local validzone=self:_GetZoneCorridor(playerData.case) + + -- Check if we are inside the moving zone. + local invalid=playerData.unit:IsNotInZone(validzone) + + -- Issue warning. + if invalid and (not playerData.warning) then + self:MessageToPlayer(playerData, "you left the approach corridor!", "AIRBOSS") + playerData.warning=true + end + + -- Back in zone. + if (not invalid) and playerData.warning then + self:MessageToPlayer(playerData, "you're back in the approach corridor.", "AIRBOSS") + playerData.warning=false + end + +end + +--- Platform at 5k ft for case II/III recoveries. Descent at 2000 ft/min. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_Platform(playerData) + + -- Check if player left or got back to the approach corridor. + self:_CheckCorridor(playerData) + + -- Check if we are inside the moving zone. + local inzone=playerData.unit:IsInZone(self:_GetZonePlatform(playerData.case)) + + -- Check if we are in zone. + if inzone then + + -- Hint for player about altitude, AoA etc. + self:_PlayerHint(playerData) + + -- Next step: depends. + local nextstep + if math.abs(self.holdingoffset)>0 and playerData.case>1 then + -- Turn to BRC (case II) or FB (case III). + nextstep=AIRBOSS.PatternStep.ARCIN + else + if playerData.case==2 then + -- Case II: Initial zone then Case I recovery. + nextstep=AIRBOSS.PatternStep.INITIAL + elseif playerData.case==3 then + -- CASE III: Dirty up. + nextstep=AIRBOSS.PatternStep.DIRTYUP + end + end + + -- Next step hint. + self:_SetPlayerStep(playerData, nextstep) + + end +end + + +--- Arc in turn for case II/III recoveries. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_ArcInTurn(playerData) + + -- Check if player left or got back to the approach corridor. + self:_CheckCorridor(playerData) + + -- Check if we are inside the moving zone. + local inzone=playerData.unit:IsInZone(self:_GetZoneArcIn(playerData.case)) + + if inzone then + + -- Hint for player about altitude, AoA etc. + self:_PlayerHint(playerData) + + -- Next step: Arc Out Turn. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.ARCOUT) + + end +end + +--- Arc out turn for case II/III recoveries. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_ArcOutTurn(playerData) + + -- Check if player left or got back to the approach corridor. + self:_CheckCorridor(playerData) + + -- Check if we are inside the moving zone. + local inzone=playerData.unit:IsInZone(self:_GetZoneArcOut(playerData.case)) + + if inzone then + + -- Hint for player about altitude, AoA etc. + self:_PlayerHint(playerData) + + -- Next step: + local nextstep + if playerData.case==3 then + -- Case III: Dirty up. + nextstep=AIRBOSS.PatternStep.DIRTYUP + else + -- Case II: Initial. + nextstep=AIRBOSS.PatternStep.INITIAL + end + + -- Next step hint. + self:_SetPlayerStep(playerData, nextstep) + end +end + +--- Dirty up and level out at 1200 ft for case III recovery. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_DirtyUp(playerData) + + -- Check if player left or got back to the approach corridor. + self:_CheckCorridor(playerData) + + -- Check if we are inside the moving zone. + local inzone=playerData.unit:IsInZone(self:_GetZoneDirtyUp(playerData.case)) + + if inzone then + + -- Hint for player about altitude, AoA etc. + self:_PlayerHint(playerData) + + -- Radio call "Say/Fly needles". Delayed by 10/15 seconds. + if playerData.actype==AIRBOSS.AircraftCarrier.HORNET or playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B then + local callsay=self:_NewRadioCall(self.MarshalCall.SAYNEEDLES, nil, nil, 5, playerData.onboard) + local callfly=self:_NewRadioCall(self.MarshalCall.FLYNEEDLES, nil, nil, 5, playerData.onboard) + self:RadioTransmission(self.MarshalRadio, callsay, false, 55, nil, true) + self:RadioTransmission(self.MarshalRadio, callfly, false, 60, nil, true) + end + + -- TODO: Make Fly Bullseye call if no automatic ICLS is active. + + -- Next step: CASE III: Intercept glide slope and follow bullseye (ICLS). + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.BULLSEYE) + + end +end + +--- Intercept glide slop and follow ICLS, aka Bullseye for case III recovery. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @return #boolean If true, player is in bullseye zone. +function AIRBOSS:_Bullseye(playerData) + + -- Check if player left or got back to the approach corridor. + self:_CheckCorridor(playerData) + + -- Check if we are inside the moving zone. + local inzone=playerData.unit:IsInZone(self:_GetZoneBullseye(playerData.case)) + + -- Relative heading to carrier direction of the runway. + local relheading=self:_GetRelativeHeading(playerData.unit, true) + + -- Check if player is in zone and flying roughly in the right direction. + if inzone and math.abs(relheading)<60 then + + -- Hint for player about altitude, AoA etc. + self:_PlayerHint(playerData) + + -- LSO expect spot 5 or 7.5 call + if playerData.actype==AIRBOSS.AircraftCarrier.AV8B and self.carriertype==AIRBOSS.CarrierType.JCARLOS then + self:RadioTransmission(self.LSORadio, self.LSOCall.EXPECTSPOT5, nil, nil, nil, true) + elseif playerData.actype==AIRBOSS.AircraftCarrier.AV8B then + self:RadioTransmission(self.LSORadio, self.LSOCall.EXPECTSPOT75, nil, nil, nil, true) + end + + -- Next step: Groove Call the ball. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_XX) + + end +end + +--- Bolter pattern. Sends player to abeam for Case I/II or Bullseye for Case III ops. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_BolterPattern(playerData) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local X, Z, rho, phi=self:_GetDistances(playerData.unit) + + -- Bolter Pattern thresholds. + local Bolter={} + Bolter.name="Bolter Pattern" + Bolter.Xmin=-UTILS.NMToMeters(5) -- Not more then 5 NM astern of boat. + Bolter.Xmax= UTILS.NMToMeters(3) -- Not more then 3 NM ahead of boat. + Bolter.Zmin=-UTILS.NMToMeters(5) -- Not more than 2 NM port. + Bolter.Zmax= UTILS.NMToMeters(1) -- Not more than 1 NM starboard. + Bolter.LimitXmin= 100 -- Check that 100 meter ahead and port + Bolter.LimitXmax= nil + Bolter.LimitZmin= nil + Bolter.LimitZmax= nil + + -- Check if we are in front of the boat (diffX > 0). + if self:_CheckLimits(X, Z, Bolter) then + local nextstep + if playerData.case<3 then + nextstep=AIRBOSS.PatternStep.ABEAM + else + nextstep=AIRBOSS.PatternStep.BULLSEYE + end + self:_SetPlayerStep(playerData, nextstep) + end +end + + + +--- Break entry for case I/II recoveries. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_BreakEntry(playerData) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local X, Z=self:_GetDistances(playerData.unit) + + -- Abort condition check. + if self:_CheckAbort(X, Z, self.BreakEntry) then + self:_AbortPattern(playerData, X, Z, self.BreakEntry, true) + return + end + + -- Check if we are in front of the boat (diffX > 0). + if self:_CheckLimits(X, Z, self.BreakEntry) then + + -- Hint for player about altitude, AoA etc. + self:_PlayerHint(playerData) + + -- Next step: Early Break. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.EARLYBREAK) + + end +end + + +--- Break. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #string part Part of the break. +function AIRBOSS:_Break(playerData, part) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local X, Z=self:_GetDistances(playerData.unit) + + -- Early or late break. + local breakpoint = self.BreakEarly + if part==AIRBOSS.PatternStep.LATEBREAK then + breakpoint = self.BreakLate + end + + -- Check abort conditions. + if self:_CheckAbort(X, Z, breakpoint) then + self:_AbortPattern(playerData, X, Z, breakpoint, true) + return + end + + -- Player made a very tight turn and did not trigger the latebreak threshold at 0.8 NM. + local tooclose=false + if part==AIRBOSS.PatternStep.LATEBREAK then + local close=0.8 + if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then + close=0.5 + end + if X<0 and Z90 and self:_CheckLimits(X, Z, self.Wake) then + -- Message to player. + self:MessageToPlayer(playerData, "you are already at the wake and have not passed the 90. Turn faster next time!", "LSO") + self:RadioTransmission(self.LSORadio, self.LSOCall.DEPARTANDREENTER, nil, nil, nil, true) + playerData.wop=true + -- Debrief. + self:_AddToDebrief(playerData, "Overshoot at wake - Pattern Waveoff!") + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.DEBRIEF) + end +end + +--- At the Wake. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_Wake(playerData) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local X, Z=self:_GetDistances(playerData.unit) + + -- Check abort conditions. + if self:_CheckAbort(X, Z, self.Wake) then + self:_AbortPattern(playerData, X, Z, self.Wake, true) + return + end + + -- Right behind the wake of the carrier dZ>0. + if self:_CheckLimits(X, Z, self.Wake) then + + -- Hint for player about altitude, AoA etc. + self:_PlayerHint(playerData) + + -- Next step: Final. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.FINAL) + + end +end + +--- Get groove data. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @return #AIRBOSS.GrooveData Groove data table. +function AIRBOSS:_GetGrooveData(playerData) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier). + local X, Z=self:_GetDistances(playerData.unit) + + -- Stern position at the rundown. + local stern=self:_GetSternCoord() + + -- Distance from rundown to player aircraft. + local rho=stern:Get2DDistance(playerData.unit:GetCoordinate()) + + -- Aircraft is behind the carrier. + local astern=X5. This would mean the player has not turned in correctly! + + -- Groove data. + playerData.groove.X0=UTILS.DeepCopy(groovedata) + + -- Set time stamp. Next call in 4 seconds. + playerData.Tlso=timer.getTime() + + -- Next step: X start. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_XX) + end + + -- Groovedata step. + groovedata.Step=playerData.step + +end + +--- In the groove. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_Groove(playerData) + + -- Ranges in the groove. + local RX0=UTILS.NMToMeters(1.000) -- Everything before X 1.00 = 1852 m + local RXX=UTILS.NMToMeters(0.750) -- Start of groove. 0.75 = 1389 m + local RIM=UTILS.NMToMeters(0.500) -- In the Middle 0.50 = 926 m (middle one third of the glideslope) + local RIC=UTILS.NMToMeters(0.250) -- In Close 0.25 = 463 m (last one third of the glideslope) + local RAR=UTILS.NMToMeters(0.040) -- At the Ramp. 0.04 = 75 m + + -- Groove data. + local groovedata=self:_GetGrooveData(playerData) + + -- Add data to trapsheet. + table.insert(playerData.trapsheet, groovedata) + + -- Coords. + local X=groovedata.X + local Z=groovedata.Z + + -- Check abort conditions. + if self:_CheckAbort(groovedata.X, groovedata.Z, self.Groove) then + self:_AbortPattern(playerData, groovedata.X, groovedata.Z, self.Groove, true) + return + end + + -- Shortcuts. + local rho=groovedata.Rho + local lineupError=groovedata.LUE + local glideslopeError=groovedata.GSE + local AoA=groovedata.AoA + + + if rho<=RXX and playerData.step==AIRBOSS.PatternStep.GROOVE_XX and (math.abs(groovedata.Roll)<=4.0 or playerData.unit:IsInZone(self:_GetZoneLineup())) then + + -- Start time in groove + playerData.TIG0=timer.getTime() + + -- LSO "Call the ball" call. + self:RadioTransmission(self.LSORadio, self.LSOCall.CALLTHEBALL, nil, nil, nil, true) + playerData.Tlso=timer.getTime() + + -- Pilot "405, Hornet Ball, 3.2". + + -- LSO "Roger ball" call in three seconds. + self:RadioTransmission(self.LSORadio, self.LSOCall.ROGERBALL, false, nil, 2, true) + + -- Store data. + playerData.groove.XX=UTILS.DeepCopy(groovedata) + + -- This is a valid approach and player did not miss any important steps in the pattern. + playerData.valid=true + + -- Next step: in the middle. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_IM) + + elseif rho<=RIM and playerData.step==AIRBOSS.PatternStep.GROOVE_IM then + + -- Store data. + playerData.groove.IM=UTILS.DeepCopy(groovedata) + + -- Next step: in close. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_IC) + + elseif rho<=RIC and playerData.step==AIRBOSS.PatternStep.GROOVE_IC then + + -- Store data. + playerData.groove.IC=UTILS.DeepCopy(groovedata) + + -- Next step: AR at the ramp. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_AR) + + elseif rho<=RAR and playerData.step==AIRBOSS.PatternStep.GROOVE_AR then + + -- Store data. + playerData.groove.AR=UTILS.DeepCopy(groovedata) + + -- Next step: in the wires. + if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_AL) + else + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_IW) + end + + elseif rho<=RAR and playerData.step==AIRBOSS.PatternStep.GROOVE_AL then + + -- Store data. + playerData.groove.AL=UTILS.DeepCopy(groovedata) + + -- Get zone abeam LDG spot. + local ZoneALS=self:_GetZoneAbeamLandingSpot() + + -- Get player velocity in km/h. + local vplayer=playerData.unit:GetVelocityKMH() + + -- Get carrier velocity in km/h. + local vcarrier=self.carrier:GetVelocityKMH() + + -- Speed difference. + local dv=math.abs(vplayer-vcarrier) + + -- Stable when speed difference < 10 km/h. + local stable=dv<10 + + -- Check if player is inside the zone. + if playerData.unit:IsInZone(ZoneALS) and stable then + + -- Radio Transmission "Cleared to land" once the aircraft is inside the zone. + self:RadioTransmission(self.LSORadio, self.LSOCall.CLEAREDTOLAND, nil, nil, nil, true) + + -- Next step: Level cross. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_LC) + end + + elseif rho<=RAR and playerData.step==AIRBOSS.PatternStep.GROOVE_LC then + + -- Store data. + playerData.groove.LC=UTILS.DeepCopy(groovedata) + + -- Get zone primary LDG spot. + local ZoneLS=self:_GetZoneLandingSpot() + + -- Get player velocity in km/h. + local vplayer=playerData.unit:GetVelocityKMH() + + -- Get carrier velocity in km/h. + local vcarrier=self.carrier:GetVelocityKMH() + + -- Speed difference. + local dv=math.abs(vplayer-vcarrier) + + -- Stable when v<7.5 km/h. + local stable=dv<7.5 + + -- Radio Transmission "Cleared to land" once the aircraft is inside the zone. + if playerData.unit:IsInZone(ZoneLS) and stable and playerData.warning==false then + self:RadioTransmission(self.LSORadio, self.LSOCall.STABILIZED, nil, nil, nil, true) + playerData.warning=true + end + + -- We keep it in this step until landed. + + end + + -------------- + -- Wave Off -- + -------------- + + -- Between IC and AR check for wave off. + if rho>=RAR and rho<=RIC and not playerData.waveoff then + + -- Check if player should wave off. + local waveoff=self:_CheckWaveOff(glideslopeError, lineupError, AoA, playerData) + + -- Let's see.. + if waveoff then + + -- Debug info. + self:T3(self.lid..string.format("Waveoff distance rho=%.1f m", rho)) + + -- LSO Wave off! + self:RadioTransmission(self.LSORadio, self.LSOCall.WAVEOFF, nil, nil, nil, true) + playerData.Tlso=timer.getTime() + + -- Player was waved off! + playerData.waveoff=true + + -- Nothing else necessary. + return + end + + end + + -- Groovedata step. + groovedata.Step=playerData.step + + ----------------- + -- Groove Data -- + ----------------- + + -- Check if we are beween 3/4 NM and end of ship. + if rho>=RAR and rho=RAR and rho<=RIM then + if gd.LUE>0.22 and lineupError<-0.22 then + env.info" Drift Right across centre ==> DR-" + gd.Drift=" DR" + self:T(self.lid..string.format("Got Drift Right across centre step %s, d=%.3f: Max LUE=%.3f, lower LUE=%.3f", gs, d, gd.LUE, lineupError)) + elseif gd.LUE<-0.22 and lineupError>0.22 then + env.info" Drift Left ==> DL-" + gd.Drift=" DL" + self:T(self.lid..string.format("Got Drift Left across centre at step %s, d=%.3f: Min LUE=%.3f, lower LUE=%.3f", gs, d, gd.LUE, lineupError)) + elseif gd.LUE>0.13 and lineupError<-0.14 then + env.info" Little Drift Right across centre ==> (DR-)" + gd.Drift=" (DR)" + self:T(self.lid..string.format("Got Little Drift Right across centre at step %s, d=%.3f: Max LUE=%.3f, lower LUE=%.3f", gs, d, gd.LUE, lineupError)) + elseif gd.LUE<-0.13 and lineupError>0.14 then + env.info" Little Drift Left across centre ==> (DL-)" + gd.Drift=" (DL)" + self:E(self.lid..string.format("Got Little Drift Left across centre at step %s, d=%.3f: Min LUE=%.3f, lower LUE=%.3f", gs, d, gd.LUE, lineupError)) + end + end + + -- Update max deviation of line up error. + if math.abs(lineupError)>math.abs(gd.LUE) then + self:T(self.lid..string.format("Got bigger LUE at step %s, d=%.3f: LUE %.3f>%.3f", gs, d, lineupError, gd.LUE)) + gd.LUE=lineupError + end + + -- Fly through good window of glideslope. + if gd.GSE>0.4 and glideslopeError<-0.3 then + -- Fly through down ==> "\" + gd.FlyThrough="\\" + self:T(self.lid..string.format("Got Fly through DOWN at step %s, d=%.3f: Max GSE=%.3f, lower GSE=%.3f", gs, d, gd.GSE, glideslopeError)) + elseif gd.GSE<-0.3 and glideslopeError>0.4 then + -- Fly through up ==> "/" + gd.FlyThrough="/" + self:E(self.lid..string.format("Got Fly through UP at step %s, d=%.3f: Min GSE=%.3f, lower GSE=%.3f", gs, d, gd.GSE, glideslopeError)) + end + + -- Update max deviation of glideslope error. + if math.abs(glideslopeError)>math.abs(gd.GSE) then + self:T(self.lid..string.format("Got bigger GSE at step %s, d=%.3f: GSE |%.3f|>|%.3f|", gs, d, glideslopeError, gd.GSE)) + gd.GSE=glideslopeError + end + + -- Get aircraft AoA parameters. + local aircraftaoa=self:_GetAircraftAoA(playerData) + + -- On Speed AoA. + local aoaopt=aircraftaoa.OnSpeed + + -- Compare AoAs wrt on speed AoA and update max deviation. + if math.abs(AoA-aoaopt)>math.abs(gd.AoA-aoaopt) then + self:T(self.lid..string.format("Got bigger AoA error at step %s, d=%.3f: AoA %.3f>%.3f.", gs, d, AoA, gd.AoA)) + gd.AoA=AoA + end + + --local gs2=self:_GS(groovedata.Step, -1) + --env.info(string.format("groovestep %s %s d=%.3f NM: GSE=%.3f %.3f, LUE=%.3f %.3f, AoA=%.3f %.3f", gs, gs2, d, groovedata.GSE, gd.GSE, groovedata.LUE, gd.LUE, groovedata.AoA, gd.AoA)) + + end + + --------------- + -- LSO Calls -- + --------------- + + -- Time since last LSO call. + local deltaT=timer.getTime()-playerData.Tlso + + -- Wait until player passed the 0.75 NM distance. + local _advice=true + if playerData.TIG0==nil and playerData.difficulty~=AIRBOSS.Difficulty.EASY then --rho>RXX + _advice=false + end + + -- LSO call if necessary. + if deltaT>=self.LSOdT and _advice then + self:_LSOadvice(playerData, glideslopeError, lineupError) + end + + end + + ---------------------------------------------------------- + --- Some time here the landing event MIGHT be triggered -- + ---------------------------------------------------------- + + -- Player infront of the carrier X>~77 m. + if X>self.carrierparam.totlength+self.carrierparam.sterndist then + + if playerData.waveoff then + + if playerData.landed then + -- This should not happen because landing event was triggered. + self:_AddToDebrief(playerData, "You were waved off but landed anyway. Airboss wants to talk to you!") + else + self:_AddToDebrief(playerData, "You were waved off.") + end + + elseif playerData.boltered then + + -- This should not happen because landing event was triggered. + self:_AddToDebrief(playerData, "You boltered.") + + else + + -- This should not happen. + self:T("Player was not waved off but flew past the carrier without landing ==> Own wave off!") + + -- We count this as OWO. + self:_AddToDebrief(playerData, "Own waveoff.") + + -- Set Owo + playerData.owo=true + + end + + -- Next step: debrief. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.DEBRIEF) + + end + +end + +--- LSO check if player needs to wave off. +-- Wave off conditions are: +-- +-- * Glideslope error <1.2 or >1.8 degrees. +-- * |Line up error| > 3 degrees. +-- * AoA check but only for TOPGUN graduates. +-- @param #AIRBOSS self +-- @param #number glideslopeError Glideslope error in degrees. +-- @param #number lineupError Line up error in degrees. +-- @param #number AoA Angle of attack of player aircraft. +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @return #boolean If true, player should wave off! +function AIRBOSS:_CheckWaveOff(glideslopeError, lineupError, AoA, playerData) + + -- Assume we're all good. + local waveoff=false + + -- Parameters + local glMax= 1.8 + local glMin=-1.2 + local luAbs= 3.0 + + -- For the harrier, we allow a bit more room. + if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then + glMax= 4.0 + glMin=-3.0 + luAbs= 5.0 + -- No waveoff for harrier pilots at the moment. + return false + end + + -- Too high or too low? + if glideslopeError>glMax then + local text=string.format("\n- Waveoff due to glideslope error %.2f > %.1f degrees!", glideslopeError, glMax) + self:T(self.lid..string.format("%s: %s", playerData.name, text)) + self:_AddToDebrief(playerData, text) + waveoff=true + elseif glideslopeErrorluAbs then + local text=string.format("\n- Waveoff due to line up error |%.1f| > %.1f degrees!", lineupError, luAbs) + self:T(self.lid..string.format("%s: %s", playerData.name, text)) + self:_AddToDebrief(playerData, text) + waveoff=true + end + + -- Too slow or too fast? Only for pros. + if playerData.difficulty==AIRBOSS.Difficulty.HARD then + -- Get aircraft specific AoA values + local aoaac=self:_GetAircraftAoA(playerData) + -- Check too slow or too fast. + if AoAaoaac.SLOW then + local text=string.format("\n- Waveoff due to AoA %.1f > %.1f!", AoA, aoaac.SLOW) + self:T(self.lid..string.format("%s: %s", playerData.name, text)) + self:_AddToDebrief(playerData, text) + waveoff=true + end + end + + return waveoff +end + +--- Check if other aircraft are currently on the landing runway. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @return boolean If true, we have a foul deck. +function AIRBOSS:_CheckFoulDeck(playerData) + + -- Assume no check necessary. + local check=false + + -- CVN: Check at IM and IC. + if playerData.step==AIRBOSS.PatternStep.GROOVE_IM or + playerData.step==AIRBOSS.PatternStep.GROOVE_IC then + check=true + end + + -- AV-8B check until + if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then + if playerData.step==AIRBOSS.PatternStep.GROOVE_AR or + playerData.step==AIRBOSS.PatternStep.GROOVE_AL then + check=true + end + end + + -- Check if player was already waved off. Should not be necessary as player step is set to debrief afterwards! + if playerData.wofd==true or check==false then + -- Player was already waved off. + return + end + + -- Landing runway zone. + local runway=self:_GetZoneRunwayBox() + + -- For AB-8B we just check the primary landing spot. + if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then + runway=self:_GetZoneLandingSpot() + end + + -- Scan radius. + local R=250 + + -- Debug info. + self:T(self.lid..string.format("Foul deck check: Scanning Carrier Runway Area. Radius=%.1f m.", R)) + + -- Scan units in carrier zone. + local _,_,_,unitscan=self:GetCoordinate():ScanObjects(R, true, false, false) + + -- Loop over all scanned units and check if they are on the runway. + local fouldeck=false + local foulunit=nil --Wrapper.Unit#UNIT + for _,_unit in pairs(unitscan) do + local unit=_unit --Wrapper.Unit#UNIT + + -- Check if unit is in zone. + local inzone=unit:IsInZone(runway) + + -- Check if aircraft and in air. + local isaircraft=unit:IsAir() + local isairborn =unit:InAir() + + if inzone and isaircraft and not isairborn then + local text=string.format("Unit %s on landing runway ==> Foul deck!", unit:GetName()) + self:T(self.lid..text) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + if self.Debug then + runway:FlareZone(FLARECOLOR.Red, 30) + end + fouldeck=true + foulunit=unit + end + end + + + -- Add to debrief and + if playerData and fouldeck then + + -- Debrief text. + local text=string.format("Foul deck waveoff due to aircraft %s!", foulunit:GetName()) + self:T(self.lid..string.format("%s: %s", playerData.name, text)) + self:_AddToDebrief(playerData, text) + + -- Foul deck + wave off radio message. + self:RadioTransmission(self.LSORadio, self.LSOCall.FOULDECK, false, 1) + self:RadioTransmission(self.LSORadio, self.LSOCall.WAVEOFF, false, 1.2, nil, true) + + -- Player hint for flight students. + if playerData.showhints then + local text=string.format("overfly landing area and enter bolter pattern.") + self:MessageToPlayer(playerData, text, "LSO", nil, nil, false, 3) + end + + -- Set player parameters for foul deck. + playerData.wofd=true + + -- Debrief. + playerData.step=AIRBOSS.PatternStep.DEBRIEF + playerData.warning=nil + + -- Pass would be invalid if the player lands. + playerData.valid=false + + -- Send a message to the player that blocks the runway. + if foulunit then + local foulflight=self:_GetFlightFromGroupInQueue(foulunit:GetGroup(), self.flights) + if foulflight and not foulflight.ai then + self:MessageToPlayer(foulflight, "move your ass from my runway. NOW!", "AIRBOSS") + end + end + end + + return fouldeck +end + +--- Get "stern" coordinate. +-- @param #AIRBOSS self +-- @return Core.Point#COORDINATE Coordinate at the rundown of the carrier. +function AIRBOSS:_GetSternCoord() + + -- Heading of carrier (true). + local hdg=self.carrier:GetHeading() + + -- Final bearing (true). + local FB=self:GetFinalBearing() + + -- Stern coordinate (sterndist<0). Also translate 10 meters starboard wrt Final bearing. + self.sterncoord:UpdateFromCoordinate(self:GetCoordinate()) + --local stern=self:GetCoordinate() + + -- Stern coordinate (sterndist<0). + if self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS then + -- Tarawa: Translate 8 meters port. + self.sterncoord:Translate(self.carrierparam.sterndist, hdg, true, true):Translate(8, FB-90, true, true) + elseif self.carriertype==AIRBOSS.CarrierType.STENNIS then + -- Stennis: translate 7 meters starboard wrt Final bearing. + self.sterncoord:Translate(self.carrierparam.sterndist, hdg, true, true):Translate(7, FB+90, true, true) + elseif self.carriertype==AIRBOSS.CarrierType.FORRESTAL then + -- Forrestal + self.sterncoord:Translate(self.carrierparam.sterndist, hdg, true, true):Translate(7.5, FB+90, true, true) + else + -- Nimitz SC: translate 8 meters starboard wrt Final bearing. + self.sterncoord:Translate(self.carrierparam.sterndist, hdg, true, true):Translate(9.5, FB+90, true, true) + end + + -- Set altitude. + self.sterncoord:SetAltitude(self.carrierparam.deckheight) + + return self.sterncoord +end + +--- Get wire from landing position. +-- @param #AIRBOSS self +-- @param Core.Point#COORDINATE Lcoord Landing position. +-- @param #number dc Distance correction. Shift the landing coord back if dc>0 and forward if dc<0. +-- @return #number Trapped wire (1-4) or 99 if no wire was trapped. +function AIRBOSS:_GetWire(Lcoord, dc) + + -- Final bearing (true). + local FB=self:GetFinalBearing() + + -- Stern coordinate (sterndist<0). Also translate 10 meters starboard wrt Final bearing. + local Scoord=self:_GetSternCoord() + + -- Distance to landing coord. + local Ldist=Lcoord:Get2DDistance(Scoord) + + -- For human (not AI) the lading event is delayed unfortunately. Therefore, we need another correction factor. + dc= dc or 65 + + -- Corrected landing distance wrt to stern. Landing distance needs to be reduced due to delayed landing event for human players. + local d=Ldist-dc + + -- Multiplayer wire correction. + if self.mpWireCorrection then + d=d-self.mpWireCorrection + end + + -- Shift wires from stern to their correct position. + local w1=self.carrierparam.wire1 + local w2=self.carrierparam.wire2 + local w3=self.carrierparam.wire3 + local w4=self.carrierparam.wire4 + + -- Which wire was caught? + local wire + if d wire=%d (dc=%.1f)", Ldist, Ldist-dc, wire, dc)) + + return wire +end + +--- Trapped? Check if in air or not after landing event. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_Trapped(playerData) + + if playerData.unit:InAir()==false then + -- Seems we have successfully landed. + + -- Lets see if we can get a good wire. + local unit=playerData.unit + + -- Coordinate of player aircraft. + local coord=unit:GetCoordinate() + + -- Get velocity in km/h. We need to substrackt the carrier velocity. + local v=unit:GetVelocityKMH()-self.carrier:GetVelocityKMH() + + -- Stern coordinate. + local stern=self:_GetSternCoord() + + -- Distance to stern pos. + local s=stern:Get2DDistance(coord) + + -- Get current wire (estimate). This now based on the position where the player comes to a standstill which should reflect the trapped wire better. + local dcorr=100 + if playerData.actype==AIRBOSS.AircraftCarrier.HORNET then + dcorr=100 + elseif playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B then + -- TODO: Check Tomcat. + dcorr=100 + elseif playerData.actype==AIRBOSS.AircraftCarrier.A4EC then + -- A-4E gets slowed down much faster the the F/A-18C! + dcorr=56 + elseif playerData.actype==AIRBOSS.AircraftCarrier.T45C then + -- T-45 also gets slowed down much faster the the F/A-18C. + dcorr=56 + end + + -- Get wire. + local wire=self:_GetWire(coord, dcorr) + + -- Debug. + local text=string.format("Player %s _Trapped: v=%.1f km/h, s-dcorr=%.1f m ==> wire=%d (dcorr=%d)", playerData.name, v, s-dcorr, wire, dcorr) + self:T(self.lid..text) + + -- Call this function again until v < threshold. Player comes to a standstill ==> Get wire! + if v>5 then + + -- Check if we passed all wires. + if wire>4 and v>10 and not playerData.warning then + -- Looks like we missed the wires ==> Bolter! + self:RadioTransmission(self.LSORadio, self.LSOCall.BOLTER, nil, nil, nil, true) + playerData.warning=true + end + + -- Call function again and check if converged or back in air. + --SCHEDULER:New(nil, self._Trapped, {self, playerData}, 0.1) + self:ScheduleOnce(0.1, self._Trapped, self, playerData) + return + end + + ---------------------------------------- + --- Form this point on we have converged + ---------------------------------------- + + -- Put some smoke and a mark. + if self.Debug then + coord:SmokeBlue() + coord:MarkToAll(text) + stern:MarkToAll("Stern") + end + + -- Set player wire. + playerData.wire=wire + + -- Message to player. + local text=string.format("Trapped %d-wire.", wire) + if wire==3 then + text=text.." Well done!" + elseif wire==2 then + text=text.." Not bad, maybe you even get the 3rd next time." + elseif wire==4 then + text=text.." That was scary. You can do better than this!" + elseif wire==1 then + text=text.." Try harder next time!" + end + + -- Message to player. + self:MessageToPlayer(playerData, text, "LSO", "") + + -- Debrief. + local hint = string.format("Trapped %d-wire.", wire) + self:_AddToDebrief(playerData, hint, "Groove: IW") + + else + + --Again in air ==> Boltered! + local text=string.format("Player %s boltered in trapped function.", playerData.name) + self:T(self.lid..text) + MESSAGE:New(text, 5, "DEBUG"):ToAllIf(self.debug) + + -- Bolter switch on. + playerData.boltered=true + + end + + -- Next step: debriefing. + playerData.step=AIRBOSS.PatternStep.DEBRIEF + playerData.warning=nil +end + +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ZONE functions +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get Initial zone for Case I or II. +-- @param #AIRBOSS self +-- @param #number case Recovery Case. +-- @return Core.Zone#ZONE_POLYGON_BASE Initial zone. +function AIRBOSS:_GetZoneInitial(case) + + self.zoneInitial=self.zoneInitial or ZONE_POLYGON_BASE:New("Zone CASE I/II Initial") + + -- Get radial, i.e. inverse of BRC. + local radial=self:GetRadial(2, false, false) + + -- Carrier coordinate. + local cv=self:GetCoordinate() + + -- Vec2 array. + local vec2={} + + if case==1 then + -- Case I + + local c1=cv:Translate(UTILS.NMToMeters(0.5), radial-90) -- 0.0 0.5 starboard + local c2=cv:Translate(UTILS.NMToMeters(1.3), radial-90):Translate(UTILS.NMToMeters(3), radial) -- -3.0 1.3 starboard, astern + local c3=cv:Translate(UTILS.NMToMeters(0.4), radial+90):Translate(UTILS.NMToMeters(3), radial) -- -3.0 -0.4 port, astern + local c4=cv:Translate(UTILS.NMToMeters(1.0), radial) + local c5=cv + + -- Vec2 array. + vec2={c1:GetVec2(), c2:GetVec2(), c3:GetVec2(), c4:GetVec2(), c5:GetVec2()} + + else + -- Case II + + -- Funnel. + local c1=cv:Translate(UTILS.NMToMeters(0.5), radial-90) -- 0.0, 0.5 + local c2=c1:Translate(UTILS.NMToMeters(0.5), radial) -- 0.5, 0.5 + local c3=cv:Translate(UTILS.NMToMeters(1.2), radial-90):Translate(UTILS.NMToMeters(3), radial) -- 3.0, 1.2 + local c4=cv:Translate(UTILS.NMToMeters(1.2), radial+90):Translate(UTILS.NMToMeters(3), radial) -- 3.0,-1.2 + local c5=cv:Translate(UTILS.NMToMeters(0.5), radial) + local c6=cv + + -- Vec2 array. + vec2={c1:GetVec2(), c2:GetVec2(), c3:GetVec2(), c4:GetVec2(), c5:GetVec2(), c6:GetVec2()} + + end + + -- Polygon zone. + --local zone=ZONE_POLYGON_BASE:New("Zone CASE I/II Initial", vec2) + + self.zoneInitial:UpdateFromVec2(vec2) + + --return zone + return self.zoneInitial +end + +--- Get lineup groove zone. +-- @param #AIRBOSS self +-- @return Core.Zone#ZONE_POLYGON_BASE Lineup zone. +function AIRBOSS:_GetZoneLineup() + + self.zoneLineup=self.zoneLineup or ZONE_POLYGON_BASE:New("Zone Lineup") + + -- Get radial, i.e. inverse of BRC. + local fbi=self:GetRadial(1, false, false) + + -- Stern coordinate. + local st=self:_GetOptLandingCoordinate() + + -- Zone points. + local c1=st + local c2=st:Translate(UTILS.NMToMeters(0.50), fbi+15) + local c3=st:Translate(UTILS.NMToMeters(0.50), fbi+self.lue._max-0.05) + local c4=st:Translate(UTILS.NMToMeters(0.77), fbi+self.lue._max-0.05) + local c5=c4:Translate(UTILS.NMToMeters(0.25), fbi-90) + + -- Vec2 array. + local vec2={c1:GetVec2(), c2:GetVec2(), c3:GetVec2(), c4:GetVec2(), c5:GetVec2()} + + self.zoneLineup:UpdateFromVec2(vec2) + + -- Polygon zone. + --local zone=ZONE_POLYGON_BASE:New("Zone Lineup", vec2) + --return zone + + return self.zoneLineup +end + + +--- Get groove zone. +-- @param #AIRBOSS self +-- @param #number l Length of the groove in NM. Default 1.5 NM. +-- @param #number w Width of the groove in NM. Default 0.25 NM. +-- @param #number b Width of the beginning in NM. Default 0.10 NM. +-- @return Core.Zone#ZONE_POLYGON_BASE Groove zone. +function AIRBOSS:_GetZoneGroove(l, w, b) + + self.zoneGroove=self.zoneGroove or ZONE_POLYGON_BASE:New("Zone Groove") + + l=l or 1.50 + w=w or 0.25 + b=b or 0.10 + + -- Get radial, i.e. inverse of BRC. + local fbi=self:GetRadial(1, false, false) + + -- Stern coordinate. + local st=self:_GetSternCoord() + + -- Zone points. + local c1=st:Translate(self.carrierparam.totwidthstarboard, fbi-90) + local c2=st:Translate(UTILS.NMToMeters(0.10), fbi-90):Translate(UTILS.NMToMeters(0.3), fbi) + local c3=st:Translate(UTILS.NMToMeters(0.25), fbi-90):Translate(UTILS.NMToMeters(l), fbi) + local c4=st:Translate(UTILS.NMToMeters(w/2), fbi+90):Translate(UTILS.NMToMeters(l), fbi) + local c5=st:Translate(UTILS.NMToMeters(b), fbi+90):Translate(UTILS.NMToMeters(0.3), fbi) + local c6=st:Translate(self.carrierparam.totwidthport, fbi+90) + + -- Vec2 array. + local vec2={c1:GetVec2(), c2:GetVec2(), c3:GetVec2(), c4:GetVec2(), c5:GetVec2(), c6:GetVec2()} + + self.zoneGroove:UpdateFromVec2(vec2) + + -- Polygon zone. + --local zone=ZONE_POLYGON_BASE:New("Zone Groove", vec2) + --return zone + + return self.zoneGroove +end + +--- Get Bullseye zone with radius 1 NM and DME 3 NM from the carrier. Radial depends on recovery case. +-- @param #AIRBOSS self +-- @param #number case Recovery case. +-- @return Core.Zone#ZONE_RADIUS Arc in zone. +function AIRBOSS:_GetZoneBullseye(case) + + -- Radius = 1 NM. + local radius=UTILS.NMToMeters(1) + + -- Distance = 3 NM + local distance=UTILS.NMToMeters(3) + + -- Zone depends on Case recovery. + local radial=self:GetRadial(case, false, false) + + -- Get coordinate and vec2. + local coord=self:GetCoordinate():Translate(distance, radial) + local vec2=coord:GetVec2() + + -- Create zone. + local zone=ZONE_RADIUS:New("Zone Bullseye", vec2, radius) + return zone + + --self.zoneBullseye=self.zoneBullseye or ZONE_RADIUS:New("Zone Bullseye", vec2, radius) +end + +--- Get dirty up zone with radius 1 NM and DME 9 NM from the carrier. Radial depends on recovery case. +-- @param #AIRBOSS self +-- @param #number case Recovery case. +-- @return Core.Zone#ZONE_RADIUS Dirty up zone. +function AIRBOSS:_GetZoneDirtyUp(case) + + -- Radius = 1 NM. + local radius=UTILS.NMToMeters(1) + + -- Distance = 9 NM + local distance=UTILS.NMToMeters(9) + + -- Zone depends on Case recovery. + local radial=self:GetRadial(case, false, false) + + -- Get coordinate and vec2. + local coord=self:GetCoordinate():Translate(distance, radial) + local vec2=coord:GetVec2() + + -- Create zone. + local zone=ZONE_RADIUS:New("Zone Dirty Up", vec2, radius) + + return zone +end + +--- Get arc out zone with radius 1 NM and DME 12 NM from the carrier. Radial depends on recovery case. +-- @param #AIRBOSS self +-- @param #number case Recovery case. +-- @return Core.Zone#ZONE_RADIUS Arc in zone. +function AIRBOSS:_GetZoneArcOut(case) + + -- Radius = 1.25 NM. + local radius=UTILS.NMToMeters(1.25) + + -- Distance = 12 NM + local distance=UTILS.NMToMeters(11.75) + + -- Zone depends on Case recovery. + local radial=self:GetRadial(case, false, false) + + -- Get coordinate of carrier and translate. + local coord=self:GetCoordinate():Translate(distance, radial) + + -- Create zone. + local zone=ZONE_RADIUS:New("Zone Arc Out", coord:GetVec2(), radius) + + return zone +end + +--- Get arc in zone with radius 1 NM and DME 14 NM from the carrier. Radial depends on recovery case. +-- @param #AIRBOSS self +-- @param #number case Recovery case. +-- @return Core.Zone#ZONE_RADIUS Arc in zone. +function AIRBOSS:_GetZoneArcIn(case) + + -- Radius = 1.25 NM. + local radius=UTILS.NMToMeters(1.25) + + -- Zone depends on Case recovery. + local radial=self:GetRadial(case, false, true) + + -- Angle between FB/BRC and holding zone. + local alpha=math.rad(self.holdingoffset) + + -- 14+x NM from carrier + local x=14 --/math.cos(alpha) + + -- Distance = 14 NM + local distance=UTILS.NMToMeters(x) + + -- Get coordinate. + local coord=self:GetCoordinate():Translate(distance, radial) + + -- Create zone. + local zone=ZONE_RADIUS:New("Zone Arc In", coord:GetVec2(), radius) + + return zone +end + +--- Get platform zone with radius 1 NM and DME 19 NM from the carrier. Radial depends on recovery case. +-- @param #AIRBOSS self +-- @param #number case Recovery case. +-- @return Core.Zone#ZONE_RADIUS Circular platform zone. +function AIRBOSS:_GetZonePlatform(case) + + -- Radius = 1 NM. + local radius=UTILS.NMToMeters(1) + + -- Zone depends on Case recovery. + local radial=self:GetRadial(case, false, true) + + -- Angle between FB/BRC and holding zone. + local alpha=math.rad(self.holdingoffset) + + -- Distance = 19 NM + local distance=UTILS.NMToMeters(19) --/math.cos(alpha) + + -- Get coordinate. + local coord=self:GetCoordinate():Translate(distance, radial) + + -- Create zone. + local zone=ZONE_RADIUS:New("Zone Platform", coord:GetVec2(), radius) + + return zone +end + + +--- Get approach corridor zone. Shape depends on recovery case. +-- @param #AIRBOSS self +-- @param #number case Recovery case. +-- @param #number l Length of the zone in NM. Default 31 (=21+10) NM. +-- @return Core.Zone#ZONE_POLYGON_BASE Box zone. +function AIRBOSS:_GetZoneCorridor(case, l) + + -- Total length. + l=l or 31 + + -- Radial and offset. + local radial=self:GetRadial(case, false, false) + local offset=self:GetRadial(case, false, true) + + -- Distance shift ahead of carrier to allow for some space to bolter. + local dx=5 + + -- Width of the box in NM. + local w=2 + local w2=w/2 + + -- Distance from carrier to arc out zone. + local d=12 + + -- Carrier position. + local cv=self:GetCoordinate() + + -- Polygon points. + local c={} + + -- First point. Carrier coordinate translated 5 NM in direction of travel to allow for bolter space. + c[1]=cv:Translate(-UTILS.NMToMeters(dx), radial) + + if math.abs(self.holdingoffset)>=5 then + + ----------------- + -- Angled Case -- + ----------------- + + c[2]=c[1]:Translate( UTILS.NMToMeters(w2), radial-90) -- 1 Right of carrier, dx ahead. + c[3]=c[2]:Translate( UTILS.NMToMeters(d+dx+w2), radial) -- 13 "south" @ 1 right + + c[4]=cv:Translate(UTILS.NMToMeters(15), offset):Translate(UTILS.NMToMeters(1), offset-90) + c[5]=cv:Translate(UTILS.NMToMeters(l), offset):Translate(UTILS.NMToMeters(1), offset-90) + c[6]=cv:Translate(UTILS.NMToMeters(l), offset):Translate(UTILS.NMToMeters(1), offset+90) + c[7]=cv:Translate(UTILS.NMToMeters(13), offset):Translate(UTILS.NMToMeters(1), offset+90) + c[8]=cv:Translate(UTILS.NMToMeters(11), radial):Translate(UTILS.NMToMeters(1), radial+90) + + c[9]=c[1]:Translate(UTILS.NMToMeters(w2), radial+90) + + else + + ----------------------------- + -- Easy case of a long box -- + ----------------------------- + + c[2]=c[1]:Translate( UTILS.NMToMeters(w2), radial-90) + c[3]=c[2]:Translate( UTILS.NMToMeters(dx+l), radial) -- Stack 1 starts at 21 and is 7 NM. + c[4]=c[3]:Translate( UTILS.NMToMeters(w), radial+90) + c[5]=c[1]:Translate( UTILS.NMToMeters(w2), radial+90) + + end + + + -- Create an array of a square! + local p={} + for _i,_c in ipairs(c) do + if self.Debug then + --_c:SmokeBlue() + end + p[_i]=_c:GetVec2() + end + + -- Square zone length=10NM width=6 NM behind the carrier starting at angels+15 NM behind the carrier. + -- So stay 0-5 NM (+1 NM error margin) port of carrier. + local zone=ZONE_POLYGON_BASE:New("CASE II/III Approach Corridor", p) + + return zone +end + + +--- Get zone of carrier. Carrier is approximated as rectangle. +-- @param #AIRBOSS self +-- @return Core.Zone#ZONE Zone surrounding the carrier. +function AIRBOSS:_GetZoneCarrierBox() + + self.zoneCarrierbox=self.zoneCarrierbox or ZONE_POLYGON_BASE:New("Carrier Box Zone") + + -- Stern coordinate. + local S=self:_GetSternCoord() + + -- Current carrier heading. + local hdg=self:GetHeading(false) + + -- Coordinate array. + local p={} + + -- Starboard stern point. + p[1]=S:Translate(self.carrierparam.totwidthstarboard, hdg+90) + + -- Starboard bow point. + p[2]=p[1]:Translate(self.carrierparam.totlength, hdg) + + -- Port bow point. + p[3]=p[2]:Translate(self.carrierparam.totwidthstarboard+self.carrierparam.totwidthport, hdg-90) + + -- Port stern point. + p[4]=p[3]:Translate(self.carrierparam.totlength, hdg-180) + + -- Convert to vec2. + local vec2={} + for _,coord in ipairs(p) do + table.insert(vec2, coord:GetVec2()) + end + + -- Create polygon zone. + --local zone=ZONE_POLYGON_BASE:New("Carrier Box Zone", vec2) + --return zone + + self.zoneCarrierbox:UpdateFromVec2(vec2) + + return self.zoneCarrierbox +end + +--- Get zone of landing runway. +-- @param #AIRBOSS self +-- @return Core.Zone#ZONE_POLYGON Zone surrounding landing runway. +function AIRBOSS:_GetZoneRunwayBox() + + self.zoneRunwaybox=self.zoneRunwaybox or ZONE_POLYGON_BASE:New("Landing Runway Zone") + + -- Stern coordinate. + local S=self:_GetSternCoord() + + -- Current carrier heading. + local FB=self:GetFinalBearing(false) + + -- Coordinate array. + local p={} + + -- Points. + p[1]=S:Translate(self.carrierparam.rwywidth*0.5, FB+90) + p[2]=p[1]:Translate(self.carrierparam.rwylength, FB) + p[3]=p[2]:Translate(self.carrierparam.rwywidth, FB-90) + p[4]=p[3]:Translate(self.carrierparam.rwylength, FB-180) + + -- Convert to vec2. + local vec2={} + for _,coord in ipairs(p) do + table.insert(vec2, coord:GetVec2()) + end + + -- Create polygon zone. + --local zone=ZONE_POLYGON_BASE:New("Landing Runway Zone", vec2) + --return zone + + self.zoneRunwaybox:UpdateFromVec2(vec2) + + return self.zoneRunwaybox +end + + +--- Get zone of primary abeam landing position of USS Tarawa. Box length and width 30 meters. +-- @param #AIRBOSS self +-- @return Core.Zone#ZONE_POLYGON Zone surrounding landing runway. +function AIRBOSS:_GetZoneAbeamLandingSpot() + + -- Primary landing Spot coordinate. + local S=self:_GetOptLandingCoordinate() + + -- Current carrier heading. + local FB=self:GetFinalBearing(false) + + -- Coordinate array. + local p={} + + -- Points. + p[1]=S:Translate( 15, FB):Translate(15, FB+90) -- Top-Right + p[2]=S:Translate(-15, FB):Translate(15, FB+90) -- Bottom-Right + p[3]=S:Translate(-15, FB):Translate(15, FB-90) -- Bottom-Left + p[4]=S:Translate( 15, FB):Translate(15, FB-90) -- Top-Left + + -- Convert to vec2. + local vec2={} + for _,coord in ipairs(p) do + table.insert(vec2, coord:GetVec2()) + end + + -- Create polygon zone. + local zone=ZONE_POLYGON_BASE:New("Abeam Landing Spot Zone", vec2) + + return zone +end + + +--- Get zone of the primary landing spot of the USS Tarawa. +-- @param #AIRBOSS self +-- @return Core.Zone#ZONE_POLYGON Zone surrounding landing runway. +function AIRBOSS:_GetZoneLandingSpot() + + -- Primary landing Spot coordinate. + local S=self:_GetLandingSpotCoordinate() + + -- Current carrier heading. + local FB=self:GetFinalBearing(false) + + -- Coordinate array. + local p={} + + -- Points. + p[1]=S:Translate( 10, FB):Translate(10, FB+90) -- Top-Right + p[2]=S:Translate(-10, FB):Translate(10, FB+90) -- Bottom-Right + p[3]=S:Translate(-10, FB):Translate(10, FB-90) -- Bottom-Left + p[4]=S:Translate( 10, FB):Translate(10, FB-90) -- Top-left + + -- Convert to vec2. + local vec2={} + for _,coord in ipairs(p) do + table.insert(vec2, coord:GetVec2()) + end + + -- Create polygon zone. + local zone=ZONE_POLYGON_BASE:New("Landing Spot Zone", vec2) + + return zone +end + + +--- Get holding zone of player. +-- @param #AIRBOSS self +-- @param #number case Recovery case. +-- @param #number stack Marshal stack number. +-- @return Core.Zone#ZONE Holding zone. +function AIRBOSS:_GetZoneHolding(case, stack) + + -- Holding zone. + local zoneHolding=nil --Core.Zone#ZONE + + -- Stack is <= 0 ==> no marshal zone. + if stack<=0 then + self:E(self.lid.."ERROR: Stack <= 0 in _GetZoneHolding!") + self:E({case=case, stack=stack}) + return nil + end + + -- Pattern altitude. + local patternalt, c1, c2=self:_GetMarshalAltitude(stack, case) + + -- Select case. + if case==1 then + -- CASE I + + -- Get current carrier heading. + local hdg=self:GetHeading() + + -- Distance to the post. + local D=UTILS.NMToMeters(2.5) + + -- Post 2.5 NM port of carrier. + local Post=self:GetCoordinate():Translate(D, hdg+270) + + --TODO: update zone not creating a new one. + + -- Create holding zone. + self.zoneHolding=ZONE_RADIUS:New("CASE I Holding Zone", Post:GetVec2(), self.marshalradius) + + -- Delta pattern. + if self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS then + self.zoneHolding=ZONE_RADIUS:New("CASE I Holding Zone", self.carrier:GetVec2(), UTILS.NMToMeters(5)) + end + + + else + -- CASE II/II + + -- Get radial. + local radial=self:GetRadial(case, false, true) + + -- Create an array of a rectangle. Length is 7 NM, width is 8 NM. One NM starboard to line up with the approach corridor. + local p={} + p[1]=c2:Translate(UTILS.NMToMeters(1), radial-90):GetVec2() --c2 is at (angels+15) NM directly behind the carrier. We translate it 1 NM starboard. + p[2]=c1:Translate(UTILS.NMToMeters(1), radial-90):GetVec2() --c1 is 7 NM further behind. Also translated 1 NM starboard. + p[3]=c1:Translate(UTILS.NMToMeters(7), radial+90):GetVec2() --p3 7 NM port of carrier. + p[4]=c2:Translate(UTILS.NMToMeters(7), radial+90):GetVec2() --p4 7 NM port of carrier. + + -- Square zone length=7NM width=6 NM behind the carrier starting at angels+15 NM behind the carrier. + -- So stay 0-5 NM (+1 NM error margin) port of carrier. + self.zoneHolding=self.zoneHolding or ZONE_POLYGON_BASE:New("CASE II/III Holding Zone") + + self.zoneHolding:UpdateFromVec2(p) + end + + return self.zoneHolding +end + +--- Get zone where player are automatically commence when enter. +-- @param #AIRBOSS self +-- @param #number case Recovery case. +-- @param #number stack Stack for Case II/III as we commence from stack>=1. +-- @return Core.Zone#ZONE Holding zone. +function AIRBOSS:_GetZoneCommence(case, stack) + + -- Commence zone. + local zone + + if case==1 then + -- Case I + + -- Get current carrier heading. + local hdg=self:GetHeading() + + -- Distance to the zone. + local D=UTILS.NMToMeters(4.75) + + -- Zone radius. + local R=UTILS.NMToMeters(1) + + -- Three position + local Three=self:GetCoordinate():Translate(D, hdg+275) + + if self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS then + + local Dx=UTILS.NMToMeters(2.25) + + local Dz=UTILS.NMToMeters(2.25) + + R=UTILS.NMToMeters(1) + + Three=self:GetCoordinate():Translate(Dz, hdg-90):Translate(Dx, hdg-180) + + end + + -- Create holding zone. + self.zoneCommence=self.zoneCommence or ZONE_RADIUS:New("CASE I Commence Zone") + + self.zoneCommence:UpdateFromVec2(Three:GetVec2(), R) + + else + -- Case II/III + + stack=stack or 1 + + -- Start point at 21 NM for stack=1. + local l=20+stack + + -- Offset angle + local offset=self:GetRadial(case, false, true) + + -- Carrier position. + local cv=self:GetCoordinate() + + -- Polygon points. + local c={} + + c[1]=cv:Translate(UTILS.NMToMeters(l), offset):Translate(UTILS.NMToMeters(1), offset-90) + c[2]=cv:Translate(UTILS.NMToMeters(l+2.5), offset):Translate(UTILS.NMToMeters(1), offset-90) + c[3]=cv:Translate(UTILS.NMToMeters(l+2.5), offset):Translate(UTILS.NMToMeters(1), offset+90) + c[4]=cv:Translate(UTILS.NMToMeters(l), offset):Translate(UTILS.NMToMeters(1), offset+90) + + -- Create an array of a square! + local p={} + for _i,_c in ipairs(c) do + p[_i]=_c:GetVec2() + end + + -- Zone polygon. + self.zoneCommence=self.zoneCommence or ZONE_POLYGON_BASE:New("CASE II/III Commence Zone") + + self.zoneCommence:UpdateFromVec2(p) + + end + + return self.zoneCommence +end + +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ORIENTATION functions +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Provide info about player status on the fly. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +function AIRBOSS:_AttitudeMonitor(playerData) + + -- Player unit. + local unit=playerData.unit + + -- Aircraft attitude. + local aoa=unit:GetAoA() + local yaw=unit:GetYaw() + local roll=unit:GetRoll() + local pitch=unit:GetPitch() + + -- Distance to the boat. + local dist=playerData.unit:GetCoordinate():Get2DDistance(self:GetCoordinate()) + local dx,dz,rho,phi=self:_GetDistances(unit) + + -- Wind vector. + local wind=unit:GetCoordinate():GetWindWithTurbulenceVec3() + + -- Aircraft veloecity vector. + local velo=unit:GetVelocityVec3() + local vabs=UTILS.VecNorm(velo) + + local rwy=false + local step=playerData.step + if playerData.step==AIRBOSS.PatternStep.FINAL or + playerData.step==AIRBOSS.PatternStep.GROOVE_XX or + playerData.step==AIRBOSS.PatternStep.GROOVE_IM or + playerData.step==AIRBOSS.PatternStep.GROOVE_IC or + playerData.step==AIRBOSS.PatternStep.GROOVE_AR or + playerData.step==AIRBOSS.PatternStep.GROOVE_AL or + playerData.step==AIRBOSS.PatternStep.GROOVE_LC or + playerData.step==AIRBOSS.PatternStep.GROOVE_IW then + step=self:_GS(step,-1) + rwy=true + end + + -- Relative heading Aircraft to Carrier. + local relhead=self:_GetRelativeHeading(playerData.unit, rwy) + + --local lc=self:_GetOptLandingCoordinate() + --lc:FlareRed() + + -- Output + local text=string.format("Pattern step: %s", step) + text=text..string.format("\nAoA=%.1f° = %.1f Units | |V|=%.1f knots", aoa, self:_AoADeg2Units(playerData, aoa), UTILS.MpsToKnots(vabs)) + if self.Debug then + -- Velocity vector. + text=text..string.format("\nVx=%.1f Vy=%.1f Vz=%.1f m/s", velo.x, velo.y, velo.z) + --Wind vector. + text=text..string.format("\nWind Vx=%.1f Vy=%.1f Vz=%.1f m/s", wind.x, wind.y, wind.z) + end + text=text..string.format("\nPitch=%.1f° | Roll=%.1f° | Yaw=%.1f°", pitch, roll, yaw) + text=text..string.format("\nClimb Angle=%.1f° | Rate=%d ft/min", unit:GetClimbAngle(), velo.y*196.85) + local dist=self:_GetOptLandingCoordinate():Get3DDistance(playerData.unit) + -- Get player velocity in km/h. + local vplayer=playerData.unit:GetVelocityKMH() + -- Get carrier velocity in km/h. + local vcarrier=self.carrier:GetVelocityKMH() + -- Speed difference. + local dv=math.abs(vplayer-vcarrier) + local alt=self:_GetAltCarrier(playerData.unit) + text=text..string.format("\nDist=%.1f m Alt=%.1f m delta|V|=%.1f km/h", dist, alt, dv) + -- If in the groove, provide line up and glide slope error. + if playerData.step==AIRBOSS.PatternStep.FINAL or + playerData.step==AIRBOSS.PatternStep.GROOVE_XX or + playerData.step==AIRBOSS.PatternStep.GROOVE_IM or + playerData.step==AIRBOSS.PatternStep.GROOVE_IC or + playerData.step==AIRBOSS.PatternStep.GROOVE_AR or + playerData.step==AIRBOSS.PatternStep.GROOVE_AL or + playerData.step==AIRBOSS.PatternStep.GROOVE_LC or + playerData.step==AIRBOSS.PatternStep.GROOVE_IW then + local lue=self:_Lineup(playerData.unit, true) + local gle=self:_Glideslope(playerData.unit) + text=text..string.format("\nGamma=%.1f° | Rho=%.1f°", relhead, phi) + text=text..string.format("\nLineUp=%.2f° | GlideSlope=%.2f° | AoA=%.1f Units", lue, gle, self:_AoADeg2Units(playerData, aoa)) + local grade, points, analysis=self:_LSOgrade(playerData) + text=text..string.format("\nTgroove=%.1f sec", self:_GetTimeInGroove(playerData)) + text=text..string.format("\nGrade: %s %.1f PT - %s", grade, points, analysis) + else + text=text..string.format("\nR=%.2f NM | X=%d Z=%d m", UTILS.MetersToNM(rho), dx, dz) + text=text..string.format("\nGamma=%.1f° | Rho=%.1f°", relhead, phi) + end + + MESSAGE:New(text, 1, nil , true):ToClient(playerData.client) +end + +--- Get glide slope of aircraft unit. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Aircraft unit. +-- @param #number optangle (Optional) Return glide slope relative to this angle, i.e. the error from the optimal glide slope ~3.5 degrees. +-- @return #number Glide slope angle in degrees measured from the deck of the carrier and third wire. +function AIRBOSS:_Glideslope(unit, optangle) + + if optangle==nil then + if unit:GetTypeName()==AIRBOSS.AircraftCarrier.AV8B then + optangle=3.0 + else + optangle=3.5 + end + end + -- Landing coordinate + local landingcoord=self:_GetOptLandingCoordinate() + + -- Distance from stern to aircraft. + local x=unit:GetCoordinate():Get2DDistance(landingcoord) + + -- Altitude of unit corrected by the deck height of the carrier. + local h=self:_GetAltCarrier(unit) + + -- Harrier should be 40-50 ft above the deck. + if unit:GetTypeName()==AIRBOSS.AircraftCarrier.AV8B then + h=unit:GetAltitude()-(UTILS.FeetToMeters(50)+self.carrierparam.deckheight+2) + end + + -- Glide slope. + local glideslope=math.atan(h/x) + + -- Glide slope (error) in degrees. + local gs=math.deg(glideslope)-optangle + + return gs +end + +--- Get glide slope of aircraft unit. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Aircraft unit. +-- @param #number optangle (Optional) Return glide slope relative to this angle, i.e. the error from the optimal glide slope ~3.5 degrees. +-- @return #number Glide slope angle in degrees measured from the deck of the carrier and third wire. +function AIRBOSS:_Glideslope2(unit, optangle) + + if optangle==nil then + if unit:GetTypeName()==AIRBOSS.AircraftCarrier.AV8B then + optangle=3.0 + else + optangle=3.5 + end + end + -- Landing coordinate + local landingcoord=self:_GetOptLandingCoordinate() + + -- Distance from stern to aircraft. + local x=unit:GetCoordinate():Get3DDistance(landingcoord) + + -- Altitude of unit corrected by the deck height of the carrier. + local h=self:_GetAltCarrier(unit) + + -- Harrier should be 40-50 ft above the deck. + if unit:GetTypeName()==AIRBOSS.AircraftCarrier.AV8B then + h=unit:GetAltitude()-(UTILS.FeetToMeters(50)+self.carrierparam.deckheight+2) + end + + -- Glide slope. + local glideslope=math.asin(h/x) + + -- Glide slope (error) in degrees. + local gs=math.deg(glideslope)-optangle + + -- Debug. + self:T3(self.lid..string.format("Glide slope error = %.1f, x=%.1f h=%.1f", gs, x, h)) + + return gs +end + +--- Get line up of player wrt to carrier. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Aircraft unit. +-- @param #boolean runway If true, include angled runway. +-- @return #number Line up with runway heading in degrees. 0 degrees = perfect line up. +1 too far left. -1 too far right. +function AIRBOSS:_Lineup(unit, runway) + + -- Landing coordinate + local landingcoord=self:_GetOptLandingCoordinate() + + -- Vector to landing coord. + local A=landingcoord:GetVec3() + + -- Vector to player. + local B=unit:GetVec3() + + -- Vector from player to carrier. + local C=UTILS.VecSubstract(A, B) + + -- Only in 2D plane. + C.y=0.0 + + -- Orientation of carrier. + local X=self.carrier:GetOrientationX() + X.y=0.0 + + -- Rotate orientation to angled runway. + if runway then + X=UTILS.Rotate2D(X, -self.carrierparam.rwyangle) + end + + -- Projection of player pos on x component. + local x=UTILS.VecDot(X, C) + + -- Orientation of carrier. + local Z=self.carrier:GetOrientationZ() + Z.y=0.0 + + -- Rotate orientation to angled runway. + if runway then + Z=UTILS.Rotate2D(Z, -self.carrierparam.rwyangle) + end + + -- Projection of player pos on z component. + local z=UTILS.VecDot(Z, C) + + --- + local lineup=math.deg(math.atan2(z, x)) + + return lineup +end + +--- Get alitude of aircraft wrt carrier deck. Should give zero when the aircraft touched down. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Aircraft unit. +-- @return #number Altitude in meters wrt carrier height. +function AIRBOSS:_GetAltCarrier(unit) + + -- TODO: Value 4 meters is for the Hornet. Adjust for Harrier, A4E and + + -- Altitude of unit corrected by the deck height of the carrier. + local h=unit:GetAltitude()-self.carrierparam.deckheight-2 + + return h +end + +--- Get optimal landing position of the aircraft. Usually between second and third wire. In case of Tarawa and America we take the abeam landing spot 120 ft abeam the 7.5 position, for the Juan Carlos I it is 120 ft and abeam the 5 position. +-- @param #AIRBOSS self +-- @return Core.Point#COORDINATE Optimal landing coordinate. +function AIRBOSS:_GetOptLandingCoordinate() + + -- Start with stern coordiante. + self.landingcoord:UpdateFromCoordinate(self:_GetSternCoord()) + + -- Stern coordinate. + --local stern=self:_GetSternCoord() + + -- Final bearing. + local FB=self:GetFinalBearing(false) + + if self.carriertype==AIRBOSS.CarrierType.TARAWA then + + -- Landing 100 ft abeam, 120 ft alt. + self.landingcoord:UpdateFromCoordinate(self:_GetLandingSpotCoordinate()):Translate(35, FB-90, true, true) + --stern=self:_GetLandingSpotCoordinate():Translate(35, FB-90) + + -- Alitude 120 ft. + self.landingcoord:SetAltitude(UTILS.FeetToMeters(120)) + elseif self.carriertype==AIRBOSS.CarrierType.AMERICA then + + -- Landing 100 ft abeam, 120 ft alt. To allow adjustments to match different deck configurations. + self.landingcoord:UpdateFromCoordinate(self:_GetLandingSpotCoordinate()):Translate(35, FB-90, true, true) + --stern=self:_GetLandingSpotCoordinate():Translate(35, FB-90) + + -- Alitude 120 ft. + self.landingcoord:SetAltitude(UTILS.FeetToMeters(120)) + + elseif self.carriertype==AIRBOSS.CarrierType.JCARLOS then + + -- Landing 100 ft abeam, 120 ft alt. + self.landingcoord:UpdateFromCoordinate(self:_GetLandingSpotCoordinate()):Translate(35, FB-100, true, true) + --stern=self:_GetLandingSpotCoordinate():Translate(35, FB-100) + + -- Alitude 120 ft. + self.landingcoord:SetAltitude(UTILS.FeetToMeters(120)) + + else + + -- Ideally we want to land between 2nd and 3rd wire. + if self.carrierparam.wire3 then + -- We take the position of the 3rd wire to approximately account for the length of the aircraft. + local w3=self.carrierparam.wire3 + self.landingcoord:Translate(w3, FB, true, true) + end + + -- Add 2 meters to account for aircraft height. + self.landingcoord.y=self.landingcoord.y+2 + + end + + return self.landingcoord +end + +--- Get landing spot on Tarawa. +-- @param #AIRBOSS self +-- @return Core.Point#COORDINATE Primary landing spot coordinate. +function AIRBOSS:_GetLandingSpotCoordinate() + + self.landingspotcoord:UpdateFromCoordinate(self:_GetSternCoord()) + + -- Stern coordinate. + --local stern=self:_GetSternCoord() + + if self.carriertype==AIRBOSS.CarrierType.TARAWA then + + -- Landing 100 ft abeam, 120 alt. + local hdg=self:GetHeading() + + -- Primary landing spot 7.5 + self.landingspotcoord:Translate(57, hdg, true, true):SetAltitude(self.carrierparam.deckheight) + elseif self.carriertype==AIRBOSS.CarrierType.AMERICA then + + -- Landing 100 ft abeam, 120 alt. + local hdg=self:GetHeading() + + -- Primary landing spot 7.5 a little further forwad on the America + self.landingspotcoord:Translate(59, hdg, true, true):SetAltitude(self.carrierparam.deckheight) + + elseif self.carriertype==AIRBOSS.CarrierType.JCARLOS then + + -- Landing 100 ft abeam, 120 alt. + local hdg=self:GetHeading() + + -- Primary landing spot 5.0 -- TODO voice for different landing Spots. + self.landingspotcoord:Translate(89, hdg, true, true):SetAltitude(self.carrierparam.deckheight) + + end + + return self.landingspotcoord +end + +--- Get true (or magnetic) heading of carrier. +-- @param #AIRBOSS self +-- @param #boolean magnetic If true, calculate magnetic heading. By default true heading is returned. +-- @return #number Carrier heading in degrees. +function AIRBOSS:GetHeading(magnetic) + self:F3({magnetic=magnetic}) + + -- Carrier heading + local hdg=self.carrier:GetHeading() + + -- Include magnetic declination. + if magnetic then + hdg=hdg-self.magvar + end + + -- Adjust negative values. + if hdg<0 then + hdg=hdg+360 + end + + return hdg +end + +--- Get base recovery course (BRC) of carrier. +-- The is the magnetic heading of the carrier. +-- @param #AIRBOSS self +-- @return #number BRC in degrees. +function AIRBOSS:GetBRC() + return self:GetHeading(true) +end + +--- Get wind direction and speed at carrier position. +-- @param #AIRBOSS self +-- @param #number alt Altitude ASL in meters. Default 50 m. +-- @param #boolean magnetic Direction including magnetic declination. +-- @param Core.Point#COORDINATE coord (Optional) Coordinate at which to get the wind. Default is current carrier position. +-- @return #number Direction the wind is blowing **from** in degrees. +-- @return #number Wind speed in m/s. +function AIRBOSS:GetWind(alt, magnetic, coord) + + -- Current position of the carrier or input. + local cv=coord or self:GetCoordinate() + + -- Wind direction and speed. By default at 50 meters ASL. + local Wdir, Wspeed=cv:GetWind(alt or 50) + + -- Include magnetic declination. + if magnetic then + Wdir=Wdir-self.magvar + -- Adjust negative values. + if Wdir<0 then + Wdir=Wdir+360 + end + end + + return Wdir, Wspeed +end + +--- Get wind speed on carrier deck parallel and perpendicular to runway. +-- @param #AIRBOSS self +-- @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. +function AIRBOSS:GetWindOnDeck(alt) + + -- Position of carrier. + local cv=self:GetCoordinate() + + -- Velocity vector of carrier. + local vc=self.carrier:GetVelocityVec3() + + -- Carrier orientation X. + local xc=self.carrier:GetOrientationX() + + -- Carrier orientation Z. + local zc=self.carrier:GetOrientationZ() + + -- Rotate back so that angled deck points to wind. + xc=UTILS.Rotate2D(xc, -self.carrierparam.rwyangle) + zc=UTILS.Rotate2D(zc, -self.carrierparam.rwyangle) + + -- Wind (from) vector + 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. + local vT=UTILS.VecSubstract(vw, vc) + + -- || Parallel component. + local vpa=UTILS.VecDot(vT,xc) + + -- == Perpendicular component. + local vpp=UTILS.VecDot(vT,zc) + + -- Strength. + local vabs=UTILS.VecNorm(vT) + + -- We return positive values as head wind and negative values as tail wind. + --TODO: Check minus sign. + return -vpa, vpp, vabs +end + + +--- Get true (or magnetic) heading of carrier into the wind. This accounts for the angled runway. +-- @param #AIRBOSS self +-- @param #boolean magnetic If true, calculate magnetic heading. By default true heading is returned. +-- @param Core.Point#COORDINATE coord (Optional) Coodinate from which heading is calculated. Default is current carrier position. +-- @return #number Carrier heading in degrees. +function AIRBOSS:GetHeadingIntoWind(magnetic, coord) + + -- Get direction the wind is blowing from. This is where we want to go. + local windfrom, vwind=self:GetWind(nil, nil, coord) + + -- Actually, we want the runway in the wind. + local intowind=windfrom-self.carrierparam.rwyangle + + -- If no wind, take current heading. + if vwind<0.1 then + intowind=self:GetHeading() + end + + -- Magnetic heading. + if magnetic then + intowind=intowind-self.magvar + end + + -- Adjust negative values. + if intowind<0 then + intowind=intowind+360 + end + + return intowind +end + +--- Get base recovery course (BRC) when the carrier would head into the wind. +-- This includes the current wind direction and accounts for the angled runway. +-- @param #AIRBOSS self +-- @return #number BRC into the wind in degrees. +function AIRBOSS:GetBRCintoWind() + -- BRC is the magnetic heading. + return self:GetHeadingIntoWind(true) +end + + +--- Get final bearing (FB) of carrier. +-- By default, the routine returns the magnetic FB depending on the current map (Caucasus, NTTR, Normandy, Persion Gulf etc). +-- The true bearing can be obtained by setting the *TrueNorth* parameter to true. +-- @param #AIRBOSS self +-- @param #boolean magnetic If true, magnetic FB is returned. +-- @return #number FB in degrees. +function AIRBOSS:GetFinalBearing(magnetic) + + -- First get the heading. + local fb=self:GetHeading(magnetic) + + -- Final baring = BRC including angled deck. + fb=fb+self.carrierparam.rwyangle + + -- Adjust negative values. + if fb<0 then + fb=fb+360 + end + + return fb +end + +--- Get radial with respect to carrier BRC or FB and (optionally) holding offset. +-- +-- * case=1: radial=FB-180 +-- * case=2: radial=HDG-180 (+offset) +-- * case=3: radial=FB-180 (+offset) +-- +-- @param #AIRBOSS self +-- @param #number case Recovery case. +-- @param #boolean magnetic If true, magnetic radial is returned. Default is true radial. +-- @param #boolean offset If true, inlcude holding offset. +-- @param #boolean inverse Return inverse, i.e. radial-180 degrees. +-- @return #number Radial in degrees. +function AIRBOSS:GetRadial(case, magnetic, offset, inverse) + + -- Case or current case. + case=case or self.case + + -- Radial. + local radial + + -- Select case. + if case==1 then + + -- Get radial. + radial=self:GetFinalBearing(magnetic)-180 + + elseif case==2 then + + -- Radial wrt to heading of carrier. + radial=self:GetHeading(magnetic)-180 + + -- Holding offset angle (+-15 or 30 degrees usually) + if offset then + radial=radial+self.holdingoffset + end + + elseif case==3 then + + -- Radial wrt angled runway. + radial=self:GetFinalBearing(magnetic)-180 + + -- Holding offset angle (+-15 or 30 degrees usually) + if offset then + radial=radial+self.holdingoffset + end + + end + + -- Adjust for negative values. + if radial<0 then + radial=radial+360 + end + + -- Inverse? + if inverse then + + -- Inverse radial + radial=radial-180 + + -- Adjust for negative values. + if radial<0 then + radial=radial+360 + end + + end + + return radial +end + +--- Get difference between to headings in degrees taking into accound the [0,360) periodocity. +-- @param #AIRBOSS self +-- @param #number hdg1 Heading one. +-- @param #number hdg2 Heading two. +-- @return #number Difference between the two headings in degrees. +function AIRBOSS:_GetDeltaHeading(hdg1, hdg2) + + local V={} --DCS#Vec3 + V.x=math.cos(math.rad(hdg1)) + V.y=0 + V.z=math.sin(math.rad(hdg1)) + + local W={} --DCS#Vec3 + W.x=math.cos(math.rad(hdg2)) + W.y=0 + W.z=math.sin(math.rad(hdg2)) + + local alpha=UTILS.VecAngle(V,W) + + return alpha +end + +--- Get relative heading of player wrt carrier. +-- This is the angle between the direction/orientation vector of the carrier and the direction/orientation vector of the provided unit. +-- Note that this is calculated in the X-Z plane, i.e. the altitude Y is not taken into account. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Player unit. +-- @param #boolean runway (Optional) If true, return relative heading of unit wrt to angled runway of the carrier. +-- @return #number Relative heading in degrees. An angle of 0 means, unit fly parallel to carrier. An angle of + or - 90 degrees means, unit flies perpendicular to carrier. +function AIRBOSS:_GetRelativeHeading(unit, runway) + + -- Direction vector of the carrier. + local vC=self.carrier:GetOrientationX() + + -- Include runway angle. + if runway then + vC=UTILS.Rotate2D(vC, -self.carrierparam.rwyangle) + end + + -- Direction vector of the unit. + local vP=unit:GetOrientationX() + + -- We only want the X-Z plane. Aircraft could fly parallel but ballistic and we dont want the "pitch" angle. + vC.y=0 ; vP.y=0 + + -- Get angle between the two orientation vectors in degrees. + local rhdg=UTILS.VecAngle(vC,vP) + + -- Return heading in degrees. + return rhdg +end + +--- Get relative velocity of player unit wrt to carrier +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Player unit. +-- @return #number Relative velocity in m/s. +function AIRBOSS:_GetRelativeVelocity(unit) + + local vC=self.carrier:GetVelocityVec3() + local vP=unit:GetVelocityVec3() + + -- Only X-Z plane is necessary here. + vC.y=0 ; vP.y=0 + + local v=UTILS.VecSubstract(vP, vC) + + return UTILS.VecNorm(v),v +end + + +--- Calculate distances between carrier and aircraft unit. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Aircraft unit. +-- @return #number Distance [m] in the direction of the orientation of the carrier. +-- @return #number Distance [m] perpendicular to the orientation of the carrier. +-- @return #number Distance [m] to the carrier. +-- @return #number Angle [Deg] from carrier to plane. Phi=0 if the plane is directly behind the carrier, phi=90 if the plane is starboard, phi=180 if the plane is in front of the carrier. +function AIRBOSS:_GetDistances(unit) + + -- Vector to carrier + local a=self.carrier:GetVec3() + + -- Vector to player + local b=unit:GetVec3() + + -- Vector from carrier to player. + local c={x=b.x-a.x, y=0, z=b.z-a.z} + + -- Orientation of carrier. + local x=self.carrier:GetOrientationX() + + -- Projection of player pos on x component. + local dx=UTILS.VecDot(x,c) + + -- Orientation of carrier. + local z=self.carrier:GetOrientationZ() + + -- Projection of player pos on z component. + local dz=UTILS.VecDot(z,c) + + -- Polar coordinates. + local rho=math.sqrt(dx*dx+dz*dz) + + + -- Not exactly sure any more what I wanted to calculate here. + local phi=math.deg(math.atan2(dz,dx)) + + -- Correct for negative values. + if phi<0 then + phi=phi+360 + end + + return dx,dz,rho,phi +end + +--- Check limits for reaching next step. +-- @param #AIRBOSS self +-- @param #number X X position of player unit. +-- @param #number Z Z position of player unit. +-- @param #AIRBOSS.Checkpoint check Checkpoint. +-- @return #boolean If true, checkpoint condition for next step was reached. +function AIRBOSS:_CheckLimits(X, Z, check) + + -- Limits + local nextXmin=check.LimitXmin==nil or (check.LimitXmin and (check.LimitXmin<0 and X<=check.LimitXmin or check.LimitXmin>=0 and X>=check.LimitXmin)) + local nextXmax=check.LimitXmax==nil or (check.LimitXmax and (check.LimitXmax<0 and X>=check.LimitXmax or check.LimitXmax>=0 and X<=check.LimitXmax)) + local nextZmin=check.LimitZmin==nil or (check.LimitZmin and (check.LimitZmin<0 and Z<=check.LimitZmin or check.LimitZmin>=0 and Z>=check.LimitZmin)) + local nextZmax=check.LimitZmax==nil or (check.LimitZmax and (check.LimitZmax<0 and Z>=check.LimitZmax or check.LimitZmax>=0 and Z<=check.LimitZmax)) + + -- Proceed to next step if all conditions are fullfilled. + local next=nextXmin and nextXmax and nextZmin and nextZmax + + -- Debug info. + local text=string.format("step=%s: next=%s: X=%d Xmin=%s Xmax=%s | Z=%d Zmin=%s Zmax=%s", + check.name, tostring(next), X, tostring(check.LimitXmin), tostring(check.LimitXmax), Z, tostring(check.LimitZmin), tostring(check.LimitZmax)) + self:T3(self.lid..text) + + return next +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- LSO functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- LSO advice radio call. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #number glideslopeError Error in degrees. +-- @param #number lineupError Error in degrees. +function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) + + -- Advice time. + local advice=0 + + -- Glideslope high/low calls. + if glideslopeError>self.gle.HIGH then --1.5 then + -- "You're high!" + self:RadioTransmission(self.LSORadio, self.LSOCall.HIGH, true, nil, nil, true) + advice=advice+self.LSOCall.HIGH.duration + elseif glideslopeError>self.gle.High then --0.8 then + -- "You're high." + self:RadioTransmission(self.LSORadio, self.LSOCall.HIGH, false, nil, nil, true) + advice=advice+self.LSOCall.HIGH.duration + elseif glideslopeErrorself.lue.RIGHT then --3 then + -- "Right for lineup!" + self:RadioTransmission(self.LSORadio, self.LSOCall.RIGHTFORLINEUP, true, nil, nil, true) + advice=advice+self.LSOCall.RIGHTFORLINEUP.duration + elseif lineupError>self.lue.Right then -- 1 then + -- "Right for lineup." + self:RadioTransmission(self.LSORadio, self.LSOCall.RIGHTFORLINEUP, false, nil, nil, true) + advice=advice+self.LSOCall.RIGHTFORLINEUP.duration + else + -- "Good lineup." + end + + -- Get current AoA. + local AOA=playerData.unit:GetAoA() + + -- Get aircraft AoA parameters. + local acaoa=self:_GetAircraftAoA(playerData) + + -- Speed via AoA - not for the Harrier. + if playerData.actype~=AIRBOSS.AircraftCarrier.AV8B then + if AOA>acaoa.SLOW then + -- "Your're slow!" + self:RadioTransmission(self.LSORadio, self.LSOCall.SLOW, true, nil, nil, true) + advice=advice+self.LSOCall.SLOW.duration + --S=underline("SLO") + elseif AOA>acaoa.Slow then + -- "Your're slow." + self:RadioTransmission(self.LSORadio, self.LSOCall.SLOW, false, nil, nil, true) + advice=advice+self.LSOCall.SLOW.duration + --S="SLO" + elseif AOA>acaoa.OnSpeedMax then + -- No call. + --S=little("SLO") + elseif AOA 24 seconds: No Grade "--" +-- +-- If you manage to be between 16.4 and and 16.6 seconds, you will even get and okay underline "\_OK\_". +-- No groove time for Harrier on LHA, LHD set to Tgroove Unicorn as starting point to allow possible _OK_ 5.0. +-- If time in the AV-8B +-- +-- * < 90 seconds: OK V/STOL +-- * > 91 Seconds: SLOW V/STOL (Early hover stop selection) +-- +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @return #string LSO grade for time in groove, i.e. \_OK\_, OK, (OK), --. +function AIRBOSS:_EvalGrooveTime(playerData) + + -- Time in groove. + local t=playerData.Tgroove + + local grade="" + if t<9 then + grade="_NESA_" + elseif t<15 then + grade="NESA" + elseif t<19 then + grade="OK Groove" + elseif t<=24 then + grade="(LIG)" + -- Time in groove for AV-8B + elseif playerData.actype==AIRBOSS.AircraftCarrier.AV8B and t<55 then -- VSTOL Late Hover stop selection too fast to Abeam LDG Spot AV-8B. + grade="FAST V/STOL Groove" + elseif playerData.actype==AIRBOSS.AircraftCarrier.AV8B and t<90 then -- VSTOL Operations with AV-8B. + grade="OK V/STOL Groove" + elseif playerData.actype==AIRBOSS.AircraftCarrier.AV8B and t>=91 then -- VSTOL Early Hover stop selection slow to Abeam LDG Spot AV-8B. + grade="SLOW V/STOL Groove" + else + grade="LIG" + end + + -- The unicorn! + if t>=16.4 and t<=16.6 then + grade="_OK_" + end + + -- V/STOL Unicorn! + if playerData.actype==AIRBOSS.AircraftCarrier.AV8B and (t>=65.0 and t<=75.0) then + grade="_OK_ V/STOL" + end + + return grade +end + +--- Grade approach. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @return #string LSO grade, i.g. _OK_, OK, (OK), --, etc. +-- @return #number Points. +-- @return #string LSO analysis of flight path. +function AIRBOSS:_LSOgrade(playerData) + + --- Count deviations. + local function count(base, pattern) + return select(2, string.gsub(base, pattern, "")) + end + + -- Analyse flight data and convert to LSO text. + local GXX,nXX=self:_Flightdata2Text(playerData, AIRBOSS.GroovePos.XX) + local GIM,nIM=self:_Flightdata2Text(playerData, AIRBOSS.GroovePos.IM) + local GIC,nIC=self:_Flightdata2Text(playerData, AIRBOSS.GroovePos.IC) + local GAR,nAR=self:_Flightdata2Text(playerData, AIRBOSS.GroovePos.AR) + + -- Put everything together. + local G=GXX.." "..GIM.." ".." "..GIC.." "..GAR + + -- Count number of minor, normal and major deviations. TODO - work on Harrier counts due slower approach speed. + local N=nXX+nIM+nIC+nAR + local nL=count(G, '_')/2 + local nS=count(G, '%(') + local nN=N-nS-nL + + -- Groove time 15-18.99 sec for a unicorn. Or 65-70 for V/STOL unicorn. + local Tgroove=playerData.Tgroove + local TgrooveUnicorn=Tgroove and (Tgroove>=15.0 and Tgroove<=18.99) or false + local TgrooveVstolUnicorn=Tgroove and (Tgroove>=65.0 and Tgroove<=70.0)and playerData.actype==AIRBOSS.AircraftCarrier.AV8B or false + + local grade + local points + if N==0 and (TgrooveUnicorn or TgrooveVstolUnicorn ) then + -- No deviations, should be REALLY RARE! + grade="_OK_" + points=5.0 + G="Unicorn" + else + + -- Add AV-8B Harrier devation allowances due to lower groundspeed and 3x conventional groove time, this allows to maintain LSO tolerances while respecting the deviations are not unsafe. (WIP requires feedback) + -- Large devaitions still result in a No Grade, A Unicorn still requires a clean pass with no deviation. + if nL>3 and playerData.actype==AIRBOSS.AircraftCarrier.AV8B then + -- Larger deviations ==> "No grade" 2.0 points. + grade="--" + points=2.0 + elseif nN>2 and playerData.actype==AIRBOSS.AircraftCarrier.AV8B then + -- Only average deviations ==> "Fair Pass" Pass with average deviations and corrections. + grade="(OK)" + points=3.0 + elseif nL>0 then + -- Larger deviations ==> "No grade" 2.0 points. + grade="--" + points=2.0 + elseif nN>0 then + -- No larger but average deviations ==> "Fair Pass" Pass with average deviations and corrections. + grade="(OK)" + points=3.0 + else + -- Only minor corrections + grade="OK" + points=4.0 + end + +end + + -- Replace" )"( and "__" + G=G:gsub("%)%(", "") + G=G:gsub("__","") + + -- Debug info + local text="LSO grade:\n" + text=text..G.."\n" + text=text.."Grade = "..grade.." points = "..points.."\n" + text=text.."# of total deviations = "..N.."\n" + text=text.."# of large deviations _ = "..nL.."\n" + text=text.."# of normal deviations = "..nN.."\n" + text=text.."# of small deviations ( = "..nS.."\n" + self:T2(self.lid..text) + + -- Special cases. + if playerData.wop then + --------------------- + -- Pattern Waveoff -- + --------------------- + if playerData.lig then + -- Long In the Groove (LIG). + -- According to Stingers this is a CUT pass and gives 1.0 points. + grade="WO" + points=1.0 + G="LIG" + else + -- Other pattern WO + grade="WOP" + points=2.0 + G="n/a" + end + elseif playerData.wofd then + ----------------------- + -- Foul Deck Waveoff -- + ----------------------- + if playerData.landed then + --AIRBOSS wants to talk to you! + grade="CUT" + points=0.0 + else + grade="WOFD" + points=-1.0 + end + G="n/a" + elseif playerData.owo then + ----------------- + -- Own Waveoff -- + ----------------- + grade="OWO" + points=2.0 + if N==0 then + G="n/a" + end + elseif playerData.waveoff then + ------------- + -- Waveoff -- + ------------- + if playerData.landed then + --AIRBOSS wants to talk to you! + grade="CUT" + points=0.0 + else + grade="WO" + points=1.0 + end + elseif playerData.boltered then + -- Bolter + grade="-- (BOLTER)" + points=2.5 + end + + return grade, points, G +end + +--- Grade flight data. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #string groovestep Step in the groove. +-- @param #AIRBOSS.GrooveData fdata Flight data in the groove. +-- @return #string LSO grade or empty string if flight data table is nil. +-- @return #number Number of deviations from perfect flight path. +function AIRBOSS:_Flightdata2Text(playerData, groovestep) + + local function little(text) + return string.format("(%s)",text) + end + local function underline(text) + return string.format("_%s_", text) + end + + -- Groove Data. + local fdata=playerData.groove[groovestep] --#AIRBOSS.GrooveData + + -- No flight data ==> return empty string. + if fdata==nil then + self:T3(self.lid.."Flight data is nil.") + return "", 0 + end + + -- Flight data. + local step=fdata.Step + local AOA=fdata.AoA + local GSE=fdata.GSE + local LUE=fdata.LUE + local ROL=fdata.Roll + + -- Aircraft specific AoA values. + local acaoa=self:_GetAircraftAoA(playerData) + + --Angled Approach. + local P=nil + if step==AIRBOSS.PatternStep.GROOVE_XX and ROL<=4.0 and playerData.case<3 then + if LUE>self.lue.RIGHT then + P=underline("AA") + elseif + LUE>self.lue.RightMed then + P="AA " + elseif + LUE>self.lue.Right then + P=little("AA") + end + end + + --Overshoot Start. + local O=nil + if step==AIRBOSS.PatternStep.GROOVE_XX then + if LUEacaoa.SLOW then + S=underline("SLO") + elseif AOA>acaoa.Slow then + S="SLO" + elseif AOA>acaoa.OnSpeedMax then + S=little("SLO") + elseif AOAself.gle.HIGH then + A=underline("H") + elseif GSE>self.gle.High then + A="H" + elseif GSE>self.gle._max then + A=little("H") + elseif GSEself.lue.RIGHT then + D=underline("LUL") + elseif LUE>self.lue.Right then + D="LUL" + elseif LUE>self.lue._max then + D=little("LUL") + elseif playerData.case<3 then + if LUEpos.Xmax then + self:T(string.format("Xmax: X=%d > %d=Xmax", X, pos.Xmax)) + abort=true + elseif pos.Zmin and Zpos.Zmax then + self:T(string.format("Zmax: Z=%d > %d=Zmax", Z, pos.Zmax)) + abort=true + end + + return abort +end + +--- Generate a text if a player is too far from where he should be. +-- @param #AIRBOSS self +-- @param #number X X distance player to carrier. +-- @param #number Z Z distance player to carrier. +-- @param #AIRBOSS.Checkpoint posData Checkpoint data. +function AIRBOSS:_TooFarOutText(X, Z, posData) + + -- Intro. + local text="you are too " + + -- X text. + local xtext=nil + if posData.Xmin and XposData.Xmax then + if posData.Xmax>=0 then + xtext="far ahead of " + else + xtext="close to " + end + end + + -- Z text. + local ztext=nil + if posData.Zmin and ZposData.Zmax then + if posData.Zmax>=0 then + ztext="far starboard of " + else + ztext="too close to " + end + end + + -- Combine X-Z text. + if xtext and ztext then + text=text..xtext.." and "..ztext + elseif xtext then + text=text..xtext + elseif ztext then + text=text..ztext + end + + -- Complete the sentence + text=text.."the carrier." + + -- If no case could be identified. + if xtext==nil and ztext==nil then + text="you are too far from where you should be!" + end + + return text +end + +--- Pattern aborted. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #number X X distance player to carrier. +-- @param #number Z Z distance player to carrier. +-- @param #AIRBOSS.Checkpoint posData Checkpoint data. +-- @param #boolean patternwo (Optional) Pattern wave off. +function AIRBOSS:_AbortPattern(playerData, X, Z, posData, patternwo) + + -- Text where we are wrong. + local text=self:_TooFarOutText(X, Z, posData) + + -- Debug. + local dtext=string.format("Abort: X=%d Xmin=%s, Xmax=%s | Z=%d Zmin=%s Zmax=%s", X, tostring(posData.Xmin), tostring(posData.Xmax), Z, tostring(posData.Zmin), tostring(posData.Zmax)) + self:T(self.lid..dtext) + + -- Message to player. + self:MessageToPlayer(playerData, text, "LSO") + + if patternwo then + + -- Pattern wave off! + playerData.wop=true + + -- Add to debrief. + self:_AddToDebrief(playerData, string.format("Pattern wave off: %s", text)) + + -- Depart and re-enter radio message. + -- TODO: Radio should depend on player step. + self:RadioTransmission(self.LSORadio, self.LSOCall.DEPARTANDREENTER, false, 3, nil, nil, true) + + -- Next step debrief. + playerData.step=AIRBOSS.PatternStep.DEBRIEF + playerData.warning=nil + end + +end + +--- Display hint to player. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #number delay Delay before playing sound messages. Default 0 sec. +-- @param #boolean soundoff If true, don't play and sound hint. +function AIRBOSS:_PlayerHint(playerData, delay, soundoff) + + -- No hint for the pros. + if not playerData.showhints then + return + end + + -- Get optimal altitude, distance and speed. + local alt, aoa, dist, speed=self:_GetAircraftParameters(playerData) + + -- Get altitude hint. + local hintAlt,debriefAlt,callAlt=self:_AltitudeCheck(playerData, alt) + + -- Get speed hint. + local hintSpeed,debriefSpeed,callSpeed=self:_SpeedCheck(playerData, speed) + + -- Get AoA hint. + local hintAoA,debriefAoA,callAoA=self:_AoACheck(playerData, aoa) + + -- Get distance to the boat hint. + local hintDist,debriefDist,callDist=self:_DistanceCheck(playerData, dist) + + -- Message to player. + local hint="" + if hintAlt and hintAlt~="" then + hint=hint.."\n"..hintAlt + end + if hintSpeed and hintSpeed~="" then + hint=hint.."\n"..hintSpeed + end + if hintAoA and hintAoA~="" then + hint=hint.."\n"..hintAoA + end + if hintDist and hintDist~="" then + hint=hint.."\n"..hintDist + end + + -- Debriefing text. + local debrief="" + if debriefAlt and debriefAlt~="" then + debrief=debrief.."\n- "..debriefAlt + end + if debriefSpeed and debriefSpeed~="" then + debrief=debrief.."\n- "..debriefSpeed + end + if debriefAoA and debriefAoA~="" then + debrief=debrief.."\n- "..debriefAoA + end + if debriefDist and debriefDist~="" then + debrief=debrief.."\n- "..debriefDist + end + + -- Add step to debriefing. + if debrief~="" then + self:_AddToDebrief(playerData, debrief) + end + + -- Voice hint. + delay=delay or 0 + if not soundoff then + if callAlt then + self:Sound2Player(playerData, self.LSORadio, callAlt, false, delay) + delay=delay+callAlt.duration+0.5 + end + if callSpeed then + self:Sound2Player(playerData, self.LSORadio, callSpeed, false, delay) + delay=delay+callSpeed.duration+0.5 + end + if callAoA then + self:Sound2Player(playerData, self.LSORadio, callAoA, false, delay) + delay=delay+callAoA.duration+0.5 + end + if callDist then + self:Sound2Player(playerData, self.LSORadio, callDist, false, delay) + delay=delay+callDist.duration+0.5 + end + end + + -- ARC IN info. + if playerData.step==AIRBOSS.PatternStep.ARCIN then + + -- Hint turn and set TACAN. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + -- Get inverse magnetic radial without offset ==> FB for Case II or BRC for Case III. + local radial=self:GetRadial(playerData.case, true, false, true) + local turn="right" + if self.holdingoffset<0 then + turn="left" + end + hint=hint..string.format("\nTurn %s and select TACAN %03d°.", turn, radial) + end + + end + + -- DIRTUP additonal info. + if playerData.step==AIRBOSS.PatternStep.DIRTYUP then + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then + hint=hint.."\nFAF! Checks completed. Nozzles 50°." + else + --TODO: Tomcat? + hint=hint.."\nDirty up! Hook, gear and flaps down." + end + end + end + + -- BULLSEYE additonal info. + if playerData.step==AIRBOSS.PatternStep.BULLSEYE then + -- Hint follow the needles. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + if playerData.actype==AIRBOSS.AircraftCarrier.HORNET then + hint=hint..string.format("\nIntercept glideslope and follow the needles.") + else + hint=hint..string.format("\nIntercept glideslope.") + end + end + end + + -- Message to player. + if hint~="" then + local text=string.format("%s%s", playerData.step, hint) + self:MessageToPlayer(playerData, hint, "AIRBOSS", "") + end +end + + +--- Display hint for flight students about the (next) step. Message is displayed after one second. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #string step Step for which hint is given. +function AIRBOSS:_StepHint(playerData, step) + + -- Set step. + step=step or playerData.step + + -- Message is only for "Flight Students". + if playerData.difficulty==AIRBOSS.Difficulty.EASY and playerData.showhints then + + -- Get optimal parameters at step. + local alt, aoa, dist, speed=self:_GetAircraftParameters(playerData, step) + + -- Hint: + local hint="" + + -- Altitude. + if alt then + hint=hint..string.format("\nAltitude %d ft", UTILS.MetersToFeet(alt)) + end + + -- AoA. + if aoa then + hint=hint..string.format("\nAoA %.1f", self:_AoADeg2Units(playerData, aoa)) + end + + -- Speed. + if speed then + hint=hint..string.format("\nSpeed %d knots", UTILS.MpsToKnots(speed)) + end + + -- Distance to the boat. + if dist then + hint=hint..string.format("\nDistance to the boat %.1f NM", UTILS.MetersToNM(dist)) + end + + -- Late break. + if step==AIRBOSS.PatternStep.LATEBREAK then + if playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B then + hint=hint.."\nWing Sweep 20°, Gear DOWN < 280 KIAS." + end + end + + -- Abeam. + if step==AIRBOSS.PatternStep.ABEAM then + if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then + hint=hint.."\nNozzles 50°-60°. Antiskid OFF. Lights OFF." + elseif playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B then + hint=hint.."\nSlats/Flaps EXTENDED < 225 KIAS. DLC SELECTED. Auto Throttle IF DESIRED." + else + hint=hint.."\nDirty up! Gear DOWN, flaps DOWN. Check hook down." + end + end + + -- Check if there was actually anything to tell. + if hint~="" then + + -- Compile text if any. + local text=string.format("Optimal setup at next step %s:%s", step, hint) + + -- Send hint to player. + self:MessageToPlayer(playerData, text, "AIRBOSS", "", nil, false, 1) + + end + + end +end + + +--- Evaluate player's altitude at checkpoint. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #number altopt Optimal altitude in meters. +-- @return #string Feedback text. +-- @return #string Debriefing text. +-- @return #AIRBOSS.RadioCall Radio call. +function AIRBOSS:_AltitudeCheck(playerData, altopt) + + if altopt==nil then + return nil, nil + end + + -- Player altitude. + local altitude=playerData.unit:GetAltitude() + + -- Get relative score. + local lowscore, badscore=self:_GetGoodBadScore(playerData) + + -- Altitude error +-X% + local _error=(altitude-altopt)/altopt*100 + + -- Radio call for flight students. + local radiocall=nil --#AIRBOSS.RadioCall + + local hint="" + if _error>badscore then + --hint=string.format("You're high.") + radiocall=self:_NewRadioCall(self.LSOCall.HIGH, "Paddles", "") + elseif _error>lowscore then + --hint= string.format("You're slightly high.") + radiocall=self:_NewRadioCall(self.LSOCall.HIGH, "Paddles", "") + elseif _error<-badscore then + --hint=string.format("You're low. ") + radiocall=self:_NewRadioCall(self.LSOCall.LOW, "Paddles", "") + elseif _error<-lowscore then + --hint=string.format("You're slightly low.") + radiocall=self:_NewRadioCall(self.LSOCall.LOW, "Paddles", "") + else + hint=string.format("Good altitude. ") + end + + -- Extend or decrease depending on skill. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + -- Also inform students about the optimal altitude. + hint=hint..string.format("Optimal altitude is %d ft.", UTILS.MetersToFeet(altopt)) + elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then + -- We keep it short normally. + hint="" + elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then + -- No hint at all for the pros. + hint="" + end + + -- Debrief text. + local debrief=string.format("Altitude %d ft = %d%% deviation from %d ft.", UTILS.MetersToFeet(altitude), _error, UTILS.MetersToFeet(altopt)) + + return hint, debrief,radiocall +end + +--- Score for correct AoA. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #number optaoa Optimal AoA. +-- @return #string Feedback message text or easy and normal difficulty level or nil for hard. +-- @return #string Debriefing text. +-- @return #AIRBOSS.RadioCall Radio call. +function AIRBOSS:_AoACheck(playerData, optaoa) + + if optaoa==nil then + return nil, nil + end + + -- Get relative score. + local lowscore, badscore = self:_GetGoodBadScore(playerData) + + -- Player AoA + local aoa=playerData.unit:GetAoA() + + -- Altitude error +-X% + local _error=(aoa-optaoa)/optaoa*100 + + -- Get aircraft AoA parameters. + local aircraftaoa=self:_GetAircraftAoA(playerData) + + -- Radio call for flight students. + local radiocall=nil --#AIRBOSS.RadioCall + + -- Rate aoa. + local hint="" + if aoa>=aircraftaoa.SLOW then + --hint="Your're slow!" + radiocall=self:_NewRadioCall(self.LSOCall.SLOW, "Paddles", "") + elseif aoa>=aircraftaoa.Slow then + --hint="Your're slow." + radiocall=self:_NewRadioCall(self.LSOCall.SLOW, "Paddles", "") + elseif aoa>=aircraftaoa.OnSpeedMax then + hint="Your're a little slow. " + elseif aoa>=aircraftaoa.OnSpeedMin then + hint="You're on speed. " + elseif aoa>=aircraftaoa.Fast then + hint="You're a little fast. " + elseif aoa>=aircraftaoa.FAST then + --hint="Your're fast." + radiocall=self:_NewRadioCall(self.LSOCall.FAST, "Paddles", "") + else + --hint="You're fast!" + radiocall=self:_NewRadioCall(self.LSOCall.FAST, "Paddles", "") + end + + -- Extend or decrease depending on skill. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + -- Also inform students about optimal value. + hint=hint..string.format("Optimal AoA is %.1f.", self:_AoADeg2Units(playerData, optaoa)) + elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then + -- We keep is short normally. + hint="" + elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then + -- No hint at all for the pros. + hint="" + end + + -- Debriefing text. + local debrief=string.format("AoA %.1f = %d%% deviation from %.1f.", self:_AoADeg2Units(playerData, aoa), _error, self:_AoADeg2Units(playerData, optaoa)) + + return hint, debrief,radiocall +end + +--- Evaluate player's speed. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #number speedopt Optimal speed in m/s. +-- @return #string Feedback text. +-- @return #string Debriefing text. +-- @return #AIRBOSS.RadioCall Radio call. +function AIRBOSS:_SpeedCheck(playerData, speedopt) + + if speedopt==nil then + return nil, nil + end + + -- Player altitude. + local speed=playerData.unit:GetVelocityMPS() + + -- Get relative score. + local lowscore, badscore=self:_GetGoodBadScore(playerData) + + -- Altitude error +-X% + local _error=(speed-speedopt)/speedopt*100 + + -- Radio call for flight students. + local radiocall=nil --#AIRBOSS.RadioCall + + local hint="" + if _error>badscore then + --hint=string.format("You're fast.") + radiocall=self:_NewRadioCall(self.LSOCall.FAST, "AIRBOSS", "") + elseif _error>lowscore then + --hint= string.format("You're slightly fast.") + radiocall=self:_NewRadioCall(self.LSOCall.FAST, "AIRBOSS", "") + elseif _error<-badscore then + --hint=string.format("You're slow.") + radiocall=self:_NewRadioCall(self.LSOCall.SLOW, "AIRBOSS", "") + elseif _error<-lowscore then + --hint=string.format("You're slightly slow.") + radiocall=self:_NewRadioCall(self.LSOCall.SLOW, "AIRBOSS", "") + else + hint=string.format("Good speed. ") + end + + -- Extend or decrease depending on skill. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + hint=hint..string.format("Optimal speed is %d knots.", UTILS.MpsToKnots(speedopt)) + elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then + -- We keep is short normally. + hint="" + elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then + -- No hint at all for pros. + hint="" + end + + -- Debrief text. + local debrief=string.format("Speed %d knots = %d%% deviation from %d knots.", UTILS.MpsToKnots(speed), _error, UTILS.MpsToKnots(speedopt)) + + return hint, debrief, radiocall +end + +--- Evaluate player's distance to the boat at checkpoint. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #number optdist Optimal distance in meters. +-- @return #string Feedback message text. +-- @return #string Debriefing text. +-- @return #AIRBOSS.RadioCall Distance radio call. Not implemented yet. +function AIRBOSS:_DistanceCheck(playerData, optdist) + + if optdist==nil then + return nil, nil + end + + -- Distance to carrier. + local distance=playerData.unit:GetCoordinate():Get2DDistance(self:GetCoordinate()) + + -- Get relative score. + local lowscore, badscore = self:_GetGoodBadScore(playerData) + + -- Altitude error +-X% + local _error=(distance-optdist)/optdist*100 + + local hint + if _error>badscore then + hint=string.format("You're too far from the boat!") + elseif _error>lowscore then + hint=string.format("You're slightly too far from the boat.") + elseif _error<-badscore then + hint=string.format( "You're too close to the boat!") + elseif _error<-lowscore then + hint=string.format("You're slightly too far from the boat.") + else + hint=string.format("Good distance to the boat.") + end + + -- Extend or decrease depending on skill. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + -- Also inform students about optimal value. + hint=hint..string.format(" Optimal distance is %.1f NM.", UTILS.MetersToNM(optdist)) + elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then + -- We keep it short normally. + hint="" + elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then + -- No hint at all for the pros. + hint="" + end + + -- Debriefing text. + local debrief=string.format("Distance %.1f NM = %d%% deviation from %.1f NM.",UTILS.MetersToNM(distance), _error, UTILS.MetersToNM(optdist)) + + return hint, debrief, nil +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- DEBRIEFING +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Append text to debriefing. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #string hint Debrief text of this step. +-- @param #string step (Optional) Current step in the pattern. Default from playerData. +function AIRBOSS:_AddToDebrief(playerData, hint, step) + step=step or playerData.step + table.insert(playerData.debrief, {step=step, hint=hint}) +end + +--- Debrief player and set next step. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +function AIRBOSS:_Debrief(playerData) + self:F(self.lid..string.format("Debriefing of player %s.", playerData.name)) + + -- Delete scheduler ID. + playerData.debriefschedulerID=nil + + -- Switch attitude monitor off if on. + playerData.attitudemonitor=false + + -- LSO grade, points, and flight data analyis. + local grade, points, analysis=self:_LSOgrade(playerData) + + -- Insert points to table of all points until player landed. + if points and points>=0 then + table.insert(playerData.points, points) + end + + -- Player has landed and is not airborne any more. + local Points=0 + if playerData.landed and not playerData.unit:InAir() then + + -- Average over all points received so far. + for _,_points in pairs(playerData.points) do + Points=Points+_points + end + + -- This is the final points. + Points=Points/#playerData.points + + -- Reset points array. + playerData.points={} + else + -- Player boltered or was waved off ==> We display the normal points. + Points=points + end + + -- My LSO grade. + local mygrade={} --#AIRBOSS.LSOgrade + mygrade.grade=grade + mygrade.points=points + mygrade.details=analysis + mygrade.wire=playerData.wire + mygrade.Tgroove=playerData.Tgroove + if playerData.landed and not playerData.unit:InAir() then + mygrade.finalscore=Points + end + mygrade.case=playerData.case + local windondeck=self:GetWindOnDeck() + mygrade.wind=tostring(UTILS.Round(UTILS.MpsToKnots(windondeck), 1)) + mygrade.modex=playerData.onboard + mygrade.airframe=playerData.actype + mygrade.carriertype=self.carriertype + mygrade.carriername=self.alias + mygrade.theatre=self.theatre + mygrade.mitime=UTILS.SecondsToClock(timer.getAbsTime()) + mygrade.midate=UTILS.GetDCSMissionDate() + mygrade.osdate="n/a" + if os then + mygrade.osdate=os.date() --os.date("%d.%m.%Y") + end + + -- Save trap sheet. + if playerData.trapon and self.trapsheet then + self:_SaveTrapSheet(playerData, mygrade) + end + + -- Add LSO grade to player grades table. + table.insert(self.playerscores[playerData.name], mygrade) + + -- Trigger grading event. + self:LSOGrade(playerData, mygrade) + + -- LSO grade: (OK) 3.0 PT - LURIM + local text=string.format("%s %.1f PT - %s", grade, Points, analysis) + if Points==-1 then + text=string.format("%s n/a PT - Foul deck", grade, Points, analysis) + end + + -- Wire and Groove time only if not pattern WO. + if not (playerData.wop or playerData.wofd) then + + -- Wire trapped. Not if pattern WI. + if playerData.wire and playerData.wire<=4 then + text=text..string.format(" %d-wire", playerData.wire) + end + + -- Time in the groove. Only Case I/II and not pattern WO. + if playerData.Tgroove and playerData.Tgroove<=360 and playerData.case<3 then + text=text..string.format("\nTime in the groove %.1f seconds: %s", playerData.Tgroove, self:_EvalGrooveTime(playerData)) + end + + end + + -- Copy debriefing text. + playerData.lastdebrief=UTILS.DeepCopy(playerData.debrief) + + -- Info text. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + text=text..string.format("\nYour detailed debriefing can be found via the F10 radio menu.") + end + + -- Message. + self:MessageToPlayer(playerData, text, "LSO", "", 30, true) + + + -- Set step to undefined and check if other cases apply. + playerData.step=AIRBOSS.PatternStep.UNDEFINED + + + -- Check what happened? + if playerData.wop then + + ---------------------- + -- Pattern Wave Off -- + ---------------------- + + -- Next step? + -- TODO: CASE I: After bolter/wo turn left and climb to 600 ft and re-enter the pattern. But do not go to initial but reenter earlier? + -- TODO: CASE I: After pattern wo? go back to initial, I guess? + -- TODO: CASE III: After bolter/wo turn left and climb to 1200 ft and re-enter pattern? + -- TODO: CASE III: After pattern wo? No idea... + + -- Can become nil when I crashed and changed to observer. Which events are captured? Nil check for unit? + if playerData.unit:IsAlive() then + + -- Heading and distance tip. + local heading, distance + + if playerData.case==1 or playerData.case==2 then + + -- Next step: Initial again. + playerData.step=AIRBOSS.PatternStep.INITIAL + + -- Create a point 3.0 NM astern for re-entry. + local initial=self:GetCoordinate():Translate(UTILS.NMToMeters(3.5), self:GetRadial(2, false, false, false)) + + -- Get heading and distance to initial zone ~3 NM astern. + heading=playerData.unit:GetCoordinate():HeadingTo(initial) + distance=playerData.unit:GetCoordinate():Get2DDistance(initial) + + elseif playerData.case==3 then + + -- Next step? Bullseye for now. + -- TODO: Could be DIRTY UP or PLATFORM or even back to MARSHAL STACK? + playerData.step=AIRBOSS.PatternStep.BULLSEYE + + -- Get heading and distance to bullseye zone ~3 NM astern. + local zone=self:_GetZoneBullseye(playerData.case) + + heading=playerData.unit:GetCoordinate():HeadingTo(zone:GetCoordinate()) + distance=playerData.unit:GetCoordinate():Get2DDistance(zone:GetCoordinate()) + + end + + -- Re-enter message. + local text=string.format("fly heading %03d° for %d NM to re-enter the pattern.", heading, UTILS.MetersToNM(distance)) + self:MessageToPlayer(playerData, text, "LSO", nil, nil, false, 5) + + else + + -- Unit does not seem to be alive! + -- TODO: What now? + self:E(self.lid..string.format("ERROR: Player unit not alive!")) + + end + + elseif playerData.wofd then + + --------------- + -- Foul Deck -- + --------------- + + if playerData.unit:InAir() then + + -- Bolter pattern. Then Abeam or bullseye. + playerData.step=AIRBOSS.PatternStep.BOLTER + + else + + -- Welcome aboard! + self:Sound2Player(playerData, self.LSORadio, self.LSOCall.WELCOMEABOARD) + + -- Airboss talkto! + local text=string.format("deck was fouled but you landed anyway. Airboss wants to talk to you!") + self:MessageToPlayer(playerData, text, "LSO", nil, nil, false, 3) + + end + + elseif playerData.owo then + + ------------------ + -- Own Wave Off -- + ------------------ + + if playerData.unit:InAir() then + + -- Bolter pattern. Then Abeam or bullseye. + playerData.step=AIRBOSS.PatternStep.BOLTER + + else + + -- Welcome aboard! + -- NOTE: This should not happen as owo is only triggered if player flew past the carrier. + self:E(self.lid.."ERROR: player landed when OWO was issues. This should not happen. Please report!") + self:Sound2Player(playerData, self.LSORadio, self.LSOCall.WELCOMEABOARD) + + end + + + elseif playerData.waveoff then + + -------------- + -- Wave Off -- + -------------- + + if playerData.unit:InAir() then + + -- Bolter pattern. Then Abeam or bullseye. + playerData.step=AIRBOSS.PatternStep.BOLTER + + else + + -- Welcome aboard! + self:Sound2Player(playerData, self.LSORadio, self.LSOCall.WELCOMEABOARD) + + -- Airboss talkto! + local text=string.format("you were waved off but landed anyway. Airboss wants to talk to you!") + self:MessageToPlayer(playerData, text, "LSO", nil, nil, false, 3) + + end + + elseif playerData.boltered then + + -------------- + -- Boltered -- + -------------- + + if playerData.unit:InAir() then + + -- Bolter pattern. Then Abeam or bullseye. + playerData.step=AIRBOSS.PatternStep.BOLTER + + end + + elseif playerData.landed then + + ------------ + -- Landed -- + ------------ + + if not playerData.unit:InAir() then + + -- Welcome aboard! + self:Sound2Player(playerData, self.LSORadio, self.LSOCall.WELCOMEABOARD) + + end + + else + + -- Message to player. + self:MessageToPlayer(playerData, "Undefined state after landing! Please report.", "ERROR", nil, 20) + + -- Next step. + playerData.step=AIRBOSS.PatternStep.UNDEFINED + + end + + -- Player landed and is not in air anymore. + if playerData.landed and not playerData.unit:InAir() then + -- Set recovered flag. + self:_RecoveredElement(playerData.unit) + + -- Check if all elements + self:_CheckSectionRecovered(playerData) + end + + -- Increase number of passes. + playerData.passes=playerData.passes+1 + + -- Next step hint for students if any. + self:_StepHint(playerData) + + -- Reinitialize player data for new approach. + self:_InitPlayer(playerData, playerData.step) + + -- Debug message. + MESSAGE:New(string.format("Player step %s.", playerData.step), 5, "DEBUG"):ToAllIf(self.Debug) + + -- Auto save player results. + if self.autosave and mygrade.finalscore then + self:Save(self.autosavepath, self.autosavefile) + end +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- CARRIER ROUTING Functions +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check for possible collisions between two coordinates. +-- @param #AIRBOSS self +-- @param Core.Point#COORDINATE coordto Coordinate to which the collision is check. +-- @param Core.Point#COORDINATE coordfrom Coordinate from which the collision is check. +-- @return #boolean If true, surface type ahead is not deep water. +-- @return #number Max free distance in meters. +function AIRBOSS:_CheckCollisionCoord(coordto, coordfrom) + + -- Increment in meters. + local dx=100 + + -- From coordinate. Default 500 in front of the carrier. + local d=0 + if coordfrom then + d=0 + else + d=250 + coordfrom=self:GetCoordinate():Translate(d, self:GetHeading()) + end + + -- Distance between the two coordinates. + local dmax=coordfrom:Get2DDistance(coordto) + + -- Direction. + local direction=coordfrom:HeadingTo(coordto) + + -- Scan path between the two coordinates. + local clear=true + while d<=dmax do + + -- Check point. + local cp=coordfrom:Translate(d, direction) + + -- Check if surface type is water. + if not cp:IsSurfaceTypeWater() then + + -- Debug mark points. + if self.Debug then + local st=cp:GetSurfaceType() + cp:MarkToAll(string.format("Collision check surface type %d", st)) + end + + -- Collision WARNING! + clear=false + break + end + + -- Increase distance. + d=d+dx + end + + local text="" + if clear then + text=string.format("Path into direction %03d° is clear for the next %.1f NM.", direction, UTILS.MetersToNM(d)) + else + text=string.format("Detected obstacle at distance %.1f NM into direction %03d°.", UTILS.MetersToNM(d), direction) + end + self:T2(self.lid..text) + + return not clear, d +end + + +--- Check Collision. +-- @param #AIRBOSS self +-- @param Core.Point#COORDINATE fromcoord Coordinate from which the path to the next WP is calculated. Default current carrier position. +-- @return #boolean If true, surface type ahead is not deep water. +function AIRBOSS:_CheckFreePathToNextWP(fromcoord) + + -- Position. + fromcoord=fromcoord or self:GetCoordinate():Translate(250, self:GetHeading()) + + -- Next wp = current+1 (or last) + local Nnextwp=math.min(self.currentwp+1, #self.waypoints) + + -- Next waypoint. + local nextwp=self.waypoints[Nnextwp] --Core.Point#COORDINATE + + -- Check for collision. + local collision=self:_CheckCollisionCoord(nextwp, fromcoord) + + return collision +end + +--- Find free path to the next waypoint. +-- @param #AIRBOSS self +function AIRBOSS:_Pathfinder() + + -- Heading and current coordiante. + local hdg=self:GetHeading() + local cv=self:GetCoordinate() + + -- Possible directions. + local directions={-20, 20, -30, 30, -40, 40, -50, 50, -60, 60, -70, 70, -80, 80, -90, 90, -100, 100} + + -- Starboard turns up to 90 degrees. + for _,_direction in pairs(directions) do + + -- New direction. + local direction=hdg+_direction + + -- Check for collisions in the next 20 NM of the current direction. + local _, dfree=self:_CheckCollisionCoord(cv:Translate(UTILS.NMToMeters(20), direction), cv) + + -- Loop over distances and find the first one which gives a clear path to the next waypoint. + local distance=500 + while distance<=dfree do + + -- Coordinate from which we calculate the path. + local fromcoord=cv:Translate(distance, direction) + + -- Check for collision between point and next waypoint. + local collision=self:_CheckFreePathToNextWP(fromcoord) + + -- Debug info. + self:T2(self.lid..string.format("Pathfinder d=%.1f m, direction=%03d°, collision=%s", distance, direction, tostring(collision))) + + -- If path is clear, we start a little detour. + if not collision then + self:CarrierDetour(fromcoord) + return + end + + distance=distance+500 + end + end +end + + +--- Carrier resumes the route at its next waypoint. +--@param #AIRBOSS self +--@param Core.Point#COORDINATE gotocoord (Optional) First goto this coordinate before resuming route. +--@return #AIRBOSS self +function AIRBOSS:CarrierResumeRoute(gotocoord) + + -- Make carrier resume its route. + AIRBOSS._ResumeRoute(self.carrier:GetGroup(), self, gotocoord) + + return self +end + + +--- Let the carrier make a detour to a given point. When it reaches the point, it will resume its normal route. +-- @param #AIRBOSS self +-- @param Core.Point#COORDINATE coord Coordinate of the detour. +-- @param #number speed Speed in knots. Default is current carrier velocity. +-- @param #boolean uturn (Optional) If true, carrier will go back to where it came from before it resumes its route to the next waypoint. +-- @param #number uspeed Speed in knots after U-turn. Default is same as before. +-- @param Core.Point#COORDINATE tcoord Additional coordinate to make turn smoother. +-- @return #AIRBOSS self +function AIRBOSS:CarrierDetour(coord, speed, uturn, uspeed, tcoord) + + -- Current coordinate of the carrier. + local pos0=self:GetCoordinate() + + -- Current speed in knots. + local vel0=self.carrier:GetVelocityKNOTS() + + -- Default. If speed is not given we take the current speed but at least 5 knots. + speed=speed or math.max(vel0, 5) + + -- Speed in km/h. At least 2 knots. + local speedkmh=math.max(UTILS.KnotsToKmph(speed), UTILS.KnotsToKmph(2)) + + -- Turn speed in km/h. At least 10 knots. + local cspeedkmh=math.max(self.carrier:GetVelocityKMH(), UTILS.KnotsToKmph(10)) + + -- U-turn speed in km/h. + local uspeedkmh=UTILS.KnotsToKmph(uspeed or speed) + + -- Waypoint table. + local wp={} + + -- Waypoint at current position. + table.insert(wp, pos0:WaypointGround(cspeedkmh)) + + -- Waypooint to help the turn. + if tcoord then + table.insert(wp, tcoord:WaypointGround(cspeedkmh)) + end + + -- Detour waypoint. + table.insert(wp, coord:WaypointGround(speedkmh)) + + -- U-turn waypoint. If enabled, go back to where you came from. + if uturn then + table.insert(wp, pos0:WaypointGround(uspeedkmh)) + end + + -- Get carrier group. + local group=self.carrier:GetGroup() + + -- Passing waypoint taskfunction + local TaskResumeRoute=group:TaskFunction("AIRBOSS._ResumeRoute", self) + + -- Set task to restart route at the last point. + group:SetTaskWaypoint(wp[#wp], TaskResumeRoute) + + -- Debug mark. + if self.Debug then + if tcoord then + tcoord:MarkToAll(string.format("Detour Turn Help WP. Speed %.1f knots", UTILS.KmphToKnots(cspeedkmh))) + end + coord:MarkToAll(string.format("Detour Waypoint. Speed %.1f knots", UTILS.KmphToKnots(speedkmh))) + if uturn then + pos0:MarkToAll(string.format("Detour U-turn WP. Speed %.1f knots", UTILS.KmphToKnots(uspeedkmh))) + end + end + + -- Detour switch true. + self.detour=true + + -- Route carrier into the wind. + self.carrier:Route(wp) +end + +--- Let the carrier turn into the wind. +-- @param #AIRBOSS self +-- @param #number time Time in seconds. +-- @param #number vdeck Speed on deck m/s. Carrier will +-- @param #boolean uturn Make U-turn and go back to initial after downwind leg. +-- @return #AIRBOSS self +function AIRBOSS:CarrierTurnIntoWind(time, vdeck, uturn) + + -- Wind speed. + local _,vwind=self:GetWind() + + -- Speed of carrier in m/s but at least 2 knots. + local vtot=math.max(vdeck-vwind, UTILS.KnotsToMps(2)) + + -- Distance to travel + local dist=vtot*time + + -- Speed in knots + local speedknots=UTILS.MpsToKnots(vtot) + local distNM=UTILS.MetersToNM(dist) + + -- Debug output + self:I(self.lid..string.format("Carrier steaming into the wind (%.1f kts). Distance=%.1f NM, Speed=%.1f knots, Time=%d sec.", UTILS.MpsToKnots(vwind), distNM, speedknots, time)) + + -- Get heading into the wind accounting for angled runway. + local hiw=self:GetHeadingIntoWind() + + -- Current heading. + local hdg=self:GetHeading() + + -- Heading difference. + local deltaH=self:_GetDeltaHeading(hdg, hiw) + + local Cv=self:GetCoordinate() + + local Ctiw=nil --Core.Point#COORDINATE + local Csoo=nil --Core.Point#COORDINATE + + -- Define path depending on turn angle. + if deltaH<45 then + -- Small turn. + + -- Point in the right direction to help turning. + Csoo=Cv:Translate(750, hdg):Translate(750, hiw) + + -- Heading into wind from Csoo. + local hsw=self:GetHeadingIntoWind(false, Csoo) + + -- Into the wind coord. + Ctiw=Csoo:Translate(dist, hsw) + + elseif deltaH<90 then + -- Medium turn. + + -- Point in the right direction to help turning. + Csoo=Cv:Translate(900, hdg):Translate(900, hiw) + + -- Heading into wind from Csoo. + local hsw=self:GetHeadingIntoWind(false, Csoo) + + -- Into the wind coord. + Ctiw=Csoo:Translate(dist, hsw) + + elseif deltaH<135 then + -- Large turn backwards. + + -- Point in the right direction to help turning. + Csoo=Cv:Translate(1100, hdg-90):Translate(1000, hiw) + + -- Heading into wind from Csoo. + local hsw=self:GetHeadingIntoWind(false, Csoo) + + -- Into the wind coord. + Ctiw=Csoo:Translate(dist, hsw) + + else + -- Huge turn backwards. + + -- Point in the right direction to help turning. + Csoo=Cv:Translate(1200, hdg-90):Translate(1000, hiw) + + -- Heading into wind from Csoo. + local hsw=self:GetHeadingIntoWind(false, Csoo) + + -- Into the wind coord. + Ctiw=Csoo:Translate(dist, hsw) + + end + + + -- Return to coordinate if collision is detected. + self.Creturnto=self:GetCoordinate() + + -- Next waypoint. + local nextwp=self:_GetNextWaypoint() + + -- For downwind, we take the velocity at the next WP. + local vdownwind=UTILS.MpsToKnots(nextwp:GetVelocity()) + + -- Make sure we move at all in case the speed at the waypoint is zero. + if vdownwind<1 then + vdownwind=10 + end + + -- Let the carrier make a detour from its route but return to its current position. + self:CarrierDetour(Ctiw, speedknots, uturn, vdownwind, Csoo) + + -- Set switch that we are currently turning into the wind. + self.turnintowind=true + + return self +end + +--- Get next waypoint of the carrier. +-- @param #AIRBOSS self +-- @return Core.Point#COORDINATE Coordinate of the next waypoint. +-- @return #number Number of waypoint. +function AIRBOSS:_GetNextWaypoint() + + -- Next waypoint. + local Nextwp=nil + if self.currentwp==#self.waypoints then + Nextwp=1 + else + Nextwp=self.currentwp+1 + end + + -- Debug output + local text=string.format("Current WP=%d/%d, next WP=%d", self.currentwp, #self.waypoints, Nextwp) + self:T2(self.lid..text) + + -- Next waypoint. + local nextwp=self.waypoints[Nextwp] --Core.Point#COORDINATE + + return nextwp,Nextwp +end + + +--- Initialize Mission Editor waypoints. +-- @param #AIRBOSS self +-- @return #AIRBOSS self +function AIRBOSS:_InitWaypoints() + + -- Waypoints of group as defined in the ME. + local Waypoints=self.carrier:GetGroup():GetTemplateRoutePoints() + + -- Init array. + self.waypoints={} + + -- Set waypoint table. + for i,point in ipairs(Waypoints) do + + -- Coordinate of the waypoint + local coord=COORDINATE:New(point.x, point.alt, point.y) + + -- Set velocity of the coordinate. + coord:SetVelocity(point.speed) + + -- Add to table. + table.insert(self.waypoints, coord) + + -- Debug info. + if self.Debug then + coord:MarkToAll(string.format("Carrier Waypoint %d, Speed=%.1f knots", i, UTILS.MpsToKnots(point.speed))) + end + + end + + return self +end + +--- Patrol carrier. +-- @param #AIRBOSS self +-- @param #number n Next waypoint number. +-- @return #AIRBOSS self +function AIRBOSS:_PatrolRoute(n) + + -- Get next waypoint coordinate and number. + local nextWP, N=self:_GetNextWaypoint() + + -- Default resume is to next waypoint. + n=n or N + + -- Get carrier group. + local CarrierGroup=self.carrier:GetGroup() + + -- Waypoints table. + local Waypoints={} + + -- Create a waypoint from the current coordinate. + local wp=self:GetCoordinate():WaypointGround(CarrierGroup:GetVelocityKMH()) + + -- Add current position as first waypoint. + table.insert(Waypoints, wp) + + -- Loop over waypoints. + for i=n,#self.waypoints do + local coord=self.waypoints[i] --Core.Point#COORDINATE + + -- Create a waypoint from the coordinate. + local wp=coord:WaypointGround(UTILS.MpsToKmph(coord.Velocity)) + + -- Passing waypoint taskfunction + local TaskPassingWP=CarrierGroup:TaskFunction("AIRBOSS._PassingWaypoint", self, i, #self.waypoints) + + -- Call task function when carrier arrives at waypoint. + CarrierGroup:SetTaskWaypoint(wp, TaskPassingWP) + + -- Add waypoint to table. + table.insert(Waypoints, wp) + end + + -- Route carrier group. + CarrierGroup:Route(Waypoints) + + return self +end + + + + +--- Estimated the carrier position at some point in the future given the current waypoints and speeds. +-- @param #AIRBOSS self +-- @return DCS#time ETA abs. time in seconds. +function AIRBOSS:_GetETAatNextWP() + + -- Current waypoint + local cwp=self.currentwp + + -- Current abs. time. + local tnow=timer.getAbsTime() + + -- Current position. + local p=self:GetCoordinate() + + -- Current velocity [m/s]. + local v=self.carrier:GetVelocityMPS() + + -- Next waypoint. + local nextWP=self:_GetNextWaypoint() + + -- Distance to next waypoint. + local s=p:Get2DDistance(nextWP) + + -- Distance to next waypoint. + --local s=0 + --if #self.waypoints>cwp then + -- s=p:Get2DDistance(self.waypoints[cwp+1]) + --end + + -- v=s/t <==> t=s/v + local t=s/v + + -- ETA + local eta=t+tnow + + return eta +end + +--- Check if carrier is turning. If turning started or stopped, we inform the players via radio message. +-- @param #AIRBOSS self +function AIRBOSS:_CheckCarrierTurning() + + -- Current orientation of carrier. + local vNew=self.carrier:GetOrientationX() + + -- Last orientation from 30 seconds ago. + local vLast=self.Corientlast + + -- We only need the X-Z plane. + vNew.y=0 ; vLast.y=0 + + -- Angle between current heading and last time we checked ~30 seconds ago. + local deltaLast=math.deg(math.acos(UTILS.VecDot(vNew,vLast)/UTILS.VecNorm(vNew)/UTILS.VecNorm(vLast))) + + -- Last orientation becomes new orientation + self.Corientlast=vNew + + -- Carrier is turning when its heading changed by at least one degree since last check. + local turning=math.abs(deltaLast)>=1 + + -- Check if turning stopped. (Carrier was turning but is not any more.) + if self.turning and not turning then + + -- Get final bearing. + local FB=self:GetFinalBearing(true) + + -- Marshal radio call: "99, new final bearing XYZ degrees." + self:_MarshalCallNewFinalBearing(FB) + + end + + -- Check if turning started. (Carrier was not turning and is now.) + if turning and not self.turning then + + -- Get heading. + local hdg + if self.turnintowind then + -- We are now steaming into the wind. + hdg=self:GetHeadingIntoWind(false) + else + -- We turn towards the next waypoint. + hdg=self:GetCoordinate():HeadingTo(self:_GetNextWaypoint()) + end + + -- Magnetic! + hdg=hdg-self.magvar + if hdg<0 then + hdg=360+hdg + end + + -- Radio call: "99, Carrier starting turn to heading XYZ degrees". + self:_MarshalCallCarrierTurnTo(hdg) + end + + -- Update turning. + self.turning=turning +end + +--- Check if heading or position of carrier have changed significantly. +-- @param #AIRBOSS self +function AIRBOSS:_CheckPatternUpdate() + + ---------------------------------------- + -- TODO: Make parameters input values -- + ---------------------------------------- + + -- Min 10 min between pattern updates. + local dTPupdate=10*60 + + -- Update if carrier moves by more than 2.5 NM. + local Dupdate=UTILS.NMToMeters(2.5) + + -- Update if carrier turned by more than 5°. + local Hupdate=5 + + ----------------------- + -- Time Update Check -- + ----------------------- + + -- Time since last pattern update + local dt=timer.getTime()-self.Tpupdate + + -- Check whether at least 10 min between updates and not turning currently. + if dt=Hupdate then + self:T(self.lid..string.format("Carrier heading changed by %d°.", deltaHeading)) + Hchange=true + end + + --------------------------- + -- Distance Update Check -- + --------------------------- + + -- Get current position and orientation of carrier. + local pos=self:GetCoordinate() + + -- Get distance to saved position. + local dist=pos:Get2DDistance(self.Cposition) + + -- Check if carrier moved more than ~10 km. + local Dchange=false + if dist>=Dupdate then + self:T(self.lid..string.format("Carrier position changed by %.1f NM.", UTILS.MetersToNM(dist))) + Dchange=true + end + + ---------------------------- + -- Update Marshal Flights -- + ---------------------------- + + -- If heading or distance changed ==> update marshal AI patterns. + if Hchange or Dchange then + + -- Loop over all marshal flights + for _,_flight in pairs(self.Qmarshal) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Update marshal pattern of AI keeping the same stack. + if flight.ai then + self:_MarshalAI(flight, flight.flag) + end + + end + + -- Reset parameters for next update check. + self.Corientation=vNew + self.Cposition=pos + self.Tpupdate=timer.getTime() + end + +end + +--- Function called when a group is passing a waypoint. +--@param Wrapper.Group#GROUP group Group that passed the waypoint +--@param #AIRBOSS airboss Airboss object. +--@param #number i Waypoint number that has been reached. +--@param #number final Final waypoint number. +function AIRBOSS._PassingWaypoint(group, airboss, i, final) + + -- Debug message. + local text=string.format("Group %s passing waypoint %d of %d.", group:GetName(), i, final) + + -- Debug smoke and marker. + if airboss.Debug and false then + local pos=group:GetCoordinate() + pos:SmokeRed() + local MarkerID=pos:MarkToAll(string.format("Group %s reached waypoint %d", group:GetName(), i)) + end + + -- Debug message. + MESSAGE:New(text,10):ToAllIf(airboss.Debug) + airboss:T(airboss.lid..text) + + -- Set current waypoint. + airboss.currentwp=i + + -- Passing Waypoint event. + airboss:PassingWaypoint(i) + + -- Reactivate beacons. + --airboss:_ActivateBeacons() + + -- If final waypoint reached, do route all over again. + if i==final and final>1 and airboss.adinfinitum then + airboss:_PatrolRoute() + end +end + +--- Carrier Strike Group resumes the route of the waypoints defined in the mission editor. +--@param Wrapper.Group#GROUP group Carrier Strike Group that passed the waypoint. +--@param #AIRBOSS airboss Airboss object. +--@param Core.Point#COORDINATE gotocoord Go to coordinate before route is resumed. +function AIRBOSS._ResumeRoute(group, airboss, gotocoord) + + -- Get next waypoint + local nextwp,Nextwp=airboss:_GetNextWaypoint() + + -- Speed set at waypoint. + local speedkmh=nextwp.Velocity*3.6 + + -- If speed at waypoint is zero, we set it to 10 knots. + if speedkmh<1 then + speedkmh=UTILS.KnotsToKmph(10) + end + + -- Waypoints array. + local waypoints={} + + -- Current position. + local c0=group:GetCoordinate() + + -- Current positon as first waypoint. + local wp0=c0:WaypointGround(speedkmh) + table.insert(waypoints, wp0) + + -- First goto this coordinate. + if gotocoord then + + --gotocoord:MarkToAll(string.format("Goto waypoint speed=%.1f km/h", speedkmh)) + + local headingto=c0:HeadingTo(gotocoord) + + local hdg1=airboss:GetHeading() + local hdg2=c0:HeadingTo(gotocoord) + local delta=airboss:_GetDeltaHeading(hdg1, hdg2) + + --env.info(string.format("FF hdg1=%d, hdg2=%d, delta=%d", hdg1, hdg2, delta)) + + + -- Add additional turn points + if delta>90 then + + -- Turn radius 3 NM. + local turnradius=UTILS.NMToMeters(3) + + local gotocoordh=c0:Translate(turnradius, hdg1+45) + --gotocoordh:MarkToAll(string.format("Goto help waypoint 1 speed=%.1f km/h", speedkmh)) + + local wp=gotocoordh:WaypointGround(speedkmh) + table.insert(waypoints, wp) + + gotocoordh=c0:Translate(turnradius, hdg1+90) + --gotocoordh:MarkToAll(string.format("Goto help waypoint 2 speed=%.1f km/h", speedkmh)) + + wp=gotocoordh:WaypointGround(speedkmh) + table.insert(waypoints, wp) + + end + + local wp1=gotocoord:WaypointGround(speedkmh) + table.insert(waypoints, wp1) + + end + + -- Debug message. + local text=string.format("Carrier is resuming route. Next waypoint %d, Speed=%.1f knots.", Nextwp, UTILS.KmphToKnots(speedkmh)) + + -- Debug message. + MESSAGE:New(text,10):ToAllIf(airboss.Debug) + airboss:I(airboss.lid..text) + + -- Loop over all remaining waypoints. + for i=Nextwp, #airboss.waypoints do + + -- Coordinate of the next WP. + local coord=airboss.waypoints[i] --Core.Point#COORDINATE + + -- Speed in km/h of that WP. Velocity is in m/s. + local speed=coord.Velocity*3.6 + + -- If speed is zero we set it to 10 knots. + if speed<1 then + speed=UTILS.KnotsToKmph(10) + end + + --coord:MarkToAll(string.format("Resume route WP %d, speed=%.1f km/h", i, speed)) + + -- Create waypoint. + local wp=coord:WaypointGround(speed) + + -- Passing waypoint task function. + local TaskPassingWP=group:TaskFunction("AIRBOSS._PassingWaypoint", airboss, i, #airboss.waypoints) + + -- Call task function when carrier arrives at waypoint. + group:SetTaskWaypoint(wp, TaskPassingWP) + + -- Add waypoints to table. + table.insert(waypoints, wp) + end + + -- Set turn into wind switch false. + airboss.turnintowind=false + airboss.detour=false + + -- Route group. + group:Route(waypoints) +end + +--- Function called when a group has reached the holding zone. +--@param Wrapper.Group#GROUP group Group that reached the holding zone. +--@param #AIRBOSS airboss Airboss object. +--@param #AIRBOSS.FlightGroup flight Flight group that has reached the holding zone. +function AIRBOSS._ReachedHoldingZone(group, airboss, flight) + + -- Debug message. + local text=string.format("Flight %s reached holding zone.", group:GetName()) + MESSAGE:New(text,10):ToAllIf(airboss.Debug) + airboss:T(airboss.lid..text) + + -- Debug mark. + if airboss.Debug then + group:GetCoordinate():MarkToAll(text) + end + + -- Set holding flag true and set timestamp for marshal time check. + if flight then + flight.holding=true + flight.time=timer.getAbsTime() + end +end + +--- Function called when a group should be send to the Marshal stack. If stack is full, it is send to wait. +--@param Wrapper.Group#GROUP group Group that reached the holding zone. +--@param #AIRBOSS airboss Airboss object. +--@param #AIRBOSS.FlightGroup flight Flight group that has reached the holding zone. +function AIRBOSS._TaskFunctionMarshalAI(group, airboss, flight) + + -- Debug message. + local text=string.format("Flight %s is send to marshal.", group:GetName()) + MESSAGE:New(text,10):ToAllIf(airboss.Debug) + airboss:T(airboss.lid..text) + + -- Get the next free stack for current recovery case. + local stack=airboss:_GetFreeStack(flight.ai) + + if stack then + + -- Send AI to marshal stack. + airboss:_MarshalAI(flight, stack) + + else + + -- Send AI to orbit outside 10 NM zone and wait until the next Marshal stack is available. + if not airboss:_InQueue(airboss.Qwaiting, flight.group) then + airboss:_WaitAI(flight) + end + + end + + -- If it came from refueling. + if flight.refueling==true then + airboss:I(airboss.lid..string.format("Flight group %s finished refueling task.", flight.groupname)) + end + + -- Not refueling any more in case it was. + flight.refueling=false + +end + +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- MISC functions +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get aircraft nickname. +-- @param #AIRBOSS self +-- @param #string actype Aircraft type name. +-- @return #string Aircraft nickname. E.g. "Hornet" for the F/A-18C or "Tomcat" For the F-14A. +function AIRBOSS:_GetACNickname(actype) + + local nickname="unknown" + if actype==AIRBOSS.AircraftCarrier.A4EC then + nickname="Skyhawk" + elseif actype==AIRBOSS.AircraftCarrier.T45C then + nickname="Goshawk" + elseif actype==AIRBOSS.AircraftCarrier.AV8B then + nickname="Harrier" + elseif actype==AIRBOSS.AircraftCarrier.E2D then + nickname="Hawkeye" + elseif actype==AIRBOSS.AircraftCarrier.F14A_AI or actype==AIRBOSS.AircraftCarrier.F14A or actype==AIRBOSS.AircraftCarrier.F14B then + nickname="Tomcat" + elseif actype==AIRBOSS.AircraftCarrier.FA18C or actype==AIRBOSS.AircraftCarrier.HORNET then + nickname="Hornet" + elseif actype==AIRBOSS.AircraftCarrier.S3B or actype==AIRBOSS.AircraftCarrier.S3BTANKER then + nickname="Viking" + end + + return nickname +end + +--- Get onboard number of player or client. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group Aircraft group. +-- @return #string Onboard number as string. +function AIRBOSS:_GetOnboardNumberPlayer(group) + return self:_GetOnboardNumbers(group, true) +end + +--- Get onboard numbers of all units in a group. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group Aircraft group. +-- @param #boolean playeronly If true, return the onboard number for player or client skill units. +-- @return #table Table of onboard numbers. +function AIRBOSS:_GetOnboardNumbers(group, playeronly) + --self:F({groupname=group:GetName}) + + -- Get group name. + local groupname=group:GetName() + + -- Debug text. + local text=string.format("Onboard numbers of group %s:", groupname) + + -- Units of template group. + local units=group:GetTemplate().units + + -- Get numbers. + local numbers={} + for _,unit in pairs(units) do + + -- Onboard number and unit name. + local n=tostring(unit.onboard_num) + local name=unit.name + local skill=unit.skill or "Unknown" + + -- Debug text. + text=text..string.format("\n- unit %s: onboard #=%s skill=%s", name, n, tostring(skill)) + + if playeronly and skill=="Client" or skill=="Player" then + -- There can be only one player in the group, so we skip everything else. + return n + end + + -- Table entry. + numbers[name]=n + end + + -- Debug info. + self:T2(self.lid..text) + + return numbers +end + + +--- Get Tower frequency of carrier. +-- @param #AIRBOSS self +function AIRBOSS:_GetTowerFrequency() + + -- Tower frequency in MHz + self.TowerFreq=0 + + -- Get Template of Strike Group + local striketemplate=self.carrier:GetGroup():GetTemplate() + + -- Find the carrier unit. + for _,unit in pairs(striketemplate.units) do + if self.carrier:GetName()==unit.name then + self.TowerFreq=unit.frequency/1000000 + return + end + end +end + +--- Get error margin depending on player skill. +-- +-- * Flight students: 10% and 20% +-- * Naval Aviators: 5% and 10% +-- * TOPGUN Graduates: 2.5% and 5% +-- +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @return #number Error margin for still being okay. +-- @return #number Error margin for really sucking. +function AIRBOSS:_GetGoodBadScore(playerData) + + local lowscore + local badscore + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + lowscore=10 + badscore=20 + elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then + lowscore=5 + badscore=10 + elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then + lowscore=2.5 + badscore=5 + end + + return lowscore, badscore +end + +--- Check if aircraft is capable of landing on this aircraft carrier. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Aircraft unit. (Will also work with groups as given parameter.) +-- @return #boolean If true, aircraft can land on a carrier. +function AIRBOSS:_IsCarrierAircraft(unit) + + -- Get aircraft type name + local aircrafttype=unit:GetTypeName() + + -- Special case for Harrier which can only land on Tarawa, LHA and LHD. + if aircrafttype==AIRBOSS.AircraftCarrier.AV8B then + if self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS then + return true + else + return false + end + end + + -- Also only Harriers can land on the Tarawa, LHA and LHD. + if self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS then + if aircrafttype~=AIRBOSS.AircraftCarrier.AV8B then + return false + end + end + + -- Loop over all other known carrier capable aircraft. + for _,actype in pairs(AIRBOSS.AircraftCarrier) do + + -- Check if this is a carrier capable aircraft type. + if actype==aircrafttype then + return true + end + end + + -- No carrier carrier aircraft. + return false +end + +--- Checks if a human player sits in the unit. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Aircraft unit. +-- @return #boolean If true, human player inside the unit. +function AIRBOSS:_IsHumanUnit(unit) + + -- Get player unit or nil if no player unit. + local playerunit=self:_GetPlayerUnitAndName(unit:GetName()) + + if playerunit then + return true + else + return false + end +end + +--- Checks if a group has a human player. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group Aircraft group. +-- @return #boolean If true, human player inside group. +function AIRBOSS:_IsHuman(group) + + -- Get all units of the group. + local units=group:GetUnits() + + -- Loop over all units. + for _,_unit in pairs(units) do + -- Check if unit is human. + local human=self:_IsHumanUnit(_unit) + if human then + return true + end + end + + return false +end + +--- Get fuel state in pounds. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit The unit for which the mass is determined. +-- @return #number Fuel state in pounds. +function AIRBOSS:_GetFuelState(unit) + + -- Get relative fuel [0,1]. + local fuel=unit:GetFuel() + + -- Get max weight of fuel in kg. + local maxfuel=self:_GetUnitMasses(unit) + + -- Fuel state, i.e. what let's + local fuelstate=fuel*maxfuel + + -- Debug info. + self:T2(self.lid..string.format("Unit %s fuel state = %.1f kg = %.1f lbs", unit:GetName(), fuelstate, UTILS.kg2lbs(fuelstate))) + + return UTILS.kg2lbs(fuelstate) +end + +--- Convert altitude from meters to angels (thousands of feet). +-- @param #AIRBOSS self +-- @param alt Alitude in meters. +-- @return #number Altitude in Anglels = thousands of feet using math.floor(). +function AIRBOSS:_GetAngels(alt) + + if alt then + local angels=UTILS.Round(UTILS.MetersToFeet(alt)/1000, 0) + return angels + else + return 0 + end + +end + +--- Get unit masses especially fuel from DCS descriptor values. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit The unit for which the mass is determined. +-- @return #number Mass of fuel in kg. +-- @return #number Empty weight of unit in kg. +-- @return #number Max weight of unit in kg. +-- @return #number Max cargo weight in kg. +function AIRBOSS:_GetUnitMasses(unit) + + -- Get DCS descriptors table. + local Desc=unit:GetDesc() + + -- Mass of fuel in kg. + local massfuel=Desc.fuelMassMax or 0 + + -- Mass of empty unit in km. + local massempty=Desc.massEmpty or 0 + + -- Max weight of unit in kg. + local massmax=Desc.massMax or 0 + + -- Rest is cargo. + local masscargo=massmax-massfuel-massempty + + -- Debug info. + self:T2(self.lid..string.format("Unit %s mass fuel=%.1f kg, empty=%.1f kg, max=%.1f kg, cargo=%.1f kg", unit:GetName(), massfuel, massempty, massmax, masscargo)) + + return massfuel, massempty, massmax, masscargo +end + +--- Get player data from unit object +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Unit in question. +-- @return #AIRBOSS.PlayerData Player data or nil if not player with this name or unit exists. +function AIRBOSS:_GetPlayerDataUnit(unit) + if unit:IsAlive() then + local unitname=unit:GetName() + local playerunit,playername=self:_GetPlayerUnitAndName(unitname) + if playerunit and playername then + return self.players[playername] + end + end + return nil +end + + +--- Get player data from group object. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group Group in question. +-- @return #AIRBOSS.PlayerData Player data or nil if not player with this name or unit exists. +function AIRBOSS:_GetPlayerDataGroup(group) + local units=group:GetUnits() + for _,unit in pairs(units) do + local playerdata=self:_GetPlayerDataUnit(unit) + if playerdata then + return playerdata + end + end + return nil +end + +--- Returns the unit of a player and the player name from the self.players table if it exists. +-- @param #AIRBOSS self +-- @param #string _unitName Name of the player unit. +-- @return Wrapper.Unit#UNIT Unit of player or nil. +-- @return #string Name of player or nil. +function AIRBOSS:_GetPlayerUnit(_unitName) + + for _,_player in pairs(self.players) do + + local player=_player --#AIRBOSS.PlayerData + + if player.unit and player.unit:GetName()==_unitName then + self:T(self.lid..string.format("Found player=%s unit=%s in players table.", tostring(player.name), tostring(_unitName))) + return player.unit, player.name + end + + end + + return nil,nil +end + +--- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned. +-- @param #AIRBOSS self +-- @param #string _unitName Name of the player unit. +-- @return Wrapper.Unit#UNIT Unit of player or nil. +-- @return #string Name of the player or nil. +function AIRBOSS:_GetPlayerUnitAndName(_unitName) + self:F2(_unitName) + + if _unitName ~= nil then + + -- First, let's look up all current players. + local u,pn=self:_GetPlayerUnit(_unitName) + + -- Return + if u and pn then + return u, pn + end + + -- Get DCS unit from its name. + local DCSunit=Unit.getByName(_unitName) + + if DCSunit then + + -- Get player name if any. + local playername=DCSunit:getPlayerName() + + -- Unit object. + local unit=UNIT:Find(DCSunit) + + -- Debug. + self:T2({DCSunit=DCSunit, unit=unit, playername=playername}) + + -- Check if enverything is there. + if DCSunit and unit and playername then + self:T(self.lid..string.format("Found DCS unit %s with player %s.", tostring(_unitName), tostring(playername))) + return unit, playername + end + + end + + end + + -- Return nil if we could not find a player. + return nil,nil +end + +--- Get carrier coalition. +-- @param #AIRBOSS self +-- @return #number Coalition side of carrier. +function AIRBOSS:GetCoalition() + return self.carrier:GetCoalition() +end + +--- Get carrier coordinate. +-- @param #AIRBOSS self +-- @return Core.Point#COORDINATE Carrier coordinate. +function AIRBOSS:GetCoordinate() + return self.carrier:GetCoord() +end + +--- Get carrier coordinate. +-- @param #AIRBOSS self +-- @return Core.Point#COORDINATE Carrier coordinate. +function AIRBOSS:GetCoord() + return self.carrier:GetCoord() +end + +--- Get static weather of this mission from env.mission.weather. +-- @param #AIRBOSS self +-- @param #table Clouds table which has entries "thickness", "density", "base", "iprecptns". +-- @param #number Visibility distance in meters. +-- @param #table Fog table, which has entries "thickness", "visibility" or nil if fog is disabled in the mission. +-- @param #number Dust density or nil if dust is disabled in the mission. +function AIRBOSS:_GetStaticWeather() + + -- Weather data from mission file. + local weather=env.mission.weather + + -- Clouds + --[[ + ["clouds"] = + { + ["thickness"] = 430, + ["density"] = 7, + ["base"] = 0, + ["iprecptns"] = 1, + }, -- end of ["clouds"] + ]] + local clouds=weather.clouds + + -- Visibilty distance in meters. + local visibility=weather.visibility.distance + + -- Dust + --[[ + ["enable_dust"] = false, + ["dust_density"] = 0, + ]] + local dust=nil + if weather.enable_dust==true then + dust=weather.dust_density + end + + -- Fog + --[[ + ["enable_fog"] = false, + ["fog"] = + { + ["thickness"] = 0, + ["visibility"] = 25, + }, -- end of ["fog"] + ]] + local fog=nil + if weather.enable_fog==true then + fog=weather.fog + end + + + return clouds, visibility, fog, dust +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- RADIO MESSAGE Functions +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Function called by DCS timer. Unused. +-- @param #table param Parameters. +-- @param #number time Time. +function AIRBOSS._CheckRadioQueueT(param, time) + AIRBOSS._CheckRadioQueue(param.airboss, param.radioqueue, param.name) + return time+0.05 +end + +--- Radio queue item. +-- @type AIRBOSS.Radioitem +-- @field #number Tplay Abs time when transmission should be played. +-- @field #number Tstarted Abs time when transmission began to play. +-- @field #boolean isplaying Currently playing. +-- @field #AIRBOSS.Radio radio Radio object. +-- @field #AIRBOSS.RadioCall call Radio call. +-- @field #boolean loud If true, play loud version of file. +-- @field #number interval Interval in seconds after the last sound was played. + +--- Check radio queue for transmissions to be broadcasted. +-- @param #AIRBOSS self +-- @param #table radioqueue The radio queue. +-- @param #string name Name of the queue. +function AIRBOSS:_CheckRadioQueue(radioqueue, name) + + --env.info(string.format("FF %s #radioqueue %d", name, #radioqueue)) + + -- Check if queue is empty. + if #radioqueue==0 then + + if name=="LSO" then + self:T(self.lid..string.format("Stopping LSO radio queue.")) + self.radiotimer:Stop(self.RQLid) + self.RQLid=nil + elseif name=="MARSHAL" then + self:T(self.lid..string.format("Stopping Marshal radio queue.")) + self.radiotimer:Stop(self.RQMid) + self.RQMid=nil + end + + return + end + + -- Get current abs time. + local _time=timer.getAbsTime() + + local playing=false + local next=nil --#AIRBOSS.Radioitem + local _remove=nil + for i,_transmission in ipairs(radioqueue) do + local transmission=_transmission --#AIRBOSS.Radioitem + + -- Check if transmission time has passed. + if _time>=transmission.Tplay then + + -- Check if transmission is currently playing. + if transmission.isplaying then + + -- Check if transmission is finished. + if _time>=transmission.Tstarted+transmission.call.duration then + + -- Transmission over. + transmission.isplaying=false + _remove=i + + if transmission.radio.alias=="LSO" then + self.TQLSO=_time + elseif transmission.radio.alias=="MARSHAL" then + self.TQMarshal=_time + end + + else -- still playing + + -- Transmission is still playing. + playing=true + + end + + else -- not playing yet + + local Tlast=nil + if transmission.interval then + if transmission.radio.alias=="LSO" then + Tlast=self.TQLSO + elseif transmission.radio.alias=="MARSHAL" then + Tlast=self.TQMarshal + end + end + + if transmission.interval==nil then + + -- Not playing ==> this will be next. + if next==nil then + next=transmission + end + + else + + if _time-Tlast>=transmission.interval then + next=transmission + else + + end + end + + -- We got a transmission or one with an interval that is not due yet. No need for anything else. + if next or Tlast then + break + end + + end + + else + + -- Transmission not due yet. + + end + end + + -- Found a new transmission. + if next~=nil and not playing then + self:Broadcast(next.radio, next.call, next.loud) + next.isplaying=true + next.Tstarted=_time + end + + -- Remove completed calls from queue. + if _remove then + table.remove(radioqueue, _remove) + end + + return +end + +--- Add Radio transmission to radio queue. +-- @param #AIRBOSS self +-- @param #AIRBOSS.Radio radio Radio sending the transmission. +-- @param #AIRBOSS.RadioCall call Radio sound files and subtitles. +-- @param #boolean loud If true, play loud sound file version. +-- @param #number delay Delay in seconds, before the message is broadcasted. +-- @param #number interval Interval in seconds after the last sound has been played. +-- @param #boolean click If true, play radio click at the end. +-- @param #boolean pilotcall If true, it's a pilot call. +function AIRBOSS:RadioTransmission(radio, call, loud, delay, interval, click, pilotcall) + self:F2({radio=radio, call=call, loud=loud, delay=delay, interval=interval, click=click}) + + -- Nil check. + if radio==nil or call==nil then + return + end + + -- Create a new radio transmission item. + local transmission={} --#AIRBOSS.Radioitem + + transmission.radio=radio + transmission.call=call + transmission.Tplay=timer.getAbsTime()+(delay or 0) + transmission.interval=interval + transmission.isplaying=false + transmission.Tstarted=nil + transmission.loud=loud and call.loud + + -- Player onboard number if sender has one. + if self:_IsOnboard(call.modexsender) then + self:_Number2Radio(radio, call.modexsender, delay, 0.3, pilotcall) + end + + -- Play onboard number if receiver has one. + if self:_IsOnboard(call.modexreceiver) then + self:_Number2Radio(radio, call.modexreceiver, delay, 0.3, pilotcall) + end + + -- Add transmission to the right queue. + local caller="" + if radio.alias=="LSO" then + + table.insert(self.RQLSO, transmission) + + caller="LSOCall" + + -- Schedule radio queue checks. + if not self.RQLid then + self:T(self.lid..string.format("Starting LSO radio queue.")) + self.RQLid=self.radiotimer:Schedule(nil, AIRBOSS._CheckRadioQueue, {self, self.RQLSO, "LSO"}, 0.02, 0.05) + end + + elseif radio.alias=="MARSHAL" then + + table.insert(self.RQMarshal, transmission) + + caller="MarshalCall" + + if not self.RQMid then + self:T(self.lid..string.format("Starting Marhal radio queue.")) + self.RQMid=self.radiotimer:Schedule(nil, AIRBOSS._CheckRadioQueue, {self, self.RQMarshal, "MARSHAL"}, 0.02, 0.05) + end + + end + + -- Append radio click sound at the end of the transmission. + if click then + self:RadioTransmission(radio, self[caller].CLICK, false, delay) + end +end + + +--- Check if a call needs a subtitle because the complete voice overs are not available. +-- @param #AIRBOSS self +-- @param #AIRBOSS.RadioCall call Radio sound files and subtitles. +-- @return #boolean If true, call needs a subtitle. +function AIRBOSS:_NeedsSubtitle(call) + -- Currently we play the noise file. + if call.file==self.MarshalCall.NOISE.file or call.file==self.LSOCall.NOISE.file then + return true + else + return false + end +end + +--- Broadcast radio message. +-- @param #AIRBOSS self +-- @param #AIRBOSS.Radio radio Radio sending transmission. +-- @param #AIRBOSS.RadioCall call Radio sound files and subtitles. +-- @param #boolean loud Play loud version of file. +function AIRBOSS:Broadcast(radio, call, loud) + self:F(call) + + -- Check which sound output method to use. + if not self.usersoundradio then + + ---------------------------- + -- Transmission via Radio -- + ---------------------------- + + -- Get unit sending the transmission. + local sender=self:_GetRadioSender(radio) + + -- Construct file name and subtitle. + local filename=self:_RadioFilename(call, loud, radio.alias) + + -- Create subtitle for transmission. + local subtitle=self:_RadioSubtitle(radio, call, loud) + + -- Debug. + self:T({filename=filename, subtitle=subtitle}) + + if sender then + + -- Broadcasting from aircraft. Only players tuned in to the right frequency will see the message. + self:T(self.lid..string.format("Broadcasting from aircraft %s", sender:GetName())) + + -- Command to set the Frequency for the transmission. + local commandFrequency={ + id="SetFrequency", + params={ + frequency=radio.frequency*1000000, -- Frequency in Hz. + modulation=radio.modulation, + }} + + -- Command to tranmit the call. + local commandTransmit={ + id = "TransmitMessage", + params = { + file=filename, + duration=call.subduration or 5, + subtitle=subtitle, + loop=false, + }} + + -- Set commend for frequency + sender:SetCommand(commandFrequency) + + -- Set command for radio transmission. + sender:SetCommand(commandTransmit) + + else + + -- Broadcasting from carrier. No subtitle possible. Need to send messages to players. + self:T(self.lid..string.format("Broadcasting from carrier via trigger.action.radioTransmission().")) + + -- Transmit from carrier position. + local vec3=self.carrier:GetPositionVec3() + + -- Transmit via trigger. + trigger.action.radioTransmission(filename, vec3, radio.modulation, false, radio.frequency*1000000, 100) + + -- Display subtitle of message to players. + for _,_player in pairs(self.players) do + local playerData=_player --#AIRBOSS.PlayerData + + -- Message to all players in CCA that have subtites on. + if playerData.unit:IsInZone(self.zoneCCA) and playerData.actype~=AIRBOSS.AircraftCarrier.A4EC then + + -- Only to players with subtitle on or if noise is played. + if playerData.subtitles or self:_NeedsSubtitle(call) then + + -- Messages to marshal to everyone. Messages on LSO radio only to those in the pattern. + if radio.alias=="MARSHAL" or (radio.alias=="LSO" and self:_InQueue(self.Qpattern, playerData.group)) then + + -- Message to player. + self:MessageToPlayer(playerData, subtitle, nil, "", call.subduration or 5) + + end + + end + + end + end + end + end + + ---------------- + -- Easy Comms -- + ---------------- + + -- Workaround for the community A-4E-C as long as their radios are not functioning properly. + for _,_player in pairs(self.players) do + local playerData=_player --#AIRBOSS.PlayerData + + -- Easy comms if globally activated but definitly for all player in the community A-4E. + if self.usersoundradio or playerData.actype==AIRBOSS.AircraftCarrier.A4EC then + + -- Messages to marshal to everyone. Messages on LSO radio only to those in the pattern. + if radio.alias=="MARSHAL" or (radio.alias=="LSO" and self:_InQueue(self.Qpattern, playerData.group)) then + + -- User sound to players (inside CCA). + self:Sound2Player(playerData, radio, call, loud) + end + + end + end + +end + +--- Player user sound to player if he is inside the CCA. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #AIRBOSS.Radio radio The radio used for transmission. +-- @param #AIRBOSS.RadioCall call Radio sound files and subtitles. +-- @param #boolean loud If true, play loud sound file version. +-- @param #number delay Delay in seconds, before the message is broadcasted. +function AIRBOSS:Sound2Player(playerData, radio, call, loud, delay) + + -- Only to players inside the CCA. + if playerData.unit:IsInZone(self.zoneCCA) and call then + + -- Construct file name. + local filename=self:_RadioFilename(call, loud, radio.alias) + + -- Get Subtitle + local subtitle=self:_RadioSubtitle(radio, call, loud) + + -- Play sound file via usersound trigger. + USERSOUND:New(filename):ToGroup(playerData.group, delay) + + -- Only to players with subtitle on or if noise is played. + if playerData.subtitles or self:_NeedsSubtitle(call) then + self:MessageToPlayer(playerData, subtitle, nil, "", call.subduration, false, delay) + end + + end +end + +--- Create radio subtitle from radio call. +-- @param #AIRBOSS self +-- @param #AIRBOSS.Radio radio The radio used for transmission. +-- @param #AIRBOSS.RadioCall call Radio sound files and subtitles. +-- @param #boolean loud If true, append "!" else ".". +-- @return #string Subtitle to be displayed. +function AIRBOSS:_RadioSubtitle(radio, call, loud) + + -- No subtitle if call is nil, or subtitle is nil or subtitle is empty. + if call==nil or call.subtitle==nil or call.subtitle=="" then + return "" + end + + -- Sender + local sender=call.sender or radio.alias + if call.modexsender then + sender=call.modexsender + end + + -- Modex of receiver. + local receiver=call.modexreceiver or "" + + -- Init subtitle. + local subtitle=string.format("%s: %s", sender, call.subtitle) + if receiver and receiver~="" then + subtitle=string.format("%s: %s, %s", sender, receiver, call.subtitle) + end + + -- Last character of the string. + local lastchar=string.sub(subtitle, -1) + + -- Append ! or . + if loud then + if lastchar=="." or lastchar=="!" then + subtitle=string.sub(subtitle, 1,-1) + end + subtitle=subtitle.."!" + else + if lastchar=="!" then + -- This also okay. + elseif lastchar=="." then + -- Nothing to do. + else + subtitle=subtitle.."." + end + end + + return subtitle +end + +--- Get full file name for radio call. +-- @param #AIRBOSS self +-- @param #AIRBOSS.RadioCall call Radio sound files and subtitles. +-- @param #boolean loud Use loud version of file if available. +-- @param #string channel Radio channel alias "LSO" or "LSOCall", "MARSHAL" or "MarshalCall". +-- @return #string The file name of the radio sound. +function AIRBOSS:_RadioFilename(call, loud, channel) + + -- Construct file name and subtitle. + local prefix=call.file or "" + local suffix=call.suffix or "ogg" + + -- Path to sound files. Default is in the ME + local path=self.soundfolder or "l10n/DEFAULT/" + + -- Check for special LSO and Marshal sound folders. + if string.find(call.file, "LSO-") and channel and (channel=="LSO" or channel=="LSOCall") then + path=self.soundfolderLSO or path + end + if string.find(call.file, "MARSHAL-") and channel and (channel=="MARSHAL" or channel=="MarshalCall") then + path=self.soundfolderMSH or path + end + + -- Loud version. + if loud then + prefix=prefix.."_Loud" + end + + -- File name inclusing path in miz file. + local filename=string.format("%s%s.%s", path, prefix, suffix) + + return filename +end + +--- Send text message to player client. +-- Message format will be "SENDER: RECCEIVER, MESSAGE". +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #string message The message to send. +-- @param #string sender The person who sends the message or nil. +-- @param #string receiver The person who receives the message. Default player's onboard number. Set to "" for no receiver. +-- @param #number duration Display message duration. Default 10 seconds. +-- @param #boolean clear If true, clear screen from previous messages. +-- @param #number delay Delay in seconds, before the message is displayed. +function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration, clear, delay) + + if playerData and message and message~="" then + + -- Default duration. + duration=duration or self.Tmessage + + -- Format message. + local text + if receiver and receiver=="" then + -- No (blank) receiver. + text=string.format("%s", message) + else + -- Default "receiver" is onboard number of player. + receiver=receiver or playerData.onboard + text=string.format("%s, %s", receiver, message) + end + self:T(self.lid..text) + + if delay and delay>0 then + -- Delayed call. + --SCHEDULER:New(nil, self.MessageToPlayer, {self, playerData, message, sender, receiver, duration, clear}, delay) + self:ScheduleOnce(delay, self.MessageToPlayer, self, playerData, message, sender, receiver, duration, clear) + else + + -- Wait until previous sound finished. + local wait=0 + + -- Onboard number to get the attention. + if receiver==playerData.onboard then + + -- Which voice over number to use. + if sender and (sender=="LSO" or sender=="MARSHAL" or sender=="AIRBOSS") then + + -- User sound of board number. + wait=wait+self:_Number2Sound(playerData, sender, receiver) + + end + end + + -- Negative. + if string.find(text:lower(), "negative") then + local filename=self:_RadioFilename(self.MarshalCall.NEGATIVE, false, "MARSHAL") + USERSOUND:New(filename):ToGroup(playerData.group, wait) + wait=wait+self.MarshalCall.NEGATIVE.duration + end + + -- Affirm. + if string.find(text:lower(), "affirm") then + local filename=self:_RadioFilename(self.MarshalCall.AFFIRMATIVE, false, "MARSHAL") + USERSOUND:New(filename):ToGroup(playerData.group, wait) + wait=wait+self.MarshalCall.AFFIRMATIVE.duration + end + + -- Roger. + if string.find(text:lower(), "roger") then + local filename=self:_RadioFilename(self.MarshalCall.ROGER, false, "MARSHAL") + USERSOUND:New(filename):ToGroup(playerData.group, wait) + wait=wait+self.MarshalCall.ROGER.duration + end + + -- Play click sound to end message. + if wait>0 then + local filename=self:_RadioFilename(self.MarshalCall.CLICK) + USERSOUND:New(filename):ToGroup(playerData.group, wait) + end + + -- Text message to player client. + if playerData.client then + MESSAGE:New(text, duration, sender, clear):ToClient(playerData.client) + end + + end + + end +end + + +--- Send text message to all players in the pattern queue. +-- Message format will be "SENDER: RECCEIVER, MESSAGE". +-- @param #AIRBOSS self +-- @param #string message The message to send. +-- @param #string sender The person who sends the message or nil. +-- @param #string receiver The person who receives the message. Default player's onboard number. Set to "" for no receiver. +-- @param #number duration Display message duration. Default 10 seconds. +-- @param #boolean clear If true, clear screen from previous messages. +-- @param #number delay Delay in seconds, before the message is displayed. +function AIRBOSS:MessageToPattern(message, sender, receiver, duration, clear, delay) + + -- Create new (fake) radio call to show the subtitile. + local call=self:_NewRadioCall(self.LSOCall.NOISE, sender or "LSO", message, duration, receiver, sender) + + -- Dummy radio transmission to display subtitle only to those who tuned in. + self:RadioTransmission(self.LSORadio, call, false, delay, nil, true) + +end + +--- Send text message to all players in the marshal queue. +-- Message format will be "SENDER: RECCEIVER, MESSAGE". +-- @param #AIRBOSS self +-- @param #string message The message to send. +-- @param #string sender The person who sends the message or nil. +-- @param #string receiver The person who receives the message. Default player's onboard number. Set to "" for no receiver. +-- @param #number duration Display message duration. Default 10 seconds. +-- @param #boolean clear If true, clear screen from previous messages. +-- @param #number delay Delay in seconds, before the message is displayed. +function AIRBOSS:MessageToMarshal(message, sender, receiver, duration, clear, delay) + + -- Create new (fake) radio call to show the subtitile. + local call=self:_NewRadioCall(self.MarshalCall.NOISE, sender or "MARSHAL", message, duration, receiver, sender) + + -- Dummy radio transmission to display subtitle only to those who tuned in. + self:RadioTransmission(self.MarshalRadio, call, false, delay, nil, true) + +end + +--- Generate a new radio call (deepcopy) from an existing default call. +-- @param #AIRBOSS self +-- @param #AIRBOSS.RadioCall call Radio call to be enhanced. +-- @param #string sender Sender of the message. Default is the radio alias. +-- @param #string subtitle Subtitle of the message. Default from original radio call. Use "" for no subtitle. +-- @param #number subduration Time in seconds the subtitle is displayed. Default 10 seconds. +-- @param #string modexreceiver Onboard number of the receiver or nil. +-- @param #string modexsender Onboard number of the sender or nil. +function AIRBOSS:_NewRadioCall(call, sender, subtitle, subduration, modexreceiver, modexsender) + + -- Create a new call + local newcall=UTILS.DeepCopy(call) --#AIRBOSS.RadioCall + + -- Sender for displaying the subtitle. + newcall.sender=sender + + -- Subtitle of the message. + newcall.subtitle=subtitle or call.subtitle + + -- Duration of subtitle display. + newcall.subduration=subduration or self.Tmessage + + -- Tail number of the receiver. + if self:_IsOnboard(modexreceiver) then + newcall.modexreceiver=modexreceiver + end + + -- Tail number of the sender. + if self:_IsOnboard(modexsender) then + newcall.modexsender=modexsender + end + + return newcall +end + +--- Get unit from which we want to transmit a radio message. This has to be an aircraft for subtitles to work. +-- @param #AIRBOSS self +-- @param #AIRBOSS.Radio radio Airboss radio data. +-- @return Wrapper.Unit#UNIT Sending aircraft unit or nil if was not setup, is not an aircraft or is not alive. +function AIRBOSS:_GetRadioSender(radio) + + -- Check if we have a sending aircraft. + local sender=nil --Wrapper.Unit#UNIT + + -- Try the general default. + if self.senderac then + sender=UNIT:FindByName(self.senderac) + end + + -- Try the specific marshal unit. + if radio.alias=="MARSHAL" then + if self.radiorelayMSH then + sender=UNIT:FindByName(self.radiorelayMSH) + end + end + + -- Try the specific LSO unit. + if radio.alias=="LSO" then + if self.radiorelayLSO then + sender=UNIT:FindByName(self.radiorelayLSO) + end + end + + -- Check that sender is alive and an aircraft. + if sender and sender:IsAlive() and sender:IsAir() then + return sender + end + + return nil +end + +--- Check if text is an onboard number of a flight. +-- @param #AIRBOSS self +-- @param #string text Text to check. +-- @return #boolean If true, text is an onboard number of a flight. +function AIRBOSS:_IsOnboard(text) + + -- Nil check. + if text==nil then + return false + end + + -- Message to all. + if text=="99" then + return true + end + + -- Loop over all flights. + for _,_flight in pairs(self.flights) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Loop over all onboard number of that flight. + for _,onboard in pairs(flight.onboardnumbers) do + if text==onboard then + return true + end + end + + end + + return false +end + +--- Convert a number (as string) into an outsound and play it to a player group. E.g. for board number or headings. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #string sender Who is sending the call, either "LSO" or "MARSHAL". +-- @param #string number Number string, e.g. "032" or "183". +-- @param #number delay Delay before transmission in seconds. +-- @return #number Duration of the call in seconds. +function AIRBOSS:_Number2Sound(playerData, sender, number, delay) + + -- Default. + delay=delay or 0 + + --- Split string into characters. + local function _split(str) + local chars={} + for i=1,#str do + local c=str:sub(i,i) + table.insert(chars, c) + end + return chars + end + + -- Sender + local Sender + if sender=="LSO" then + Sender="LSOCall" + elseif sender=="MARSHAL" or sender=="AIRBOSS" then + Sender="MarshalCall" + else + self:E(self.lid..string.format("ERROR: Unknown radio sender %s!", tostring(sender))) + return + end + + -- Split string into characters. + local numbers=_split(number) + + local wait=0 + for i=1,#numbers do + + -- Current number + local n=numbers[i] + + -- Convert to N0, N1, ... + local N=string.format("N%s", n) + + -- Radio call. + local call=self[Sender][N] --#AIRBOSS.RadioCall + + -- Create file name. + local filename=self:_RadioFilename(call, false, Sender) + + -- Play sound. + USERSOUND:New(filename):ToGroup(playerData.group, delay+wait) + + -- Wait until this call is over before playing the next. + wait=wait+call.duration + end + + return wait +end + +--- Convert a number (as string) into a radio message. +-- E.g. for board number or headings. +-- @param #AIRBOSS self +-- @param #AIRBOSS.Radio radio Radio used for transmission. +-- @param #string number Number string, e.g. "032" or "183". +-- @param #number delay Delay before transmission in seconds. +-- @param #number interval Interval between the next call. +-- @param #boolean pilotcall If true, use pilot sound files. +-- @return #number Duration of the call in seconds. +function AIRBOSS:_Number2Radio(radio, number, delay, interval, pilotcall) + + --- Split string into characters. + local function _split(str) + local chars={} + for i=1,#str do + local c=str:sub(i,i) + table.insert(chars, c) + end + return chars + end + + -- Sender. + local Sender="" + if radio.alias=="LSO" then + Sender="LSOCall" + elseif radio.alias=="MARSHAL" then + Sender="MarshalCall" + else + self:E(self.lid..string.format("ERROR: Unknown radio alias %s!", tostring(radio.alias))) + end + + if pilotcall then + Sender="PilotCall" + end + + -- Split string into characters. + local numbers=_split(number) + + local wait=0 + for i=1,#numbers do + + -- Current number + local n=numbers[i] + + -- Convert to N0, N1, ... + local N=string.format("N%s", n) + + -- Radio call. + local call=self[Sender][N] --#AIRBOSS.RadioCall + + if interval and i==1 then + -- Transmit. + self:RadioTransmission(radio, call, false, delay, interval) + else + self:RadioTransmission(radio, call, false, delay) + end + + -- Add up duration of the number. + wait=wait+call.duration + end + + -- Return the total duration of the call. + return wait +end + + +--- AI aircraft calls the ball. +-- @param #AIRBOSS self +-- @param #string modex Tail number. +-- @param #string nickname Aircraft nickname. +-- @param #number fuelstate Aircraft fuel state in thouthands of pounds. +function AIRBOSS:_LSOCallAircraftBall(modex, nickname, fuelstate) + + -- Pilot: "405, Hornet Ball, 3.2" + local text=string.format("%s Ball, %.1f.", nickname, fuelstate) + + -- Debug message. + self:I(self.lid..text) + + -- Nickname UPPERCASE. + local NICKNAME=nickname:upper() + + -- Fuel state. + local FS=UTILS.Split(string.format("%.1f", fuelstate), ".") + + -- Create new call to display complete subtitle. + local call=self:_NewRadioCall(self.PilotCall[NICKNAME], modex, text, self.Tmessage, nil, modex) + + -- Hornet .. + self:RadioTransmission(self.LSORadio, call, nil, nil, nil, nil, true) + -- Ball, + self:RadioTransmission(self.LSORadio, self.PilotCall.BALL, nil, nil, nil, nil, true) + -- X.. + self:_Number2Radio(self.LSORadio, FS[1], nil, nil, true) + -- Point.. + self:RadioTransmission(self.LSORadio, self.PilotCall.POINT, nil, nil, nil, nil, true) + -- Y. + self:_Number2Radio(self.LSORadio, FS[2], nil, nil, true) + + -- CLICK! + self:RadioTransmission(self.LSORadio, self.LSOCall.CLICK) + +end + +--- AI is bingo and goes to the recovery tanker. +-- @param #AIRBOSS self +-- @param #string modex Tail number. +function AIRBOSS:_MarshalCallGasAtTanker(modex) + + -- Subtitle. + local text=string.format("Bingo fuel! Going for gas at the recovery tanker.") + + -- Debug message. + self:I(self.lid..text) + + -- Create new call to display complete subtitle. + local call=self:_NewRadioCall(self.PilotCall.BINGOFUEL, modex, text, self.Tmessage, nil, modex) + + -- MODEX, bingo fuel! + self:RadioTransmission(self.MarshalRadio, call, nil, nil, nil, nil, true) + + -- Going for fuel at the recovery tanker. Click! + self:RadioTransmission(self.MarshalRadio, self.PilotCall.GASATTANKER, nil, nil, nil, true, true) + +end + +--- AI is bingo and goes to the divert field. +-- @param #AIRBOSS self +-- @param #string modex Tail number. +-- @param #string divertname Name of the divert field. +function AIRBOSS:_MarshalCallGasAtDivert(modex, divertname) + + -- Subtitle. + local text=string.format("Bingo fuel! Going for gas at divert field %s.", divertname) + + -- Debug message. + self:I(self.lid..text) + + -- Create new call to display complete subtitle. + local call=self:_NewRadioCall(self.PilotCall.BINGOFUEL, modex, text, self.Tmessage, nil, modex) + + -- MODEX, bingo fuel! + self:RadioTransmission(self.MarshalRadio, call, nil, nil, nil, nil, true) + + -- Going for fuel at the divert field. Click! + self:RadioTransmission(self.MarshalRadio, self.PilotCall.GASATDIVERT, nil, nil, nil, true, true) + +end + + +--- Inform everyone that recovery ops are stopped and deck is closed. +-- @param #AIRBOSS self +-- @param #number case Recovery case. +function AIRBOSS:_MarshalCallRecoveryStopped(case) + + -- Subtitle. + local text=string.format("Case %d recovery ops are stopped. Deck is closed.", case) + + -- Debug message. + self:I(self.lid..text) + + -- Create new call to display complete subtitle. + local call=self:_NewRadioCall(self.MarshalCall.CASE, "AIRBOSS", text, self.Tmessage, "99") + + -- 99, Case.. + self:RadioTransmission(self.MarshalRadio, call) + -- X. + self:_Number2Radio(self.MarshalRadio, tostring(case)) + -- recovery ops are stopped. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.RECOVERYOPSSTOPPED, nil, nil, 0.2) + -- Deck is closed. Click! + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.DECKCLOSED, nil, nil, nil, true) + +end + +--- Inform everyone that recovery is paused and will resume at a certain time. +-- @param #AIRBOSS self +function AIRBOSS:_MarshalCallRecoveryPausedUntilFurtherNotice() + + -- Create new call. Subtitle already set. + local call=self:_NewRadioCall(self.MarshalCall.RECOVERYPAUSEDNOTICE, "AIRBOSS", nil, self.Tmessage, "99") + + -- 99, aircraft recovery is paused until further notice. + self:RadioTransmission(self.MarshalRadio, call, nil, nil, nil, true) + +end + +--- Inform everyone that recovery is paused and will resume at a certain time. +-- @param #AIRBOSS self +-- @param #string clock Time. +function AIRBOSS:_MarshalCallRecoveryPausedResumedAt(clock) + + -- Get relevant part of clock. + local _clock=UTILS.Split(clock, "+") + local CT=UTILS.Split(_clock[1], ":") + + -- Subtitle. + local text=string.format("aircraft recovery is paused and will be resumed at %s.", clock) + + -- Debug message. + self:I(self.lid..text) + + -- Create new call with full subtitle. + local call=self:_NewRadioCall(self.MarshalCall.RECOVERYPAUSEDRESUMED, "AIRBOSS", text, self.Tmessage, "99") + + -- 99, aircraft recovery is paused and will resume at... + self:RadioTransmission(self.MarshalRadio, call) + + -- XY.. (hours) + self:_Number2Radio(self.MarshalRadio, CT[1]) + -- XY (minutes).. + self:_Number2Radio(self.MarshalRadio, CT[2]) + -- hours. Click! + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.HOURS, nil, nil, nil, true) + +end + + +--- Inform flight that he is cleared for recovery. +-- @param #AIRBOSS self +-- @param #string modex Tail number. +-- @param #number case Recovery case. +function AIRBOSS:_MarshalCallClearedForRecovery(modex, case) + + -- Subtitle. + local text=string.format("you're cleared for Case %d recovery.", case) + + -- Debug message. + self:I(self.lid..text) + + -- Create new call with full subtitle. + local call=self:_NewRadioCall(self.MarshalCall.CLEAREDFORRECOVERY, "MARSHAL", text, self.Tmessage, modex) + + -- Two second delay. + local delay=2 + + -- XYZ, you're cleared for case.. + self:RadioTransmission(self.MarshalRadio, call, nil, delay) + -- X.. + self:_Number2Radio(self.MarshalRadio, tostring(case), delay) + -- recovery. Click! + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.RECOVERY, nil, delay, nil, true) + +end + +--- Inform everyone that recovery is resumed after pause. +-- @param #AIRBOSS self +function AIRBOSS:_MarshalCallResumeRecovery() + + -- Create new call with full subtitle. + local call=self:_NewRadioCall(self.MarshalCall.RESUMERECOVERY, "AIRBOSS", nil, self.Tmessage, "99") + + -- 99, aircraft recovery resumed. Click! + self:RadioTransmission(self.MarshalRadio, call, nil, nil, nil, true) + +end + +--- Inform everyone about new final bearing. +-- @param #AIRBOSS self +-- @param #number FB Final Bearing in degrees. +function AIRBOSS:_MarshalCallNewFinalBearing(FB) + + -- Subtitle. + local text=string.format("new final bearing %03d°.", FB) + + -- Debug message. + self:I(self.lid..text) + + -- Create new call with full subtitle. + local call=self:_NewRadioCall(self.MarshalCall.NEWFB, "AIRBOSS", text, self.Tmessage, "99") + + -- 99, new final bearing.. + self:RadioTransmission(self.MarshalRadio, call) + -- XYZ.. + self:_Number2Radio(self.MarshalRadio, string.format("%03d", FB), nil, 0.2) + -- Degrees. Click! + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.DEGREES, nil, nil, nil, true) + +end + +--- Compile a radio call when Marshal tells a flight the holding alitude. +-- @param #AIRBOSS self +-- @param #number hdg Heading in degrees. +function AIRBOSS:_MarshalCallCarrierTurnTo(hdg) + + -- Subtitle. + local text=string.format("carrier is now starting turn to heading %03d°.", hdg) + + -- Debug message. + self:I(self.lid..text) + + -- Create new call with full subtitle. + local call=self:_NewRadioCall(self.MarshalCall.CARRIERTURNTOHEADING, "AIRBOSS", text, self.Tmessage, "99") + + -- 99, turning to heading... + self:RadioTransmission(self.MarshalRadio, call) + -- XYZ.. + self:_Number2Radio(self.MarshalRadio, string.format("%03d", hdg), nil, 0.2) + -- Degrees. Click! + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.DEGREES, nil, nil, nil, true) + +end + +--- Compile a radio call when Marshal tells a flight the holding alitude. +-- @param #AIRBOSS self +-- @param #string modex Tail number. +-- @param #number nwaiting Number of flights already waiting. +function AIRBOSS:_MarshalCallStackFull(modex, nwaiting) + + -- Subtitle. + local text=string.format("Marshal stack is currently full. Hold outside 10 NM zone and wait for further instructions. ") + if nwaiting==1 then + text=text..string.format("There is one flight ahead of you.") + elseif nwaiting>1 then + text=text..string.format("There are %d flights ahead of you.", nwaiting) + else + text=text..string.format("You are next in line.") + end + + -- Debug message. + self:I(self.lid..text) + + -- Create new call with full subtitle. + local call=self:_NewRadioCall(self.MarshalCall.STACKFULL, "AIRBOSS", text, self.Tmessage, modex) + + -- XYZ, Marshal stack is currently full. + self:RadioTransmission(self.MarshalRadio, call, nil, nil, nil, true) +end + +--- Compile a radio call when Marshal tells a flight the holding alitude. +-- @param #AIRBOSS self +function AIRBOSS:_MarshalCallRecoveryStart(case) + + -- Marshal radial. + local radial=self:GetRadial(case, true, true, false) + + -- Debug output. + local text=string.format("Starting aircraft recovery Case %d ops.", case) + if case==1 then + text=text..string.format(" BRC %03d°.", self:GetBRC()) + elseif case==2 then + text=text..string.format(" Marshal radial %03d°. BRC %03d°.", radial, self:GetBRC()) + elseif case==3 then + text=text..string.format(" Marshal radial %03d°. Final heading %03d°.", radial, self:GetFinalBearing(false)) + end + self:T(self.lid..text) + + -- New call including the subtitle. + local call=self:_NewRadioCall(self.MarshalCall.STARTINGRECOVERY, "AIRBOSS", text, self.Tmessage, "99") + + -- 99, Starting aircraft recovery case.. + self:RadioTransmission(self.MarshalRadio, call) + -- X.. + self:_Number2Radio(self.MarshalRadio,tostring(case), nil, 0.1) + -- ops. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.OPS) + + --Marshal Radial + if case>1 then + -- Marshal radial.. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.MARSHALRADIAL) + -- XYZ.. + self:_Number2Radio(self.MarshalRadio, string.format("%03d", radial), nil, 0.2) + -- Degrees. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.DEGREES, nil, nil, nil, true) + end + +end + +--- Compile a radio call when Marshal tells a flight the holding alitude. +-- @param #AIRBOSS self +-- @param #string modex Tail number. +-- @param #number case Recovery case. +-- @param #number brc Base recovery course. +-- @param #number altitude Holding alitude. +-- @param #string charlie Charlie Time estimate. +-- @param #number qfe Alitmeter inHg. +function AIRBOSS:_MarshalCallArrived(modex, case, brc, altitude, charlie, qfe) + self:F({modex=modex,case=case,brc=brc,altitude=altitude,charlie=charlie,qfe=qfe}) + + -- Split strings etc. + local angels=self:_GetAngels(altitude) + --local QFE=UTILS.Split(tostring(UTILS.Round(qfe,2)), ".") + local QFE=UTILS.Split(string.format("%.2f", qfe), ".") + local clock=UTILS.Split(charlie, "+") + local CT=UTILS.Split(clock[1], ":") + + -- Subtitle text. + local text=string.format("Case %d, expected BRC %03d°, hold at angels %d. Expected Charlie Time %s. Altimeter %.2f. Report see me.", case, brc, angels, charlie, qfe) + + -- Debug message. + self:I(self.lid..text) + + -- Create new call to display complete subtitle. + local casecall=self:_NewRadioCall(self.MarshalCall.CASE, "MARSHAL", text, self.Tmessage, modex) + + -- Case.. + self:RadioTransmission(self.MarshalRadio, casecall) + -- X. + self:_Number2Radio(self.MarshalRadio, tostring(case)) + + -- Expected.. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.EXPECTED, nil, nil, 0.5) + -- BRC.. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.BRC) + -- XYZ... + self:_Number2Radio(self.MarshalRadio, string.format("%03d", brc)) + -- Degrees. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.DEGREES) + + + -- Hold at.. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.HOLDATANGELS, nil, nil, 0.5) + -- X. + self:_Number2Radio(self.MarshalRadio, tostring(angels)) + + -- Expected.. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.EXPECTED, nil, nil, 0.5) + -- Charlie time.. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.CHARLIETIME) + -- XY.. (hours) + self:_Number2Radio(self.MarshalRadio, CT[1]) + -- XY (minutes). + self:_Number2Radio(self.MarshalRadio, CT[2]) + -- hours. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.HOURS) + + + -- Altimeter.. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.ALTIMETER, nil, nil, 0.5) + -- XY.. + self:_Number2Radio(self.MarshalRadio, QFE[1]) + -- Point.. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.POINT) + -- XY. + self:_Number2Radio(self.MarshalRadio, QFE[2]) + + -- Report see me. Click! + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.REPORTSEEME, nil, nil, 0.5, true) + +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- RADIO MENU Functions +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add menu commands for player. +-- @param #AIRBOSS self +-- @param #string _unitName Name of player unit. +function AIRBOSS:_AddF10Commands(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check for player unit. + if _unit and playername then + + -- Get group and ID. + local group=_unit:GetGroup() + local gid=group:GetID() + + if group and gid then + + if not self.menuadded[gid] then + + -- Enable switch so we don't do this twice. + self.menuadded[gid]=true + + -- Set menu root path. + local _rootPath=nil + if AIRBOSS.MenuF10Root then + ------------------------ + -- MISSON LEVEL MENUE -- + ------------------------ + + if self.menusingle then + -- F10/Airboss/... + _rootPath=AIRBOSS.MenuF10Root + else + -- F10/Airboss//... + _rootPath=missionCommands.addSubMenuForGroup(gid, self.alias, AIRBOSS.MenuF10Root) + end + + else + ------------------------ + -- GROUP LEVEL MENUES -- + ------------------------ + + -- Main F10 menu: F10/Airboss/ + if AIRBOSS.MenuF10[gid]==nil then + AIRBOSS.MenuF10[gid]=missionCommands.addSubMenuForGroup(gid, "Airboss") + end + + + if self.menusingle then + -- F10/Airboss/... + _rootPath=AIRBOSS.MenuF10[gid] + else + -- F10/Airboss//... + _rootPath=missionCommands.addSubMenuForGroup(gid, self.alias, AIRBOSS.MenuF10[gid]) + end + + end + + + -------------------------------- + -- F10/Airboss//F1 Help + -------------------------------- + local _helpPath=missionCommands.addSubMenuForGroup(gid, "Help", _rootPath) + -- F10/Airboss//F1 Help/F1 Mark Zones + if self.menumarkzones then + local _markPath=missionCommands.addSubMenuForGroup(gid, "Mark Zones", _helpPath) + -- F10/Airboss//F1 Help/F1 Mark Zones/ + if self.menusmokezones then + missionCommands.addCommandForGroup(gid, "Smoke Pattern Zones", _markPath, self._MarkCaseZones, self, _unitName, false) -- F1 + end + missionCommands.addCommandForGroup(gid, "Flare Pattern Zones", _markPath, self._MarkCaseZones, self, _unitName, true) -- F2 + if self.menusmokezones then + missionCommands.addCommandForGroup(gid, "Smoke Marshal Zone", _markPath, self._MarkMarshalZone, self, _unitName, false) -- F3 + end + missionCommands.addCommandForGroup(gid, "Flare Marshal Zone", _markPath, self._MarkMarshalZone, self, _unitName, true) -- F4 + end + -- F10/Airboss//F1 Help/F2 Skill Level + local _skillPath=missionCommands.addSubMenuForGroup(gid, "Skill Level", _helpPath) + -- F10/Airboss//F1 Help/F2 Skill Level/ + missionCommands.addCommandForGroup(gid, "Flight Student", _skillPath, self._SetDifficulty, self, _unitName, AIRBOSS.Difficulty.EASY) -- F1 + missionCommands.addCommandForGroup(gid, "Naval Aviator", _skillPath, self._SetDifficulty, self, _unitName, AIRBOSS.Difficulty.NORMAL) -- F2 + missionCommands.addCommandForGroup(gid, "TOPGUN Graduate", _skillPath, self._SetDifficulty, self, _unitName, AIRBOSS.Difficulty.HARD) -- F3 + missionCommands.addCommandForGroup(gid, "Hints On/Off", _skillPath, self._SetHintsOnOff, self, _unitName) -- F4 + -- F10/Airboss//F1 Help/ + missionCommands.addCommandForGroup(gid, "My Status", _helpPath, self._DisplayPlayerStatus, self, _unitName) -- F3 + missionCommands.addCommandForGroup(gid, "Attitude Monitor", _helpPath, self._DisplayAttitude, self, _unitName) -- F4 + missionCommands.addCommandForGroup(gid, "Radio Check LSO", _helpPath, self._LSORadioCheck, self, _unitName) -- F5 + missionCommands.addCommandForGroup(gid, "Radio Check Marshal", _helpPath, self._MarshalRadioCheck, self, _unitName) -- F6 + missionCommands.addCommandForGroup(gid, "Subtitles On/Off", _helpPath, self._SubtitlesOnOff, self, _unitName) -- F7 + missionCommands.addCommandForGroup(gid, "Trapsheet On/Off", _helpPath, self._TrapsheetOnOff, self, _unitName) -- F8 + + ------------------------------------- + -- F10/Airboss//F2 Kneeboard + ------------------------------------- + local _kneeboardPath=missionCommands.addSubMenuForGroup(gid, "Kneeboard", _rootPath) + -- F10/Airboss//F2 Kneeboard/F1 Results + local _resultsPath=missionCommands.addSubMenuForGroup(gid, "Results", _kneeboardPath) + -- F10/Airboss//F2 Kneeboard/F1 Results/ + missionCommands.addCommandForGroup(gid, "Greenie Board", _resultsPath, self._DisplayScoreBoard, self, _unitName) -- F1 + missionCommands.addCommandForGroup(gid, "My LSO Grades", _resultsPath, self._DisplayPlayerGrades, self, _unitName) -- F2 + missionCommands.addCommandForGroup(gid, "Last Debrief", _resultsPath, self._DisplayDebriefing, self, _unitName) -- F3 + + -- F10/Airboss//F2 Kneeboard/F2 Skipper/ + if self.skipperMenu then + local _skipperPath =missionCommands.addSubMenuForGroup(gid, "Skipper", _kneeboardPath) + local _menusetspeed=missionCommands.addSubMenuForGroup(gid, "Set Speed", _skipperPath) + missionCommands.addCommandForGroup(gid, "10 knots", _menusetspeed, self._SkipperRecoverySpeed, self, _unitName, 10) + missionCommands.addCommandForGroup(gid, "15 knots", _menusetspeed, self._SkipperRecoverySpeed, self, _unitName, 15) + missionCommands.addCommandForGroup(gid, "20 knots", _menusetspeed, self._SkipperRecoverySpeed, self, _unitName, 20) + missionCommands.addCommandForGroup(gid, "25 knots", _menusetspeed, self._SkipperRecoverySpeed, self, _unitName, 25) + missionCommands.addCommandForGroup(gid, "30 knots", _menusetspeed, self._SkipperRecoverySpeed, self, _unitName, 30) + local _menusetrtime=missionCommands.addSubMenuForGroup(gid, "Set Time", _skipperPath) + missionCommands.addCommandForGroup(gid, "15 min", _menusetrtime, self._SkipperRecoveryTime, self, _unitName, 15) + missionCommands.addCommandForGroup(gid, "30 min", _menusetrtime, self._SkipperRecoveryTime, self, _unitName, 30) + missionCommands.addCommandForGroup(gid, "45 min", _menusetrtime, self._SkipperRecoveryTime, self, _unitName, 45) + missionCommands.addCommandForGroup(gid, "60 min", _menusetrtime, self._SkipperRecoveryTime, self, _unitName, 60) + missionCommands.addCommandForGroup(gid, "90 min", _menusetrtime, self._SkipperRecoveryTime, self, _unitName, 90) + local _menusetrtime=missionCommands.addSubMenuForGroup(gid, "Set Marshal Radial", _skipperPath) + missionCommands.addCommandForGroup(gid, "+30°", _menusetrtime, self._SkipperRecoveryOffset, self, _unitName, 30) + missionCommands.addCommandForGroup(gid, "+15°", _menusetrtime, self._SkipperRecoveryOffset, self, _unitName, 15) + missionCommands.addCommandForGroup(gid, "0°", _menusetrtime, self._SkipperRecoveryOffset, self, _unitName, 0) + missionCommands.addCommandForGroup(gid, "-15°", _menusetrtime, self._SkipperRecoveryOffset, self, _unitName, -15) + missionCommands.addCommandForGroup(gid, "-30°", _menusetrtime, self._SkipperRecoveryOffset, self, _unitName, -30) + missionCommands.addCommandForGroup(gid, "U-turn On/Off", _skipperPath, self._SkipperRecoveryUturn, self, _unitName) + missionCommands.addCommandForGroup(gid, "Start CASE I", _skipperPath, self._SkipperStartRecovery, self, _unitName, 1) + missionCommands.addCommandForGroup(gid, "Start CASE II", _skipperPath, self._SkipperStartRecovery, self, _unitName, 2) + missionCommands.addCommandForGroup(gid, "Start CASE III",_skipperPath, self._SkipperStartRecovery, self, _unitName, 3) + missionCommands.addCommandForGroup(gid, "Stop Recovery", _skipperPath, self._SkipperStopRecovery, self, _unitName) + end + + -- F10/Airboss// + ------------------------- + missionCommands.addCommandForGroup(gid, "Request Marshal", _rootPath, self._RequestMarshal, self, _unitName) -- F3 + missionCommands.addCommandForGroup(gid, "Request Commence", _rootPath, self._RequestCommence, self, _unitName) -- F4 + missionCommands.addCommandForGroup(gid, "Request Refueling", _rootPath, self._RequestRefueling, self, _unitName) -- F5 + missionCommands.addCommandForGroup(gid, "Spinning", _rootPath, self._RequestSpinning, self, _unitName) -- F6 + missionCommands.addCommandForGroup(gid, "Emergency Landing", _rootPath, self._RequestEmergency, self, _unitName) -- F7 + missionCommands.addCommandForGroup(gid, "[Reset My Status]", _rootPath, self._ResetPlayerStatus, self, _unitName) -- F8 + end + else + self:E(self.lid..string.format("ERROR: Could not find group or group ID in AddF10Menu() function. Unit name: %s.", _unitName)) + end + else + self:E(self.lid..string.format("ERROR: Player unit does not exist in AddF10Menu() function. Unit name: %s.", _unitName)) + end + +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- SKIPPER MENU +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Reset player status. Player is removed from all queues and its status is set to undefined. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +-- @param #number case Recovery case. +function AIRBOSS:_SkipperStartRecovery(_unitName, case) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Inform player. + local text=string.format("affirm, Case %d recovery will start in 5 min for %d min. Wind on deck %d knots. U-turn=%s.", case, self.skipperTime, self.skipperSpeed, tostring(self.skipperUturn)) + if case>1 then + text=text..string.format(" Marshal radial %d°.", self.skipperOffset) + end + if self:IsRecovering() then + text="negative, carrier is already recovering." + self:MessageToPlayer(playerData, text, "AIRBOSS") + return + end + self:MessageToPlayer(playerData, text, "AIRBOSS") + + -- Recovery staring in 5 min for 30 min. + local t0=timer.getAbsTime()+5*60 + local t9=t0+self.skipperTime*60 + local C0=UTILS.SecondsToClock(t0) + local C9=UTILS.SecondsToClock(t9) + + -- Carrier will turn into the wind. Wind on deck 25 knots. U-turn on. + self:AddRecoveryWindow(C0, C9, case, self.skipperOffset, true, self.skipperSpeed, self.skipperUturn) + + end + end +end + +--- Skipper Stop recovery function. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_SkipperStopRecovery(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Inform player. + local text="roger, stopping recovery right away." + if not self:IsRecovering() then + text="negative, carrier is currently not recovering." + self:MessageToPlayer(playerData, text, "AIRBOSS") + return + end + self:MessageToPlayer(playerData, text, "AIRBOSS") + + self:RecoveryStop() + end + end +end + +--- Skipper set recovery offset angle. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +-- @param #number offset Recovery holding offset angle in degrees for Case II/III. +function AIRBOSS:_SkipperRecoveryOffset(_unitName, offset) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Inform player. + local text=string.format("roger, relative CASE II/III Marshal radial set to %d°.", offset) + self:MessageToPlayer(playerData, text, "AIRBOSS") + + self.skipperOffset=offset + end + end +end + +--- Skipper set recovery time. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +-- @param #number time Recovery time in minutes. +function AIRBOSS:_SkipperRecoveryTime(_unitName, time) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Inform player. + local text=string.format("roger, manual recovery time set to %d min.", time) + self:MessageToPlayer(playerData, text, "AIRBOSS") + + self.skipperTime=time + + end + end +end + +--- Skipper set recovery speed. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +-- @param #number speed Recovery speed in knots. +function AIRBOSS:_SkipperRecoverySpeed(_unitName, speed) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Inform player. + local text=string.format("roger, wind on deck set to %d knots.", speed) + self:MessageToPlayer(playerData, text, "AIRBOSS") + + self.skipperSpeed=speed + end + end +end + +--- Skipper set recovery speed. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_SkipperRecoveryUturn(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + self.skipperUturn=not self.skipperUturn + + -- Inform player. + local text=string.format("roger, U-turn is now %s.", tostring(self.skipperUturn)) + self:MessageToPlayer(playerData, text, "AIRBOSS") + + end + end +end + + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ROOT MENU +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Reset player status. Player is removed from all queues and its status is set to undefined. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_ResetPlayerStatus(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Inform player. + local text="roger, status reset executed! You have been removed from all queues." + self:MessageToPlayer(playerData, text, "AIRBOSS") + + -- Remove flight from queues. Collapse marshal stack if necessary. + -- Section members are removed from the Spinning queue. If flight is member, he is removed from the section. + self:_RemoveFlight(playerData) + + -- Stop pending debrief scheduler. + if playerData.debriefschedulerID and self.Scheduler then + self.Scheduler:Stop(playerData.debriefschedulerID) + end + + -- Initialize player data. + self:_InitPlayer(playerData) + + end + end +end + +--- Request marshal. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_RequestMarshal(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Check if player is in CCA + local inCCA=playerData.unit:IsInZone(self.zoneCCA) + + if inCCA then + + if self:_InQueue(self.Qmarshal, playerData.group) then + + -- Flight group is already in marhal queue. + local text=string.format("negative, you are already in the Marshal queue. New marshal request denied!") + self:MessageToPlayer(playerData, text, "MARSHAL") + + elseif self:_InQueue(self.Qpattern, playerData.group) then + + -- Flight group is already in pattern queue. + local text=string.format("negative, you are already in the Pattern queue. Marshal request denied!") + self:MessageToPlayer(playerData, text, "MARSHAL") + + elseif self:_InQueue(self.Qwaiting, playerData.group) then + + -- Flight group is already in pattern queue. + local text=string.format("negative, you are in the Waiting queue with %d flights ahead of you. Marshal request denied!", #self.Qwaiting) + self:MessageToPlayer(playerData, text, "MARSHAL") + + elseif not _unit:InAir() then + + -- Flight group is already in pattern queue. + local text=string.format("negative, you are not airborne. Marshal request denied!") + self:MessageToPlayer(playerData, text, "MARSHAL") + + elseif playerData.name~=playerData.seclead then + + -- Flight group is already in pattern queue. + local text=string.format("negative, your section lead %s needs to request Marshal.", playerData.seclead) + self:MessageToPlayer(playerData, text, "MARSHAL") + + else + + -- Get next free Marshal stack. + local freestack=self:_GetFreeStack(playerData.ai) + + -- Check if stack is available. For Case I the number is limited. + if freestack then + + -- Add flight to marshal stack. + self:_MarshalPlayer(playerData, freestack) + + else + + -- Add flight to waiting queue. + self:_WaitPlayer(playerData) + + end + + end + + else + + -- Flight group is not in CCA yet. + local text=string.format("negative, you are not inside CCA. Marshal request denied!") + self:MessageToPlayer(playerData, text, "MARSHAL") + + end + end + end +end + +--- Request emergency landing. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_RequestEmergency(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + local text="" + if not self.emergency then + + -- Mission designer did not allow emergency landing. + text="negative, no emergency landings on my carrier. We are currently busy. See how you get along!" + + elseif not _unit:InAir() then + + -- Carrier zone. + local zone=self:_GetZoneCarrierBox() + + -- Check if player is on the carrier. + if playerData.unit:IsInZone(zone) then + + -- Bolter pattern. + text="roger, you are now technically in the bolter pattern. Your next step after takeoff is abeam!" + + -- Get flight lead. + local lead=self:_GetFlightLead(playerData) + + -- Set set for lead. + self:_SetPlayerStep(lead, AIRBOSS.PatternStep.BOLTER) + + -- Also set bolter pattern for all members. + for _,sec in pairs(lead.section) do + local sectionmember=sec --#AIRBOSS.PlayerData + self:_SetPlayerStep(sectionmember, AIRBOSS.PatternStep.BOLTER) + end + + -- Remove flight from waiting queue just in case. + self:_RemoveFlightFromQueue(self.Qwaiting, lead) + + if self:_InQueue(self.Qmarshal, lead.group) then + -- Remove flight from Marshal queue and add to pattern. + self:_RemoveFlightFromMarshalQueue(lead) + else + -- Add flight to pattern if he was not. + if not self:_InQueue(self.Qpattern, lead.group) then + self:_AddFlightToPatternQueue(lead) + end + end + + else + -- Flight group is not in air. + text=string.format("negative, you are not airborne. Request denied!") + end + + else + + -- Cleared. + text="affirmative, you can bypass the pattern and are cleared for final approach!" + + -- Now, if player is in the marshal or waiting queue he will be removed. But the new leader should stay in or not. + local lead=self:_GetFlightLead(playerData) + + -- Set set for lead. + self:_SetPlayerStep(lead, AIRBOSS.PatternStep.EMERGENCY) + + -- Also set emergency landing for all members. + for _,sec in pairs(lead.section) do + local sectionmember=sec --#AIRBOSS.PlayerData + self:_SetPlayerStep(sectionmember, AIRBOSS.PatternStep.EMERGENCY) + + -- Remove flight from spinning queue just in case (everone can spin on his own). + self:_RemoveFlightFromQueue(self.Qspinning, sectionmember) + end + + -- Remove flight from waiting queue just in case. + self:_RemoveFlightFromQueue(self.Qwaiting, lead) + + if self:_InQueue(self.Qmarshal, lead.group) then + -- Remove flight from Marshal queue and add to pattern. + self:_RemoveFlightFromMarshalQueue(lead) + else + -- Add flight to pattern if he was not. + if not self:_InQueue(self.Qpattern, lead.group) then + self:_AddFlightToPatternQueue(lead) + end + end + + end + + -- Send message. + self:MessageToPlayer(playerData, text, "AIRBOSS") + + end + + end +end + +--- Request spinning. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_RequestSpinning(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + local text="" + if not self:_InQueue(self.Qpattern, playerData.group) then + + -- Player not in pattern queue. + text="negative, you have to be in the pattern to spin it!" + + elseif playerData.step==AIRBOSS.PatternStep.SPINNING then + + -- Player is already spinning. + text="negative, you are already spinning." + + -- Check if player is in the right step. + elseif not (playerData.step==AIRBOSS.PatternStep.BREAKENTRY or + playerData.step==AIRBOSS.PatternStep.EARLYBREAK or + playerData.step==AIRBOSS.PatternStep.LATEBREAK) then + + -- Player is not in the right step. + text="negative, you have to be in the right step to spin it!" + + else + + -- Set player step. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.SPINNING) + + -- Add player to spinning queue. + table.insert(self.Qspinning, playerData) + + -- 405, Spin it! Click. + local call=self:_NewRadioCall(self.LSOCall.SPINIT, "AIRBOSS", "Spin it!", self.Tmessage, playerData.onboard) + self:RadioTransmission(self.LSORadio, call, nil, nil, nil, true) + + -- Some advice. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + local text="Climb to 1200 feet and proceed to the initial again." + self:MessageToPlayer(playerData, text, "INSTRUCTOR", "") + end + + return + end + + -- Send message. + self:MessageToPlayer(playerData, text, "AIRBOSS") + + end + end +end + +--- Request to commence landing approach. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_RequestCommence(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Check if unit is in CCA. + local text="" + local cleared=false + if _unit:IsInZone(self.zoneCCA) then + + -- Get stack value. + local stack=playerData.flag + + -- Number of airborne aircraft currently in pattern. + local _,npattern=self:_GetQueueInfo(self.Qpattern) + + -- TODO: Check distance to initial or platform. Only allow commence if < max distance. Otherwise say bearing. + + if self:_InQueue(self.Qpattern, playerData.group) then + + -- Flight group is already in pattern queue. + text=string.format("negative, %s, you are already in the Pattern queue.", playerData.name) + + elseif not _unit:InAir() then + + -- Flight group is already in pattern queue. + text=string.format("negative, %s, you are not airborne.", playerData.name) + + elseif playerData.seclead~=playerData.name then + + -- Flight group is already in pattern queue. + text=string.format("negative, %s, your section leader %s has to request commence!", playerData.name, playerData.seclead) + + elseif stack>1 then + + -- We are in a higher stack. + text=string.format("negative, %s, it's not your turn yet! You are in stack no. %s.", playerData.name, stack) + + elseif npattern>=self.Nmaxpattern then + + -- Patern is full! + text=string.format("negative ghostrider, pattern is full!\nThere are %d aircraft currently in the pattern.", npattern) + + elseif self:IsRecovering()==false and not self.airbossnice then + + -- Carrier is not recovering right now. + if self.recoverywindow then + local clock=UTILS.SecondsToClock(self.recoverywindow.START) + text=string.format("negative, carrier is currently not recovery. Next window will open at %s.", clock) + else + text=string.format("negative, carrier is not recovering. No future windows planned.") + end + + elseif not self:_InQueue(self.Qmarshal, playerData.group) and not self.airbossnice then + + text="negative, you have to request Marshal before you can commence." + + else + + ----------------------- + -- Positive Response -- + ----------------------- + + text=text.."roger." + + -- Carrier is not recovering but Airboss has a good day. + if not self:IsRecovering() then + text=text.." Carrier is not recovering currently! However, you are cleared anyway as I have a nice day." + end + + -- If player is not in the Marshal queue set player case to current case. + if not self:_InQueue(self.Qmarshal, playerData.group) then + + -- Set current case. + playerData.case=self.case + + -- Hint about TACAN bearing. + if self.TACANon and playerData.difficulty~=AIRBOSS.Difficulty.HARD then + -- Get inverse magnetic radial potential offset. + local radial=self:GetRadial(playerData.case, true, true, true) + if playerData.case==1 then + -- For case 1 we want the BRC but above routine return FB. + radial=self:GetBRC() + end + text=text..string.format("\nSelect TACAN %03d°, Channel %d%s (%s).\n", radial, self.TACANchannel,self.TACANmode, self.TACANmorse) + end + + -- TODO: Inform section members. + + -- Set case of section members as well. Not sure if necessary any more since it is set as soon as the recovery case is changed. + for _,flight in pairs(playerData.section) do + flight.case=playerData.case + end + + -- Add player to pattern queue. Usually this is done when the stack is collapsed but this player is not in the Marshal queue. + self:_AddFlightToPatternQueue(playerData) + end + + -- Clear player for commence. + cleared=true + end + + else + -- This flight is not yet registered! + text=string.format("negative, %s, you are not inside the CCA!", playerData.name) + end + + -- Debug + self:T(self.lid..text) + + -- Send message. + self:MessageToPlayer(playerData, text, "MARSHAL") + + -- Check if player was cleard. Need to do this after the message above is displayed. + if cleared then + -- Call commence routine. No zone check. NOTE: Commencing will set step for all section members as well. + self:_Commencing(playerData, false) + end + end + end +end + +--- Player requests refueling. +-- @param #AIRBOSS self +-- @param #string _unitName Name of the player unit. +function AIRBOSS:_RequestRefueling(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Check if there is a recovery tanker defined. + local text + if self.tanker then + + -- Check if player is in CCA. + if _unit:IsInZone(self.zoneCCA) then + + -- Check if tanker is running or refueling or returning. + if self.tanker:IsRunning() or self.tanker:IsRefueling() then + + -- Get alt of tanker in angels. + --local angels=UTILS.Round(UTILS.MetersToFeet(self.tanker.altitude)/1000, 0) + local angels=self:_GetAngels(self.tanker.altitude) + + -- Tanker is up and running. + text=string.format("affirmative, proceed to tanker at angels %d.", angels) + + -- State TACAN channel of tanker if defined. + if self.tanker.TACANon then + text=text..string.format("\nTanker TACAN channel %d%s (%s).", self.tanker.TACANchannel, self.tanker.TACANmode, self.tanker.TACANmorse) + text=text..string.format("\nRadio frequency %.3f MHz AM.", self.tanker.RadioFreq) + end + + -- Tanker is currently refueling. Inform player. + if self.tanker:IsRefueling() then + text=text.."\nTanker is currently refueling. You might have to queue up." + end + + -- Collapse marshal stack if player is in queue. + self:_RemoveFlightFromMarshalQueue(playerData, true) + + -- Set step to refueling. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.REFUELING) + + -- Inform section and set step. + for _,sec in pairs(playerData.section) do + local sectext="follow your section leader to the tanker." + self:MessageToPlayer(sec, sectext, "MARSHAL") + self:_SetPlayerStep(sec, AIRBOSS.PatternStep.REFUELING) + end + + elseif self.tanker:IsReturning() then + -- Tanker is RTB. + text="negative, tanker is RTB. Request denied!\nWait for the tanker to be back on station if you can." + end + + else + text="negative, you are not inside the CCA yet." + end + else + text="negative, no refueling tanker available." + end + + -- Send message. + self:MessageToPlayer(playerData, text, "MARSHAL") + end + end +end + + +--- Remove a member from the player's section. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player +-- @param #AIRBOSS.PlayerData sectionmember The section member to be removed. +-- @return #boolean If true, flight was a section member and could be removed. False otherwise. +function AIRBOSS:_RemoveSectionMember(playerData, sectionmember) + -- Loop over all flights in player's section + for i,_flight in pairs(playerData.section) do + local flight=_flight --#AIRBOSS.PlayerData + if flight.name==sectionmember.name then + table.remove(playerData.section, i) + return true + end + end + return false +end + +--- Set all flights within 100 meters to be part of my section. +-- @param #AIRBOSS self +-- @param #string _unitName Name of the player unit. +function AIRBOSS:_SetSection(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Coordinate of flight lead. + local mycoord=_unit:GetCoordinate() + + -- Max distance up to which section members are allowed. + local dmax=100 + + -- Check if player is in Marshal or pattern queue already. + local text + if self.NmaxSection==0 then + text=string.format("negative, setting sections is disabled in this mission. You stay alone.") + elseif self:_InQueue(self.Qmarshal,playerData.group) then + text=string.format("negative, you are already in the Marshal queue. Setting section not possible any more!") + elseif self:_InQueue(self.Qpattern, playerData.group) then + text=string.format("negative, you are already in the Pattern queue. Setting section not possible any more!") + else + + -- Check if player is member of another section already. If so, remove him from his current section. + if playerData.seclead~=playerData.name then + local lead=self.players[playerData.seclead] --#AIRBOSS.PlayerData + if lead then + + -- Remove player from his old section lead. + local removed=self:_RemoveSectionMember(lead, playerData) + if removed then + self:MessageToPlayer(lead, string.format("Flight %s has been removed from your section.", playerData.name), "AIRBOSS", "", 5) + self:MessageToPlayer(playerData, string.format("You have been removed from %s's section.", lead.name), "AIRBOSS", "", 5) + end + + end + end + + -- Potential section members. + local section={} + + -- Loop over all registered flights. + for _,_flight in pairs(self.flights) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Only human flight groups excluding myself. Also only flights that dont have a section itself (would get messy) or are part of another section (no double membership). + if flight.ai==false and flight.groupname~=playerData.groupname and #flight.section==0 and flight.seclead==flight.name then + + -- Distance (3D) to other flight group. + local distance=flight.group:GetCoordinate():Get3DDistance(mycoord) + + -- Check distance. + if distance remove it. + if not gotit then + self:MessageToPlayer(flight, string.format("you were removed from %s's section and are on your own now.", playerData.name), "AIRBOSS", "", 5) + flight.seclead=flight.name + self:_RemoveSectionMember(playerData, flight) + end + end + + -- Remove all flights that are currently in the player's section already from scanned potential new section members. + for i,_new in pairs(section) do + local newflight=_new.flight --#AIRBOSS.PlayerData + for _,_flight in pairs(playerData.section) do + local currentflight=_flight --#AIRBOSS.PlayerData + if newflight.name==currentflight.name then + table.remove(section, i) + end + end + end + + -- Init section table. Should not be necessary as all members are removed anyhow above. + --playerData.section={} + + -- Output text. + text=string.format("Registered flight section:") + text=text..string.format("\n- %s (lead)", playerData.seclead) + -- Old members that stay (if any). + for _,_flight in pairs(playerData.section) do + local flight=_flight --#AIRBOSS.PlayerData + text=text..string.format("\n- %s", flight.name) + end + -- New members (if any). + for i=1,math.min(self.NmaxSection-#playerData.section, #section) do + local flight=section[i].flight --#AIRBOSS.PlayerData + + -- New flight members. + text=text..string.format("\n- %s", flight.name) + + -- Set section lead of player flight. + flight.seclead=playerData.name + + -- Set case of f + flight.case=playerData.case + + -- Inform player that he is now part of a section. + self:MessageToPlayer(flight, string.format("your section lead is now %s.", playerData.name), "AIRBOSS") + + -- Add flight to section table. + table.insert(playerData.section, flight) + end + + -- Section is empty. + if #playerData.section==0 then + text=text..string.format("\n- No other human flights found within radius of %.1f meters!", dmax) + end + + end + + -- Message to section lead. + self:MessageToPlayer(playerData, text, "MARSHAL") + end + end +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- RESULTS MENU +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Display top 10 player scores. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_DisplayScoreBoard(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + + -- Results table. + local _playerResults={} + + -- Calculate average points for all players. + for playerName,playerGrades in pairs(self.playerscores) do + + if playerGrades then + + -- Loop over all grades + local Paverage=0 + local n=0 + for _,_grade in pairs(playerGrades) do + local grade=_grade --#AIRBOSS.LSOgrade + + -- Add up only final scores for the average. + if grade.finalscore then --grade.points>=0 then + Paverage=Paverage+grade.finalscore + n=n+1 + else + -- Case when the player just leaves after an unfinished pass, e.g bolter, without landing. + -- But this should now be solved by deleteing all unfinished results. + end + end + + -- We dont want to devide by zero. + if n>0 then + _playerResults[playerName]=Paverage/n + end + + end + end + + -- Message text. + local text = string.format("Greenie Board (top ten):") + local i=1 + for _playerName,_points in UTILS.spairs(_playerResults, function(t, a, b) return t[b] < t[a] end) do + + -- Text. + text=text..string.format("\n[%d] %s %.1f||", i,_playerName, _points) + + -- All player grades. + local playerGrades=self.playerscores[_playerName] + + -- Add grades of passes. We use the actual grade of each pass here and not the average after player has landed. + for _,_grade in pairs(playerGrades) do + local grade=_grade --#AIRBOSS.LSOgrade + if grade.finalscore then + text=text..string.format("%.1f|", grade.points) + elseif grade.points>=0 then -- Only points >=0 as foul deck gives -1. + text=text..string.format("(%.1f)", grade.points) + end + end + + -- Display only the top ten. + i=i+1 + if i>10 then + break + end + end + + -- If no results yet. + if i==1 then + text=text.."\nNo results yet." + end + + -- Send message. + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + if playerData.client then + MESSAGE:New(text, 30, nil, true):ToClient(playerData.client) + end + + end +end + +--- Display top 10 player scores. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_DisplayPlayerGrades(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Grades of player: + local text=string.format("Your last 10 grades, %s:", _playername) + + -- All player grades. + local playerGrades=self.playerscores[_playername] or {} + + local p=0 -- Average points. + local n=0 -- Number of final passes. + local m=0 -- Number of total passes. + --for i,_grade in pairs(playerGrades) do + for i=#playerGrades,1,-1 do + --local grade=_grade --#AIRBOSS.LSOgrade + local grade=playerGrades[i] --#AIRBOSS.LSOgrade + + -- Check if points >=0. For foul deck WO we give -1 and pass is not counted. + if grade.points>=0 then + + -- Show final points or points of pass. + local points=grade.finalscore or grade.points + + -- Display max 10 results. + if m<10 then + text=text..string.format("\n[%d] %s %.1f PT - %s", i, grade.grade, points, grade.details) + + -- Wire trapped if any. + if grade.wire and grade.wire<=4 then + text=text..string.format(" %d-wire", grade.wire) + end + + -- Time in the groove if any. + if grade.Tgroove and grade.Tgroove<=360 then + text=text..string.format(" Tgroove=%.1f s", grade.Tgroove) + end + end + + -- Add up final points. + if grade.finalscore then + p=p+grade.finalscore + n=n+1 + end + + -- Total passes + m=m+1 + end + end + + + if n>0 then + text=text..string.format("\nAverage points = %.1f", p/n) + else + text=text..string.format("\nNo data available.") + end + + -- Send message. + if playerData.client then + MESSAGE:New(text, 30, nil, true):ToClient(playerData.client) + end + end + end +end + +--- Display last debriefing. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_DisplayDebriefing(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Debriefing text. + local text=string.format("Debriefing:") + + -- Check if data is present. + if #playerData.lastdebrief>0 then + text=text..string.format("\n================================\n") + for _,_data in pairs(playerData.lastdebrief) do + local step=_data.step + local comment=_data.hint + text=text..string.format("* %s:",step) + text=text..string.format("%s\n", comment) + end + else + text=text.." Nothing to show yet." + end + + -- Send debrief message to player + self:MessageToPlayer(playerData, text, nil , "", 30, true) + + end + end +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- KNEEBOARD MENU +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Display marshal or pattern queue. +-- @param #AIRBOSS self +-- @param #string _unitname Name of the player unit. +-- @param #string qname Name of the queue. +function AIRBOSS:_DisplayQueue(_unitname, qname) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Player data. + local playerData=self.players[playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Queue to display. + local queue=nil + if qname=="Marshal" then + queue=self.Qmarshal + elseif qname=="Pattern" then + queue=self.Qpattern + elseif qname=="Waiting" then + queue=self.Qwaiting + end + + -- Number of group and units in queue + local Nqueue,nqueue=self:_GetQueueInfo(queue, playerData.case) + + local text=string.format("%s Queue:", qname) + if #queue==0 then + text=text.." empty" + else + local N=0 + if qname=="Marshal" then + for i,_flight in pairs(queue) do + local flight=_flight --#AIRBOSS.FlightGroup + local charlie=self:_GetCharlieTime(flight) + local Charlie=UTILS.SecondsToClock(charlie) + local stack=flight.flag + local angels=self:_GetAngels(self:_GetMarshalAltitude(stack, flight.case)) + local _,nunit,nsec=self:_GetFlightUnits(flight, true) + local nick=self:_GetACNickname(flight.actype) + N=N+nunit + text=text..string.format("\n[Stack %d] %s (%s*%d+%d): Case %d, Angels %d, Charlie %s", stack, flight.onboard, nick, nunit, nsec, flight.case, angels, tostring(Charlie)) + end + elseif qname=="Pattern" or qname=="Waiting" then + for i,_flight in pairs(queue) do + local flight=_flight --#AIRBOSS.FlightGroup + local _,nunit,nsec=self:_GetFlightUnits(flight, true) + local nick=self:_GetACNickname(flight.actype) + local ptime=UTILS.SecondsToClock(timer.getAbsTime()-flight.time) + N=N+nunit + text=text..string.format("\n[%d] %s (%s*%d+%d): Case %d, T=%s", i, flight.onboard, nick, nunit, nsec, flight.case, ptime) + end + end + text=text..string.format("\nTotal AC: %d (airborne %d)", N, nqueue) + end + + -- Send message. + self:MessageToPlayer(playerData, text, nil, "", nil, true) + end + end +end + + +--- Report information about carrier. +-- @param #AIRBOSS self +-- @param #string _unitname Name of the player unit. +function AIRBOSS:_DisplayCarrierInfo(_unitname) + self:F2(_unitname) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Player data. + local playerData=self.players[playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Current coordinates. + local coord=self:GetCoordinate() + + -- Carrier speed and heading. + local carrierheading=self.carrier:GetHeading() + local carrierspeed=UTILS.MpsToKnots(self.carrier:GetVelocityMPS()) + + -- TACAN/ICLS. + local tacan="unknown" + local icls="unknown" + if self.TACANon and self.TACANchannel~=nil then + tacan=string.format("%d%s (%s)", self.TACANchannel, self.TACANmode, self.TACANmorse) + end + if self.ICLSon and self.ICLSchannel~=nil then + icls=string.format("%d (%s)", self.ICLSchannel, self.ICLSmorse) + end + + -- Wind on flight deck + local wind=UTILS.MpsToKnots(select(1, self:GetWindOnDeck())) + + -- Get groups, units in queues. + local Nmarshal,nmarshal = self:_GetQueueInfo(self.Qmarshal, playerData.case) + local Npattern,npattern = self:_GetQueueInfo(self.Qpattern) + local Nspinning,nspinning = self:_GetQueueInfo(self.Qspinning) + local Nwaiting,nwaiting = self:_GetQueueInfo(self.Qwaiting) + local Ntotal,ntotal = self:_GetQueueInfo(self.flights) + + -- Current abs time. + local Tabs=timer.getAbsTime() + + -- Get recovery times of carrier. + local recoverytext="Recovery time windows (max 5):" + if #self.recoverytimes==0 then + recoverytext=recoverytext.." none." + else + -- Loop over recovery windows. + local rw=0 + for _,_recovery in pairs(self.recoverytimes) do + local recovery=_recovery --#AIRBOSS.Recovery + -- Only include current and future recovery windows. + if Tabs=5 then + -- Break the loop after 5 recovery times. + break + end + end + end + end + + -- Recovery tanker TACAN text. + local tankertext=nil + if self.tanker then + tankertext=string.format("Recovery tanker frequency %.3f MHz\n", self.tanker.RadioFreq) + if self.tanker.TACANon then + tankertext=tankertext..string.format("Recovery tanker TACAN %d%s (%s)",self.tanker.TACANchannel, self.tanker.TACANmode, self.tanker.TACANmorse) + else + tankertext=tankertext.."Recovery tanker TACAN n/a" + end + end + + -- Carrier FSM state. Idle is not clear enough. + local state=self:GetState() + if state=="Idle" then + state="Deck closed" + end + if self.turning then + state=state.." (turning currently)" + end + + -- Message text. + local text=string.format("%s info:\n", self.alias) + text=text..string.format("================================\n") + text=text..string.format("Carrier state: %s\n", state) + if self.case==1 then + text=text..string.format("Case %d recovery ops\n", self.case) + else + local radial=self:GetRadial(self.case, true, true, false) + text=text..string.format("Case %d recovery ops\nMarshal radial %03d°\n", self.case, radial) + end + text=text..string.format("BRC %03d° - FB %03d°\n", self:GetBRC(), self:GetFinalBearing(true)) + text=text..string.format("Speed %.1f kts - Wind on deck %.1f kts\n", carrierspeed, wind) + text=text..string.format("Tower frequency %.3f MHz\n", self.TowerFreq) + text=text..string.format("Marshal radio %.3f MHz\n", self.MarshalFreq) + text=text..string.format("LSO radio %.3f MHz\n", self.LSOFreq) + text=text..string.format("TACAN Channel %s\n", tacan) + text=text..string.format("ICLS Channel %s\n", icls) + if tankertext then + text=text..tankertext.."\n" + end + text=text..string.format("# A/C total %d (%d)\n", Ntotal, ntotal) + text=text..string.format("# A/C marshal %d (%d)\n", Nmarshal, nmarshal) + text=text..string.format("# A/C pattern %d (%d) - spinning %d (%d)\n", Npattern, npattern, Nspinning, nspinning) + text=text..string.format("# A/C waiting %d (%d)\n", Nwaiting, nwaiting) + text=text..string.format(recoverytext) + self:T2(self.lid..text) + + -- Send message. + self:MessageToPlayer(playerData, text, nil, "", 30, true) + + else + self:E(self.lid..string.format("ERROR: Could not get player data for player %s.", playername)) + end + end + +end + + +--- Report weather conditions at the carrier location. Temperature, QFE pressure and wind data. +-- @param #AIRBOSS self +-- @param #string _unitname Name of the player unit. +function AIRBOSS:_DisplayCarrierWeather(_unitname) + self:F2(_unitname) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Message text. + local text="" + + -- Current coordinates. + local coord=self:GetCoordinate() + + -- Get atmospheric data at carrier location. + local T=coord:GetTemperature() + local P=coord:GetPressure() + + -- Get wind direction (magnetic) and strength. + local Wd,Ws=self:GetWind(nil, true) + + -- Get Beaufort wind scale. + local Bn,Bd=UTILS.BeaufortScale(Ws) + + -- Wind on flight deck. + local WodPA,WodPP=self:GetWindOnDeck() + local WodPA=UTILS.MpsToKnots(WodPA) + local WodPP=UTILS.MpsToKnots(WodPP) + + local WD=string.format('%03d°', Wd) + local Ts=string.format("%d°C",T) + + local tT=string.format("%d°C",T) + local tW=string.format("%.1f knots", UTILS.MpsToKnots(Ws)) + local tP=string.format("%.2f inHg", UTILS.hPa2inHg(P)) + + -- Report text. + text=text..string.format("Weather Report at Carrier %s:\n", self.alias) + text=text..string.format("================================\n") + text=text..string.format("Temperature %s\n", tT) + text=text..string.format("Wind from %s at %s (%s)\n", WD, tW, Bd) + text=text..string.format("Wind on deck || %.1f kts, == %.1f kts\n", WodPA, WodPP) + text=text..string.format("QFE %.1f hPa = %s", P, tP) + + -- More info only reliable if Mission uses static weather. + if self.staticweather then + local clouds, visibility, fog, dust=self:_GetStaticWeather() + text=text..string.format("\nVisibility %.1f NM", UTILS.MetersToNM(visibility)) + text=text..string.format("\nCloud base %d ft", UTILS.MetersToFeet(clouds.base)) + text=text..string.format("\nCloud thickness %d ft", UTILS.MetersToFeet(clouds.thickness)) + text=text..string.format("\nCloud density %d", clouds.density) + text=text..string.format("\nPrecipitation %d", clouds.iprecptns) + if fog then + text=text..string.format("\nFog thickness %d ft", UTILS.MetersToFeet(fog.thickness)) + text=text..string.format("\nFog visibility %d ft", UTILS.MetersToFeet(fog.visibility)) + else + text=text..string.format("\nNo fog") + end + if dust then + text=text..string.format("\nDust density %d", dust) + else + text=text..string.format("\nNo dust") + end + end + + -- Debug output. + self:T2(self.lid..text) + + -- Send message to player group. + self:MessageToPlayer(self.players[playername], text, nil, "", 30, true) + + else + self:E(self.lid..string.format("ERROR! Could not find player unit in CarrierWeather! Unit name = %s", _unitname)) + end +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- HELP MENU +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set difficulty level. +-- @param #AIRBOSS self +-- @param #string _unitname Name of the player unit. +-- @param #AIRBOSS.Difficulty difficulty Difficulty level. +function AIRBOSS:_SetDifficulty(_unitname, difficulty) + self:T2({difficulty=difficulty, unitname=_unitname}) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Player data. + local playerData=self.players[playername] --#AIRBOSS.PlayerData + + if playerData then + playerData.difficulty=difficulty + local text=string.format("roger, your skill level is now: %s.", difficulty) + self:MessageToPlayer(playerData, text, nil, playerData.name, 5) + else + self:E(self.lid..string.format("ERROR: Could not get player data for player %s.", playername)) + end + + -- Set hints as well. + if playerData.difficulty==AIRBOSS.Difficulty.HARD then + playerData.showhints=false + else + playerData.showhints=true + end + + end +end + +--- Turn player's aircraft attitude display on or off. +-- @param #AIRBOSS self +-- @param #string _unitname Name of the player unit. +function AIRBOSS:_SetHintsOnOff(_unitname) + self:F2(_unitname) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Player data. + local playerData=self.players[playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Invert hints. + playerData.showhints=not playerData.showhints + + -- Inform player. + local text="" + if playerData.showhints==true then + text=string.format("roger, hints are now ON.") + else + text=string.format("affirm, hints are now OFF.") + end + self:MessageToPlayer(playerData, text, nil, playerData.name, 5) + + end + end +end + +--- Turn player's aircraft attitude display on or off. +-- @param #AIRBOSS self +-- @param #string _unitname Name of the player unit. +function AIRBOSS:_DisplayAttitude(_unitname) + self:F2(_unitname) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Player data. + local playerData=self.players[playername] --#AIRBOSS.PlayerData + + if playerData then + playerData.attitudemonitor=not playerData.attitudemonitor + end + end + +end + +--- Turn radio subtitles of player on or off. +-- @param #AIRBOSS self +-- @param #string _unitname Name of the player unit. +function AIRBOSS:_SubtitlesOnOff(_unitname) + self:F2(_unitname) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Player data. + local playerData=self.players[playername] --#AIRBOSS.PlayerData + + if playerData then + playerData.subtitles=not playerData.subtitles + -- Inform player. + local text="" + if playerData.subtitles==true then + text=string.format("roger, subtitiles are now ON.") + elseif playerData.subtitles==false then + text=string.format("affirm, subtitiles are now OFF.") + end + self:MessageToPlayer(playerData, text, nil, playerData.name, 5) + end + end + +end + +--- Turn radio subtitles of player on or off. +-- @param #AIRBOSS self +-- @param #string _unitname Name of the player unit. +function AIRBOSS:_TrapsheetOnOff(_unitname) + self:F2(_unitname) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Player data. + local playerData=self.players[playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Check if option is enabled at all. + local text="" + if self.trapsheet then + + -- Invert current setting. + playerData.trapon=not playerData.trapon + + -- Inform player. + if playerData.trapon==true then + text=string.format("roger, your trapsheets are now SAVED.") + else + text=string.format("affirm, your trapsheets are NOT SAVED.") + end + + else + text="negative, trap sheet data recorder is broken on this carrier." + end + + -- Message to player. + self:MessageToPlayer(playerData, text, nil, playerData.name, 5) + end + end + +end + + +--- Display player status. +-- @param #AIRBOSS self +-- @param #string _unitName Name of the player unit. +function AIRBOSS:_DisplayPlayerStatus(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Pattern step text. + local steptext=playerData.step + if playerData.step==AIRBOSS.PatternStep.HOLDING then + if playerData.holding==nil then + steptext="Transit to Marshal" + elseif playerData.holding==false then + steptext="Marshal (outside zone)" + elseif playerData.holding==true then + steptext="Marshal Stack Holding" + end + end + + -- Stack. + local stack=playerData.flag + + -- Stack text. + local stacktext=nil + if stack>0 then + local stackalt=self:_GetMarshalAltitude(stack) + local angels=self:_GetAngels(stackalt) + stacktext=string.format("Marshal Stack %d, Angels %d\n", stack, angels) + + + -- Hint about TACAN bearing. + if playerData.step==AIRBOSS.PatternStep.HOLDING and playerData.case>1 then + -- Get inverse magnetic radial potential offset. + local radial=self:GetRadial(playerData.case, true, true, true) + stacktext=stacktext..string.format("Select TACAN %03d°, %d DME\n", radial, angels+15) + end + end + + -- Fuel and fuel state. + local fuel=playerData.unit:GetFuel()*100 + local fuelstate=self:_GetFuelState(playerData.unit) + + -- Number of units in group. + local _,nunitsGround=self:_GetFlightUnits(playerData, true) + local _,nunitsAirborne=self:_GetFlightUnits(playerData, false) + + -- Player data. + local text=string.format("Status of player %s (%s)\n", playerData.name, playerData.callsign) + text=text..string.format("================================\n") + text=text..string.format("Step: %s\n", steptext) + if stacktext then + text=text..stacktext + end + text=text..string.format("Recovery Case: %d\n", playerData.case) + text=text..string.format("Skill Level: %s\n", playerData.difficulty) + text=text..string.format("Modex: %s (%s)\n", playerData.onboard, self:_GetACNickname(playerData.actype)) + text=text..string.format("Fuel State: %.1f lbs/1000 (%.1f %%)\n", fuelstate/1000, fuel) + text=text..string.format("# units: %d (%d airborne)\n", nunitsGround, nunitsAirborne) + text=text..string.format("Section Lead: %s (%d/%d)", tostring(playerData.seclead), #playerData.section+1, self.NmaxSection+1) + for _,_sec in pairs(playerData.section) do + local sec=_sec --#AIRBOSS.PlayerData + text=text..string.format("\n- %s", sec.name) + end + + if playerData.step==AIRBOSS.PatternStep.INITIAL then + + -- Create a point 3.0 NM astern for re-entry. + local zoneinitial=self:GetCoordinate():Translate(UTILS.NMToMeters(3.5), self:GetRadial(2, false, false, false)) + + -- Heading and distance to initial zone. + local flyhdg=playerData.unit:GetCoordinate():HeadingTo(zoneinitial) + local flydist=UTILS.MetersToNM(playerData.unit:GetCoordinate():Get2DDistance(zoneinitial)) + local brc=self:GetBRC() + + -- Help player to find its way to the initial zone. + text=text..string.format("\nTo Initial: Fly heading %03d° for %.1f NM and turn to BRC %03d°", flyhdg, flydist, brc) + + elseif playerData.step==AIRBOSS.PatternStep.PLATFORM then + + -- Coordinate of the platform zone. + local zoneplatform=self:_GetZonePlatform(playerData.case):GetCoordinate() + + -- Heading and distance to platform zone. + local flyhdg=playerData.unit:GetCoordinate():HeadingTo(zoneplatform) + local flydist=UTILS.MetersToNM(playerData.unit:GetCoordinate():Get2DDistance(zoneplatform)) + + -- Get heading. + local hdg=self:GetRadial(playerData.case, true, true, true) + + -- Help player to find its way to the initial zone. + text=text..string.format("\nTo Platform: Fly heading %03d° for %.1f NM and turn to %03d°", flyhdg, flydist, hdg) + + end + + -- Send message. + self:MessageToPlayer(playerData, text, nil, "", 30, true) + else + self:E(self.lid..string.format("ERROR: playerData=nil. Unit name=%s, player name=%s", _unitName, _playername)) + end + else + self:E(self.lid..string.format("ERROR: could not find player for unit %s", _unitName)) + end + +end + +--- Mark current marshal zone of player by either smoke or flares. +-- @param #AIRBOSS self +-- @param #string _unitName Name of the player unit. +-- @param #boolean flare If true, flare the zone. If false, smoke the zone. +function AIRBOSS:_MarkMarshalZone(_unitName, flare) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Get player stack and recovery case. + local stack=playerData.flag + local case=playerData.case + + local text="" + if stack>0 then + + -- Get current holding zone. + local zoneHolding=self:_GetZoneHolding(case, stack) + + -- Get Case I commence zone at three position. + local zoneThree=self:_GetZoneCommence(case, stack) + + -- Pattern alitude. + local patternalt=self:_GetMarshalAltitude(stack, case) + + -- Flare and smoke at the ground. + patternalt=5 + + -- Roger! + text="roger, marking" + if flare then + + -- Marshal WHITE flares. + text=text..string.format("\n* Marshal zone stack %d with WHITE flares.", stack) + zoneHolding:FlareZone(FLARECOLOR.White, 45, nil, patternalt) + + -- Commence RED flares. + text=text.."\n* Commence zone with RED flares." + zoneThree:FlareZone(FLARECOLOR.Red, 45, nil, patternalt) + + else + + -- Marshal WHITE smoke. + text=text..string.format("\n* Marshal zone stack %d with WHITE smoke.", stack) + zoneHolding:SmokeZone(SMOKECOLOR.White, 45, patternalt) + + -- Commence RED smoke + text=text.."\n* Commence zone with RED smoke." + zoneThree:SmokeZone(SMOKECOLOR.Red, 45, patternalt) + + end + + else + text="negative, you are currently not in a Marshal stack. No zones will be marked!" + end + + -- Send message to player. + self:MessageToPlayer(playerData, text, "MARSHAL", playerData.name) + end + end + +end + + +--- Mark CASE I or II/II zones by either smoke or flares. +-- @param #AIRBOSS self +-- @param #string _unitName Name of the player unit. +-- @param #boolean flare If true, flare the zone. If false, smoke the zone. +function AIRBOSS:_MarkCaseZones(_unitName, flare) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Player's recovery case. + local case=playerData.case + + -- Initial + local text=string.format("affirm, marking CASE %d zones", case) + + -- Flare or smoke? + if flare then + + ----------- + -- Flare -- + ----------- + + -- Case I/II: Initial + if case==1 or case==2 then + text=text.."\n* initial with GREEN flares" + self:_GetZoneInitial(case):FlareZone(FLARECOLOR.Green, 45) + end + + -- Case II/III: approach corridor + if case==2 or case==3 then + text=text.."\n* approach corridor with GREEN flares" + self:_GetZoneCorridor(case):FlareZone(FLARECOLOR.Green, 45) + end + + -- Case II/III: platform + if case==2 or case==3 then + text=text.."\n* platform with RED flares" + self:_GetZonePlatform(case):FlareZone(FLARECOLOR.Red, 45) + end + + -- Case III: dirty up + if case==3 then + text=text.."\n* dirty up with YELLOW flares" + self:_GetZoneDirtyUp(case):FlareZone(FLARECOLOR.Yellow, 45) + end + + -- Case II/III: arc in/out + if case==2 or case==3 then + if math.abs(self.holdingoffset)>0 then + self:_GetZoneArcIn(case):FlareZone(FLARECOLOR.White, 45) + text=text.."\n* arc turn in with WHITE flares" + self:_GetZoneArcOut(case):FlareZone(FLARECOLOR.White, 45) + text=text.."\n* arc trun out with WHITE flares" + end + end + + -- Case III: bullseye + if case==3 then + text=text.."\n* bullseye with GREEN flares" + self:_GetZoneBullseye(case):FlareZone(FLARECOLOR.Green, 45) + end + + -- Tarawa, LHA and LHD landing spots. + if self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS then + text=text.."\n* abeam landing stop with RED flares" + -- Abeam landing spot zone. + local ALSPT=self:_GetZoneAbeamLandingSpot() + ALSPT:FlareZone(FLARECOLOR.Red, 5, nil, UTILS.FeetToMeters(110)) + -- Primary landing spot zone. + text=text.."\n* primary landing spot with GREEN flares" + local LSPT=self:_GetZoneLandingSpot() + LSPT:FlareZone(FLARECOLOR.Green, 5, nil, self.carrierparam.deckheight) + end + + else + + ----------- + -- Smoke -- + ----------- + + -- Case I/II: Initial + if case==1 or case==2 then + text=text.."\n* initial with GREEN smoke" + self:_GetZoneInitial(case):SmokeZone(SMOKECOLOR.Green, 45) + end + + -- Case II/III: Approach Corridor + if case==2 or case==3 then + text=text.."\n* approach corridor with GREEN smoke" + self:_GetZoneCorridor(case):SmokeZone(SMOKECOLOR.Green, 45) + end + + -- Case II/III: platform + if case==2 or case==3 then + text=text.."\n* platform with RED smoke" + self:_GetZonePlatform(case):SmokeZone(SMOKECOLOR.Red, 45) + end + + -- Case II/III: arc in/out if offset>0. + if case==2 or case==3 then + if math.abs(self.holdingoffset)>0 then + self:_GetZoneArcIn(case):SmokeZone(SMOKECOLOR.Blue, 45) + text=text.."\n* arc turn in with BLUE smoke" + self:_GetZoneArcOut(case):SmokeZone(SMOKECOLOR.Blue, 45) + text=text.."\n* arc trun out with BLUE smoke" + end + end + + -- Case III: dirty up + if case==3 then + text=text.."\n* dirty up with ORANGE smoke" + self:_GetZoneDirtyUp(case):SmokeZone(SMOKECOLOR.Orange, 45) + end + + -- Case III: bullseye + if case==3 then + text=text.."\n* bullseye with GREEN smoke" + self:_GetZoneBullseye(case):SmokeZone(SMOKECOLOR.Green, 45) + end + + end + + -- Send message to player. + self:MessageToPlayer(playerData, text, "MARSHAL", playerData.name) + end + end + +end + +--- LSO radio check. Will broadcase LSO message at given LSO frequency. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_LSORadioCheck(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + if playerData then + -- Broadcase LSO radio check message on LSO radio. + self:RadioTransmission(self.LSORadio, self.LSOCall.RADIOCHECK, nil, nil, nil, true) + end + end +end + +--- Marshal radio check. Will broadcase Marshal message at given Marshal frequency. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_MarshalRadioCheck(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + if playerData then + -- Broadcase Marshal radio check message on Marshal radio. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.RADIOCHECK, nil, nil, nil, true) + end + end +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ +-- Persistence Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + +--- Save trapsheet data. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #AIRBOSS.LSOgrade grade LSO grad data. +function AIRBOSS:_SaveTrapSheet(playerData, grade) + + -- Nothing to save. + if playerData.trapsheet==nil or #playerData.trapsheet==0 or not io then + return + end + + --- Function that saves data to file + local function _savefile(filename, data) + local f = io.open(filename, "wb") + if f then + f:write(data) + f:close() + else + self:E(self.lid..string.format("ERROR: could not save trap sheet to file %s.\nFile may contain invalid characters.", tostring(filename))) + end + end + + -- Set path or default. + local path=self.trappath + if lfs then + path=path or lfs.writedir() + end + + + -- Create unused file name. + local filename=nil + for i=1,9999 do + + -- Create file name + if self.trapprefix then + filename=string.format("%s_%s-%04d.csv", self.trapprefix, playerData.actype, i) + else + local name=UTILS.ReplaceIllegalCharacters(playerData.name, "_") + filename=string.format("AIRBOSS-%s_Trapsheet-%s_%s-%04d.csv", self.alias, name, playerData.actype, i) + end + + -- Set path. + if path~=nil then + filename=path.."\\"..filename + end + + -- Check if file exists. + local _exists=UTILS.FileExists(filename) + if not _exists then + break + end + end + + + -- Info + local text=string.format("Saving player %s trapsheet to file %s", playerData.name, filename) + self:I(self.lid..text) + + -- Header line + local data="#Time,Rho,X,Z,Alt,AoA,GSE,LUE,Vtot,Vy,Gamma,Pitch,Roll,Yaw,Step,Grade,Points,Details\n" + + local g0=playerData.trapsheet[1] --#AIRBOSS.GrooveData + local T0=g0.Time + + --for _,_groove in ipairs(playerData.trapsheet) do + for i=1,#playerData.trapsheet do + --local groove=_groove --#AIRBOSS.GrooveData + local groove=playerData.trapsheet[i] + local t=groove.Time-T0 + local a=UTILS.MetersToNM(groove.Rho or 0) + local b=-groove.X or 0 + local c=groove.Z or 0 + local d=UTILS.MetersToFeet(groove.Alt or 0) + local e=groove.AoA or 0 + local f=groove.GSE or 0 + local g=-groove.LUE or 0 + local h=UTILS.MpsToKnots(groove.Vel or 0) + local i=(groove.Vy or 0)*196.85 + local j=groove.Gamma or 0 + local k=groove.Pitch or 0 + local l=groove.Roll or 0 + local m=groove.Yaw or 0 + local n=self:_GS(groove.Step, -1) or "n/a" + local o=groove.Grade or "n/a" + local p=groove.GradePoints or 0 + local q=groove.GradeDetail or "n/a" + -- t a b c d e f g h i j k l m n o p q + data=data..string.format("%.2f,%.3f,%.1f,%.1f,%.1f,%.2f,%.2f,%.2f,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f,%s,%s,%.1f,%s\n",t,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q) + end + + -- Save file. + _savefile(filename, data) +end + +--- On before "Save" event. Checks if io and lfs are available. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string path (Optional) Path where the file is saved. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if the lfs module is desanitized. +-- @param #string filename (Optional) File name for saving the player grades. Default is "AIRBOSS-_LSOgrades.csv". +function AIRBOSS:onbeforeSave(From, Event, To, path, filename) + + -- Check io module is available. + if not io then + self:E(self.lid.."ERROR: io not desanitized. Can't save player grades.") + return false + end + + -- Check default path. + if path==nil and not lfs then + self:E(self.lid.."WARNING: lfs not desanitized. Results will be saved in DCS installation root directory rather than your \"Saved Games\\DCS\" folder.") + end + + return true +end + +--- On after "Save" event. Player data is saved to file. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string path Path where the file is saved. If nil, file is saved in the DCS root installtion directory or your "Saved Games" folder if lfs was desanitized. +-- @param #string filename (Optional) File name for saving the player grades. Default is "AIRBOSS-_LSOgrades.csv". +function AIRBOSS:onafterSave(From, Event, To, path, filename) + + --- Function that saves data to file + local function _savefile(filename, data) + local f = assert(io.open(filename, "wb")) + f:write(data) + f:close() + end + + -- Set path or default. + if lfs then + path=path or lfs.writedir() + end + + -- Set file name. + filename=filename or string.format("AIRBOSS-%s_LSOgrades.csv", self.alias) + + -- Set path. + if path~=nil then + filename=path.."\\"..filename + end + + -- Header line + local scores="Name,Pass,Points Final,Points Pass,Grade,Details,Wire,Tgroove,Case,Wind,Modex,Airframe,Carrier Type,Carrier Name,Theatre,Mission Time,Mission Date,OS Date\n" + + -- Loop over all players. + local n=0 + for playername,grades in pairs(self.playerscores) do + + -- Loop over player grades table. + for i,_grade in pairs(grades) do + local grade=_grade --#AIRBOSS.LSOgrade + + -- Check some stuff that could be nil. + local wire="n/a" + if grade.wire and grade.wire<=4 then + wire=tostring(grade.wire) + end + + local Tgroove="n/a" + if grade.Tgroove and grade.Tgroove<=360 and grade.case<3 then + Tgroove=tostring(UTILS.Round(grade.Tgroove, 1)) + end + + local finalscore="n/a" + if grade.finalscore then + finalscore=tostring(UTILS.Round(grade.finalscore, 1)) + end + + -- Compile grade line. + scores=scores..string.format("%s,%d,%s,%.1f,%s,%s,%s,%s,%d,%s,%s,%s,%s,%s,%s,%s,%s,%s\n", + playername, i, finalscore, grade.points, grade.grade, grade.details, wire, Tgroove, grade.case, + grade.wind, grade.modex, grade.airframe, grade.carriertype, grade.carriername, grade.theatre, grade.mitime, grade.midate, grade.osdate) + n=n+1 + end + end + + -- Info + local text=string.format("Saving %d player LSO grades to file %s", n, filename) + self:I(self.lid..text) + + -- Save file. + _savefile(filename, scores) +end + + +--- On before "Load" event. Checks if the file that the player grades from exists. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string path (Optional) Path where the file is loaded from. Default is the DCS installation root directory or your "Saved Games\\DCS" folder if lfs was desanizized. +-- @param #string filename (Optional) File name for saving the player grades. Default is "AIRBOSS-_LSOgrades.csv". +function AIRBOSS:onbeforeLoad(From, Event, To, path, filename) + + --- Function that check if a file exists. + local function _fileexists(name) + local f=io.open(name,"r") + if f~=nil then + io.close(f) + return true + else + return false + end + end + + -- Check io module is available. + if not io then + self:E(self.lid.."WARNING: io not desanitized. Can't load player grades.") + return false + end + + -- Check default path. + if path==nil and not lfs then + self:E(self.lid.."WARNING: lfs not desanitized. Results will be saved in DCS installation root directory rather than your \"Saved Games\\DCS\" folder.") + end + + -- Set path or default. + if lfs then + path=path or lfs.writedir() + end + + -- Set file name. + filename=filename or string.format("AIRBOSS-%s_LSOgrades.csv", self.alias) + + -- Set path. + if path~=nil then + filename=path.."\\"..filename + end + + -- Check if file exists. + local exists=_fileexists(filename) + + if exists then + return true + else + self:E(self.lid..string.format("WARNING: Player LSO grades file %s does not exist.", filename)) + return false + end + +end + + +--- On after "Load" event. Loads grades of all players from file. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string path Path where the file is loaded from. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if lfs was desanizied. +-- @param #string filename (Optional) File name for saving the player grades. Default is "AIRBOSS-_LSOgrades.csv". +function AIRBOSS:onafterLoad(From, Event, To, path, filename) + + --- Function that load data from a file. + local function _loadfile(filename) + local f=assert(io.open(filename, "rb")) + local data=f:read("*all") + f:close() + return data + end + + -- Set path or default. + if lfs then + path=path or lfs.writedir() + end + + -- Set file name. + filename=filename or string.format("AIRBOSS-%s_LSOgrades.csv", self.alias) + + -- Set path. + if path~=nil then + filename=path.."\\"..filename + end + + -- Info message. + local text=string.format("Loading player LSO grades from file %s", filename) + MESSAGE:New(text,10):ToAllIf(self.Debug) + self:I(self.lid..text) + + -- Load asset data from file. + local data=_loadfile(filename) + + -- Split by line break. + local playergrades=UTILS.Split(data,"\n") + + -- Remove first header line. + table.remove(playergrades, 1) + + -- Init player scores table. + self.playerscores={} + + -- Loop over all lines. + local n=0 + for _,gradeline in pairs(playergrades) do + + -- Parameters are separated by commata. + local gradedata=UTILS.Split(gradeline, ",") + + -- Debug info. + self:T2(gradedata) + + -- Grade table + local grade={} --#AIRBOSS.LSOgrade + + --- Line format: + -- playername, i, grade.finalscore, grade.points, grade.grade, grade.details, wire, Tgroove, case, + -- time, wind, airframe, modex, carriertype, carriername, theatre, date + local playername=gradedata[1] + if gradedata[3]~=nil and gradedata[3]~="n/a" then + grade.finalscore=tonumber(gradedata[3]) + end + grade.points=tonumber(gradedata[4]) + grade.grade=tostring(gradedata[5]) + grade.details=tostring(gradedata[6]) + if gradedata[7]~=nil and gradedata[7]~="n/a" then + grade.wire=tonumber(gradedata[7]) + end + if gradedata[8]~=nil and gradedata[8]~="n/a" then + grade.Tgroove=tonumber(gradedata[8]) + end + grade.case=tonumber(gradedata[9]) + -- new + grade.wind=gradedata[10] or "n/a" + grade.modex=gradedata[11] or "n/a" + grade.airframe=gradedata[12] or "n/a" + grade.carriertype=gradedata[13] or "n/a" + grade.carriername=gradedata[14] or "n/a" + grade.theatre=gradedata[15] or "n/a" + grade.mitime=gradedata[16] or "n/a" + grade.midate=gradedata[17] or "n/a" + grade.osdate=gradedata[18] or "n/a" + + -- Init player table if necessary. + self.playerscores[playername]=self.playerscores[playername] or {} + + -- Add grade to table. + table.insert(self.playerscores[playername], grade) + + n=n+1 + + -- Debug info. + self:T2({playername, self.playerscores[playername]}) + end + + -- Info message. + local text=string.format("Loaded %d player LSO grades from file %s", n, filename) + self:I(self.lid..text) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ +--- **Ops** - Recovery tanker for carrier operations. +-- +-- Tanker aircraft flying a racetrack pattern overhead an aircraft carrier. +-- +-- **Main Features:** +-- +-- * Regular pattern update with respect to carrier position. +-- * No restrictions regarding carrier waypoints and heading. +-- * Automatic respawning when tanker runs out of fuel for 24/7 operations. +-- * Tanker can be spawned cold or hot on the carrier or at any other airbase or directly in air. +-- * Automatic AA TACAN beacon setting. +-- * Multiple tankers at the same carrier. +-- * Multiple carriers due to object oriented approach. +-- * Finite State Machine (FSM) implementation, which allows the mission designer to hook into certain events. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- ### Special thanks to **HighwaymanEd** for testing and suggesting improvements! +-- +-- @module Ops.RecoveryTanker +-- @image Ops_RecoveryTanker.png + +--- RECOVERYTANKER class. +-- @type RECOVERYTANKER +-- @field #string ClassName Name of the class. +-- @field #boolean Debug Debug mode. +-- @field #string lid Log debug id text. +-- @field Wrapper.Unit#UNIT carrier The carrier the tanker is attached to. +-- @field #string carriertype Carrier type. +-- @field #string tankergroupname Name of the late activated tanker template group. +-- @field Wrapper.Group#GROUP tanker Tanker group. +-- @field Wrapper.Airbase#AIRBASE airbase The home airbase object of the tanker. Normally the aircraft carrier. +-- @field Core.Radio#BEACON beacon Tanker TACAN beacon. +-- @field #number TACANchannel TACAN channel. Default 1. +-- @field #string TACANmode TACAN mode, i.e. "X" or "Y". Default "Y". Use only "Y" for AA TACAN stations! +-- @field #string TACANmorse TACAN morse code. Three letters identifying the TACAN station. Default "TKR". +-- @field #boolean TACANon If true, TACAN is automatically activated. If false, TACAN is disabled. +-- @field #number RadioFreq Radio frequency in MHz of the tanker. Default 251 MHz. +-- @field #string RadioModu Radio modulation "AM" or "FM". Default "AM". +-- @field #number speed Tanker speed when flying pattern. +-- @field #number altitude Tanker orbit pattern altitude. +-- @field #number distStern Race-track distance astern. distStern is <0. +-- @field #number distBow Race-track distance bow. distBow is >0. +-- @field #number Dupdate Pattern update when carrier changes its position by more than this distance (meters). +-- @field #number Hupdate Pattern update when carrier changes its heading by more than this number (degrees). +-- @field #number dTupdate Minimum time interval in seconds before the next pattern update can happen. +-- @field #number Tupdate Last time the pattern was updated. +-- @field #number takeoff Takeoff type (cold, hot, air). +-- @field #number lowfuel Low fuel threshold in percent. +-- @field #boolean respawn If true, tanker be respawned (default). If false, no respawning will happen. +-- @field #boolean respawninair If true, tanker will always be respawned in air. This has no impact on the initial spawn setting. +-- @field #boolean uncontrolledac If true, use and uncontrolled tanker group already present in the mission. +-- @field DCS#Vec3 orientation Orientation of the carrier. Used to monitor changes and update the pattern if heading changes significantly. +-- @field DCS#Vec3 orientlast Orientation of the carrier for checking if carrier is currently turning. +-- @field Core.Point#COORDINATE position Position of carrier. Used to monitor if carrier significantly changed its position and then update the tanker pattern. +-- @field #string alias Alias of the spawn group. +-- @field #number uid Unique ID of this tanker. +-- @field #boolean awacs If true, the groups gets the enroute task AWACS instead of tanker. +-- @field #number callsignname Number for the callsign name. +-- @field #number callsignnumber Number of the callsign name. +-- @field #string modex Tail number of the tanker. +-- @field #boolean eplrs If true, enable data link, e.g. if used as AWACS. +-- @field #boolean recovery If true, tanker will recover using the AIRBOSS marshal pattern. +-- @field #number terminaltype Terminal type of used parking spots on airbases. +-- @extends Core.Fsm#FSM + +--- Recovery Tanker. +-- +-- === +-- +-- ![Banner Image](..\Presentations\RECOVERYTANKER\RecoveryTanker_Main.png) +-- +-- # Recovery Tanker +-- +-- A recovery tanker acts as refueling unit flying overhead an aircraft carrier in order to supply incoming flights with gas if they go "*Bingo on the Ball*". +-- +-- # Simple Script +-- +-- In the mission editor you have to set up a carrier unit, which will act as "mother". In the following, this unit will be named **"USS Stennis"**. +-- +-- Secondly, you need to define a recovery tanker group in the mission editor and set it to **"LATE ACTIVATED"**. The name of the group we'll use is **"Texaco"**. +-- +-- The basic script is very simple and consists of only two lines: +-- +-- TexacoStennis=RECOVERYTANKER:New(UNIT:FindByName("USS Stennis"), "Texaco") +-- TexacoStennis:Start() +-- +-- The first line will create a new RECOVERYTANKER object and the second line starts the process. +-- +-- With this setup, the tanker will be spawned on the USS Stennis with running engines. After it takes off, it will fly a position ~10 NM astern of the boat and from there start its +-- pattern. This is a counter clockwise racetrack pattern at angels 6. +-- +-- A TACAN beacon will be automatically activated at channel 1Y with morse code "TKR". See below how to change this setting. +-- +-- Note that the Tanker entry in the F10 radio menu will appear once the tanker is on station and not before. If you spawn the tanker cold or hot on the carrier, this will take ~10 minutes. +-- +-- Also note, that currently the only carrier capable aircraft in DCS is the S-3B Viking (tanker version). If you want to use another refueling aircraft, you need to activate air spawn +-- or set a different land based airport of the map. This will be explained below. +-- +-- ![Banner Image](..\Presentations\RECOVERYTANKER\RecoveryTanker_Pattern.jpg) +-- +-- The "downwind" leg of the pattern is normally used for refueling. +-- +-- Once the tanker runs out of fuel itself, it will return to the carrier, respawn with full fuel and take up its pattern again. +-- +-- # Options and Fine Tuning +-- +-- Several parameters can be customized by the mission designer via user API functions. +-- +-- ## Takeoff Type +-- +-- By default, the tanker is spawned with running engines on the carrier. The mission designer has set option to set the take off type via the @{#RECOVERYTANKER.SetTakeoff} function. +-- Or via shortcuts +-- +-- * @{#RECOVERYTANKER.SetTakeoffHot}(): Will set the takeoff to hot, which is also the default. +-- * @{#RECOVERYTANKER.SetTakeoffCold}(): Will set the takeoff type to cold, i.e. with engines off. +-- * @{#RECOVERYTANKER.SetTakeoffAir}(): Will set the takeoff type to air, i.e. the tanker will be spawned in air ~10 NM astern the carrier. +-- +-- For example, +-- TexacoStennis=RECOVERYTANKER:New(UNIT:FindByName("USS Stennis"), "Texaco") +-- TexacoStennis:SetTakeoffAir() +-- TexacoStennis:Start() +-- will spawn the tanker several nautical miles astern the carrier. From there it will start its pattern. +-- +-- Spawning in air is not as realistic but can be useful do avoid DCS bugs and shortcomings like aircraft crashing into each other on the flight deck. +-- +-- **Note** that when spawning in air is set, the tanker will also not return to the boat, once it is out of fuel. Instead it will be respawned directly in air. +-- +-- If only the first spawning should happen on the carrier, one use the @{#RECOVERYTANKER.SetRespawnInAir}() function to command that all subsequent spawning +-- will happen in air. +-- +-- If the tanker should not be respawned at all, one can set @{#RECOVERYTANKER.SetRespawnOff}(). +-- +-- ## Pattern Parameters +-- +-- The racetrack pattern parameters can be fine tuned via the following functions: +-- +-- * @{#RECOVERYTANKER.SetAltitude}(*altitude*), where *altitude* is the pattern altitude in feet. Default 6000 ft. +-- * @{#RECOVERYTANKER.SetSpeed}(*speed*), where *speed* is the pattern speed in knots. Default is 274 knots TAS which results in ~250 KIAS. +-- * @{#RECOVERYTANKER.SetRacetrackDistances}(*distbow*, *diststern*), where *distbow* and *diststern* are the distances ahead and astern the boat (default 10 and 4 NM), respectively. +-- In principle, these number should be more like 8 and 6 NM but since the carrier is moving, we give translate the pattern points a bit forward. +-- +-- ## Home Base +-- +-- The home base is the airbase where the tanker is spawned (if not in air) and where it will go once it is running out of fuel. The default home base is the carrier itself. +-- The home base can be changed via the @{#RECOVERYTANKER.SetHomeBase}(*airbase*) function, where *airbase* can be a MOOSE @{Wrapper.Airbase#AIRBASE} object or simply the +-- name of the airbase passed as string. +-- +-- Note that only the S3B Viking is a refueling aircraft that is carrier capable. You can use other tanker aircraft types, e.g. the KC-130, but in this case you must either +-- set an airport of the map as home base or activate spawning in air via @{#RECOVERYTANKER.SetTakeoffAir}. +-- +-- ## TACAN +-- +-- A TACAN beacon for the tanker can be activated via scripting, i.e. no need to do this within the mission editor. +-- +-- The beacon is create with the @{#RECOVERYTANKER.SetTACAN}(*channel*, *morse*) function, where *channel* is the TACAN channel (a number), +-- and *morse* a three letter string that is send as morse code to identify the tanker: +-- +-- TexacoStennis:SetTACAN(10, "TKR") +-- +-- will activate a TACAN beacon 10Y with more code "TKR". +-- +-- If you do not set a TACAN beacon explicitly, it is automatically create on channel 1Y and morse code "TKR". +-- The mode is *always* "Y" for AA TACAN stations since mode "X" does not work! +-- +-- In order to completely disable the TACAN beacon, you can use the @{#RECOVERYTANKER.SetTACANoff}() function in your script. +-- +-- ## Radio +-- +-- The radio frequency on optionally modulation can be set via the @{#RECOVERYTANKER.SetRadio}(*frequency*, *modulation*) function. The first parameter denotes the radio frequency the tanker uses in MHz. +-- The second parameter is *optional* and sets the modulation to either AM (default) or FM. +-- +-- For example, +-- +-- TexacoStennis:SetRadio(260) +-- +-- will set the frequency of the tanker to 260 MHz AM. +-- +-- **Note** that if this is not set, the tanker frequency will be automatically set to **251 MHz AM**. +-- +-- ## Pattern Update +-- +-- The pattern of the tanker is updated if at least one of the two following conditions apply: +-- +-- * The aircraft carrier changes its position by more than 5 NM (see @{#RECOVERYTANKER.SetPatternUpdateDistance}) and/or +-- * The aircraft carrier changes its heading by more than 5 degrees (see @{#RECOVERYTANKER.SetPatternUpdateHeading}) +-- +-- **Note** that updating the pattern often leads to a more or less small disruption of the perfect racetrack pattern of the tanker. This is because a new waypoint and new racetrack points +-- need to be set as DCS task. This is the reason why the pattern is not constantly updated but rather when the position or heading of the carrier changes significantly. +-- +-- The maximum update frequency is set to 10 minutes. You can adjust this by @{#RECOVERYTANKER.SetPatternUpdateInterval}. +-- Also the pattern will not be updated whilst the carrier is turning or the tanker is currently refueling another unit. +-- +-- ## Callsign +-- +-- The callsign of the tanker can be set via the @{#RECOVERYTANKER.SetCallsign}(*callsignname*, *callsignnumber*) function. Both parameters are *numbers*. +-- The first parameter *callsignname* defines the name (1=Texaco, 2=Arco, 3=Shell). The second (optional) parameter specifies the first number and has to be between 1-9. +-- Also see [DCS_enum_callsigns](https://wiki.hoggitworld.com/view/DCS_enum_callsigns) and [DCS_command_setCallsign](https://wiki.hoggitworld.com/view/DCS_command_setCallsign). +-- +-- TexacoStennis:SetCAllsign(CALLSIGN.Tanker.Arco) +-- +-- For convenience, MOOSE has a CALLSIGN enumerator introduced. +-- +-- ## AWACS +-- +-- You can use the class also to have an AWACS orbiting overhead the carrier. This requires to add the @{#RECOVERYTANKER.SetAWACS}(*switch*, *eplrs*) function to the script, which sets the enroute tasks AWACS +-- as soon as the aircraft enters its pattern. Note that the EPLRS data link is enabled by default. To disable it, the second parameter *eplrs* must be set to *false*. +-- +-- A simple script could look like this: +-- +-- -- E-2D at USS Stennis spawning in air. +-- local awacsStennis=RECOVERYTANKER:New("USS Stennis", "E2D Group") +-- +-- -- Custom settings: +-- awacsStennis:SetAWACS() +-- awacsStennis:SetCallsign(CALLSIGN.AWACS.Wizard, 1) +-- awacsStennis:SetTakeoffAir() +-- awacsStennis:SetAltitude(20000) +-- awacsStennis:SetRadio(262) +-- awacsStennis:SetTACAN(2, "WIZ") +-- +-- -- Start AWACS. +-- awacsStennis:Start() +-- +-- # Finite State Machine +-- +-- The implementation uses a Finite State Machine (FSM). This allows the mission designer to hook in to certain events. +-- +-- * @{#RECOVERYTANKER.Start}: This event starts the FMS process and initialized parameters and spawns the tanker. DCS event handling is started. +-- * @{#RECOVERYTANKER.Status}: This event is called in regular intervals (~60 seconds) and checks the status of the tanker and carrier. It triggers other events if necessary. +-- * @{#RECOVERYTANKER.PatternUpdate}: This event commands the tanker to update its pattern +-- * @{#RECOVERYTANKER.RTB}: This events sends the tanker to its home base (usually the carrier). This is called once the tanker runs low on gas. +-- * @{#RECOVERYTANKER.RefuelStart}: This event is called when a tanker starts to refuel another unit. +-- * @{#RECOVERYTANKER.RefuelStop}: This event is called when a tanker stopped to refuel another unit. +-- * @{#RECOVERYTANKER.Run}: This event is called when the tanker resumes normal operations, e.g. after refueling stopped or tanker finished refueling. +-- * @{#RECOVERYTANKER.Stop}: This event stops the FSM by unhandling DCS events. +-- +-- The mission designer can capture these events by RECOVERYTANKER.OnAfter*Eventname* functions, e.g. @{#RECOVERYTANKER.OnAfterPatternUpdate}. +-- +-- # Debugging +-- +-- In case you have problems, it is always a good idea to have a look at your DCS log file. You find it in your "Saved Games" folder, so for example in +-- C:\Users\\Saved Games\DCS\Logs\dcs.log +-- All output concerning the @{#RECOVERYTANKER} class should have the string "RECOVERYTANKER" in the corresponding line. +-- Searching for lines that contain the string "error" or "nil" can also give you a hint what's wrong. +-- +-- The verbosity of the output can be increased by adding the following lines to your script: +-- +-- BASE:TraceOnOff(true) +-- BASE:TraceLevel(1) +-- BASE:TraceClass("RECOVERYTANKER") +-- +-- To get even more output you can increase the trace level to 2 or even 3, c.f. @{Core.Base#BASE} for more details. +-- +-- ## Debug Mode +-- +-- You have the option to enable the debug mode for this class via the @{#RECOVERYTANKER.SetDebugModeON} function. +-- If enabled, text messages about the tanker status will be displayed on screen and marks of the pattern created on the F10 map. +-- +-- @field #RECOVERYTANKER +RECOVERYTANKER = { + ClassName = "RECOVERYTANKER", + Debug = false, + lid = nil, + carrier = nil, + carriertype = nil, + tankergroupname = nil, + tanker = nil, + airbase = nil, + beacon = nil, + TACANchannel = nil, + TACANmode = nil, + TACANmorse = nil, + TACANon = nil, + RadioFreq = nil, + RadioModu = nil, + altitude = nil, + speed = nil, + distStern = nil, + distBow = nil, + dTupdate = nil, + Dupdate = nil, + Hupdate = nil, + Tupdate = nil, + takeoff = nil, + lowfuel = nil, + respawn = nil, + respawninair = nil, + uncontrolledac = nil, + orientation = nil, + orientlast = nil, + position = nil, + alias = nil, + uid = 0, + awacs = nil, + callsignname = nil, + callsignnumber = nil, + modex = nil, + eplrs = nil, + recovery = nil, + terminaltype = nil, +} + +--- Unique ID (global). +-- @field #number UID Unique ID (global). +_RECOVERYTANKERID=0 + +--- Class version. +-- @field #string version +RECOVERYTANKER.version="1.0.9" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- DONE: Is alive check for tanker necessary? +-- DONE: Seamless change of position update. Get good updated waypoint and update position if tanker position is right. Not really possiple atm. +-- DONE: Check if TACAN mode "X" is allowed for AA TACAN stations. Nope +-- DONE: Check if tanker is going back to "Running" state after RTB and respawn. +-- DONE: Write documentation. +-- DONE: Trace functions self:T instead of self:I for less output. +-- DONE: Make pattern update parameters (distance, orientation) input parameters. +-- DONE: Add FSM event for pattern update. +-- DONE: Smarter pattern update function. E.g. (small) zone around carrier. Only update position when carrier leaves zone or changes heading? +-- DONE: Set AA TACAN. +-- DONE: Add refueling event/state. +-- DONE: Possibility to add already present/spawned aircraft, e.g. for warehouse. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create new RECOVERYTANKER object. +-- @param #RECOVERYTANKER self +-- @param Wrapper.Unit#UNIT carrierunit Carrier unit. +-- @param #string tankergroupname Name of the late activated tanker aircraft template group. +-- @return #RECOVERYTANKER RECOVERYTANKER object. +function RECOVERYTANKER:New(carrierunit, tankergroupname) + + -- Inherit everthing from FSM class. + local self = BASE:Inherit(self, FSM:New()) -- #RECOVERYTANKER + + if type(carrierunit)=="string" then + self.carrier=UNIT:FindByName(carrierunit) + else + self.carrier=carrierunit + end + + -- Carrier type. + self.carriertype=self.carrier:GetTypeName() + + -- Tanker group name. + self.tankergroupname=tankergroupname + + -- Increase unique ID. + _RECOVERYTANKERID=_RECOVERYTANKERID+1 + + -- Unique ID of this tanker. + self.uid=_RECOVERYTANKERID + + -- Save self in static object. Easier to retrieve later. + self.carrier:SetState(self.carrier, string.format("RECOVERYTANKER_%d", self.uid) , self) + + -- Set unique spawn alias. + self.alias=string.format("%s_%s_%02d", self.carrier:GetName(), self.tankergroupname, _RECOVERYTANKERID) + + -- Log ID. + self.lid=string.format("RECOVERYTANKER %s | ", self.alias) + + -- Init default parameters. + self:SetAltitude() + self:SetSpeed() + self:SetRacetrackDistances() + self:SetHomeBase(AIRBASE:FindByName(self.carrier:GetName())) + self:SetTakeoffHot() + self:SetLowFuelThreshold() + self:SetRespawnOnOff() + self:SetTACAN() + self:SetRadio() + self:SetPatternUpdateDistance() + self:SetPatternUpdateHeading() + self:SetPatternUpdateInterval() + self:SetAWACS(false) + self:SetRecoveryAirboss(false) + self.terminaltype=AIRBASE.TerminalType.OpenMedOrBig + + -- Debug trace. + if false then + BASE:TraceOnOff(true) + BASE:TraceClass(self.ClassName) + BASE:TraceLevel(1) + end + + ----------------------- + --- FSM Transitions --- + ----------------------- + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start the FSM. + self:AddTransition("*", "RefuelStart", "Refueling") -- Tanker has started to refuel another unit. + self:AddTransition("*", "RefuelStop", "Running") -- Tanker starts to refuel. + self:AddTransition("*", "Run", "Running") -- Tanker starts normal operation again. + self:AddTransition("Running", "RTB", "Returning") -- Tanker is returning to base (for fuel). + self:AddTransition("Returning", "Returned", "Returned") -- Tanker has returned to its airbase (i.e. landed). + self:AddTransition("*", "Status", "*") -- Status update. + self:AddTransition("Running", "PatternUpdate", "*") -- Update pattern wrt to carrier. + self:AddTransition("*", "Stop", "Stopped") -- Stop the FSM. + + + --- Triggers the FSM event "Start" that starts the recovery tanker. Initializes parameters and starts event handlers. + -- @function [parent=#RECOVERYTANKER] Start + -- @param #RECOVERYTANKER self + + --- Triggers the FSM event "Start" that starts the recovery tanker after a delay. Initializes parameters and starts event handlers. + -- @function [parent=#RECOVERYTANKER] __Start + -- @param #RECOVERYTANKER self + -- @param #number delay Delay in seconds. + + --- On after "Start" event function. Called when FSM is started. + -- @function [parent=#RECOVERYTANKER] OnAfterStart + -- @param #RECOVERYTANKER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "RefuelStart" when the tanker starts refueling another aircraft. + -- @function [parent=#RECOVERYTANKER] RefuelStart + -- @param #RECOVERYTANKER self + -- @param Wrapper.Unit#UNIT receiver Unit receiving fuel from the tanker. + + --- On after "RefuelStart" event user function. Called when a the the tanker started to refuel another unit. + -- @function [parent=#RECOVERYTANKER] OnAfterRefuelStart + -- @param #RECOVERYTANKER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Unit#UNIT receiver Unit receiving fuel from the tanker. + + + --- Triggers the FSM event "RefuelStop" when the tanker stops refueling another aircraft. + -- @function [parent=#RECOVERYTANKER] RefuelStop + -- @param #RECOVERYTANKER self + -- @param Wrapper.Unit#UNIT receiver Unit stoped receiving fuel from the tanker. + + --- On after "RefuelStop" event user function. Called when a the the tanker stopped to refuel another unit. + -- @function [parent=#RECOVERYTANKER] OnAfterRefuelStop + -- @param #RECOVERYTANKER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Unit#UNIT receiver Unit that received fuel from the tanker. + + + --- Triggers the FSM event "Run". Simply puts the group into "Running" state. + -- @function [parent=#RECOVERYTANKER] Run + -- @param #RECOVERYTANKER self + + --- Triggers delayed the FSM event "Run". Simply puts the group into "Running" state. + -- @function [parent=#RECOVERYTANKER] __Run + -- @param #RECOVERYTANKER self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "RTB" that sends the tanker home. + -- @function [parent=#RECOVERYTANKER] RTB + -- @param #RECOVERYTANKER self + -- @param Wrapper.Airbase#AIRBASE airbase The airbase where the tanker should return to. + + --- Triggers the FSM event "RTB" that sends the tanker home after a delay. + -- @function [parent=#RECOVERYTANKER] __RTB + -- @param #RECOVERYTANKER self + -- @param #number delay Delay in seconds. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase where the tanker should return to. + + --- On after "RTB" event user function. Called when a the the tanker returns to its home base. + -- @function [parent=#RECOVERYTANKER] OnAfterRTB + -- @param #RECOVERYTANKER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase where the tanker should return to. + + + --- Triggers the FSM event "Returned" after the tanker has landed. + -- @function [parent=#RECOVERYTANKER] Returned + -- @param #RECOVERYTANKER self + -- @param Wrapper.Airbase#AIRBASE airbase The airbase the tanker has landed. + + --- Triggers the delayed FSM event "Returned" after the tanker has landed. + -- @function [parent=#RECOVERYTANKER] __Returned + -- @param #RECOVERYTANKER self + -- @param #number delay Delay in seconds. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase the tanker has landed. + + --- On after "Returned" event user function. Called when a the the tanker has landed at an airbase. + -- @function [parent=#RECOVERYTANKER] OnAfterReturned + -- @param #RECOVERYTANKER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase the tanker has landed. + + + --- Triggers the FSM event "Status" that updates the tanker status. + -- @function [parent=#RECOVERYTANKER] Status + -- @param #RECOVERYTANKER self + + --- Triggers the delayed FSM event "Status" that updates the tanker status. + -- @function [parent=#RECOVERYTANKER] __Status + -- @param #RECOVERYTANKER self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "PatternUpdate" that updates the pattern of the tanker wrt to the carrier position. + -- @function [parent=#RECOVERYTANKER] PatternUpdate + -- @param #RECOVERYTANKER self + + --- Triggers the delayed FSM event "PatternUpdate" that updates the pattern of the tanker wrt to the carrier position. + -- @function [parent=#RECOVERYTANKER] __PatternUpdate + -- @param #RECOVERYTANKER self + -- @param #number delay Delay in seconds. + + --- On after "PatternEvent" event user function. Called when a the pattern of the tanker is updated. + -- @function [parent=#RECOVERYTANKER] OnAfterPatternUpdate + -- @param #RECOVERYTANKER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Stop" that stops the recovery tanker. Event handlers are stopped. + -- @function [parent=#RECOVERYTANKER] Stop + -- @param #RECOVERYTANKER self + + --- Triggers the FSM event "Stop" that stops the recovery tanker after a delay. Event handlers are stopped. + -- @function [parent=#RECOVERYTANKER] __Stop + -- @param #RECOVERYTANKER self + -- @param #number delay Delay in seconds. + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set the speed the tanker flys in its orbit pattern. +-- @param #RECOVERYTANKER self +-- @param #number speed True air speed (TAS) in knots. Default 274 knots, which results in ~250 KIAS. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetSpeed(speed) + self.speed=UTILS.KnotsToMps(speed or 274) + return self +end + +--- Set orbit pattern altitude of the tanker. +-- @param #RECOVERYTANKER self +-- @param #number altitude Tanker altitude in feet. Default 6000 ft. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetAltitude(altitude) + self.altitude=UTILS.FeetToMeters(altitude or 6000) + return self +end + +--- Set race-track distances. +-- @param #RECOVERYTANKER self +-- @param #number distbow Distance [NM] in front of the carrier. Default 10 NM. +-- @param #number diststern Distance [NM] behind the carrier. Default 4 NM. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetRacetrackDistances(distbow, diststern) + self.distBow=UTILS.NMToMeters(distbow or 10) + self.distStern=-UTILS.NMToMeters(diststern or 4) + return self +end + +--- Set minimum pattern update interval. After a pattern update this time interval has to pass before the next update is allowed. +-- @param #RECOVERYTANKER self +-- @param #number interval Min interval in minutes. Default is 10 minutes. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetPatternUpdateInterval(interval) + self.dTupdate=(interval or 10)*60 + return self +end + +--- Set pattern update distance threshold. Tanker will update its pattern when the carrier changes its position by more than this distance. +-- @param #RECOVERYTANKER self +-- @param #number distancechange Distance threshold in NM. Default 5 NM (=9.62 km). +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetPatternUpdateDistance(distancechange) + self.Dupdate=UTILS.NMToMeters(distancechange or 5) + return self +end + +--- Set pattern update heading threshold. Tanker will update its pattern when the carrier changes its heading by more than this value. +-- @param #RECOVERYTANKER self +-- @param #number headingchange Heading threshold in degrees. Default 5 degrees. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetPatternUpdateHeading(headingchange) + self.Hupdate=headingchange or 5 + return self +end + +--- Set low fuel state of tanker. When fuel is below this threshold, the tanker will RTB or be respawned if takeoff type is in air. +-- @param #RECOVERYTANKER self +-- @param #number fuelthreshold Low fuel threshold in percent. Default 10 % of max fuel. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetLowFuelThreshold(fuelthreshold) + self.lowfuel=fuelthreshold or 10 + return self +end + +--- Set home airbase of the tanker. This is the airbase where the tanker will go when it is out of fuel. +-- @param #RECOVERYTANKER self +-- @param Wrapper.Airbase#AIRBASE airbase The home airbase. Can be the airbase name or a Moose AIRBASE object. +-- @param #number terminaltype (Optional) The terminal type of parking spots used for spawning at airbases. Default AIRBASE.TerminalType.OpenMedOrBig. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetHomeBase(airbase, terminaltype) + if type(airbase)=="string" then + self.airbase=AIRBASE:FindByName(airbase) + else + self.airbase=airbase + end + if not self.airbase then + self:E(self.lid.."ERROR: Airbase is nil!") + end + if terminaltype then + self.terminaltype=terminaltype + end + return self +end + +--- Activate recovery by the AIRBOSS class. Tanker will get a Marshal stack and perform a CASE I, II or III recovery when RTB. +-- @param #RECOVERYTANKER self +-- @param #boolean switch If true or nil, recovery is done by AIRBOSS. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetRecoveryAirboss(switch) + if switch==true or switch==nil then + self.recovery=true + else + self.recovery=false + end + return self +end + +--- Set that the group takes the roll of an AWACS instead of a refueling tanker. +-- @param #RECOVERYTANKER self +-- @param #boolean switch If true or nil, set roll AWACS. +-- @param #boolean eplrs If true or nil, enable EPLRS. If false, EPLRS will be off. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetAWACS(switch, eplrs) + if switch==nil or switch==true then + self.awacs=true + else + self.awacs=false + end + if eplrs==nil or eplrs==true then + self.eplrs=true + else + self.eplrs=false + end + + return self +end + + +--- Set callsign of the tanker group. +-- @param #RECOVERYTANKER self +-- @param #number callsignname Number +-- @param #number callsignnumber Number +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetCallsign(callsignname, callsignnumber) + self.callsignname=callsignname + self.callsignnumber=callsignnumber + return self +end + +--- Set modex (tail number) of the tanker. +-- @param #RECOVERYTANKER self +-- @param #number modex Tail number. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetModex(modex) + self.modex=modex + return self +end + +--- Set takeoff type. +-- @param #RECOVERYTANKER self +-- @param #number takeofftype Takeoff type. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetTakeoff(takeofftype) + self.takeoff=takeofftype + return self +end + +--- Set takeoff with engines running (hot). +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetTakeoffHot() + self:SetTakeoff(SPAWN.Takeoff.Hot) + return self +end + +--- Set takeoff with engines off (cold). +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetTakeoffCold() + self:SetTakeoff(SPAWN.Takeoff.Cold) + return self +end + +--- Set takeoff in air at the defined pattern altitude and ~10 NM astern the carrier. +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetTakeoffAir() + self:SetTakeoff(SPAWN.Takeoff.Air) + return self +end + +--- Enable respawning of tanker. Note that this is the default behaviour. +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetRespawnOn() + self.respawn=true + return self +end + +--- Disable respawning of tanker. +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetRespawnOff() + self.respawn=false + return self +end + +--- Set whether tanker shall be respawned or not. +-- @param #RECOVERYTANKER self +-- @param #boolean switch If true (or nil), tanker will be respawned. If false, tanker will not be respawned. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetRespawnOnOff(switch) + if switch==nil or switch==true then + self.respawn=true + else + self.respawn=false + end + return self +end + +--- Tanker will be respawned in air, even it was initially spawned on the carrier. +-- So only the first spawn will be on the carrier while all subsequent spawns will happen in air. +-- This allows for undisrupted operations and less problems on the carrier deck. +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetRespawnInAir() + self.respawninair=true + return self +end + +--- Use an uncontrolled aircraft already present in the mission rather than spawning a new tanker as initial recovery thanker. +-- This can be useful when interfaced with, e.g., a MOOSE @{Functional.Warehouse#WAREHOUSE}. +-- The group name is the one specified in the @{#RECOVERYTANKER.New} function. +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetUseUncontrolledAircraft() + self.uncontrolledac=true + return self +end + + +--- Disable automatic TACAN activation. +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetTACANoff() + self.TACANon=false + return self +end + +--- Set TACAN channel of tanker. Note that mode is automatically set to "Y" for AA TACAN since only that works. +-- @param #RECOVERYTANKER self +-- @param #number channel TACAN channel. Default 1. +-- @param #string morse TACAN morse code identifier. Three letters. Default "TKR". +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetTACAN(channel, morse) + self.TACANchannel=channel or 1 + self.TACANmode="Y" + self.TACANmorse=morse or "TKR" + self.TACANon=true + return self +end + +--- Set radio frequency and optionally modulation of the tanker. +-- @param #RECOVERYTANKER self +-- @param #number frequency Radio frequency in MHz. Default 251 MHz. +-- @param #string modulation Radio modulation, either "AM" or "FM". Default "AM". +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetRadio(frequency, modulation) + self.RadioFreq=frequency or 251 + self.RadioModu=modulation or "AM" + return self +end + +--- Activate debug mode. Marks of pattern on F10 map and debug messages displayed on screen. +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetDebugModeON() + self.Debug=true + return self +end + +--- Deactivate debug mode. This is also the default setting. +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetDebugModeOFF() + self.Debug=false + return self +end + +--- Check if tanker is currently returning to base. +-- @param #RECOVERYTANKER self +-- @return #boolean If true, tanker is returning to base. +function RECOVERYTANKER:IsReturning() + return self:is("Returning") +end + +--- Check if tanker has returned to base. +-- @param #RECOVERYTANKER self +-- @return #boolean If true, tanker has returned to base. +function RECOVERYTANKER:IsReturned() + return self:is("Returned") +end + +--- Check if tanker is currently operating. +-- @param #RECOVERYTANKER self +-- @return #boolean If true, tanker is operating. +function RECOVERYTANKER:IsRunning() + return self:is("Running") +end + +--- Check if tanker is currently refueling another aircraft. +-- @param #RECOVERYTANKER self +-- @return #boolean If true, tanker is refueling. +function RECOVERYTANKER:IsRefueling() + return self:is("Refueling") +end + +--- Check if FMS was stopped. +-- @param #RECOVERYTANKER self +-- @return #boolean If true, is stopped. +function RECOVERYTANKER:IsStopped() + return self:is("Stopped") +end + +--- Alias of tanker spawn group. +-- @param #RECOVERYTANKER self +-- @return #string Alias of the tanker. +function RECOVERYTANKER:GetAlias() + return self.alias +end + +--- Get unit name of the spawned tanker. +-- @param #RECOVERYTANKER self +-- @return #string Name of the tanker unit or nil if it does not exist. +function RECOVERYTANKER:GetUnitName() + local unit=self.tanker:GetUnit(1) + if unit then + return unit:GetName() + end + return nil +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM states +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after Start event. Starts the warehouse. Addes event handlers and schedules status updates of reqests and queue. +-- @param #RECOVERYTANKER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RECOVERYTANKER:onafterStart(From, Event, To) + + -- Info on start. + self:I(string.format("Starting Recovery Tanker v%s for carrier unit %s of type %s for tanker group %s.", RECOVERYTANKER.version, self.carrier:GetName(), self.carriertype, self.tankergroupname)) + + -- Handle events. + self:HandleEvent(EVENTS.EngineShutdown) + self:HandleEvent(EVENTS.Land) + self:HandleEvent(EVENTS.Refueling, self._RefuelingStart) --Need explicit functions since OnEventRefueling and OnEventRefuelingStop did not hook! + self:HandleEvent(EVENTS.RefuelingStop, self._RefuelingStop) + self:HandleEvent(EVENTS.Crash, self._OnEventCrashOrDead) + self:HandleEvent(EVENTS.Dead, self._OnEventCrashOrDead) + + -- Spawn tanker. We need to introduce an alias in case this class is used twice. This would confuse the spawn routine. + local Spawn=SPAWN:NewWithAlias(self.tankergroupname, self.alias) + + -- Set radio frequency and modulation. + Spawn:InitRadioCommsOnOff(true) + Spawn:InitRadioFrequency(self.RadioFreq) + Spawn:InitRadioModulation(self.RadioModu) + Spawn:InitModex(self.modex) + + -- Spawn on carrier. + if self.takeoff==SPAWN.Takeoff.Air then + + -- Carrier heading + local hdg=self.carrier:GetHeading() + + -- Spawn distance behind the carrier. + local dist=-self.distStern+UTILS.NMToMeters(4) + + -- Coordinate behind the carrier and slightly port. + local Carrier=self.carrier:GetCoordinate():Translate(dist, hdg+190):SetAltitude(self.altitude) + + -- Orientation of spawned group. + Spawn:InitHeading(hdg+10) + + -- Spawn at coordinate. + self.tanker=Spawn:SpawnFromCoordinate(Carrier) + + else + + -- Check if an uncontrolled tanker group was requested. + if self.uncontrolledac then + + -- Use an uncontrolled aircraft group. + self.tanker=GROUP:FindByName(self.tankergroupname) + + if self.tanker:IsAlive() then + + -- Start uncontrolled group. + self.tanker:StartUncontrolled() + + else + -- No group by that name! + self:E(string.format("ERROR: No uncontrolled (alive) tanker group with name %s could be found!", self.tankergroupname)) + return + end + + else + + -- Spawn tanker at airbase. + self.tanker=Spawn:SpawnAtAirbase(self.airbase, self.takeoff, nil, self.terminaltype) + + end + + end + + -- Initialize route. self.distStern<0! + self:ScheduleOnce(1, self._InitRoute, self, -self.distStern+UTILS.NMToMeters(3)) + + -- Create tanker beacon. + if self.TACANon then + self:_ActivateTACAN(2) + end + + -- Set callsign. + if self.callsignname then + self.tanker:CommandSetCallsign(self.callsignname, self.callsignnumber, 2) + end + + -- Turn EPLRS datalink on. + if self.eplrs then + self.tanker:CommandEPLRS(true, 3) + end + + + -- Get initial orientation and position of carrier. + self.orientation=self.carrier:GetOrientationX() + self.orientlast=self.carrier:GetOrientationX() + self.position=self.carrier:GetCoordinate() + + -- Init status updates in 10 seconds. + self:__Status(10) +end + + +--- On after Status event. Checks player status. +-- @param #RECOVERYTANKER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RECOVERYTANKER:onafterStatus(From, Event, To) + + -- Get current time. + local time=timer.getTime() + + if self.tanker and self.tanker:IsAlive() then + + --------------------- + -- TANKER is ALIVE -- + --------------------- + + -- Get fuel of tanker. + local fuel=self.tanker:GetFuel()*100 + local life=self.tanker:GetUnit(1):GetLife() + local life0=self.tanker:GetUnit(1):GetLife0() + local lifeR=self.tanker:GetUnit(1):GetLifeRelative() + + -- Report fuel and life. + local text=string.format("Recovery tanker %s: state=%s fuel=%.1f, life=%.1f/%.1f=%d", self.tanker:GetName(), self:GetState(), fuel, life, life0, lifeR*100) + self:T(self.lid..text) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + + -- Check if tanker is running and not RTBing or refueling. + if self:IsRunning() then + + -- Check fuel. + if fuel 100 meters, this should be another tanker. + if dist>100 then + return + end + + -- Info message. + local text=string.format("Recovery tanker %s started refueling unit %s", self.tanker:GetName(), receiver:GetName()) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) + + -- FMS state "Refueling". + self:RefuelStart(receiver) + end + +end + +--- Event handler for refueling stopped. +-- @param #RECOVERYTANKER self +-- @param Core.Event#EVENTDATA EventData Event data. +function RECOVERYTANKER:_RefuelingStop(EventData) + + if EventData and EventData.IniUnit and EventData.IniUnit:IsAlive() then + + -- Unit receiving fuel. + local receiver=EventData.IniUnit + + -- Get distance to tanker to check that unit is receiving fuel from this tanker. + local dist=receiver:GetCoordinate():Get2DDistance(self.tanker:GetCoordinate()) + + -- If distance > 100 meters, this should be another tanker. + if dist>100 then + return + end + + -- Info message. + local text=string.format("Recovery tanker %s stopped refueling unit %s", self.tanker:GetName(), receiver:GetName()) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) + + -- FSM state "Running". + self:RefuelStop(receiver) + end + +end + +--- A unit crashed or died. +-- @param #RECOVERYTANKER self +-- @param Core.Event#EVENTDATA EventData Event data. +function RECOVERYTANKER:_OnEventCrashOrDead(EventData) + self:F2({eventdata=EventData}) + + -- Check that there is an initiating unit in the event data. + if EventData and EventData.IniUnit then + + -- Crashed or dead unit. + local unit=EventData.IniUnit + local unitname=tostring(EventData.IniUnitName) + + -- Check that it was the tanker that crashed. + if EventData.IniGroupName==self.tanker:GetName() then + + -- Error message. + self:E(self.lid..string.format("Recovery tanker %s crashed!", unitname)) + + -- Stop FSM. + self:Stop() + + -- Restart. + if self.respawn then + self:__Start(5) + end + + end + + end +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- MISC functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Task function to +-- @param #RECOVERYTANKER self +function RECOVERYTANKER:_InitPatternTaskFunction() + + -- Name of the warehouse (static) object. + local carriername=self.carrier:GetName() + + -- Task script. + local DCSScript = {} + DCSScript[#DCSScript+1] = string.format('local mycarrier = UNIT:FindByName(\"%s\") ', carriername) -- The carrier unit that holds the self object. + DCSScript[#DCSScript+1] = string.format('local mytanker = mycarrier:GetState(mycarrier, \"RECOVERYTANKER_%d\") ', self.uid) -- Get the RECOVERYTANKER self object. + DCSScript[#DCSScript+1] = string.format('mytanker:PatternUpdate()') -- Call the function, e.g. mytanker.(self) + + -- Create task. + local DCSTask = CONTROLLABLE.TaskWrappedAction(self, CONTROLLABLE.CommandDoScript(self, table.concat(DCSScript))) + + return DCSTask +end + +--- Init waypoint after spawn. Tanker is first guided to a position astern the carrier and starts its racetrack pattern from there. +-- @param #RECOVERYTANKER self +-- @param #number dist Distance [NM] of initial waypoint astern carrier. Default 8 NM. +-- @param #number delay Delay before routing in seconds. Default 1 second. +function RECOVERYTANKER:_InitRoute(dist, delay) + + -- Defaults. + dist=dist or UTILS.NMToMeters(8) + delay=delay or 1 + + -- Debug message. + self:T(self.lid..string.format("Initializing route of recovery tanker %s.", self.tanker:GetName())) + + -- Carrier position. + local Carrier=self.carrier:GetCoordinate() + + -- Carrier heading. + local hdg=self.carrier:GetHeading() + + -- First waypoint is ~10 NM behind and slightly port the boat. + local p=Carrier:Translate(dist, hdg+190):SetAltitude(self.altitude) + + -- Speed for waypoints in km/h. + -- This causes a problem, because the tanker might not be alive yet ==> We schedule the call of _InitRoute + local speed=self.tanker:GetSpeedMax()*0.8 + + -- Set to 280 knots and convert to km/h. + --local speed=280/0.539957 + + -- Debug mark. + if self.Debug then + p:MarkToAll(string.format("Enter Pattern WP: alt=%d ft, speed=%d kts", UTILS.MetersToFeet(self.altitude), speed*0.539957)) + end + + -- Task to update pattern when wp 2 is reached. + local task=self:_InitPatternTaskFunction() + + -- Waypoints. + local wp={} + if self.takeoff==SPAWN.Takeoff.Air then + wp[#wp+1]=self.tanker:GetCoordinate():SetAltitude(self.altitude):WaypointAirTurningPoint(nil, speed, {}, "Spawn Position") + else + wp[#wp+1]=Carrier:WaypointAirTakeOffParking() + end + wp[#wp+1]=p:WaypointAirTurningPoint(nil, speed, {task}, "Enter Pattern") + + -- Set route. + self.tanker:Route(wp, delay) + + -- Set state to Running. Necessary when tanker was RTB and respawned since it is probably in state "Returning". + self:__Run(1) + + -- No update yet, wait until the function is called (avoids checks if pattern update is needed). + self.Tupdate=nil +end + +--- Check if heading or position have changed significantly. +-- @param #RECOVERYTANKER self +-- @param #number dt Time since last update in seconds. +-- @return #boolean If true, heading and/or position have changed more than 5 degrees or 10 km, respectively. +function RECOVERYTANKER:_CheckPatternUpdate(dt) + + -- Get current position and orientation of carrier. + local pos=self.carrier:GetCoordinate() + + -- Current orientation of carrier. + local vNew=self.carrier:GetOrientationX() + + -- Reference orientation of carrier after the last update + local vOld=self.orientation + + -- Last orientation from 30 seconds ago. + local vLast=self.orientlast + + -- We only need the X-Z plane. + vNew.y=0 ; vOld.y=0 ; vLast.y=0 + + -- Get angle between old and new orientation vectors in rad and convert to degrees. + local deltaHeading=math.deg(math.acos(UTILS.VecDot(vNew,vOld)/UTILS.VecNorm(vNew)/UTILS.VecNorm(vOld))) + + -- Angle between current heading and last time we checked ~30 seconds ago. + local deltaLast=math.deg(math.acos(UTILS.VecDot(vNew,vLast)/UTILS.VecNorm(vNew)/UTILS.VecNorm(vLast))) + + -- Last orientation becomes new orientation + self.orientlast=vNew + + -- Carrier is turning when its heading changed by at least one degree since last check. + local turning=deltaLast>=1 + + -- Debug output if turning + if turning then + self:T2(self.lid..string.format("Carrier is turning. Delta Heading = %.1f", deltaLast)) + end + + -- Check if orientation changed. + local Hchange=false + if math.abs(deltaHeading)>=self.Hupdate then + self:T(self.lid..string.format("Carrier heading changed by %d degrees. Turning=%s.", deltaHeading, tostring(turning))) + Hchange=true + end + + -- Get distance to saved position. + local dist=pos:Get2DDistance(self.position) + + -- Check if carrier moved more than ~5 NM. + local Dchange=false + if dist>self.Dupdate then + self:T(self.lid..string.format("Carrier position changed by %.1f NM. Turning=%s.", UTILS.MetersToNM(dist), tostring(turning))) + Dchange=true + end + + -- Assume no update necessary. + local update=false + + -- No update if currently turning! Also must be running (not RTB or refueling) and T>~10 min since last position update. + if self:IsRunning() and dt>self.dTupdate and not turning then + + -- Update if heading or distance changed. + if Hchange or Dchange then + -- Debug message. + local text=string.format("Updating tanker %s pattern due to carrier position=%s or heading=%s change.", self.tanker:GetName(), tostring(Dchange), tostring(Hchange)) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) + + -- Update pos and orientation. + self.orientation=vNew + self.position=pos + update=true + end + + end + + return update +end + +--- Activate TACAN of tanker. +-- @param #RECOVERYTANKER self +-- @param #number delay Delay in seconds. +function RECOVERYTANKER:_ActivateTACAN(delay) + + if delay and delay>0 then + + -- Schedule TACAN activation. + --SCHEDULER:New(nil, self._ActivateTACAN, {self}, delay) + self:ScheduleOnce(delay, RECOVERYTANKER._ActivateTACAN, self) + + else + + -- Get tanker unit. + local unit=self.tanker:GetUnit(1) + + -- Check if unit is alive. + if unit and unit:IsAlive() then + + -- Debug message. + local text=string.format("Activating TACAN beacon: channel=%d mode=%s, morse=%s.", self.TACANchannel, self.TACANmode, self.TACANmorse) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) + + -- Create a new beacon and activate TACAN. + self.beacon=BEACON:New(unit) + self.beacon:ActivateTACAN(self.TACANchannel, self.TACANmode, self.TACANmorse, true) + + else + self:E(self.lid.."ERROR: Recovery tanker is not alive!") + end + + end + +end + +--- Self made race track pattern. Not working as desired, since tanker changes course too rapidly after each waypoint. +-- @param #RECOVERYTANKER self +-- @return #table Table of pattern waypoints. +function RECOVERYTANKER:_Pattern() + + -- Carrier heading. + local hdg=self.carrier:GetHeading() + + -- Pattern altitude + local alt=self.altitude + + -- Carrier position. + local Carrier=self.carrier:GetCoordinate() + + local width=UTILS.NMToMeters(8) + + -- Define race-track pattern. + local p={} + p[1]=self.tanker:GetCoordinate() -- Tanker position + p[2]=Carrier:SetAltitude(alt) -- Carrier position + p[3]=p[2]:Translate(self.distBow, hdg) -- In front of carrier + p[4]=p[3]:Translate(width/math.sqrt(2), hdg-45) -- Middle front for smoother curve + -- Probably need one more to make it go -hdg at the waypoint. + p[5]=p[3]:Translate(width, hdg-90) -- In front on port + p[6]=p[5]:Translate(self.distStern-self.distBow, hdg) -- Behind on port (sterndist<0!) + p[7]=p[2]:Translate(self.distStern, hdg) -- Behind carrier + + local wp={} + for i=1,#p do + local coord=p[i] --Core.Point#COORDINATE + coord:MarkToAll(string.format("Waypoint %d", i)) + --table.insert(wp, coord:WaypointAirFlyOverPoint(nil , self.speed)) + table.insert(wp, coord:WaypointAirTurningPoint(nil , UTILS.MpsToKmph(self.speed))) + end + + return wp +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- **Ops** - Rescue helicopter for carrier operations. +-- +-- Recue helicopter for carrier operations. +-- +-- **Main Features:** +-- +-- * Close formation with carrier. +-- * No restrictions regarding carrier waypoints and heading. +-- * Automatic respawning on empty fuel for 24/7 operations. +-- * Automatic rescuing of crashed or ejected pilots in the vicinity of the carrier. +-- * Multiple helos at different carriers due to object oriented approach. +-- * Finite State Machine (FSM) implementation. +-- +-- ## Known (DCS) Issues +-- +-- * CH-53E does only report 27.5% fuel even if fuel is set to 100% in the ME. See [bug report](https://forums.eagle.ru/showthread.php?t=223712) +-- * CH-53E does not accept USS Tarawa as landing airbase (even it can be spawned on it). +-- * Helos dont move away from their landing position on carriers. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- ### Contributions: Flightcontrol (@{AI.AI_Formation} class being used here) +-- +-- @module Ops.RescueHelo +-- @image Ops_RescueHelo.png + +--- RESCUEHELO class. +-- @type RESCUEHELO +-- @field #string ClassName Name of the class. +-- @field #boolean Debug Debug mode on/off. +-- @field #string lid Log debug id text. +-- @field Wrapper.Unit#UNIT carrier The carrier the helo is attached to. +-- @field #string carriertype Carrier type. +-- @field #string helogroupname Name of the late activated helo template group. +-- @field Wrapper.Group#GROUP helo Helo group. +-- @field #number takeoff Takeoff type. +-- @field Wrapper.Airbase#AIRBASE airbase The airbase object acting as home base of the helo. +-- @field Core.Set#SET_GROUP followset Follow group set. +-- @field AI.AI_Formation#AI_FORMATION formation AI_FORMATION object. +-- @field #number lowfuel Low fuel threshold of helo in percent. +-- @field #number altitude Altitude of helo in meters. +-- @field #number offsetX Offset in meters to carrier in longitudinal direction. +-- @field #number offsetZ Offset in meters to carrier in latitudinal direction. +-- @field Core.Zone#ZONE_RADIUS rescuezone Zone around the carrier in which helo will rescue crashed or ejected units. +-- @field #boolean respawn If true, helo be respawned (default). If false, no respawning will happen. +-- @field #boolean respawninair If true, helo will always be respawned in air. This has no impact on the initial spawn setting. +-- @field #boolean uncontrolledac If true, use and uncontrolled helo group already present in the mission. +-- @field #boolean rescueon If true, helo will rescue crashed pilots. If false, no recuing will happen. +-- @field #number rescueduration Time the rescue helicopter hovers over the crash site in seconds. +-- @field #number rescuespeed Speed in m/s the rescue helicopter hovers at over the crash site. +-- @field #boolean rescuestopboat If true, stop carrier during rescue operations. +-- @field #boolean carrierstop If true, route of carrier was stopped. +-- @field #number HeloFuel0 Initial fuel of helo in percent. Necessary due to DCS bug that helo with full tank does not return fuel via API function. +-- @field #boolean rtb If true, Helo will be return to base on the next status check. +-- @field #number hid Unit ID of the helo group. (Global) Running number. +-- @field #string alias Alias of the spawn group. +-- @field #number uid Unique ID of this helo. +-- @field #number modex Tail number of the helo. +-- @field #number dtFollow Follow time update interval in seconds. Default 1.0 sec. +-- @extends Core.Fsm#FSM + +--- Rescue Helo +-- +-- === +-- +-- ![Banner Image](..\Presentations\RESCUEHELO\RescueHelo_Main.png) +-- +-- # Recue Helo +-- +-- The rescue helo will fly in close formation with another unit, which is typically an aircraft carrier. +-- It's mission is to rescue crashed or ejected pilots. Well, and to look cool... +-- +-- # Simple Script +-- +-- In the mission editor you have to set up a carrier unit, which will act as "mother". In the following, this unit will be named "*USS Stennis*". +-- +-- Secondly, you need to define a rescue helicopter group in the mission editor and set it to "**LATE ACTIVATED**". The name of the group we'll use is "*Recue Helo*". +-- +-- The basic script is very simple and consists of only two lines. +-- +-- RescueheloStennis=RESCUEHELO:New(UNIT:FindByName("USS Stennis"), "Rescue Helo") +-- RescueheloStennis:Start() +-- +-- The first line will create a new @{#RESCUEHELO} object via @{#RESCUEHELO.New} and the second line starts the process by calling @{#RESCUEHELO.Start}. +-- +-- **NOTE** that it is *very important* to define the RESCUEHELO object as **global** variable. Otherwise, the lua garbage collector will kill the formation for unknown reasons! +-- +-- By default, the helo will be spawned on the *USS Stennis* with hot engines. Then it will take off and go on station on the starboard side of the boat. +-- +-- Once the helo is out of fuel, it will return to the carrier. When the helo lands, it will be respawned immidiately and go back on station. +-- +-- If a unit crashes or a pilot ejects within a radius of 30 km from the USS Stennis, the helo will automatically fly to the crash side and +-- rescue to pilot. This will take around 5 minutes. After that, the helo will return to the Stennis, land there and bring back the poor guy. +-- When this is done, the helo will go back on station. +-- +-- # Fine Tuning +-- +-- The implementation allows to customize quite a few settings easily via user API functions. +-- +-- ## Takeoff Type +-- +-- By default, the helo is spawned with running engines on the carrier. The mission designer has set option to set the take off type via the @{#RESCUEHELO.SetTakeoff} function. +-- Or via shortcuts +-- +-- * @{#RESCUEHELO.SetTakeoffHot}(): Will set the takeoff to hot, which is also the default. +-- * @{#RESCUEHELO.SetTakeoffCold}(): Will set the takeoff type to cold, i.e. with engines off. +-- * @{#RESCUEHELO.SetTakeoffAir}(): Will set the takeoff type to air, i.e. the helo will be spawned in air near the unit which he follows. +-- +-- For example, +-- RescueheloStennis=RESCUEHELO:New(UNIT:FindByName("USS Stennis"), "Rescue Helo") +-- RescueheloStennis:SetTakeoffAir() +-- RescueheloStennis:Start() +-- will spawn the helo near the USS Stennis in air. +-- +-- Spawning in air is not as realistic but can be useful do avoid DCS bugs and shortcomings like aircraft crashing into each other on the flight deck. +-- +-- **Note** that when spawning in air is set, the helo will also not return to the boat, once it is out of fuel. Instead it will be respawned in air. +-- +-- If only the first spawning should happen on the carrier, one use the @{#RESCUEHELO.SetRespawnInAir}() function to command that all subsequent spawning +-- will happen in air. +-- +-- If the helo should no be respawned at all, one can set @{#RESCUEHELO.SetRespawnOff}(). +-- +-- ## Home Base +-- +-- It is possible to define a "home base" other than the aircraft carrier using the @{#RESCUEHELO.SetHomeBase}(*airbase*) function, where *airbase* is +-- a @{Wrapper.Airbase#AIRBASE} object or simply the name of the airbase. +-- +-- For example, one could imagine a strike group, and the helo will be spawned from another ship which has a helo pad. +-- +-- RescueheloStennis=RESCUEHELO:New(UNIT:FindByName("USS Stennis"), "Rescue Helo") +-- RescueheloStennis:SetHomeBase(AIRBASE:FindByName("USS Normandy")) +-- RescueheloStennis:Start() +-- +-- In this case, the helo will be spawned on the USS Normandy and then make its way to the USS Stennis to establish the formation. +-- Note that the distance to the mother ship should be rather small since the helo will go there very slowly. +-- +-- Once the helo runs out of fuel, it will return to the USS Normandy and not the Stennis for respawning. +-- +-- ## Formation Position +-- +-- The position of the helo relative to the mother ship can be tuned via the functions +-- +-- * @{#RESCUEHELO.SetAltitude}(*altitude*), where *altitude* is the altitude the helo flies at in meters. Default is 70 meters. +-- * @{#RESCUEHELO.SetOffsetX}(*distance*), where *distance is the distance in the direction of movement of the carrier. Default is 200 meters. +-- * @{#RESCUEHELO.SetOffsetZ}(*distance*), where *distance is the distance on the starboard side. Default is 100 meters. +-- +-- ## Rescue Operations +-- +-- By default the rescue helo will start a rescue operation if an aircraft crashes or a pilot ejects in the vicinity of the carrier. +-- This is restricted to aircraft of the same coalition as the rescue helo. Enemy (or neutral) pilots will be left on their own. +-- +-- The standard "rescue zone" has a radius of 15 NM (~28 km) around the carrier. The radius can be adjusted via the @{#RESCUEHELO.SetRescueZone}(*radius*) functions, +-- where *radius* is the radius of the zone in nautical miles. If you use multiple rescue helos in the same mission, you might want to ensure that the radii +-- are not overlapping so that two helos try to rescue the same pilot. But it should not hurt either way. +-- +-- Once the helo reaches the crash site, the rescue operation will last 5 minutes. This time can be changed by @{#RESCUEHELO.SetRescueDuration(*time*), +-- where *time* is the duration in minutes. +-- +-- During the rescue operation, the helo will hover (orbit) over the crash site at a speed of 5 knots. The speed can be set by @{#RESCUEHELO.SetRescueHoverSpeed}(*speed*), +-- where the *speed* is given in knots. +-- +-- If no rescue operations should be carried out by the helo, this option can be completely disabled by using @{#RESCUEHELO.SetRescueOff}(). +-- +-- # Finite State Machine +-- +-- The implementation uses a Finite State Machine (FSM). This allows the mission designer to hook in to certain events. +-- +-- * @{#RESCUEHELO.Start}: This eventfunction starts the FMS process and initialized parameters and spawns the helo. DCS event handling is started. +-- * @{#RESCUEHELO.Status}: This eventfunction is called in regular intervals (~60 seconds) and checks the status of the helo and carrier. It triggers other events if necessary. +-- * @{#RESCUEHELO.Rescue}: This eventfunction commands the helo to go on a rescue operation at a certain coordinate. +-- * @{#RESCUEHELO.RTB}: This eventsfunction sends the helo to its home base (usually the carrier). This is called once the helo runs low on gas. +-- * @{#RESCUEHELO.Run}: This eventfunction is called when the helo resumes normal operations and goes back on station. +-- * @{#RESCUEHELO.Stop}: This eventfunction stops the FSM by unhandling DCS events. +-- +-- The mission designer can capture these events by RESCUEHELO.OnAfter*Eventname* functions, e.g. @{#RESCUEHELO.OnAfterRescue}. +-- +-- # Debugging +-- +-- In case you have problems, it is always a good idea to have a look at your DCS log file. You find it in your "Saved Games" folder, so for example in +-- C:\Users\\Saved Games\DCS\Logs\dcs.log +-- All output concerning the @{#RESCUEHELO} class should have the string "RESCUEHELO" in the corresponding line. +-- Searching for lines that contain the string "error" or "nil" can also give you a hint what's wrong. +-- +-- The verbosity of the output can be increased by adding the following lines to your script: +-- +-- BASE:TraceOnOff(true) +-- BASE:TraceLevel(1) +-- BASE:TraceClass("RESCUEHELO") +-- +-- To get even more output you can increase the trace level to 2 or even 3, c.f. @{Core.Base#BASE} for more details. +-- +-- ## Debug Mode +-- +-- You have the option to enable the debug mode for this class via the @{#RESCUEHELO.SetDebugModeON} function. +-- If enabled, text messages about the helo status will be displayed on screen and marks of the pattern created on the F10 map. +-- +-- +-- @field #RESCUEHELO +RESCUEHELO = { + ClassName = "RESCUEHELO", + Debug = false, + lid = nil, + carrier = nil, + carriertype = nil, + helogroupname = nil, + helo = nil, + airbase = nil, + takeoff = nil, + followset = nil, + formation = nil, + lowfuel = nil, + altitude = nil, + offsetX = nil, + offsetZ = nil, + rescuezone = nil, + respawn = nil, + respawninair = nil, + uncontrolledac = nil, + rescueon = nil, + rescueduration = nil, + rescuespeed = nil, + rescuestopboat = nil, + HeloFuel0 = nil, + rtb = nil, + carrierstop = nil, + alias = nil, + uid = 0, + modex = nil, + dtFollow = nil, +} + +--- Unique ID (global). +-- @field #number uid Unique ID (global). +_RESCUEHELOID=0 + +--- Class version. +-- @field #string version +RESCUEHELO.version="1.1.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- NOPE: Add messages for rescue mission. +-- NOPE: Add option to stop carrier while rescue operation is in progress? Done but NOT working. Postponed... +-- DONE: Write documentation. +-- DONE: Add option to deactivate the rescuing. +-- DONE: Possibility to add already present/spawned aircraft, e.g. for warehouse. +-- DONE: Add rescue event when aircraft crashes. +-- DONE: Make offset input parameter. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new RESCUEHELO object. +-- @param #RESCUEHELO self +-- @param Wrapper.Unit#UNIT carrierunit Carrier unit object or simply the unit name. +-- @param #string helogroupname Name of the late activated rescue helo template group. +-- @return #RESCUEHELO RESCUEHELO object. +function RESCUEHELO:New(carrierunit, helogroupname) + + -- Inherit everthing from FSM class. + local self = BASE:Inherit(self, FSM:New()) -- #RESCUEHELO + + -- Catch case when just the unit name is passed. + if type(carrierunit)=="string" then + self.carrier=UNIT:FindByName(carrierunit) + else + self.carrier=carrierunit + end + + -- Carrier type. + self.carriertype=self.carrier:GetTypeName() + + -- Helo group name. + self.helogroupname=helogroupname + + -- Increase ID. + _RESCUEHELOID=_RESCUEHELOID+1 + + -- Unique ID of this helo. + self.uid=_RESCUEHELOID + + -- Save self in static object. Easier to retrieve later. + self.carrier:SetState(self.carrier, string.format("RESCUEHELO_%d", self.uid) , self) + + -- Set unique spawn alias. + self.alias=string.format("%s_%s_%02d", self.carrier:GetName(), self.helogroupname, _RESCUEHELOID) + + -- Log ID. + self.lid=string.format("RESCUEHELO %s | ", self.alias) + + -- Init defaults. + self:SetHomeBase(AIRBASE:FindByName(self.carrier:GetName())) + self:SetTakeoffHot() + self:SetLowFuelThreshold() + self:SetAltitude() + self:SetOffsetX() + self:SetOffsetZ() + self:SetRespawnOn() + self:SetRescueOn() + self:SetRescueZone() + self:SetRescueHoverSpeed() + self:SetRescueDuration() + self:SetFollowTimeInterval() + self:SetRescueStopBoatOff() + + -- Some more. + self.rtb=false + self.carrierstop=false + + -- Debug trace. + if false then + self.Debug=true + BASE:TraceOnOff(true) + BASE:TraceClass(self.ClassName) + BASE:TraceLevel(1) + end + + ----------------------- + --- FSM Transitions --- + ----------------------- + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") + self:AddTransition("Running", "Rescue", "Rescuing") + self:AddTransition("Running", "RTB", "Returning") + self:AddTransition("Rescuing", "RTB", "Returning") + self:AddTransition("Returning", "Returned", "Returned") + self:AddTransition("Running", "Run", "Running") + self:AddTransition("Returned", "Run", "Running") + self:AddTransition("*", "Status", "*") + self:AddTransition("*", "Stop", "Stopped") + + + --- Triggers the FSM event "Start" that starts the rescue helo. Initializes parameters and starts event handlers. + -- @function [parent=#RESCUEHELO] Start + -- @param #RESCUEHELO self + + --- Triggers the FSM event "Start" that starts the rescue helo after a delay. Initializes parameters and starts event handlers. + -- @function [parent=#RESCUEHELO] __Start + -- @param #RESCUEHELO self + -- @param #number delay Delay in seconds. + + --- On after "Start" event function. Called when FSM is started. + -- @function [parent=#RESCUEHELO] OnAfterStart + -- @param #RECOVERYTANKER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- Triggers the FSM event "Rescue" that sends the helo on a rescue mission to a specifc coordinate. + -- @function [parent=#RESCUEHELO] Rescue + -- @param #RESCUEHELO self + -- @param Core.Point#COORDINATE RescueCoord Coordinate where the resue mission takes place. + + --- Triggers the delayed FSM event "Rescue" that sends the helo on a rescue mission to a specifc coordinate. + -- @function [parent=#RESCUEHELO] __Rescue + -- @param #RESCUEHELO self + -- @param #number delay Delay in seconds. + -- @param Core.Point#COORDINATE RescueCoord Coordinate where the resue mission takes place. + + --- On after "Rescue" event user function. Called when a the the helo goes on a rescue mission. + -- @function [parent=#RESCUEHELO] OnAfterRescue + -- @param #RESCUEHELO self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Point#COORDINATE RescueCoord Crash site where the rescue operation takes place. + + + --- Triggers the FSM event "RTB" that sends the helo home. + -- @function [parent=#RESCUEHELO] RTB + -- @param #RESCUEHELO self + -- @param Wrapper.Airbase#AIRBASE airbase The airbase to return to. Default is the home base. + + --- Triggers the FSM event "RTB" that sends the helo home after a delay. + -- @function [parent=#RESCUEHELO] __RTB + -- @param #RESCUEHELO self + -- @param #number delay Delay in seconds. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase to return to. Default is the home base. + + --- On after "RTB" event user function. Called when a the the helo returns to its home base. + -- @function [parent=#RESCUEHELO] OnAfterRTB + -- @param #RESCUEHELO self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase to return to. Default is the home base. + + + --- Triggers the FSM event "Returned" after the helo has landed. + -- @function [parent=#RESCUEHELO] Returned + -- @param #RESCUEHELO self + -- @param Wrapper.Airbase#AIRBASE airbase The airbase the helo has landed. + + --- Triggers the delayed FSM event "Returned" after the helo has landed. + -- @function [parent=#RESCUEHELO] __Returned + -- @param #RESCUEHELO self + -- @param #number delay Delay in seconds. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase the helo has landed. + + --- On after "Returned" event user function. Called when a the the helo has landed at an airbase. + -- @function [parent=#RESCUEHELO] OnAfterReturned + -- @param #RESCUEHELO self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase the helo has landed. + + + --- Triggers the FSM event "Run". + -- @function [parent=#RESCUEHELO] Run + -- @param #RESCUEHELO self + + --- Triggers the delayed FSM event "Run". + -- @function [parent=#RESCUEHELO] __Run + -- @param #RESCUEHELO self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Status" that updates the helo status. + -- @function [parent=#RESCUEHELO] Status + -- @param #RESCUEHELO self + + --- Triggers the delayed FSM event "Status" that updates the helo status. + -- @function [parent=#RESCUEHELO] __Status + -- @param #RESCUEHELO self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Stop" that stops the rescue helo. Event handlers are stopped. + -- @function [parent=#RESCUEHELO] Stop + -- @param #RESCUEHELO self + + --- Triggers the FSM event "Stop" that stops the rescue helo after a delay. Event handlers are stopped. + -- @function [parent=#RESCUEHELO] __Stop + -- @param #RESCUEHELO self + -- @param #number delay Delay in seconds. + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set low fuel state of helo. When fuel is below this threshold, the helo will RTB or be respawned if takeoff type is in air. +-- @param #RESCUEHELO self +-- @param #number threshold Low fuel threshold in percent. Default 5%. +-- @return #RESCUEHELO self +function RESCUEHELO:SetLowFuelThreshold(threshold) + self.lowfuel=threshold or 5 + return self +end + +--- Set home airbase of the helo. This is the airbase where the helo is spawned (if not in air) and will go when it is out of fuel. +-- @param #RESCUEHELO self +-- @param Wrapper.Airbase#AIRBASE airbase The home airbase. Can be the airbase name (passed as a string) or a Moose AIRBASE object. +-- @return #RESCUEHELO self +function RESCUEHELO:SetHomeBase(airbase) + if type(airbase)=="string" then + self.airbase=AIRBASE:FindByName(airbase) + else + self.airbase=airbase + end + if not self.airbase then + self:E(self.lid.."ERROR: Airbase is nil!") + end + return self +end + +--- Set rescue zone radius. Crashed or ejected units inside this radius of the carrier will be rescued if possible. +-- @param #RESCUEHELO self +-- @param #number radius Radius of rescue zone in nautical miles. Default is 15 NM. +-- @return #RESCUEHELO self +function RESCUEHELO:SetRescueZone(radius) + radius=UTILS.NMToMeters(radius or 15) + self.rescuezone=ZONE_UNIT:New("Rescue Zone", self.carrier, radius) + return self +end + +--- Set rescue hover speed. +-- @param #RESCUEHELO self +-- @param #number speed Speed in knots. Default 5 kts. +-- @return #RESCUEHELO self +function RESCUEHELO:SetRescueHoverSpeed(speed) + self.rescuespeed=UTILS.KnotsToMps(speed or 5) + return self +end + +--- Set rescue duration. This is the time it takes to rescue a pilot at the crash site. +-- @param #RESCUEHELO self +-- @param #number duration Duration in minutes. Default 5 min. +-- @return #RESCUEHELO self +function RESCUEHELO:SetRescueDuration(duration) + self.rescueduration=(duration or 5)*60 + return self +end + +--- Activate rescue option. Crashed and ejected pilots will be rescued. This is the default setting. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetRescueOn() + self.rescueon=true + return self +end + +--- Deactivate rescue option. Crashed and ejected pilots will not be rescued. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetRescueOff() + self.rescueon=false + return self +end + +--- Stop carrier during rescue operations. NOT WORKING! +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetRescueStopBoatOn() + self.rescuestopboat=true + return self +end + +--- Do not stop carrier during rescue operations. This is the default setting. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetRescueStopBoatOff() + self.rescuestopboat=false + return self +end + + +--- Set takeoff type. +-- @param #RESCUEHELO self +-- @param #number takeofftype Takeoff type. Default SPAWN.Takeoff.Hot. +-- @return #RESCUEHELO self +function RESCUEHELO:SetTakeoff(takeofftype) + self.takeoff=takeofftype or SPAWN.Takeoff.Hot + return self +end + +--- Set takeoff with engines running (hot). +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetTakeoffHot() + self:SetTakeoff(SPAWN.Takeoff.Hot) + return self +end + +--- Set takeoff with engines off (cold). +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetTakeoffCold() + self:SetTakeoff(SPAWN.Takeoff.Cold) + return self +end + +--- Set takeoff in air near the carrier. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetTakeoffAir() + self:SetTakeoff(SPAWN.Takeoff.Air) + return self +end + +--- Set altitude of helo. +-- @param #RESCUEHELO self +-- @param #number alt Altitude in meters. Default 70 m. +-- @return #RESCUEHELO self +function RESCUEHELO:SetAltitude(alt) + self.altitude=alt or 70 + return self +end + +--- Set offset parallel to orientation of carrier. +-- @param #RESCUEHELO self +-- @param #number distance Offset distance in meters. Default 200 m (~660 ft). +-- @return #RESCUEHELO self +function RESCUEHELO:SetOffsetX(distance) + self.offsetX=distance or 200 + return self +end + +--- Set offset perpendicular to orientation to carrier. +-- @param #RESCUEHELO self +-- @param #number distance Offset distance in meters. Default 240 m (~780 ft). +-- @return #RESCUEHELO self +function RESCUEHELO:SetOffsetZ(distance) + self.offsetZ=distance or 240 + return self +end + + +--- Enable respawning of helo. Note that this is the default behaviour. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetRespawnOn() + self.respawn=true + return self +end + +--- Disable respawning of helo. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetRespawnOff() + self.respawn=false + return self +end + +--- Set whether helo shall be respawned or not. +-- @param #RESCUEHELO self +-- @param #boolean switch If true (or nil), helo will be respawned. If false, helo will not be respawned. +-- @return #RESCUEHELO self +function RESCUEHELO:SetRespawnOnOff(switch) + if switch==nil or switch==true then + self.respawn=true + else + self.respawn=false + end + return self +end + +--- Helo will be respawned in air, even it was initially spawned on the carrier. +-- So only the first spawn will be on the carrier while all subsequent spawns will happen in air. +-- This allows for undisrupted operations and less problems on the carrier deck. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetRespawnInAir() + self.respawninair=true + return self +end + +--- Set modex (tail number) of the helo. +-- @param #RESCUEHELO self +-- @param #number modex Tail number. +-- @return #RESCUEHELO self +function RESCUEHELO:SetModex(modex) + self.modex=modex + return self +end + +--- Set follow time update interval. +-- @param #RESCUEHELO self +-- @param #number dt Time interval in seconds. Default 1.0 sec. +-- @return #RESCUEHELO self +function RESCUEHELO:SetFollowTimeInterval(dt) + self.dtFollow=dt or 1.0 + return self +end + +--- Use an uncontrolled aircraft already present in the mission rather than spawning a new helo as initial rescue helo. +-- This can be useful when interfaced with, e.g., a warehouse. +-- The group name is the one specified in the @{#RESCUEHELO.New} function. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetUseUncontrolledAircraft() + self.uncontrolledac=true + return self +end + +--- Activate debug mode. Display debug messages on screen. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetDebugModeON() + self.Debug=true + return self +end + +--- Deactivate debug mode. This is also the default setting. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetDebugModeOFF() + self.Debug=false + return self +end + +--- Check if helo is returning to base. +-- @param #RESCUEHELO self +-- @return #boolean If true, helo is returning to base. +function RESCUEHELO:IsReturning() + return self:is("Returning") +end + +--- Check if helo is operating. +-- @param #RESCUEHELO self +-- @return #boolean If true, helo is operating. +function RESCUEHELO:IsRunning() + return self:is("Running") +end + +--- Check if helo is on a rescue mission. +-- @param #RESCUEHELO self +-- @return #boolean If true, helo is rescuing somebody. +function RESCUEHELO:IsRescuing() + return self:is("Rescuing") +end + +--- Check if FMS was stopped. +-- @param #RESCUEHELO self +-- @return #boolean If true, is stopped. +function RESCUEHELO:IsStopped() + return self:is("Stopped") +end + +--- Alias of helo spawn group. +-- @param #RESCUEHELO self +-- @return #string Alias of the helo. +function RESCUEHELO:GetAlias() + return self.alias +end + +--- Get unit name of the spawned helo. +-- @param #RESCUEHELO self +-- @return #string Name of the helo unit or nil if it does not exist. +function RESCUEHELO:GetUnitName() + local unit=self.helo:GetUnit(1) + if unit then + return unit:GetName() + end + return nil +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- EVENT functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Handle landing event of rescue helo. +-- @param #RESCUEHELO self +-- @param Core.Event#EVENTDATA EventData Event data. +function RESCUEHELO:OnEventLand(EventData) + local group=EventData.IniGroup --Wrapper.Group#GROUP + + if group and group:IsAlive() then + + -- Group name that landed. + local groupname=group:GetName() + + -- Check that it was our helo that landed. + if groupname==self.helo:GetName() then + + local airbase=nil --Wrapper.Airbase#AIRBASE + local airbasename="unknown" + if EventData.Place then + airbase=EventData.Place + airbasename=airbase:GetName() + end + + -- Respawn the Helo. + local text=string.format("Rescue helo group %s landed at airbase %s.", groupname, airbasename) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) + + -- Helo has rescued someone. + -- TODO: Add "Rescued" event. + if self:IsRescuing() then + self:T(self.lid..string.format("Rescue helo %s returned from rescue operation.", groupname)) + end + + -- Check if takeoff air or respawn in air is set. Landing event should not happen unless the helo was on a rescue mission. + if self.takeoff==SPAWN.Takeoff.Air or self.respawninair then + + if not self:IsRescuing() then + + self:E(self.lid..string.format("WARNING: Rescue helo %s landed. This should not happen for Takeoff=Air or respawninair=true and no rescue operation in progress.", groupname)) + + end + end + + -- Trigger returned event. Respawn at current airbase. + self:__Returned(3, airbase) + + end + end +end + +--- A unit crashed or a player ejected. +-- @param #RESCUEHELO self +-- @param Core.Event#EVENTDATA EventData Event data. +function RESCUEHELO:_OnEventCrashOrEject(EventData) + self:F2({eventdata=EventData}) + + -- NOTE: Careful here. Eject and crash events will probably happen for the same unit! + + -- Check that there is an initiating unit in the event data. + if EventData and EventData.IniUnit then + + -- Crashed or ejected unit. + local unit=EventData.IniUnit + local unitname=tostring(EventData.IniUnitName) + + -- Check that it was not the rescue helo itself that crashed. + if EventData.IniGroupName~=self.helo:GetName() then + + -- Debug. + local text=string.format("Unit %s crashed or ejected.", unitname) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:I(self.lid..text) + + -- Get coordinate of unit. + local coord=unit:GetCoordinate() + + if coord and self.rescuezone:IsCoordinateInZone(coord) then + + -- This does not seem to work any more. Is:Alive returns flase on ejection. + -- Unit "alive" and in our rescue zone. + --if unit:IsAlive() and unit:IsInZone(self.rescuezone) then + -- Get coordinate of crashed unit. + --local coord=unit:GetCoordinate() + + -- Debug mark on map. + if self.Debug then + coord:MarkToCoalition(self.lid..string.format("Crash site of unit %s.", unitname), self.helo:GetCoalition()) + end + + -- Check that coalition is the same. + local rightcoalition=EventData.IniGroup:GetCoalition()==self.helo:GetCoalition() + + -- Only rescue if helo is "running" and not, e.g., rescuing already. + if self:IsRunning() and self.rescueon and rightcoalition then + self:Rescue(coord) + end + + end + + else + + -- Error message. + self:E(self.lid..string.format("Rescue helo %s crashed!", unitname)) + + -- Stop FSM. + self:Stop() + + -- Restart. + if self.respawn then + self:__Start(5) + end + + end + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM states +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after Start event. Starts the warehouse. Addes event handlers and schedules status updates of reqests and queue. +-- @param #RESCUEHELO self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RESCUEHELO:onafterStart(From, Event, To) + + -- Events are handled my MOOSE. + local text=string.format("Starting Rescue Helo Formation v%s for carrier unit %s of type %s.", RESCUEHELO.version, self.carrier:GetName(), self.carriertype) + self:I(self.lid..text) + + -- Handle events. + self:HandleEvent(EVENTS.Land) + self:HandleEvent(EVENTS.Crash, self._OnEventCrashOrEject) + self:HandleEvent(EVENTS.Ejection, self._OnEventCrashOrEject) + + -- Delay before formation is started. + local delay=120 + + -- Spawn helo. We need to introduce an alias in case this class is used twice. This would confuse the spawn routine. + local Spawn=SPAWN:NewWithAlias(self.helogroupname, self.alias) + + -- Set modex for spawn. + Spawn:InitModex(self.modex) + + -- Spawn in air or at airbase. + if self.takeoff==SPAWN.Takeoff.Air then + + -- Carrier heading + local hdg=self.carrier:GetHeading() + + -- Spawn distance in front of carrier. + local dist=UTILS.NMToMeters(0.2) + + -- Coordinate behind the carrier. Altitude at least 100 meters for spawning because it drops down a bit. + local Carrier=self.carrier:GetCoordinate():Translate(dist, hdg):SetAltitude(math.max(100, self.altitude)) + + -- Orientation of spawned group. + Spawn:InitHeading(hdg) + + -- Spawn at coordinate. + self.helo=Spawn:SpawnFromCoordinate(Carrier) + + -- Start formation in 1 seconds + delay=1 + + else + + -- Check if an uncontrolled helo group was requested. + if self.uncontrolledac then + + -- Use an uncontrolled aircraft group. + self.helo=GROUP:FindByName(self.helogroupname) + + if self.helo and self.helo:IsAlive() then + + -- Start uncontrolled group. + self.helo:StartUncontrolled() + + -- Delay before formation is started. + delay=60 + + else + -- No group of that name! + self:E(string.format("ERROR: No uncontrolled (alive) rescue helo group with name %s could be found!", self.helogroupname)) + return + end + + else + + -- Spawn at airbase. + self.helo=Spawn:SpawnAtAirbase(self.airbase, self.takeoff, nil, AIRBASE.TerminalType.HelicopterUsable) + + -- Delay before formation is started. + if self.takeoff==SPAWN.Takeoff.Runway then + delay=5 + elseif self.takeoff==SPAWN.Takeoff.Hot then + delay=30 + elseif self.takeoff==SPAWN.Takeoff.Cold then + delay=60 + end + + end + + end + + -- Set of group(s) to follow Mother. + self.followset=SET_GROUP:New() + self.followset:AddGroup(self.helo) + + -- Get initial fuel. + self.HeloFuel0=self.helo:GetFuel() + + -- Define AI Formation object. + self.formation=AI_FORMATION:New(self.carrier, self.followset, "Helo Formation with Carrier", "Follow Carrier at given parameters.") + + -- Formation parameters. + self.formation:FormationCenterWing(-self.offsetX, 50, math.abs(self.altitude), 50, self.offsetZ, 50) + + -- Set follow time interval. + self.formation:SetFollowTimeInterval(self.dtFollow) + + -- Formation mode. + self.formation:SetFlightModeFormation(self.helo) + + -- Start formation FSM. + self.formation:__Start(delay) + + -- Init status check + self:__Status(1) +end + +--- On after Status event. Checks player status. +-- @param #RESCUEHELO self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RESCUEHELO:onafterStatus(From, Event, To) + + -- Get current time. + local time=timer.getTime() + + -- Check if helo is running and not RTBing already or rescuing. + if self.helo and self.helo:IsAlive() then + + ------------------- + -- HELO is ALIVE -- + ------------------- + + -- Get (relative) fuel wrt to initial fuel of helo (DCS bug https://forums.eagle.ru/showthread.php?t=223712) + local fuel=self.helo:GetFuel()*100 + local fuelrel=fuel/self.HeloFuel0 + local life=self.helo:GetUnit(1):GetLife() + local life0=self.helo:GetUnit(1):GetLife0() + local lifeR=self.helo:GetUnit(1):GetLifeRelative() + + -- Report current fuel. + local text=string.format("Rescue Helo %s: state=%s fuel=%.1f, rel.fuel=%.1f, life=%.1f/%.1f=%d", self.helo:GetName(), self:GetState(), fuel, fuelrel, life, life0, lifeR*100) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) + + if self:IsRunning() then + + -- Check if fuel is low. + if fuel= 1.9.6.0) for broadcasing. +-- Advantages are that **no sound files** or radio relay units are necessary. Also the issue that FC3 aircraft hear all transmissions will be circumvented. +-- +-- The @{#ATIS.SetSRS}() requires you to specify the path to the SRS install directory or more specifically the path to the DCS-SR-ExternalAudio.exe file. +-- +-- Unfortunately, it is not possible to determine the duration of the complete transmission. So once the transmission is finished, there might be some radio silence before +-- the next iteration begins. You can fine tune the time interval between transmissions with the @{#ATIS.SetQueueUpdateTime}() function. The default interval is 90 seconds. +-- +-- # Examples +-- +-- ## Caucasus: Batumi +-- +-- -- ATIS Batumi Airport on 143.00 MHz AM. +-- atisBatumi=ATIS:New(AIRBASE.Caucasus.Batumi, 143.00) +-- atisBatumi:SetRadioRelayUnitName("Radio Relay Batumi") +-- atisBatumi:Start() +-- +-- ## Nevada: Nellis AFB +-- +-- -- ATIS Nellis AFB on 270.10 MHz AM. +-- atisNellis=ATIS:New(AIRBASE.Nevada.Nellis_AFB, 270.1) +-- atisNellis:SetRadioRelayUnitName("Radio Relay Nellis") +-- atisNellis:SetActiveRunway("21L") +-- atisNellis:SetTowerFrequencies({327.000, 132.550}) +-- atisNellis:SetTACAN(12) +-- atisNellis:AddILS(109.1, "21") +-- atisNellis:Start() +-- +-- ## Persian Gulf: Abu Dhabi International Airport +-- +-- -- ATIS Abu Dhabi International on 125.1 MHz AM. +-- atisAbuDhabi=ATIS:New(AIRBASE.PersianGulf.Abu_Dhabi_International_Airport, 125.1) +-- atisAbuDhabi:SetRadioRelayUnitName("Radio Relay Abu Dhabi International Airport") +-- atisAbuDhabi:SetMetricUnits() +-- atisAbuDhabi:SetActiveRunway("L") +-- atisAbuDhabi:SetTowerFrequencies({250.5, 119.2}) +-- atisAbuDhabi:SetVOR(114.25) +-- atisAbuDhabi:Start() +-- +-- ## SRS +-- +-- atis=ATIS:New("Batumi", 305, radio.modulation.AM) +-- atis:SetSRS("D:\\DCS\\_SRS\\", "male", "en-US") +-- atis:Start() +-- +-- This uses a male voice with US accent. It requires SRS to be installed in the `D:\DCS\_SRS\` directory. Not that backslashes need to be escaped or simply use slashes (as in linux). +-- +-- @field #ATIS +ATIS = { + ClassName = "ATIS", + Debug = false, + lid = nil, + theatre = nil, + airbasename = nil, + airbase = nil, + frequency = nil, + modulation = nil, + power = nil, + radioqueue = nil, + soundpath = nil, + relayunitname = nil, + towerfrequency = nil, + activerunway = nil, + subduration = nil, + metric = nil, + PmmHg = nil, + qnhonly = false, + TDegF = nil, + zuludiff = nil, + zulutimeonly = false, + magvar = nil, + ils = {}, + ndbinner = {}, + ndbouter = {}, + vor = nil, + tacan = nil, + rsbn = nil, + prmg = {}, + rwylength = nil, + elevation = nil, + runwaymag = {}, + runwaym2t = nil, + windtrue = nil, + altimeterQNH = nil, + usemarker = nil, + markerid = nil, + relHumidity = nil, +} + +--- NATO alphabet. +-- @type ATIS.Alphabet +ATIS.Alphabet = { + [1] = "Alfa", + [2] = "Bravo", + [3] = "Charlie", + [4] = "Delta", + [5] = "Echo", + [6] = "Delta", + [7] = "Echo", + [8] = "Foxtrot", + [9] = "Golf", + [10] = "Hotel", + [11] = "India", + [12] = "Juliett", + [13] = "Kilo", + [14] = "Lima", + [15] = "Mike", + [16] = "November", + [17] = "Oscar", + [18] = "Papa", + [19] = "Quebec", + [20] = "Romeo", + [21] = "Sierra", + [22] = "Tango", + [23] = "Uniform", + [24] = "Victor", + [25] = "Whiskey", + [26] = "Xray", + [27] = "Yankee", + [28] = "Zulu", +} + +--- Runway correction for converting true to magnetic heading. +-- @type ATIS.RunwayM2T +-- @field #number Caucasus 0° (East). +-- @field #number Nevada +12° (East). +-- @field #number Normandy -10° (West). +-- @field #number PersianGulf +2° (East). +-- @field #number TheChannel -10° (West). +-- @field #number Syria +5° (East). +-- @field #number MarianaIslands +2° (East). +ATIS.RunwayM2T={ + Caucasus=0, + Nevada=12, + Normandy=-10, + PersianGulf=2, + TheChannel=-10, + Syria=5, + MarianaIslands=2, +} + +--- Whether ICAO phraseology is used for ATIS broadcasts. +-- @type ATIS.ICAOPhraseology +-- @field #boolean Caucasus true. +-- @field #boolean Nevada false. +-- @field #boolean Normandy true. +-- @field #boolean PersianGulf true. +-- @field #boolean TheChannel true. +-- @field #boolean Syria true. +-- @field #boolean MarianaIslands true. +ATIS.ICAOPhraseology={ + Caucasus=true, + Nevada=false, + Normandy=true, + PersianGulf=true, + TheChannel=true, + Syria=true, + MarianaIslands=true, +} + +--- Nav point data. +-- @type ATIS.NavPoint +-- @field #number frequency Nav point frequency. +-- @field #string runway Runway, *e.g.* "21". +-- @field #boolean leftright If true, runway has left "L" and right "R" runways. + +--- Sound file data. +-- @type ATIS.Soundfile +-- @field #string filename Name of the file +-- @field #number duration Duration in seconds. + +--- Sound files. +-- @type ATIS.Sound +-- @field #ATIS.Soundfile ActiveRunway +-- @field #ATIS.Soundfile AdviceOnInitial +-- @field #ATIS.Soundfile Airport +-- @field #ATIS.Soundfile Altimeter +-- @field #ATIS.Soundfile At +-- @field #ATIS.Soundfile CloudBase +-- @field #ATIS.Soundfile CloudCeiling +-- @field #ATIS.Soundfile CloudsBroken +-- @field #ATIS.Soundfile CloudsFew +-- @field #ATIS.Soundfile CloudsNo +-- @field #ATIS.Soundfile CloudsNotAvailable +-- @field #ATIS.Soundfile CloudsOvercast +-- @field #ATIS.Soundfile CloudsScattered +-- @field #ATIS.Soundfile Decimal +-- @field #ATIS.Soundfile DegreesCelsius +-- @field #ATIS.Soundfile DegreesFahrenheit +-- @field #ATIS.Soundfile DewPoint +-- @field #ATIS.Soundfile Dust +-- @field #ATIS.Soundfile Elevation +-- @field #ATIS.Soundfile EndOfInformation +-- @field #ATIS.Soundfile Feet +-- @field #ATIS.Soundfile Fog +-- @field #ATIS.Soundfile Gusting +-- @field #ATIS.Soundfile HectoPascal +-- @field #ATIS.Soundfile Hundred +-- @field #ATIS.Soundfile InchesOfMercury +-- @field #ATIS.Soundfile Information +-- @field #ATIS.Soundfile Kilometers +-- @field #ATIS.Soundfile Knots +-- @field #ATIS.Soundfile Left +-- @field #ATIS.Soundfile MegaHertz +-- @field #ATIS.Soundfile Meters +-- @field #ATIS.Soundfile MetersPerSecond +-- @field #ATIS.Soundfile Miles +-- @field #ATIS.Soundfile MillimetersOfMercury +-- @field #ATIS.Soundfile N0 +-- @field #ATIS.Soundfile N1 +-- @field #ATIS.Soundfile N2 +-- @field #ATIS.Soundfile N3 +-- @field #ATIS.Soundfile N4 +-- @field #ATIS.Soundfile N5 +-- @field #ATIS.Soundfile N6 +-- @field #ATIS.Soundfile N7 +-- @field #ATIS.Soundfile N8 +-- @field #ATIS.Soundfile N9 +-- @field #ATIS.Soundfile NauticalMiles +-- @field #ATIS.Soundfile None +-- @field #ATIS.Soundfile QFE +-- @field #ATIS.Soundfile QNH +-- @field #ATIS.Soundfile Rain +-- @field #ATIS.Soundfile Right +-- @field #ATIS.Soundfile Snow +-- @field #ATIS.Soundfile SnowStorm +-- @field #ATIS.Soundfile SunriseAt +-- @field #ATIS.Soundfile SunsetAt +-- @field #ATIS.Soundfile Temperature +-- @field #ATIS.Soundfile Thousand +-- @field #ATIS.Soundfile ThunderStorm +-- @field #ATIS.Soundfile TimeLocal +-- @field #ATIS.Soundfile TimeZulu +-- @field #ATIS.Soundfile TowerFrequency +-- @field #ATIS.Soundfile Visibilty +-- @field #ATIS.Soundfile WeatherPhenomena +-- @field #ATIS.Soundfile WindFrom +-- @field #ATIS.Soundfile ILSFrequency +-- @field #ATIS.Soundfile InnerNDBFrequency +-- @field #ATIS.Soundfile OuterNDBFrequency +-- @field #ATIS.Soundfile PRMGChannel +-- @field #ATIS.Soundfile RSBNChannel +-- @field #ATIS.Soundfile RunwayLength +-- @field #ATIS.Soundfile TACANChannel +-- @field #ATIS.Soundfile VORFrequency +ATIS.Sound = { + ActiveRunway={filename="ActiveRunway.ogg", duration=0.99}, + AdviceOnInitial={filename="AdviceOnInitial.ogg", duration=3.00}, + Airport={filename="Airport.ogg", duration=0.66}, + Altimeter={filename="Altimeter.ogg", duration=0.68}, + At={filename="At.ogg", duration=0.41}, + CloudBase={filename="CloudBase.ogg", duration=0.82}, + CloudCeiling={filename="CloudCeiling.ogg", duration=0.61}, + CloudsBroken={filename="CloudsBroken.ogg", duration=1.07}, + CloudsFew={filename="CloudsFew.ogg", duration=0.99}, + CloudsNo={filename="CloudsNo.ogg", duration=1.01}, + CloudsNotAvailable={filename="CloudsNotAvailable.ogg", duration=2.35}, + CloudsOvercast={filename="CloudsOvercast.ogg", duration=0.83}, + CloudsScattered={filename="CloudsScattered.ogg", duration=1.18}, + Decimal={filename="Decimal.ogg", duration=0.54}, + DegreesCelsius={filename="DegreesCelsius.ogg", duration=1.27}, + DegreesFahrenheit={filename="DegreesFahrenheit.ogg", duration=1.23}, + DewPoint={filename="DewPoint.ogg", duration=0.65}, + Dust={filename="Dust.ogg", duration=0.54}, + Elevation={filename="Elevation.ogg", duration=0.78}, + EndOfInformation={filename="EndOfInformation.ogg", duration=1.15}, + Feet={filename="Feet.ogg", duration=0.45}, + Fog={filename="Fog.ogg", duration=0.47}, + Gusting={filename="Gusting.ogg", duration=0.55}, + HectoPascal={filename="HectoPascal.ogg", duration=1.15}, + Hundred={filename="Hundred.ogg", duration=0.47}, + InchesOfMercury={filename="InchesOfMercury.ogg", duration=1.16}, + Information={filename="Information.ogg", duration=0.85}, + Kilometers={filename="Kilometers.ogg", duration=0.78}, + Knots={filename="Knots.ogg", duration=0.59}, + Left={filename="Left.ogg", duration=0.54}, + MegaHertz={filename="MegaHertz.ogg", duration=0.87}, + Meters={filename="Meters.ogg", duration=0.59}, + MetersPerSecond={filename="MetersPerSecond.ogg", duration=1.14}, + Miles={filename="Miles.ogg", duration=0.60}, + MillimetersOfMercury={filename="MillimetersOfMercury.ogg", duration=1.53}, + Minus={filename="Minus.ogg", duration=0.64}, + N0={filename="N-0.ogg", duration=0.55}, + N1={filename="N-1.ogg", duration=0.41}, + N2={filename="N-2.ogg", duration=0.37}, + N3={filename="N-3.ogg", duration=0.41}, + N4={filename="N-4.ogg", duration=0.37}, + N5={filename="N-5.ogg", duration=0.43}, + N6={filename="N-6.ogg", duration=0.55}, + N7={filename="N-7.ogg", duration=0.43}, + N8={filename="N-8.ogg", duration=0.38}, + N9={filename="N-9.ogg", duration=0.55}, + NauticalMiles={filename="NauticalMiles.ogg", duration=1.04}, + None={filename="None.ogg", duration=0.43}, + QFE={filename="QFE.ogg", duration=0.63}, + QNH={filename="QNH.ogg", duration=0.71}, + Rain={filename="Rain.ogg", duration=0.41}, + Right={filename="Right.ogg", duration=0.44}, + Snow={filename="Snow.ogg", duration=0.48}, + SnowStorm={filename="SnowStorm.ogg", duration=0.82}, + StatuteMiles={filename="StatuteMiles.ogg", duration=1.15}, + SunriseAt={filename="SunriseAt.ogg", duration=0.92}, + SunsetAt={filename="SunsetAt.ogg", duration=0.95}, + Temperature={filename="Temperature.ogg", duration=0.64}, + Thousand={filename="Thousand.ogg", duration=0.55}, + ThunderStorm={filename="ThunderStorm.ogg", duration=0.81}, + TimeLocal={filename="TimeLocal.ogg", duration=0.90}, + TimeZulu={filename="TimeZulu.ogg", duration=0.86}, + TowerFrequency={filename="TowerFrequency.ogg", duration=1.19}, + Visibilty={filename="Visibility.ogg", duration=0.79}, + WeatherPhenomena={filename="WeatherPhenomena.ogg", duration=1.07}, + WindFrom={filename="WindFrom.ogg", duration=0.60}, + ILSFrequency={filename="ILSFrequency.ogg", duration=1.30}, + InnerNDBFrequency={filename="InnerNDBFrequency.ogg", duration=1.56}, + OuterNDBFrequency={filename="OuterNDBFrequency.ogg", duration=1.59}, + RunwayLength={filename="RunwayLength.ogg", duration=0.91}, + VORFrequency={filename="VORFrequency.ogg", duration=1.38}, + TACANChannel={filename="TACANChannel.ogg", duration=0.88}, + PRMGChannel={filename="PRMGChannel.ogg", duration=1.18}, + RSBNChannel={filename="RSBNChannel.ogg", duration=1.14}, + Zulu={filename="Zulu.ogg", duration=0.62}, +} + + +--- ATIS table containing all defined ATISes. +-- @field #table _ATIS +_ATIS={} + +--- ATIS class version. +-- @field #string version +ATIS.version="0.9.6" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Add new Normany airfields. +-- TODO: Zulu time --> Zulu in output. +-- TODO: Correct fog for elevation. +-- DONE: Add text report for output. +-- DONE: Add stop FMS functions. +-- NOGO: Use local time. Not realisitc! +-- DONE: Dew point. Approx. done. +-- DONE: Metric units. +-- DONE: Set UTC correction. +-- DONE: Set magnetic variation. +-- DONE: New DCS 2.7 weather presets. +-- DONE: whatever + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new ATIS class object for a specific aircraft carrier unit. +-- @param #ATIS self +-- @param #string airbasename Name of the airbase. +-- @param #number frequency Radio frequency in MHz. Default 143.00 MHz. +-- @param #number modulation Radio modulation: 0=AM, 1=FM. Default 0=AM. See `radio.modulation.AM` and `radio.modulation.FM` enumerators +-- @return #ATIS self +function ATIS:New(airbasename, frequency, modulation) + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #ATIS + + self.airbasename=airbasename + self.airbase=AIRBASE:FindByName(airbasename) + + if self.airbase==nil then + self:E("ERROR: Airbase %s for ATIS could not be found!", tostring(airbasename)) + return nil + end + + -- Default freq and modulation. + self.frequency=frequency or 143.00 + self.modulation=modulation or 0 + + -- Get map. + self.theatre=env.mission.theatre + + -- Set some string id for output to DCS.log file. + self.lid=string.format("ATIS %s | ", self.airbasename) + + -- This is just to hinder the garbage collector deallocating the ATIS object. + _ATIS[#_ATIS+1]=self + + -- Defaults: + self:SetSoundfilesPath() + self:SetSubtitleDuration() + self:SetMagneticDeclination() + self:SetRunwayCorrectionMagnetic2True() + self:SetRadioPower() + self:SetAltimeterQNH(true) + self:SetMapMarks(false) + self:SetRelativeHumidity() + self:SetQueueUpdateTime() + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start FSM. + self:AddTransition("*", "Status", "*") -- Update status. + self:AddTransition("*", "Broadcast", "*") -- Broadcast ATIS message. + self:AddTransition("*", "CheckQueue", "*") -- Check if radio queue is empty. + self:AddTransition("*", "Report", "*") -- Report ATIS text. + self:AddTransition("*", "Stop", "Stopped") -- Stop. + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the ATIS. + -- @function [parent=#ATIS] Start + -- @param #ATIS self + + --- Triggers the FSM event "Start" after a delay. + -- @function [parent=#ATIS] __Start + -- @param #ATIS self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Stop". Stops the ATIS. + -- @function [parent=#ATIS] Stop + -- @param #ATIS self + + --- Triggers the FSM event "Stop" after a delay. + -- @function [parent=#ATIS] __Stop + -- @param #ATIS self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Status". + -- @function [parent=#ATIS] Status + -- @param #ATIS self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#ATIS] __Status + -- @param #ATIS self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Broadcast". + -- @function [parent=#ATIS] Broadcast + -- @param #ATIS self + + --- Triggers the FSM event "Broadcast" after a delay. + -- @function [parent=#ATIS] __Broadcast + -- @param #ATIS self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "CheckQueue". + -- @function [parent=#ATIS] CheckQueue + -- @param #ATIS self + + --- Triggers the FSM event "CheckQueue" after a delay. + -- @function [parent=#ATIS] __CheckQueue + -- @param #ATIS self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Report". + -- @function [parent=#ATIS] Report + -- @param #ATIS self + -- @param #string Text Report text. + + --- Triggers the FSM event "Report" after a delay. + -- @function [parent=#ATIS] __Report + -- @param #ATIS self + -- @param #number delay Delay in seconds. + -- @param #string Text Report text. + + --- On after "Report" event user function. + -- @function [parent=#ATIS] OnAfterReport + -- @param #ATIS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string Text Report text. + + + -- Debug trace. + if false then + self.Debug=true + BASE:TraceOnOff(true) + BASE:TraceClass(self.ClassName) + BASE:TraceLevel(1) + end + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set sound files folder within miz file. +-- @param #ATIS self +-- @param #string path Path for sound files. Default "ATIS Soundfiles/". Mind the slash "/" at the end! +-- @return #ATIS self +function ATIS:SetSoundfilesPath(path) + self.soundpath=tostring(path or "ATIS Soundfiles/") + self:I(self.lid..string.format("Setting sound files path to %s", self.soundpath)) + return self +end + +--- Set airborne unit (airplane or helicopter), used to transmit radio messages including subtitles. +-- Best is to place the unit on a parking spot of the airbase and set it to *uncontrolled* in the mission editor. +-- @param #ATIS self +-- @param #string unitname Name of the unit. +-- @return #ATIS self +function ATIS:SetRadioRelayUnitName(unitname) + self.relayunitname=unitname + self:I(self.lid..string.format("Setting radio relay unit to %s", self.relayunitname)) + return self +end + +--- Set tower frequencies. +-- @param #ATIS self +-- @param #table freqs Table of frequencies in MHz. A single frequency can be given as a plain number (*i.e.* must not be table). +-- @return #ATIS self +function ATIS:SetTowerFrequencies(freqs) + if type(freqs)=="table" then + -- nothing to do + else + freqs={freqs} + end + self.towerfrequency=freqs + return self +end + +--- Set active runway. This can be used if the automatic runway determination via the wind direction gives incorrect results. +-- For example, use this if there are two runways with the same directions. +-- @param #ATIS self +-- @param #string runway Active runway, *e.g.* "31L". +-- @return #ATIS self +function ATIS:SetActiveRunway(runway) + self.activerunway=tostring(runway) + return self +end + +--- Give information on runway length. +-- @param #ATIS self +-- @return #ATIS self +function ATIS:SetRunwayLength() + self.rwylength=true + return self +end + +--- Give information on airfield elevation +-- @param #ATIS self +-- @return #ATIS self +function ATIS:SetElevation() + self.elevation=true + return self +end + +--- Set radio power. Note that this only applies if no relay unit is used. +-- @param #ATIS self +-- @param #number power Radio power in Watts. Default 100 W. +-- @return #ATIS self +function ATIS:SetRadioPower(power) + self.power=power or 100 + return self +end + +--- Use F10 map mark points. +-- @param #ATIS self +-- @param #boolean switch If *true* or *nil*, marks are placed on F10 map. If *false* this feature is set to off (default). +-- @return #ATIS self +function ATIS:SetMapMarks(switch) + if switch==nil or switch==true then + self.usemarker=true + else + self.usemarker=false + end + return self +end + +--- Set magnetic runway headings as depicted on the runway, *e.g.* "13" for 130° or "25L" for the left runway with magnetic heading 250°. +-- @param #ATIS self +-- @param #table headings Magnetic headings. Inverse (-180°) headings are added automatically. You only need to specify one heading per runway direction. "L"eft and "R" right can also be appended. +-- @return #ATIS self +function ATIS:SetRunwayHeadingsMagnetic(headings) + + -- First make sure, we have a table. + if type(headings)=="table" then + -- nothing to do + else + headings={headings} + end + + for _,heading in pairs(headings) do + + if type(heading)=="number" then + heading=string.format("%02d", heading) + end + + -- Add runway heading to table. + self:I(self.lid..string.format("Adding user specified magnetic runway heading %s", heading)) + table.insert(self.runwaymag, heading) + + local h=self:GetRunwayWithoutLR(heading) + + local head2=tonumber(h)-18 + if head2<0 then + head2=head2+36 + end + + -- Convert to string. + head2=string.format("%02d", head2) + + -- Append "L" or "R" if necessary. + local left=self:GetRunwayLR(heading) + if left==true then + head2=head2.."L" + elseif left==false then + head2=head2.."R" + end + + -- Add inverse runway heading to table. + self:I(self.lid..string.format("Adding user specified magnetic runway heading %s (inverse)", head2)) + table.insert(self.runwaymag, head2) + end + + return self +end + +--- Set duration how long subtitles are displayed. +-- @param #ATIS self +-- @param #number duration Duration in seconds. Default 10 seconds. +-- @return #ATIS self +function ATIS:SetSubtitleDuration(duration) + self.subduration=tonumber(duration or 10) + return self +end + +--- Set unit system to metric units. +-- @param #ATIS self +-- @return #ATIS self +function ATIS:SetMetricUnits() + self.metric=true + return self +end + +--- Set unit system to imperial units. +-- @param #ATIS self +-- @return #ATIS self +function ATIS:SetImperialUnits() + self.metric=false + return self +end + +--- Set pressure unit to millimeters of mercury (mmHg). +-- Default is inHg for imperial and hPa (=mBar) for metric units. +-- @param #ATIS self +-- @return #ATIS self +function ATIS:SetPressureMillimetersMercury() + self.PmmHg=true + return self +end + +--- Set temperature to be given in degrees Fahrenheit. +-- @param #ATIS self +-- @return #ATIS self +function ATIS:SetTemperatureFahrenheit() + self.TDegF=true + return self +end + +--- Set relative humidity. This is used to approximately calculate the dew point. +-- Note that the dew point is only an artificial information as DCS does not have an atmospheric model that includes humidity (yet). +-- @param #ATIS self +-- @param #number Humidity Relative Humidity, i.e. a number between 0 and 100 %. Default is 50 %. +-- @return #ATIS self +function ATIS:SetRelativeHumidity(Humidity) + self.relHumidity=Humidity or 50 + return self +end + +--- Report altimeter QNH. +-- @param #ATIS self +-- @param #boolean switch If true or nil, report altimeter QHN. If false, report QFF. +-- @return #ATIS self +function ATIS:SetAltimeterQNH(switch) + + if switch==true or switch==nil then + self.altimeterQNH=true + else + self.altimeterQNH=false + end + + return self +end + +--- Suppresses QFE readout. Default is to report both QNH and QFE. +-- @param #ATIS self +-- @return #ATIS self +function ATIS:ReportQNHOnly() + self.qnhonly=true + return self +end + +--- Set magnetic declination/variation at the airport. +-- +-- Default is per map: +-- +-- * Caucasus +6 (East), year ~ 2011 +-- * NTTR +12 (East), year ~ 2011 +-- * Normandy -10 (West), year ~ 1944 +-- * Persian Gulf +2 (East), year ~ 2011 +-- +-- To get *true* from *magnetic* heading one has to add easterly or substract westerly variation, e.g +-- +-- A magnetic heading of 180° corresponds to a true heading of +-- +-- * 186° on the Caucaus map +-- * 192° on the Nevada map +-- * 170° on the Normany map +-- * 182° on the Persian Gulf map +-- +-- Likewise, to convert *true* into *magnetic* heading, one has to substract easterly and add westerly variation. +-- +-- Or you make your life simple and just include the sign so you don't have to bother about East/West. +-- +-- @param #ATIS self +-- @param #number magvar Magnetic variation in degrees. Positive for easterly and negative for westerly variation. Default is magnatic declinaton of the used map, c.f. @{Utilities.UTils#UTILS.GetMagneticDeclination}. +-- @return #ATIS self +function ATIS:SetMagneticDeclination(magvar) + self.magvar=magvar or UTILS.GetMagneticDeclination() + return self +end + +--- Explicitly set correction of magnetic to true heading for runways. +-- @param #ATIS self +-- @param #number correction Correction of magnetic to true heading for runways in degrees. +-- @return #ATIS self +function ATIS:SetRunwayCorrectionMagnetic2True(correction) + self.runwaym2t=correction or ATIS.RunwayM2T[UTILS.GetDCSMap()] + return self +end + +--- Set wind direction (from) to be reported as *true* heading. Default is magnetic. +-- @param #ATIS self +-- @return #ATIS self +function ATIS:SetReportWindTrue() + self.windtrue=true + return self +end + +--- Set time local difference with respect to Zulu time. +-- Default is per map: +-- +-- * Caucasus +4 +-- * Nevada -8 +-- * Normandy 0 +-- * Persian Gulf +4 +-- * The Channel +2 (should be 0) +-- +-- @param #ATIS self +-- @param #number delta Time difference in hours. +-- @return #ATIS self +function ATIS:SetZuluTimeDifference(delta) + self.zuludiff=delta + return self +end + +--- Suppresses local time, sunrise, and sunset. Default is to report all these times. +-- @param #ATIS self +-- @return #ATIS self +function ATIS:ReportZuluTimeOnly() + self.zulutimeonly=true + return self +end + +--- Add ILS station. Note that this can be runway specific. +-- @param #ATIS self +-- @param #number frequency ILS frequency in MHz. +-- @param #string runway (Optional) Runway for which the given ILS frequency applies. Default all (*nil*). +-- @return #ATIS self +function ATIS:AddILS(frequency, runway) + local ils={} --#ATIS.NavPoint + ils.frequency=tonumber(frequency) + ils.runway=runway and tostring(runway) or nil + table.insert(self.ils, ils) + return self +end + +--- Set VOR station. +-- @param #ATIS self +-- @param #number frequency VOR frequency. +-- @return #ATIS self +function ATIS:SetVOR(frequency) + self.vor=frequency + return self +end + +--- Add outer NDB. Note that this can be runway specific. +-- @param #ATIS self +-- @param #number frequency NDB frequency in MHz. +-- @param #string runway (Optional) Runway for which the given NDB frequency applies. Default all (*nil*). +-- @return #ATIS self +function ATIS:AddNDBouter(frequency, runway) + local ndb={} --#ATIS.NavPoint + ndb.frequency=tonumber(frequency) + ndb.runway=runway and tostring(runway) or nil + table.insert(self.ndbouter, ndb) + return self +end + +--- Add inner NDB. Note that this can be runway specific. +-- @param #ATIS self +-- @param #number frequency NDB frequency in MHz. +-- @param #string runway (Optional) Runway for which the given NDB frequency applies. Default all (*nil*). +-- @return #ATIS self +function ATIS:AddNDBinner(frequency, runway) + local ndb={} --#ATIS.NavPoint + ndb.frequency=tonumber(frequency) + ndb.runway=runway and tostring(runway) or nil + table.insert(self.ndbinner, ndb) + return self +end + +--- Set TACAN channel. +-- @param #ATIS self +-- @param #number channel TACAN channel. +-- @return #ATIS self +function ATIS:SetTACAN(channel) + self.tacan=channel + return self +end + +--- Set RSBN channel. +-- @param #ATIS self +-- @param #number channel RSBN channel. +-- @return #ATIS self +function ATIS:SetRSBN(channel) + self.rsbn=channel + return self +end + +--- Add PRMG channel. Note that this can be runway specific. +-- @param #ATIS self +-- @param #number channel PRMG channel. +-- @param #string runway (Optional) Runway for which the given PRMG channel applies. Default all (*nil*). +-- @return #ATIS self +function ATIS:AddPRMG(channel, runway) + local ndb={} --#ATIS.NavPoint + ndb.frequency=tonumber(channel) + ndb.runway=runway and tostring(runway) or nil + table.insert(self.prmg, ndb) + return self +end + + +--- Place marks with runway data on the F10 map. +-- @param #ATIS self +-- @param #boolean markall If true, mark all runways of the map. By default only the current ATIS runways are marked. +function ATIS:MarkRunways(markall) + local airbases=AIRBASE.GetAllAirbases() + for _,_airbase in pairs(airbases) do + local airbase=_airbase --Wrapper.Airbase#AIRBASE + if (not markall and airbase:GetName()==self.airbasename) or markall==true then + airbase:GetRunwayData(self.runwaym2t, true) + end + end +end + +--- Use SRS Simple-Text-To-Speech for transmissions. No sound files necessary. +-- @param #ATIS self +-- @param #string PathToSRS Path to SRS directory. +-- @param #string Gender Gender: "male" or "female" (default). +-- @param #string Culture Culture, e.g. "en-GB" (default). +-- @param #string Voice Specific voice. Overrides `Gender` and `Culture`. +-- @param #number Port SRS port. Default 5002. +-- @return #ATIS self +function ATIS:SetSRS(PathToSRS, Gender, Culture, Voice, Port) + self.useSRS=true + self.msrs=MSRS:New(PathToSRS, self.frequency, self.modulation) + self.msrs:SetGender(Gender) + self.msrs:SetCulture(Culture) + self.msrs:SetVoice(Voice) + self.msrs:SetPort(Port) + self.msrs:SetCoalition(self:GetCoalition()) + if self.dTQueueCheck<=10 then + self:SetQueueUpdateTime(90) + end + return self +end + +--- Set the time interval between radio queue updates. +-- @param #ATIS self +-- @param #number TimeInterval Interval in seconds. Default 5 sec. +-- @return #ATIS self +function ATIS:SetQueueUpdateTime(TimeInterval) + self.dTQueueCheck=TimeInterval or 5 +end + +--- Get the coalition of the associated airbase. +-- @param #ATIS self +-- @return #number Coalition of the associcated airbase. +function ATIS:GetCoalition() + local coal=self.airbase and self.airbase:GetCoalition() or nil + return coal +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Start & Status +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Start ATIS FSM. +-- @param #ATIS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ATIS:onafterStart(From, Event, To) + + -- Check that this is an airdrome. + if self.airbase:GetAirbaseCategory()~=Airbase.Category.AIRDROME then + self:E(self.lid..string.format("ERROR: Cannot start ATIS for airbase %s! Only AIRDROMES are supported but NOT FARPS or SHIPS.", self.airbasename)) + return + end + + -- Info. + self:I(self.lid..string.format("Starting ATIS v%s for airbase %s on %.3f MHz Modulation=%d", ATIS.version, self.airbasename, self.frequency, self.modulation)) + + -- Start radio queue. + self.radioqueue=RADIOQUEUE:New(self.frequency, self.modulation, string.format("ATIS %s", self.airbasename)) + + -- Send coordinate is airbase coord. + self.radioqueue:SetSenderCoordinate(self.airbase:GetCoordinate()) + + -- Set relay unit if we have one. + self.radioqueue:SetSenderUnitName(self.relayunitname) + + -- Set radio power. + self.radioqueue:SetRadioPower(self.power) + + -- Init numbers. + self.radioqueue:SetDigit(0, ATIS.Sound.N0.filename, ATIS.Sound.N0.duration, self.soundpath) + self.radioqueue:SetDigit(1, ATIS.Sound.N1.filename, ATIS.Sound.N1.duration, self.soundpath) + self.radioqueue:SetDigit(2, ATIS.Sound.N2.filename, ATIS.Sound.N2.duration, self.soundpath) + self.radioqueue:SetDigit(3, ATIS.Sound.N3.filename, ATIS.Sound.N3.duration, self.soundpath) + self.radioqueue:SetDigit(4, ATIS.Sound.N4.filename, ATIS.Sound.N4.duration, self.soundpath) + self.radioqueue:SetDigit(5, ATIS.Sound.N5.filename, ATIS.Sound.N5.duration, self.soundpath) + self.radioqueue:SetDigit(6, ATIS.Sound.N6.filename, ATIS.Sound.N6.duration, self.soundpath) + self.radioqueue:SetDigit(7, ATIS.Sound.N7.filename, ATIS.Sound.N7.duration, self.soundpath) + self.radioqueue:SetDigit(8, ATIS.Sound.N8.filename, ATIS.Sound.N8.duration, self.soundpath) + self.radioqueue:SetDigit(9, ATIS.Sound.N9.filename, ATIS.Sound.N9.duration, self.soundpath) + + -- Start radio queue. + self.radioqueue:Start(1, 0.1) + + -- Handle airbase capture + -- Handle events. + self:HandleEvent(EVENTS.BaseCaptured) + + -- Init status updates. + self:__Status(-2) + self:__CheckQueue(-3) +end + +--- Update status. +-- @param #ATIS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ATIS:onafterStatus(From, Event, To) + + -- Get FSM state. + local fsmstate=self:GetState() + + local relayunitstatus="N/A" + if self.relayunitname then + local ru=UNIT:FindByName(self.relayunitname) + if ru then + relayunitstatus=tostring(ru:IsAlive()) + end + end + + -- Info text. + local text=string.format("State %s: Freq=%.3f MHz %s", fsmstate, self.frequency, UTILS.GetModulationName(self.modulation)) + if self.useSRS then + text=text..string.format(", SRS path=%s (%s), gender=%s, culture=%s, voice=%s", + tostring(self.msrs.path), tostring(self.msrs.port), tostring(self.msrs.gender), tostring(self.msrs.culture), tostring(self.msrs.voice)) + else + text=text..string.format(", Relay unit=%s (alive=%s)", tostring(self.relayunitname), relayunitstatus) + end + self:I(self.lid..text) + + self:__Status(-60) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Events +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check if radio queue is empty. If so, start broadcasting the message again. +-- @param #ATIS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ATIS:onafterCheckQueue(From, Event, To) + + if self.useSRS then + + self:Broadcast() + + else + + if #self.radioqueue.queue==0 then + self:T(self.lid..string.format("Radio queue empty. Repeating message.")) + self:Broadcast() + else + self:T2(self.lid..string.format("Radio queue %d transmissions queued.", #self.radioqueue.queue)) + end + + + + end + + -- Check back in 5 seconds. + self:__CheckQueue(-math.abs(self.dTQueueCheck)) +end + +--- Broadcast ATIS radio message. +-- @param #ATIS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ATIS:onafterBroadcast(From, Event, To) + + -- Get current coordinate. + local coord=self.airbase:GetCoordinate() + + -- Get elevation. + local height=coord:GetLandHeight() + + ---------------- + --- Pressure --- + ---------------- + + -- Pressure in hPa. + local qfe=coord:GetPressure(height) + local qnh=coord:GetPressure(0) + + if self.altimeterQNH then + + -- Some constants. + local L=-0.0065 --[K/m] + local R= 8.31446 --[J/mol/K] + local g= 9.80665 --[m/s^2] + local M= 0.0289644 --[kg/mol] + local T0=coord:GetTemperature(0)+273.15 --[K] Temp at sea level. + local TS=288.15 -- Standard Temperature assumed by Altimeter is 15°C + local q=qnh*100 + + -- Calculate Pressure. + local P=q*(1+L*height/T0)^(-g*M/(R*L)) -- Pressure at sea level + local Q=P/(1+L*height/TS)^(-g*M/(R*L)) -- Altimeter QNH + local A=(T0/L)*((P/q)^(((-R*L)/(g*M)))-1) -- Altitude check + + + -- Debug aoutput + self:T2(self.lid..string.format("height=%.1f, A=%.1f, T0=%.1f, QFE=%.1f, QNH=%.1f, P=%.1f, Q=%.1f hPa = %.2f", height, A, T0-273.15, qfe, qnh, P/100, Q/100, UTILS.hPa2inHg(Q/100))) + + -- Set QNH value in hPa. + qnh=Q/100 + + end + + + -- Convert to inHg. + if self.PmmHg then + qfe=UTILS.hPa2mmHg(qfe) + qnh=UTILS.hPa2mmHg(qnh) + else + if not self.metric then + qfe=UTILS.hPa2inHg(qfe) + qnh=UTILS.hPa2inHg(qnh) + end + end + + local QFE=UTILS.Split(string.format("%.2f", qfe), ".") + local QNH=UTILS.Split(string.format("%.2f", qnh), ".") + + if self.PmmHg then + QFE=UTILS.Split(string.format("%.1f", qfe), ".") + QNH=UTILS.Split(string.format("%.1f", qnh), ".") + else + if self.metric then + QFE=UTILS.Split(string.format("%.1f", qfe), ".") + QNH=UTILS.Split(string.format("%.1f", qnh), ".") + end + end + + ------------ + --- Wind --- + ------------ + + -- Get wind direction and speed in m/s. + local windFrom, windSpeed=coord:GetWind(height+10) + + -- Wind in magnetic or true. + local magvar=self.magvar + if self.windtrue then + magvar=0 + end + windFrom=windFrom-magvar + + -- Correct negative values. + if windFrom<0 then + windFrom=windFrom+360 + end + + local WINDFROM=string.format("%03d", windFrom) + local WINDSPEED=string.format("%d", UTILS.MpsToKnots(windSpeed)) + + -- Report North as 0. + if WINDFROM=="000" then + WINDFROM="360" + end + + if self.metric then + WINDSPEED=string.format("%d", windSpeed) + end + + -------------- + --- Runway --- + -------------- + + local runway, rwyLeft=self:GetActiveRunway() + + ------------ + --- Time --- + ------------ + local time=timer.getAbsTime() + + -- Conversion to Zulu time. + if self.zuludiff then + -- User specified. + time=time-self.zuludiff*60*60 + else + time=time-UTILS.GMTToLocalTimeDifference()*60*60 + end + + if time < 0 then + time = 24*60*60 + time --avoid negative time around midnight + end + + local clock=UTILS.SecondsToClock(time) + local zulu=UTILS.Split(clock, ":") + local ZULU=string.format("%s%s", zulu[1], zulu[2]) + if self.useSRS then + ZULU=string.format("%s hours", zulu[1]) + end + + + -- NATO time stamp. 0=Alfa, 1=Bravo, 2=Charlie, etc. + local NATO=ATIS.Alphabet[tonumber(zulu[1])+1] + + -- Debug. + self:T3(string.format("clock=%s", tostring(clock))) + self:T3(string.format("zulu1=%s", tostring(zulu[1]))) + self:T3(string.format("zulu2=%s", tostring(zulu[2]))) + self:T3(string.format("ZULU =%s", tostring(ZULU))) + self:T3(string.format("NATO =%s", tostring(NATO))) + + -------------------------- + --- Sunrise and Sunset --- + -------------------------- + + local sunrise=coord:GetSunrise() + sunrise=UTILS.Split(sunrise, ":") + local SUNRISE=string.format("%s%s", sunrise[1], sunrise[2]) + if self.useSRS then + SUNRISE=string.format("%s %s hours", sunrise[1], sunrise[2]) + end + + local sunset=coord:GetSunset() + sunset=UTILS.Split(sunset, ":") + local SUNSET=string.format("%s%s", sunset[1], sunset[2]) + if self.useSRS then + SUNSET=string.format("%s %s hours", sunset[1], sunset[2]) + end + + + --------------------------------- + --- Temperature and Dew Point --- + --------------------------------- + + -- Temperature in °C. + local temperature=coord:GetTemperature(height+5) + + -- Dew point in °C. + local dewpoint=temperature-(100-self.relHumidity)/5 + + -- Convert to °F. + if self.TDegF then + temperature=UTILS.CelciusToFarenheit(temperature) + dewpoint=UTILS.CelciusToFarenheit(dewpoint) + end + + local TEMPERATURE=string.format("%d", math.abs(temperature)) + local DEWPOINT=string.format("%d", math.abs(dewpoint)) + + --------------- + --- Weather --- + --------------- + + -- Get mission weather info. Most of this is static. + local clouds, visibility, turbulence, fog, dust, static=self:GetMissionWeather() + + -- Check that fog is actually "thick" enough to reach the airport. If an airport is in the mountains, fog might not affect it as it is measured from sea level. + if fog and fog.thicknessUTILS.FeetToMeters(1500) then + dust=nil + end + + ------------------ + --- Visibility --- + ------------------ + + -- Get min visibility. + local visibilitymin=visibility + + if fog then + if fog.visibility 10 then + reportedviz=10 + end + VISIBILITY=string.format("%d", reportedviz) + else + -- max reported visibility 10 NM + local reportedviz=UTILS.Round(UTILS.MetersToSM(visibilitymin)) + if reportedviz > 10 then + reportedviz=10 + end + VISIBILITY=string.format("%d", reportedviz) + end + + -------------- + --- Clouds --- + -------------- + + local cloudbase=clouds.base + local cloudceil=clouds.base+clouds.thickness + local clouddens=clouds.density + + -- Cloud preset (DCS 2.7) + local cloudspreset=clouds.preset or "Nothing" + + -- Precepitation: 0=None, 1=Rain, 2=Thunderstorm, 3=Snow, 4=Snowstorm. + local precepitation=0 + + if cloudspreset:find("Preset10") then + -- Scattered 5 + clouddens=4 + elseif cloudspreset:find("Preset11") then + -- Scattered 6 + clouddens=4 + elseif cloudspreset:find("Preset12") then + -- Scattered 7 + clouddens=4 + elseif cloudspreset:find("Preset13") then + -- Broken 1 + clouddens=7 + elseif cloudspreset:find("Preset14") then + -- Broken 2 + clouddens=7 + elseif cloudspreset:find("Preset15") then + -- Broken 3 + clouddens=7 + elseif cloudspreset:find("Preset16") then + -- Broken 4 + clouddens=7 + elseif cloudspreset:find("Preset17") then + -- Broken 5 + clouddens=7 + elseif cloudspreset:find("Preset18") then + -- Broken 6 + clouddens=7 + elseif cloudspreset:find("Preset19") then + -- Broken 7 + clouddens=7 + elseif cloudspreset:find("Preset20") then + -- Broken 8 + clouddens=7 + elseif cloudspreset:find("Preset21") then + -- Overcast 1 + clouddens=9 + elseif cloudspreset:find("Preset22") then + -- Overcast 2 + clouddens=9 + elseif cloudspreset:find("Preset23") then + -- Overcast 3 + clouddens=9 + elseif cloudspreset:find("Preset24") then + -- Overcast 4 + clouddens=9 + elseif cloudspreset:find("Preset25") then + -- Overcast 5 + clouddens=9 + elseif cloudspreset:find("Preset26") then + -- Overcast 6 + clouddens=9 + elseif cloudspreset:find("Preset27") then + -- Overcast 7 + clouddens=9 + elseif cloudspreset:find("Preset1") then + -- Light Scattered 1 + clouddens=1 + elseif cloudspreset:find("Preset2") then + -- Light Scattered 2 + clouddens=1 + elseif cloudspreset:find("Preset3") then + -- High Scattered 1 + clouddens=4 + elseif cloudspreset:find("Preset4") then + -- High Scattered 2 + clouddens=4 + elseif cloudspreset:find("Preset5") then + -- Scattered 1 + clouddens=4 + elseif cloudspreset:find("Preset6") then + -- Scattered 2 + clouddens=4 + elseif cloudspreset:find("Preset7") then + -- Scattered 3 + clouddens=4 + elseif cloudspreset:find("Preset8") then + -- High Scattered 3 + clouddens=4 + elseif cloudspreset:find("Preset9") then + -- Scattered 4 + clouddens=4 + elseif cloudspreset:find("RainyPreset") then + -- Overcast + Rain + clouddens=9 + if temperature>5 then + precepitation=1 -- rain + else + precepitation=3 -- snow + end + elseif cloudspreset:find("RainyPreset1") then + -- Overcast + Rain + clouddens=9 + if temperature>5 then + precepitation=1 -- rain + else + precepitation=3 -- snow + end + elseif cloudspreset:find("RainyPreset2") then + -- Overcast + Rain + clouddens=9 + if temperature>5 then + precepitation=1 -- rain + else + precepitation=3 -- snow + end + elseif cloudspreset:find("RainyPreset3") then + -- Overcast + Rain + clouddens=9 + if temperature>5 then + precepitation=1 -- rain + else + precepitation=3 -- snow + end + end + + local CLOUDBASE=string.format("%d", UTILS.MetersToFeet(cloudbase)) + local CLOUDCEIL=string.format("%d", UTILS.MetersToFeet(cloudceil)) + + if self.metric then + CLOUDBASE=string.format("%d", cloudbase) + CLOUDCEIL=string.format("%d", cloudceil) + end + + -- Cloud base/ceiling in thousands and hundrets of ft/meters. + local CLOUDBASE1000, CLOUDBASE0100=self:_GetThousandsAndHundreds(UTILS.MetersToFeet(cloudbase)) + local CLOUDCEIL1000, CLOUDCEIL0100=self:_GetThousandsAndHundreds(UTILS.MetersToFeet(cloudceil)) + + if self.metric then + CLOUDBASE1000, CLOUDBASE0100=self:_GetThousandsAndHundreds(cloudbase) + CLOUDCEIL1000, CLOUDCEIL0100=self:_GetThousandsAndHundreds(cloudceil) + end + + -- No cloud info for dynamic weather. + local CloudCover={} --#ATIS.Soundfile + CloudCover=ATIS.Sound.CloudsNotAvailable + local CLOUDSsub="Cloud coverage information not available" + + -- Only valid for static weather. + if static then + if clouddens>=9 then + -- Overcast 9,10 + CloudCover=ATIS.Sound.CloudsOvercast + CLOUDSsub="Overcast" + elseif clouddens>=7 then + -- Broken 7,8 + CloudCover=ATIS.Sound.CloudsBroken + CLOUDSsub="Broken clouds" + elseif clouddens>=4 then + -- Scattered 4,5,6 + CloudCover=ATIS.Sound.CloudsScattered + CLOUDSsub="Scattered clouds" + elseif clouddens>=1 then + -- Few 1,2,3 + CloudCover=ATIS.Sound.CloudsFew + CLOUDSsub="Few clouds" + else + -- No clouds + CLOUDBASE=nil + CLOUDCEIL=nil + CloudCover=ATIS.Sound.CloudsNo + CLOUDSsub="No clouds" + end + end + + -------------------- + --- Transmission --- + -------------------- + + -- Subtitle + local subtitle="" + + --Airbase name + subtitle=string.format("%s", self.airbasename) + if self.airbasename:find("AFB")==nil and self.airbasename:find("Airport")==nil and self.airbasename:find("Airstrip")==nil and self.airbasename:find("airfield")==nil and self.airbasename:find("AB")==nil then + subtitle=subtitle.." Airport" + end + if not self.useSRS then + self.radioqueue:NewTransmission(string.format("%s/%s.ogg", self.theatre, self.airbasename), 3.0, self.soundpath, nil, nil, subtitle, self.subduration) + end + local alltext=subtitle + + -- Information tag + subtitle=string.format("Information %s", NATO) + local _INFORMATION=subtitle + if not self.useSRS then + self:Transmission(ATIS.Sound.Information, 0.5, subtitle) + self.radioqueue:NewTransmission(string.format("NATO Alphabet/%s.ogg", NATO), 0.75, self.soundpath) + end + alltext=alltext..";\n"..subtitle + + -- Zulu Time + subtitle=string.format("%s Zulu", ZULU) + if not self.useSRS then + self.radioqueue:Number2Transmission(ZULU, nil, 0.5) + self:Transmission(ATIS.Sound.Zulu, 0.2, subtitle) + end + alltext=alltext..";\n"..subtitle + + if not self.zulutimeonly then + + -- Sunrise Time + subtitle=string.format("Sunrise at %s local time", SUNRISE) + if not self.useSRS then + self:Transmission(ATIS.Sound.SunriseAt, 0.5, subtitle) + self.radioqueue:Number2Transmission(SUNRISE, nil, 0.2) + self:Transmission(ATIS.Sound.TimeLocal, 0.2) + end + alltext=alltext..";\n"..subtitle + + -- Sunset Time + subtitle=string.format("Sunset at %s local time", SUNSET) + if not self.useSRS then + self:Transmission(ATIS.Sound.SunsetAt, 0.5, subtitle) + self.radioqueue:Number2Transmission(SUNSET, nil, 0.5) + self:Transmission(ATIS.Sound.TimeLocal, 0.2) + end + alltext=alltext..";\n"..subtitle + end + + -- Wind + if self.metric then + subtitle=string.format("Wind from %s at %s m/s", WINDFROM, WINDSPEED) + else + subtitle=string.format("Wind from %s at %s knots", WINDFROM, WINDSPEED) + end + if turbulence>0 then + subtitle=subtitle..", gusting" + end + local _WIND=subtitle + if not self.useSRS then + self:Transmission(ATIS.Sound.WindFrom, 1.0, subtitle) + self.radioqueue:Number2Transmission(WINDFROM) + self:Transmission(ATIS.Sound.At, 0.2) + self.radioqueue:Number2Transmission(WINDSPEED) + if self.metric then + self:Transmission(ATIS.Sound.MetersPerSecond, 0.2) + else + self:Transmission(ATIS.Sound.Knots, 0.2) + end + if turbulence>0 then + self:Transmission(ATIS.Sound.Gusting, 0.2) + end + end + alltext=alltext..";\n"..subtitle + + -- Visibility + if self.metric then + subtitle=string.format("Visibility %s km", VISIBILITY) + else + subtitle=string.format("Visibility %s SM", VISIBILITY) + end + if not self.useSRS then + self:Transmission(ATIS.Sound.Visibilty, 1.0, subtitle) + self.radioqueue:Number2Transmission(VISIBILITY) + if self.metric then + self:Transmission(ATIS.Sound.Kilometers, 0.2) + else + self:Transmission(ATIS.Sound.StatuteMiles, 0.2) + end + end + alltext=alltext..";\n"..subtitle + + -- Weather phenomena + local wp=false + local wpsub="" + if precepitation==1 then + wp=true + wpsub=wpsub.." rain" + elseif precepitation==2 then + if wp then + wpsub=wpsub.."," + end + wpsub=wpsub.." thunderstorm" + wp=true + elseif precepitation==3 then + wpsub=wpsub.." snow" + wp=true + elseif precepitation==4 then + wpsub=wpsub.." snowstorm" + wp=true + end + if fog then + if wp then + wpsub=wpsub.."," + end + wpsub=wpsub.." fog" + wp=true + end + if dust then + if wp then + wpsub=wpsub.."," + end + wpsub=wpsub.." dust" + wp=true + end + -- Actual output + if wp then + subtitle=string.format("Weather phenomena:%s", wpsub) + if not self.useSRS then + self:Transmission(ATIS.Sound.WeatherPhenomena, 1.0, subtitle) + if precepitation==1 then + self:Transmission(ATIS.Sound.Rain, 0.5) + elseif precepitation==2 then + self:Transmission(ATIS.Sound.ThunderStorm, 0.5) + elseif precepitation==3 then + self:Transmission(ATIS.Sound.Snow, 0.5) + elseif precepitation==4 then + self:Transmission(ATIS.Sound.SnowStorm, 0.5) + end + if fog then + self:Transmission(ATIS.Sound.Fog, 0.5) + end + if dust then + self:Transmission(ATIS.Sound.Dust, 0.5) + end + end + alltext=alltext..";\n"..subtitle + end + + -- Cloud base + if not self.useSRS then + self:Transmission(CloudCover, 1.0, CLOUDSsub) + end + if CLOUDBASE and static then + -- Base + local cbase=tostring(tonumber(CLOUDBASE1000)*1000+tonumber(CLOUDBASE0100)*100) + local cceil=tostring(tonumber(CLOUDCEIL1000)*1000+tonumber(CLOUDCEIL0100)*100) + if self.metric then + --subtitle=string.format("Cloud base %s, ceiling %s meters", CLOUDBASE, CLOUDCEIL) + subtitle=string.format("Cloud base %s, ceiling %s meters", cbase, cceil) + else + --subtitle=string.format("Cloud base %s, ceiling %s feet", CLOUDBASE, CLOUDCEIL) + subtitle=string.format("Cloud base %s, ceiling %s feet", cbase, cceil) + end + if not self.useSRS then + self:Transmission(ATIS.Sound.CloudBase, 1.0, subtitle) + if tonumber(CLOUDBASE1000)>0 then + self.radioqueue:Number2Transmission(CLOUDBASE1000) + self:Transmission(ATIS.Sound.Thousand, 0.1) + end + if tonumber(CLOUDBASE0100)>0 then + self.radioqueue:Number2Transmission(CLOUDBASE0100) + self:Transmission(ATIS.Sound.Hundred, 0.1) + end + -- Ceiling + self:Transmission(ATIS.Sound.CloudCeiling, 0.5) + if tonumber(CLOUDCEIL1000)>0 then + self.radioqueue:Number2Transmission(CLOUDCEIL1000) + self:Transmission(ATIS.Sound.Thousand, 0.1) + end + if tonumber(CLOUDCEIL0100)>0 then + self.radioqueue:Number2Transmission(CLOUDCEIL0100) + self:Transmission(ATIS.Sound.Hundred, 0.1) + end + if self.metric then + self:Transmission(ATIS.Sound.Meters, 0.1) + else + self:Transmission(ATIS.Sound.Feet, 0.1) + end + end + end + alltext=alltext..";\n"..subtitle + + -- Temperature + if self.TDegF then + if temperature<0 then + subtitle=string.format("Temperature -%s °F", TEMPERATURE) + else + subtitle=string.format("Temperature %s °F", TEMPERATURE) + end + else + if temperature<0 then + subtitle=string.format("Temperature -%s °C", TEMPERATURE) + else + subtitle=string.format("Temperature %s °C", TEMPERATURE) + end + end + local _TEMPERATURE=subtitle + if not self.useSRS then + self:Transmission(ATIS.Sound.Temperature, 1.0, subtitle) + if temperature<0 then + self:Transmission(ATIS.Sound.Minus, 0.2) + end + self.radioqueue:Number2Transmission(TEMPERATURE) + if self.TDegF then + self:Transmission(ATIS.Sound.DegreesFahrenheit, 0.2) + else + self:Transmission(ATIS.Sound.DegreesCelsius, 0.2) + end + end + alltext=alltext..";\n"..subtitle + + -- Dew point + if self.TDegF then + if dewpoint<0 then + subtitle=string.format("Dew point -%s °F", DEWPOINT) + else + subtitle=string.format("Dew point %s °F", DEWPOINT) + end + else + if dewpoint<0 then + subtitle=string.format("Dew point -%s °C", DEWPOINT) + else + subtitle=string.format("Dew point %s °C", DEWPOINT) + end + end + local _DEWPOINT=subtitle + if not self.useSRS then + self:Transmission(ATIS.Sound.DewPoint, 1.0, subtitle) + if dewpoint<0 then + self:Transmission(ATIS.Sound.Minus, 0.2) + end + self.radioqueue:Number2Transmission(DEWPOINT) + if self.TDegF then + self:Transmission(ATIS.Sound.DegreesFahrenheit, 0.2) + else + self:Transmission(ATIS.Sound.DegreesCelsius, 0.2) + end + end + alltext=alltext..";\n"..subtitle + + -- Altimeter QNH/QFE. + if self.PmmHg then + if self.qnhonly then + subtitle=string.format("Altimeter %s.%s mmHg", QNH[1], QNH[2]) + else + subtitle=string.format("Altimeter: QNH %s.%s, QFE %s.%s mmHg", QNH[1], QNH[2], QFE[1], QFE[2]) + end + else + if self.metric then + if self.qnhonly then + subtitle=string.format("Altimeter %s.%s hPa", QNH[1], QNH[2]) + else + subtitle=string.format("Altimeter: QNH %s.%s, QFE %s.%s hPa", QNH[1], QNH[2], QFE[1], QFE[2]) + end + else + if self.qnhonly then + subtitle=string.format("Altimeter %s.%s inHg", QNH[1], QNH[2]) + else + subtitle=string.format("Altimeter: QNH %s.%s, QFE %s.%s inHg", QNH[1], QNH[2], QFE[1], QFE[2]) + end + end + end + local _ALTIMETER=subtitle + if not self.useSRS then + self:Transmission(ATIS.Sound.Altimeter, 1.0, subtitle) + if not self.qnhonly then + self:Transmission(ATIS.Sound.QNH, 0.5) + end + self.radioqueue:Number2Transmission(QNH[1]) + + if ATIS.ICAOPhraseology[UTILS.GetDCSMap()] then + self:Transmission(ATIS.Sound.Decimal, 0.2) + end + self.radioqueue:Number2Transmission(QNH[2]) + + if not self.qnhonly then + self:Transmission(ATIS.Sound.QFE, 0.75) + self.radioqueue:Number2Transmission(QFE[1]) + if ATIS.ICAOPhraseology[UTILS.GetDCSMap()] then + self:Transmission(ATIS.Sound.Decimal, 0.2) + end + self.radioqueue:Number2Transmission(QFE[2]) + end + + if self.PmmHg then + self:Transmission(ATIS.Sound.MillimetersOfMercury, 0.1) + else + if self.metric then + self:Transmission(ATIS.Sound.HectoPascal, 0.1) + else + self:Transmission(ATIS.Sound.InchesOfMercury, 0.1) + end + end + end + alltext=alltext..";\n"..subtitle + + -- Active runway. + local subtitle=string.format("Active runway %s", runway) + if rwyLeft==true then + subtitle=subtitle.." Left" + elseif rwyLeft==false then + subtitle=subtitle.." Right" + end + local _RUNACT=subtitle + if not self.useSRS then + self:Transmission(ATIS.Sound.ActiveRunway, 1.0, subtitle) + self.radioqueue:Number2Transmission(runway) + if rwyLeft==true then + self:Transmission(ATIS.Sound.Left, 0.2) + elseif rwyLeft==false then + self:Transmission(ATIS.Sound.Right, 0.2) + end + end + alltext=alltext..";\n"..subtitle + + -- Runway length. + if self.rwylength then + + local runact=self.airbase:GetActiveRunway(self.runwaym2t) + local length=runact.length + if not self.metric then + length=UTILS.MetersToFeet(length) + end + + -- Length in thousands and hundrets of ft/meters. + local L1000, L0100=self:_GetThousandsAndHundreds(length) + + -- Subtitle. + local subtitle=string.format("Runway length %d", length) + if self.metric then + subtitle=subtitle.." meters" + else + subtitle=subtitle.." feet" + end + + -- Transmit. + if not self.useSRS then + self:Transmission(ATIS.Sound.RunwayLength, 1.0, subtitle) + if tonumber(L1000)>0 then + self.radioqueue:Number2Transmission(L1000) + self:Transmission(ATIS.Sound.Thousand, 0.1) + end + if tonumber(L0100)>0 then + self.radioqueue:Number2Transmission(L0100) + self:Transmission(ATIS.Sound.Hundred, 0.1) + end + if self.metric then + self:Transmission(ATIS.Sound.Meters, 0.1) + else + self:Transmission(ATIS.Sound.Feet, 0.1) + end + end + alltext=alltext..";\n"..subtitle + end + + -- Airfield elevation + if self.elevation then + + local elevation=self.airbase:GetHeight() + if not self.metric then + elevation=UTILS.MetersToFeet(elevation) + end + + -- Length in thousands and hundrets of ft/meters. + local L1000, L0100=self:_GetThousandsAndHundreds(elevation) + + -- Subtitle. + local subtitle=string.format("Elevation %d", elevation) + if self.metric then + subtitle=subtitle.." meters" + else + subtitle=subtitle.." feet" + end + + -- Transmit. + if not self.useSRS then + self:Transmission(ATIS.Sound.Elevation, 1.0, subtitle) + if tonumber(L1000)>0 then + self.radioqueue:Number2Transmission(L1000) + self:Transmission(ATIS.Sound.Thousand, 0.1) + end + if tonumber(L0100)>0 then + self.radioqueue:Number2Transmission(L0100) + self:Transmission(ATIS.Sound.Hundred, 0.1) + end + if self.metric then + self:Transmission(ATIS.Sound.Meters, 0.1) + else + self:Transmission(ATIS.Sound.Feet, 0.1) + end + end + alltext=alltext..";\n"..subtitle + end + + -- Tower frequency. + if self.towerfrequency then + local freqs="" + for i,freq in pairs(self.towerfrequency) do + freqs=freqs..string.format("%.3f MHz", freq) + if i<#self.towerfrequency then + freqs=freqs..", " + end + end + subtitle=string.format("Tower frequency %s", freqs) + if not self.useSRS then + self:Transmission(ATIS.Sound.TowerFrequency, 1.0, subtitle) + for _,freq in pairs(self.towerfrequency) do + local f=string.format("%.3f", freq) + f=UTILS.Split(f, ".") + self.radioqueue:Number2Transmission(f[1], nil, 0.5) + if tonumber(f[2])>0 then + self:Transmission(ATIS.Sound.Decimal, 0.2) + self.radioqueue:Number2Transmission(f[2]) + end + self:Transmission(ATIS.Sound.MegaHertz, 0.2) + end + end + alltext=alltext..";\n"..subtitle + end + + -- ILS + local ils=self:GetNavPoint(self.ils, runway, rwyLeft) + if ils then + subtitle=string.format("ILS frequency %.2f MHz", ils.frequency) + if not self.useSRS then + self:Transmission(ATIS.Sound.ILSFrequency, 1.0, subtitle) + local f=string.format("%.2f", ils.frequency) + f=UTILS.Split(f, ".") + self.radioqueue:Number2Transmission(f[1], nil, 0.5) + if tonumber(f[2])>0 then + self:Transmission(ATIS.Sound.Decimal, 0.2) + self.radioqueue:Number2Transmission(f[2]) + end + self:Transmission(ATIS.Sound.MegaHertz, 0.2) + end + alltext=alltext..";\n"..subtitle + end + + -- Outer NDB + local ndb=self:GetNavPoint(self.ndbouter, runway, rwyLeft) + if ndb then + subtitle=string.format("Outer NDB frequency %.2f MHz", ndb.frequency) + if not self.useSRS then + self:Transmission(ATIS.Sound.OuterNDBFrequency, 1.0, subtitle) + local f=string.format("%.2f", ndb.frequency) + f=UTILS.Split(f, ".") + self.radioqueue:Number2Transmission(f[1], nil, 0.5) + if tonumber(f[2])>0 then + self:Transmission(ATIS.Sound.Decimal, 0.2) + self.radioqueue:Number2Transmission(f[2]) + end + self:Transmission(ATIS.Sound.MegaHertz, 0.2) + end + alltext=alltext..";\n"..subtitle + end + + -- Inner NDB + local ndb=self:GetNavPoint(self.ndbinner, runway, rwyLeft) + if ndb then + subtitle=string.format("Inner NDB frequency %.2f MHz", ndb.frequency) + if not self.useSRS then + self:Transmission(ATIS.Sound.InnerNDBFrequency, 1.0, subtitle) + local f=string.format("%.2f", ndb.frequency) + f=UTILS.Split(f, ".") + self.radioqueue:Number2Transmission(f[1], nil, 0.5) + if tonumber(f[2])>0 then + self:Transmission(ATIS.Sound.Decimal, 0.2) + self.radioqueue:Number2Transmission(f[2]) + end + self:Transmission(ATIS.Sound.MegaHertz, 0.2) + end + alltext=alltext..";\n"..subtitle + end + + -- VOR + if self.vor then + subtitle=string.format("VOR frequency %.2f MHz", self.vor) + if self.useSRS then + subtitle=string.format("V O R frequency %.2f MHz", self.vor) + end + if not self.useSRS then + self:Transmission(ATIS.Sound.VORFrequency, 1.0, subtitle) + local f=string.format("%.2f", self.vor) + f=UTILS.Split(f, ".") + self.radioqueue:Number2Transmission(f[1], nil, 0.5) + if tonumber(f[2])>0 then + self:Transmission(ATIS.Sound.Decimal, 0.2) + self.radioqueue:Number2Transmission(f[2]) + end + self:Transmission(ATIS.Sound.MegaHertz, 0.2) + end + alltext=alltext..";\n"..subtitle + end + + -- TACAN + if self.tacan then + subtitle=string.format("TACAN channel %dX", self.tacan) + if not self.useSRS then + self:Transmission(ATIS.Sound.TACANChannel, 1.0, subtitle) + self.radioqueue:Number2Transmission(tostring(self.tacan), nil, 0.2) + self.radioqueue:NewTransmission("NATO Alphabet/Xray.ogg", 0.75, self.soundpath, nil, 0.2) + end + alltext=alltext..";\n"..subtitle + end + + -- RSBN + if self.rsbn then + subtitle=string.format("RSBN channel %d", self.rsbn) + if not self.useSRS then + self:Transmission(ATIS.Sound.RSBNChannel, 1.0, subtitle) + self.radioqueue:Number2Transmission(tostring(self.rsbn), nil, 0.2) + end + alltext=alltext..";\n"..subtitle + end + + -- PRMG + local ndb=self:GetNavPoint(self.prmg, runway, rwyLeft) + if ndb then + subtitle=string.format("PRMG channel %d", ndb.frequency) + if not self.useSRS then + self:Transmission(ATIS.Sound.PRMGChannel, 1.0, subtitle) + self.radioqueue:Number2Transmission(tostring(ndb.frequency), nil, 0.5) + end + alltext=alltext..";\n"..subtitle + end + + -- Advice on initial... + subtitle=string.format("Advise on initial contact, you have information %s", NATO) + if not self.useSRS then + self:Transmission(ATIS.Sound.AdviceOnInitial, 0.5, subtitle) + self.radioqueue:NewTransmission(string.format("NATO Alphabet/%s.ogg", NATO), 0.75, self.soundpath) + end + alltext=alltext..";\n"..subtitle + + -- Report ATIS text. + self:Report(alltext) + + -- Update F10 marker. + if self.usemarker then + self:UpdateMarker(_INFORMATION, _RUNACT, _WIND, _ALTIMETER, _TEMPERATURE) + end + +end + +--- Text report of ATIS information. Information delimitor is a semicolon ";" and a line break "\n". +-- @param #ATIS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string Text Report text. +function ATIS:onafterReport(From, Event, To, Text) + self:T(self.lid..string.format("Report:\n%s", Text)) + + if self.useSRS and self.msrs then + + -- Remove line breaks + local text=string.gsub(Text, "[\r\n]", "") + + -- Replace other stuff. + local text=string.gsub(text, "SM", "statute miles") + local text=string.gsub(text, "°C", "degrees Celsius") + local text=string.gsub(text, "°F", "degrees Fahrenheit") + local text=string.gsub(text, "inHg", "inches of Mercury") + local text=string.gsub(text, "mmHg", "millimeters of Mercury") + local text=string.gsub(text, "hPa", "hecto Pascals") + local text=string.gsub(text, "m/s", "meters per second") + + -- Replace ";" by "." + local text=string.gsub(text, ";", " . ") + + --Debug output. + self:T("SRS TTS: "..text) + + -- Play text-to-speech report. + self.msrs:PlayText(text) + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Event Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Base captured +-- @param #ATIS self +-- @param Core.Event#EVENTDATA EventData Event data. +function ATIS:OnEventBaseCaptured(EventData) + + if EventData and EventData.Place then + + -- Place is the airbase that was captured. + local airbase=EventData.Place --Wrapper.Airbase#AIRBASE + + -- Check that this airbase belongs or did belong to this warehouse. + if EventData.PlaceName==self.airbasename then + + -- New coalition of airbase after it was captured. + local NewCoalitionAirbase=airbase:GetCoalition() + + if self.useSRS and self.msrs and self.msrs.coalition~=NewCoalitionAirbase then + self.msrs:SetCoalition(NewCoalitionAirbase) + end + + end + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Update F10 map marker. +-- @param #ATIS self +-- @param #string information Information tag text. +-- @param #string runact Active runway text. +-- @param #string wind Wind text. +-- @param #string altimeter Altimeter text. +-- @param #string temperature Temperature text. +-- @return #number Marker ID. +function ATIS:UpdateMarker(information, runact, wind, altimeter, temperature) + + if self.markerid then + self.airbase:GetCoordinate():RemoveMark(self.markerid) + end + + local text=string.format("ATIS on %.3f %s, %s:\n", self.frequency, UTILS.GetModulationName(self.modulation), tostring(information)) + text=text..string.format("%s\n", tostring(runact)) + text=text..string.format("%s\n", tostring(wind)) + text=text..string.format("%s\n", tostring(altimeter)) + text=text..string.format("%s", tostring(temperature)) + -- More info is not displayed on the marker! + + -- Place new mark + self.markerid=self.airbase:GetCoordinate():MarkToAll(text, true) + + return self.markerid +end + +--- Get active runway runway. +-- @param #ATIS self +-- @return #string Active runway, e.g. "31" for 310 deg. +-- @return #boolean Use Left=true, Right=false, or nil. +function ATIS:GetActiveRunway() + + local coord=self.airbase:GetCoordinate() + local height=coord:GetLandHeight() + + -- Get wind direction and speed in m/s. + local windFrom, windSpeed=coord:GetWind(height+10) + + -- Get active runway data based on wind direction. + local runact=self.airbase:GetActiveRunway(self.runwaym2t) + + -- Active runway "31". + local runway=self:GetMagneticRunway(windFrom) or runact.idx + + -- Left or right in case there are two runways with the same heading. + local rwyLeft=nil + + -- Check if user explicitly specified a runway. + if self.activerunway then + + -- Get explicit runway heading if specified. + local runwayno=self:GetRunwayWithoutLR(self.activerunway) + if runwayno~="" then + runway=runwayno + end + + -- Was "L"eft or "R"ight given? + rwyLeft=self:GetRunwayLR(self.activerunway) + end + + return runway, rwyLeft +end + +--- Get runway from user supplied magnetic heading. +-- @param #ATIS self +-- @param #number windfrom Wind direction (from) in degrees. +-- @return #string Runway magnetic heading divided by ten (and rounded). Eg, "13" for 130°. +function ATIS:GetMagneticRunway(windfrom) + + local diffmin=nil + local runway=nil + for _,heading in pairs(self.runwaymag) do + + local hdg=self:GetRunwayWithoutLR(heading) + + local diff=UTILS.HdgDiff(windfrom, tonumber(hdg)*10) + if diffmin==nil or diff data is valid for all runways. + return nav + else + + local navy=tonumber(self:GetRunwayWithoutLR(nav.runway))*10 + local rwyy=tonumber(self:GetRunwayWithoutLR(runway))*10 + + local navL=self:GetRunwayLR(nav.runway) + local hdgD=UTILS.HdgDiff(navy,rwyy) + + if hdgD<=15 then --We allow an error of +-15° here. + if navL==nil or (navL==true and left==true) or (navL==false and left==false) then + return nav + end + end + end + end + + return nil +end + +--- Get runway heading without left or right info. +-- @param #ATIS self +-- @param #string runway Runway heading, *e.g.* "31L". +-- @return #string Runway heading without left or right, *e.g.* "31". +function ATIS:GetRunwayWithoutLR(runway) + local rwywo=runway:gsub("%D+", "") + --self:I(string.format("FF runway=%s ==> rwywo=%s", runway, rwywo)) + return rwywo +end + +--- Get info if left or right runway is active. +-- @param #ATIS self +-- @param #string runway Runway heading, *e.g.* "31L". +-- @return #boolean If *true*, left runway is active. If *false*, right runway. If *nil*, neither applies. +function ATIS:GetRunwayLR(runway) + + -- Get left/right if specified. + local rwyL=runway:lower():find("l") + local rwyR=runway:lower():find("r") + + if rwyL then + return true + elseif rwyR then + return false + else + return nil + end + +end + +--- Transmission via RADIOQUEUE. +-- @param #ATIS self +-- @param #ATIS.Soundfile sound ATIS sound object. +-- @param #number interval Interval in seconds after the last transmission finished. +-- @param #string subtitle Subtitle of the transmission. +-- @param #string path Path to sound file. Default self.soundpath. +function ATIS:Transmission(sound, interval, subtitle, path) + self.radioqueue:NewTransmission(sound.filename, sound.duration, path or self.soundpath, nil, interval, subtitle, self.subduration) +end + +--- Play all audio files. +-- @param #ATIS self +function ATIS:SoundCheck() + + for _,_sound in pairs(ATIS.Sound) do + local sound=_sound --#ATIS.Soundfile + local subtitle=string.format("Playing sound file %s, duration %.2f sec", sound.filename, sound.duration) + self:Transmission(sound, nil, subtitle) + MESSAGE:New(subtitle, 5, "ATIS"):ToAll() + end + +end + +--- Get weather of this mission from env.mission.weather variable. +-- @param #ATIS self +-- @return #table Clouds table which has entries "thickness", "density", "base", "iprecptns". +-- @return #number Visibility distance in meters. +-- @return #number Ground turbulence in m/s. +-- @return #table Fog table, which has entries "thickness", "visibility" or nil if fog is disabled in the mission. +-- @return #number Dust density or nil if dust is disabled in the mission. +-- @return #boolean static If true, static weather is used. If false, dynamic weather is used. +function ATIS:GetMissionWeather() + + -- Weather data from mission file. + local weather=env.mission.weather + + -- Clouds + --[[ + ["clouds"] = + { + ["thickness"] = 430, + ["density"] = 7, + ["base"] = 0, + ["iprecptns"] = 1, + }, -- end of ["clouds"] + ]] + local clouds=weather.clouds + + -- 0=static, 1=dynamic + local static=weather.atmosphere_type==0 + + -- Visibilty distance in meters. + local visibility=weather.visibility.distance + + -- Ground turbulence. + local turbulence=weather.groundTurbulence + + -- Dust + --[[ + ["enable_dust"] = false, + ["dust_density"] = 0, + ]] + local dust=nil + if weather.enable_dust==true then + dust=weather.dust_density + end + + -- Fog + --[[ + ["enable_fog"] = false, + ["fog"] = + { + ["thickness"] = 0, + ["visibility"] = 25, + }, -- end of ["fog"] + ]] + local fog=nil + if weather.enable_fog==true then + fog=weather.fog + end + + self:T("FF weather:") + self:T({clouds=clouds}) + self:T({visibility=visibility}) + self:T({turbulence=turbulence}) + self:T({fog=fog}) + self:T({dust=dust}) + self:T({static=static}) + return clouds, visibility, turbulence, fog, dust, static +end + + +--- Get thousands of a number. +-- @param #ATIS self +-- @param #number n Number, *e.g.* 4359. +-- @return #string Thousands of n, *e.g.* "4" for 4359. +-- @return #string Hundreds of n, *e.g.* "4" for 4359 because its rounded. +function ATIS:_GetThousandsAndHundreds(n) + + local N=UTILS.Round(n/1000, 1) + + local S=UTILS.Split(string.format("%.1f", N), ".") + + local t=S[1] + local h=S[2] + + return t, h +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- **Ops** - Auftrag (mission) for Ops. +-- +-- ## Main Features: +-- +-- * Simplifies defining and executing DCS tasks +-- * Additional useful events +-- * Set mission start/stop times +-- * Set mission priority and urgency (can cancel running missions) +-- * Specific mission options for ROE, ROT, formation, etc. +-- * Compatible with FLIGHTGROUP, NAVYGROUP, ARMYGROUP, AIRWING, WINGCOMMANDER and CHIEF classes +-- * FSM events when a mission is done, successful or failed +-- +-- === +-- +-- ## Example Missions: +-- +-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/OPS%20-%20Auftrag). +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- === +-- @module Ops.Auftrag +-- @image OPS_Auftrag.png + + +--- AUFTRAG class. +-- @type AUFTRAG +-- @field #string ClassName Name of the class. +-- @field #boolean Debug Debug mode. Messages to all about status. +-- @field #number verbose Verbosity level. +-- @field #string lid Class id string for output to DCS log file. +-- @field #number auftragsnummer Auftragsnummer. +-- @field #string type Mission type. +-- @field #string status Mission status. +-- @field #table groupdata Group specific data. +-- @field #string name Mission name. +-- @field #number prio Mission priority. +-- @field #boolean urgent Mission is urgent. Running missions with lower prio might be cancelled. +-- @field #number importance Importance. +-- @field #number Tstart Mission start time in seconds. +-- @field #number Tstop Mission stop time in seconds. +-- @field #number duration Mission duration in seconds. +-- @field Wrapper.Marker#MARKER marker F10 map marker. +-- @field #boolean markerOn If true, display marker on F10 map with the AUFTRAG status. +-- @field #number markerCoaliton Coalition to which the marker is dispayed. +-- @field #table DCStask DCS task structure. +-- @field #number Ncasualties Number of own casualties during mission. +-- @field #number Nkills Number of (enemy) units killed by assets of this mission. +-- @field #number Nelements Number of elements (units) assigned to mission. +-- @field #number dTevaluate Time interval in seconds before the mission result is evaluated after mission is over. +-- @field #number Tover Mission abs. time stamp, when mission was over. +-- @field #table conditionStart Condition(s) that have to be true, before the mission will be started. +-- @field #table conditionSuccess If all stop conditions are true, the mission is cancelled. +-- @field #table conditionFailure If all stop conditions are true, the mission is cancelled. +-- +-- @field #number orbitSpeed Orbit speed in m/s. +-- @field #number orbitAltitude Orbit altitude in meters. +-- @field #number orbitHeading Orbit heading in degrees. +-- @field #number orbitLeg Length of orbit leg in meters. +-- @field Core.Point#COORDINATE orbitRaceTrack Race-track orbit coordinate. +-- +-- @field Ops.Target#TARGET engageTarget Target data to engage. +-- +-- @field Core.Zone#ZONE_RADIUS engageZone *Circular* engagement zone. +-- @field #table engageTargetTypes Table of target types that are engaged in the engagement zone. +-- @field #number engageAltitude Engagement altitude in meters. +-- @field #number engageDirection Engagement direction in degrees. +-- @field #number engageQuantity Number of times a target is engaged. +-- @field #number engageWeaponType Weapon type used. +-- @field #number engageWeaponExpend How many weapons are used. +-- @field #boolean engageAsGroup Group attack. +-- @field #number engageMaxDistance Max engage distance. +-- @field #number refuelSystem Refuel type (boom or probe) for TANKER missions. +-- +-- @field Wrapper.Group#GROUP escortGroup The group to be escorted. +-- @field DCS#Vec3 escortVec3 The 3D offset vector from the escorted group to the escort group. +-- +-- @field #number facDesignation FAC designation type. +-- @field #boolean facDatalink FAC datalink enabled. +-- @field #number facFreq FAC radio frequency in MHz. +-- @field #number facModu FAC radio modulation 0=AM 1=FM. +-- +-- @field Core.Set#SET_GROUP transportGroupSet Groups to be transported. +-- @field Core.Point#COORDINATE transportPickup Coordinate where to pickup the cargo. +-- @field Core.Point#COORDINATE transportDropoff Coordinate where to drop off the cargo. +-- +-- @field #number artyRadius Radius in meters. +-- @field #number artyShots Number of shots fired. +-- +-- @field Ops.WingCommander#WINGCOMMANDER wingcommander The WINGCOMMANDER managing this mission. +-- @field Ops.AirWing#AIRWING airwing The assigned airwing. +-- @field #table assets Airwing Assets assigned for this mission. +-- @field #number nassets Number of required assets by the Airwing. +-- @field #number requestID The ID of the queued warehouse request. Necessary to cancel the request if the mission was cancelled before the request is processed. +-- @field #boolean cancelContactLost If true, cancel mission if the contact is lost. +-- @field #table squadrons User specified airwing squadrons assigned for this mission. Only these will be considered for the job! +-- @field #table payloads User specified airwing payloads for this mission. Only these will be considered for the job! +-- @field Ops.AirWing#AIRWING.PatrolData patroldata Patrol data. +-- +-- @field #string missionTask Mission task. See `ENUMS.MissionTask`. +-- @field #number missionAltitude Mission altitude in meters. +-- @field #number missionSpeed Mission speed in km/h. +-- @field #number missionFraction Mission coordiante fraction. Default is 0.5. +-- @field #number missionRange Mission range in meters. Used in AIRWING class. +-- @field Core.Point#COORDINATE missionWaypointCoord Mission waypoint coordinate. +-- +-- @field #table enrouteTasks Mission enroute tasks. +-- +-- @field #number repeated Number of times mission was repeated. +-- @field #number repeatedSuccess Number of times mission was repeated after a success. +-- @field #number repeatedFailure Number of times mission was repeated after a failure. +-- @field #number Nrepeat Number of times the mission is repeated. +-- @field #number NrepeatFailure Number of times mission is repeated if failed. +-- @field #number NrepeatSuccess Number of times mission is repeated if successful. +-- +-- @field Ops.OpsGroup#OPSGROUP.Radio radio Radio freq and modulation. +-- @field Ops.OpsGroup#OPSGROUP.Beacon tacan TACAN setting. +-- @field Ops.OpsGroup#OPSGROUP.Beacon icls ICLS setting. +-- +-- @field #number optionROE ROE. +-- @field #number optionROT ROT. +-- @field #number optionAlarm Alarm state. +-- @field #number optionFormation Formation. +-- @field #number optionCM Counter measures. +-- @field #number optionRTBammo RTB on out-of-ammo. +-- @field #number optionRTBfuel RTB on out-of-fuel. +-- @field #number optionECM ECM. +-- +-- @extends Core.Fsm#FSM + +--- *A warrior's mission is to foster the success of others.* - Morihei Ueshiba +-- +-- === +-- +-- ![Banner Image](..\Presentations\OPS\Auftrag\_Main.png) +-- +-- # The AUFTRAG Concept +-- +-- The AUFTRAG class significantly simplifies the workflow of using DCS tasks. +-- +-- You can think of an AUFTRAG as document, which contains the mission briefing, i.e. information about the target location, mission altitude, speed and various other parameters. +-- This document can be handed over directly to a pilot (or multiple pilots) via the @{Ops.FlightGroup#FLIGHTGROUP} class. The pilots will then execute the mission. +-- The AUFTRAG document can also be given to an AIRWING. The airwing will then determine the best assets (pilots and payloads) available for the job. +-- One more up the food chain, an AUFTRAG can be passed to a WINGCOMMANDER. The wing commander will find the best AIRWING and pass the job over to it. +-- +-- # Airborne Missions +-- +-- Several mission types are supported by this class. +-- +-- ## Anti-Ship +-- +-- An anti-ship mission can be created with the @{#AUFTRAG.NewANTISHIP}(*Target, Altitude*) function. +-- +-- ## AWACS +-- +-- An AWACS mission can be created with the @{#AUFTRAG.NewAWACS}() function. +-- +-- ## BAI +-- +-- A BAI mission can be created with the @{#AUFTRAG.NewBAI}() function. +-- +-- ## Bombing +-- +-- A bombing mission can be created with the @{#AUFTRAG.NewBOMBING}() function. +-- +-- ## Bombing Runway +-- +-- A bombing runway mission can be created with the @{#AUFTRAG.NewBOMBRUNWAY}() function. +-- +-- ## Bombing Carpet +-- +-- A carpet bombing mission can be created with the @{#AUFTRAG.NewBOMBCARPET}() function. +-- +-- ## CAP +-- +-- A CAP mission can be created with the @{#AUFTRAG.NewCAP}() function. +-- +-- ## CAS +-- +-- A CAS mission can be created with the @{#AUFTRAG.NewCAS}() function. +-- +-- ## Escort +-- +-- An escort mission can be created with the @{#AUFTRAG.NewESCORT}() function. +-- +-- ## FACA +-- +-- An FACA mission can be created with the @{#AUFTRAG.NewFACA}() function. +-- +-- ## Ferry +-- +-- Not implemented yet. +-- +-- ## Intercept +-- +-- An intercept mission can be created with the @{#AUFTRAG.NewINTERCEPT}() function. +-- +-- ## Orbit +-- +-- An orbit mission can be created with the @{#AUFTRAG.NewORBIT}() function. +-- +-- ## GCICAP +-- +-- An patrol mission can be created with the @{#AUFTRAG.NewGCICAP}() function. +-- +-- ## RECON +-- +-- Not implemented yet. +-- +-- ## RESCUE HELO +-- +-- An rescue helo mission can be created with the @{#AUFTRAG.NewRESCUEHELO}() function. +-- +-- ## SEAD +-- +-- An SEAD mission can be created with the @{#AUFTRAG.NewSEAD}() function. +-- +-- ## STRIKE +-- +-- An strike mission can be created with the @{#AUFTRAG.NewSTRIKE}() function. +-- +-- ## Tanker +-- +-- A refueling tanker mission can be created with the @{#AUFTRAG.NewTANKER}() function. +-- +-- ## TROOPTRANSPORT +-- +-- A troop transport mission can be created with the @{#AUFTRAG.NewTROOPTRANSPORT}() function. +-- +-- # Ground Missions +-- +-- ## ARTY +-- +-- An arty mission can be created with the @{#AUFTRAG.NewARTY}() function. +-- +-- # Options and Parameters +-- +-- +-- # Assigning Missions +-- +-- An AUFTRAG can be assigned to groups, airwings or wingcommanders +-- +-- ## Group Level +-- +-- ### Flight Group +-- +-- Assigning an AUFTRAG to a flight groups is done via the @{Ops.FlightGroup#FLIGHTGROUP.AddMission} function. See FLIGHTGROUP docs for details. +-- +-- ### Navy Group +-- +-- Assigning an AUFTRAG to a navy groups is done via the @{Ops.NavyGroup#NAVYGROUP.AddMission} function. See NAVYGROUP docs for details. +-- +-- ## Airwing Level +-- +-- Adding an AUFTRAG to an airwing is done via the @{Ops.AirWing#AIRWING.AddMission} function. See AIRWING docs for further details. +-- +-- ## Wing Commander Level +-- +-- Assigning an AUFTRAG to a wing commander is done via the @{Ops.WingCommander#WINGCOMMANDER.AddMission} function. See WINGCOMMADER docs for details. +-- +-- +-- # Events +-- +-- The AUFTRAG class creates many useful (FSM) events, which can be used in the mission designers script. +-- +-- +-- # Examples +-- +-- +-- @field #AUFTRAG +AUFTRAG = { + ClassName = "AUFTRAG", + Debug = false, + verbose = 0, + lid = nil, + auftragsnummer = nil, + groupdata = {}, + assets = {}, + missionFraction = 0.5, + enrouteTasks = {}, + marker = nil, + markerOn = nil, + markerCoalition = nil, + conditionStart = {}, + conditionSuccess = {}, + conditionFailure = {}, +} + +--- Global mission counter. +_AUFTRAGSNR=0 + + +--- Mission types. +-- @type AUFTRAG.Type +-- @field #string ANTISHIP Anti-ship mission. +-- @field #string AWACS AWACS mission. +-- @field #string BAI Battlefield Air Interdiction. +-- @field #string BOMBING Bombing mission. +-- @field #string BOMBRUNWAY Bomb runway of an airbase. +-- @field #string BOMBCARPET Carpet bombing. +-- @field #string CAP Combat Air Patrol. +-- @field #string CAS Close Air Support. +-- @field #string ESCORT Escort mission. +-- @field #string FACA Forward AirController airborne mission. +-- @field #string FERRY Ferry flight mission. +-- @field #string INTERCEPT Intercept mission. +-- @field #string ORBIT Orbit mission. +-- @field #string GCICAP Similar to CAP but no auto engage targets. +-- @field #string RECON Recon mission. +-- @field #string RECOVERYTANKER Recovery tanker mission. Not implemented yet. +-- @field #string RESCUEHELO Rescue helo. +-- @field #string SEAD Suppression/destruction of enemy air defences. +-- @field #string STRIKE Strike mission. +-- @field #string TANKER Tanker mission. +-- @field #string TROOPTRANSPORT Troop transport mission. +-- @field #string ARTY Fire at point. +-- @field #string PATROLZONE Patrol a zone. +AUFTRAG.Type={ + ANTISHIP="Anti Ship", + AWACS="AWACS", + BAI="BAI", + BOMBING="Bombing", + BOMBRUNWAY="Bomb Runway", + BOMBCARPET="Carpet Bombing", + CAP="CAP", + CAS="CAS", + ESCORT="Escort", + FACA="FAC-A", + FERRY="Ferry Flight", + INTERCEPT="Intercept", + ORBIT="Orbit", + GCICAP="Ground Controlled CAP", + RECON="Recon", + RECOVERYTANKER="Recovery Tanker", + RESCUEHELO="Rescue Helo", + SEAD="SEAD", + STRIKE="Strike", + TANKER="Tanker", + TROOPTRANSPORT="Troop Transport", + ARTY="Fire At Point", + PATROLZONE="Patrol Zone", +} + +--- Mission status. +-- @type AUFTRAG.Status +-- @field #string PLANNED Mission is at the early planning stage. +-- @field #string QUEUED Mission is queued at an airwing. +-- @field #string REQUESTED Mission assets were requested from the warehouse. +-- @field #string SCHEDULED Mission is scheduled in a FLIGHGROUP queue waiting to be started. +-- @field #string STARTED Mission has started but is not executed yet. +-- @field #string EXECUTING Mission is being executed. +-- @field #string DONE Mission is over. +-- @field #string CANCELLED Mission was cancelled. +-- @field #string SUCCESS Mission was a success. +-- @field #string FAILED Mission failed. +AUFTRAG.Status={ + PLANNED="planned", + QUEUED="queued", + REQUESTED="requested", + SCHEDULED="scheduled", + STARTED="started", + EXECUTING="executing", + DONE="done", + CANCELLED="cancelled", + SUCCESS="success", + FAILED="failed", +} + +--- Mission status of an assigned group. +-- @type AUFTRAG.GroupStatus +-- @field #string SCHEDULED Mission is scheduled in a FLIGHGROUP queue waiting to be started. +-- @field #string STARTED Ops group started this mission but it is not executed yet. +-- @field #string EXECUTING Ops group is executing this mission. +-- @field #string PAUSED Ops group has paused this mission, e.g. for refuelling. +-- @field #string DONE Mission task of the Ops group is done. +-- @field #string CANCELLED Mission was cancelled. +AUFTRAG.GroupStatus={ + SCHEDULED="scheduled", + STARTED="started", + EXECUTING="executing", + PAUSED="paused", + DONE="done", + CANCELLED="cancelled", +} + +--- Target type. +-- @type AUFTRAG.TargetType +-- @field #string GROUP Target is a GROUP object. +-- @field #string UNIT Target is a UNIT object. +-- @field #string STATIC Target is a STATIC object. +-- @field #string COORDINATE Target is a COORDINATE. +-- @field #string AIRBASE Target is an AIRBASE. +-- @field #string SETGROUP Target is a SET of GROUPs. +-- @field #string SETUNIT Target is a SET of UNITs. +AUFTRAG.TargetType={ + GROUP="Group", + UNIT="Unit", + STATIC="Static", + COORDINATE="Coordinate", + AIRBASE="Airbase", + SETGROUP="SetGroup", + SETUNIT="SetUnit", +} + +--- Target data. +-- @type AUFTRAG.TargetData +-- @field Wrapper.Positionable#POSITIONABLE Target Target Object. +-- @field #string Type Target type: "Group", "Unit", "Static", "Coordinate", "Airbase", "SetGroup", "SetUnit". +-- @field #string Name Target name. +-- @field #number Ninital Number of initial targets. +-- @field #number Lifepoints Total life points. +-- @field #number Lifepoints0 Inital life points. + +--- Mission capability. +-- @type AUFTRAG.Capability +-- @field #string MissionType Type of mission. +-- @field #number Performance Number describing the performance level. The higher the better. + +--- Mission success. +-- @type AUFTRAG.Success +-- @field #string SURVIVED Group did survive. +-- @field #string ENGAGED Target was engaged. +-- @field #string DAMAGED Target was damaged. +-- @field #string DESTROYED Target was destroyed. + +--- Generic mission condition. +-- @type AUFTRAG.Condition +-- @field #function func Callback function to check for a condition. Should return a #boolean. +-- @field #table arg Optional arguments passed to the condition callback function. + +--- Group specific data. Each ops group subscribed to this mission has different data for this. +-- @type AUFTRAG.GroupData +-- @field Ops.OpsGroup#OPSGROUP opsgroup The OPS group. +-- @field Core.Point#COORDINATE waypointcoordinate Waypoint coordinate. +-- @field #number waypointindex Waypoint index. +-- @field Ops.OpsGroup#OPSGROUP.Task waypointtask Waypoint task. +-- @field #string status Group mission status. +-- @field Ops.AirWing#AIRWING.SquadronAsset asset The squadron asset. + + +--- AUFTRAG class version. +-- @field #string version +AUFTRAG.version="0.6.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- DONE: Option to assign a specific payload for the mission (requires an AIRWING). +-- TODO: Mission success options damaged, destroyed. +-- TODO: Recon mission. What input? Set of coordinates? +-- NOPE: Clone mission. How? Deepcopy? ==> Create a new auftrag. +-- TODO: F10 marker to create new missions. +-- TODO: Add recovery tanker mission for boat ops. +-- DONE: Option to assign mission to specific squadrons (requires an AIRWING). +-- DONE: Add mission start conditions. +-- DONE: Add rescue helo mission for boat ops. +-- DONE: Mission ROE and ROT. +-- DONE: Mission frequency and TACAN. +-- DONE: Mission formation, etc. +-- DONE: FSM events. +-- DONE: F10 marker functions that are updated on Status event. +-- DONE: Evaluate mission result ==> SUCCESS/FAILURE +-- DONE: NewAUTO() NewA2G NewA2A +-- DONE: Transport mission. +-- DONE: Set mission coalition, e.g. for F10 markers. Could be derived from target if target has a coalition. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new generic AUFTRAG object. +-- @param #AUFTRAG self +-- @param #string Type Mission type. +-- @return #AUFTRAG self +function AUFTRAG:New(Type) + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #AUFTRAG + + -- Increase global counter. + _AUFTRAGSNR=_AUFTRAGSNR+1 + + -- Mission type. + self.type=Type + + -- Auftragsnummer. + self.auftragsnummer=_AUFTRAGSNR + + -- Log ID. + self:_SetLogID() + + -- State is planned. + self.status=AUFTRAG.Status.PLANNED + + -- Defaults + --self:SetVerbosity(0) + self:SetName() + self:SetPriority() + self:SetTime() + self.engageAsGroup=true + self.repeated=0 + self.repeatedSuccess=0 + self.repeatedFailure=0 + self.Nrepeat=0 + self.NrepeatFailure=0 + self.NrepeatSuccess=0 + self.nassets=1 + self.dTevaluate=5 + self.Ncasualties=0 + self.Nkills=0 + self.Nelements=0 + + -- FMS start state is PLANNED. + self:SetStartState(self.status) + + -- PLANNED --> (QUEUED) --> (REQUESTED) --> SCHEDULED --> STARTED --> EXECUTING --> DONE + + self:AddTransition("*", "Planned", AUFTRAG.Status.PLANNED) -- Mission is in planning stage. + self:AddTransition(AUFTRAG.Status.PLANNED, "Queued", AUFTRAG.Status.QUEUED) -- Mission is in queue of an AIRWING. + self:AddTransition(AUFTRAG.Status.QUEUED, "Requested", AUFTRAG.Status.REQUESTED) -- Mission assets have been requested from the warehouse. + self:AddTransition(AUFTRAG.Status.REQUESTED, "Scheduled", AUFTRAG.Status.SCHEDULED) -- Mission added to the first ops group queue. + + self:AddTransition(AUFTRAG.Status.PLANNED, "Scheduled", AUFTRAG.Status.SCHEDULED) -- From planned directly to scheduled. + + self:AddTransition(AUFTRAG.Status.SCHEDULED, "Started", AUFTRAG.Status.STARTED) -- First asset has started the mission + self:AddTransition(AUFTRAG.Status.STARTED, "Executing", AUFTRAG.Status.EXECUTING) -- First asset is executing the mission. + + self:AddTransition("*", "Done", AUFTRAG.Status.DONE) -- All assets have reported that mission is done. + + self:AddTransition("*", "Cancel", "*") -- Command to cancel the mission. + + self:AddTransition("*", "Success", AUFTRAG.Status.SUCCESS) + self:AddTransition("*", "Failed", AUFTRAG.Status.FAILED) + + self:AddTransition("*", "Status", "*") + self:AddTransition("*", "Stop", "*") + + self:AddTransition("*", "Repeat", AUFTRAG.Status.PLANNED) + + self:AddTransition("*", "ElementDestroyed", "*") + self:AddTransition("*", "GroupDead", "*") + self:AddTransition("*", "AssetDead", "*") + + -- Init status update. + self:__Status(-1) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Create Missions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create an ANTI-SHIP mission. +-- @param #AUFTRAG self +-- @param Wrapper.Positionable#POSITIONABLE Target The target to attack. Can be passed as a @{Wrapper.Group#GROUP} or @{Wrapper.Unit#UNIT} object. +-- @param #number Altitude Engage altitude in feet. Default 2000 ft. +-- @return #AUFTRAG self +function AUFTRAG:NewANTISHIP(Target, Altitude) + + local mission=AUFTRAG:New(AUFTRAG.Type.ANTISHIP) + + mission:_TargetFromObject(Target) + + -- DCS task parameters: + mission.engageWeaponType=ENUMS.WeaponFlag.Auto + mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL + mission.engageAltitude=UTILS.FeetToMeters(Altitude or 2000) + + -- Mission options: + mission.missionTask=ENUMS.MissionTask.ANTISHIPSTRIKE + mission.missionAltitude=mission.engageAltitude + mission.missionFraction=0.4 + mission.optionROE=ENUMS.ROE.OpenFire + mission.optionROT=ENUMS.ROT.EvadeFire + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- Create an ORBIT mission, which can be either a circular orbit or a race-track pattern. +-- @param #AUFTRAG self +-- @param Core.Point#COORDINATE Coordinate Where to orbit. +-- @param #number Altitude Orbit altitude in feet. Default is y component of `Coordinate`. +-- @param #number Speed Orbit speed in knots. Default 350 KIAS. +-- @param #number Heading Heading of race-track pattern in degrees. If not specified, a circular orbit is performed. +-- @param #number Leg Length of race-track in NM. If not specified, a circular orbit is performed. +-- @return #AUFTRAG self +function AUFTRAG:NewORBIT(Coordinate, Altitude, Speed, Heading, Leg) + + local mission=AUFTRAG:New(AUFTRAG.Type.ORBIT) + + -- Altitude. + if Altitude then + mission.orbitAltitude=UTILS.FeetToMeters(Altitude) + else + mission.orbitAltitude=Coordinate.y + end + Coordinate.y=mission.orbitAltitude + + mission:_TargetFromObject(Coordinate) + + mission.orbitSpeed = UTILS.KnotsToMps(Speed or 350) + + if Heading and Leg then + mission.orbitHeading=Heading + mission.orbitLeg=UTILS.NMToMeters(Leg) + mission.orbitRaceTrack=Coordinate:Translate(mission.orbitLeg, mission.orbitHeading, true) + end + + + -- Mission options: + mission.missionAltitude=mission.orbitAltitude*0.9 + mission.missionFraction=0.9 + mission.optionROE=ENUMS.ROE.ReturnFire + mission.optionROT=ENUMS.ROT.PassiveDefense + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- Create an ORBIT mission, where the aircraft will go in a circle around the specified coordinate. +-- @param #AUFTRAG self +-- @param Core.Point#COORDINATE Coordinate Position where to orbit around. +-- @param #number Altitude Orbit altitude in feet. Default is y component of `Coordinate`. +-- @param #number Speed Orbit speed in knots. Default 350 KIAS. +-- @return #AUFTRAG self +function AUFTRAG:NewORBIT_CIRCLE(Coordinate, Altitude, Speed) + + local mission=AUFTRAG:NewORBIT(Coordinate, Altitude, Speed) + + return mission +end + +--- Create an ORBIT mission, where the aircraft will fly a race-track pattern. +-- @param #AUFTRAG self +-- @param Core.Point#COORDINATE Coordinate Where to orbit. +-- @param #number Altitude Orbit altitude in feet. Default is y component of `Coordinate`. +-- @param #number Speed Orbit speed in knots. Default 350 KIAS. +-- @param #number Heading Heading of race-track pattern in degrees. Default random in [0, 360) degrees. +-- @param #number Leg Length of race-track in NM. Default 10 NM. +-- @return #AUFTRAG self +function AUFTRAG:NewORBIT_RACETRACK(Coordinate, Altitude, Speed, Heading, Leg) + + Heading = Heading or math.random(360) + Leg = Leg or 10 + + local mission=AUFTRAG:NewORBIT(Coordinate, Altitude, Speed, Heading, Leg) + + return mission +end + +--- Create a Ground Controlled CAP (GCICAP) mission. Flights with this task are considered for A2A INTERCEPT missions by the CHIEF class. They will perform a compat air patrol but not engage by +-- themselfs. They wait for the CHIEF to tell them whom to engage. +-- @param #AUFTRAG self +-- @param Core.Point#COORDINATE Coordinate Where to orbit. +-- @param #number Altitude Orbit altitude in feet. Default is y component of `Coordinate`. +-- @param #number Speed Orbit speed in knots. Default 350 kts. +-- @param #number Heading Heading of race-track pattern in degrees. Default random in [0, 360) degrees. +-- @param #number Leg Length of race-track in NM. Default 10 NM. +-- @return #AUFTRAG self +function AUFTRAG:NewGCICAP(Coordinate, Altitude, Speed, Heading, Leg) + + -- Create ORBIT first. + local mission=AUFTRAG:NewORBIT_RACETRACK(Coordinate, Altitude, Speed, Heading, Leg) + + -- Mission type GCICAP. + mission.type=AUFTRAG.Type.GCICAP + + mission:_SetLogID() + + -- Mission options: + mission.missionTask=ENUMS.MissionTask.INTERCEPT + mission.optionROT=ENUMS.ROT.PassiveDefense + + return mission +end + +--- Create a TANKER mission. +-- @param #AUFTRAG self +-- @param Core.Point#COORDINATE Coordinate Where to orbit. +-- @param #number Altitude Orbit altitude in feet. Default is y component of `Coordinate`. +-- @param #number Speed Orbit speed in knots. Default 350 kts. +-- @param #number Heading Heading of race-track pattern in degrees. Default 270 (East to West). +-- @param #number Leg Length of race-track in NM. Default 10 NM. +-- @param #number RefuelSystem Refueling system (1=boom, 0=probe). This info is *only* for AIRWINGs so they launch the right tanker type. +-- @return #AUFTRAG self +function AUFTRAG:NewTANKER(Coordinate, Altitude, Speed, Heading, Leg, RefuelSystem) + + -- Create ORBIT first. + local mission=AUFTRAG:NewORBIT_RACETRACK(Coordinate, Altitude, Speed, Heading, Leg) + + -- Mission type TANKER. + mission.type=AUFTRAG.Type.TANKER + + mission:_SetLogID() + + mission.refuelSystem=RefuelSystem + + -- Mission options: + mission.missionTask=ENUMS.MissionTask.REFUELING + mission.optionROE=ENUMS.ROE.WeaponHold + mission.optionROT=ENUMS.ROT.PassiveDefense + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- Create a AWACS mission. +-- @param #AUFTRAG self +-- @param Core.Point#COORDINATE Coordinate Where to orbit. Altitude is also taken from the coordinate. +-- @param #number Altitude Orbit altitude in feet. Default is y component of `Coordinate`. +-- @param #number Speed Orbit speed in knots. Default 350 kts. +-- @param #number Heading Heading of race-track pattern in degrees. Default 270 (East to West). +-- @param #number Leg Length of race-track in NM. Default 10 NM. +-- @return #AUFTRAG self +function AUFTRAG:NewAWACS(Coordinate, Altitude, Speed, Heading, Leg) + + -- Create ORBIT first. + local mission=AUFTRAG:NewORBIT_RACETRACK(Coordinate, Altitude, Speed, Heading, Leg) + + -- Mission type AWACS. + mission.type=AUFTRAG.Type.AWACS + + mission:_SetLogID() + + -- Mission options: + mission.missionTask=ENUMS.MissionTask.AWACS + mission.optionROE=ENUMS.ROE.WeaponHold + mission.optionROT=ENUMS.ROT.PassiveDefense + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + + + +--- Create an INTERCEPT mission. +-- @param #AUFTRAG self +-- @param Wrapper.Positionable#POSITIONABLE Target The target to intercept. Can also be passed as simple @{Wrapper.Group#GROUP} or @{Wrapper.Unit#UNIT} object. +-- @return #AUFTRAG self +function AUFTRAG:NewINTERCEPT(Target) + + local mission=AUFTRAG:New(AUFTRAG.Type.INTERCEPT) + + mission:_TargetFromObject(Target) + + -- Mission options: + mission.missionTask=ENUMS.MissionTask.INTERCEPT + mission.missionFraction=0.1 + mission.optionROE=ENUMS.ROE.OpenFire + mission.optionROT=ENUMS.ROT.EvadeFire + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- Create a CAP mission. +-- @param #AUFTRAG self +-- @param Core.Zone#ZONE_RADIUS ZoneCAP Circular CAP zone. Detected targets in this zone will be engaged. +-- @param #number Altitude Altitude at which to orbit in feet. Default is 10,000 ft. +-- @param #number Speed Orbit speed in knots. Default 350 kts. +-- @param Core.Point#COORDINATE Coordinate Where to orbit. Default is the center of the CAP zone. +-- @param #number Heading Heading of race-track pattern in degrees. If not specified, a simple circular orbit is performed. +-- @param #number Leg Length of race-track in NM. If not specified, a simple circular orbit is performed. +-- @param #table TargetTypes Table of target types. Default {"Air"}. +-- @return #AUFTRAG self +function AUFTRAG:NewCAP(ZoneCAP, Altitude, Speed, Coordinate, Heading, Leg, TargetTypes) + + -- Ensure given TargetTypes parameter is a table. + if TargetTypes then + if type(TargetTypes)~="table" then + TargetTypes={TargetTypes} + end + end + + -- Create ORBIT first. + local mission=AUFTRAG:NewORBIT(Coordinate or ZoneCAP:GetCoordinate(), Altitude or 10000, Speed, Heading, Leg) + + -- Mission type CAP. + mission.type=AUFTRAG.Type.CAP + mission:_SetLogID() + + -- DCS task parameters: + mission.engageZone=ZoneCAP + mission.engageTargetTypes=TargetTypes or {"Air"} + + -- Mission options: + mission.missionTask=ENUMS.MissionTask.CAP + mission.optionROE=ENUMS.ROE.OpenFire + mission.optionROT=ENUMS.ROT.EvadeFire + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- Create a CAS mission. +-- @param #AUFTRAG self +-- @param Core.Zone#ZONE_RADIUS ZoneCAS Circular CAS zone. Detected targets in this zone will be engaged. +-- @param #number Altitude Altitude at which to orbit. Default is 10,000 ft. +-- @param #number Speed Orbit speed in knots. Default 350 KIAS. +-- @param Core.Point#COORDINATE Coordinate Where to orbit. Default is the center of the CAS zone. +-- @param #number Heading Heading of race-track pattern in degrees. If not specified, a simple circular orbit is performed. +-- @param #number Leg Length of race-track in NM. If not specified, a simple circular orbit is performed. +-- @param #table TargetTypes (Optional) Table of target types. Default {"Helicopters", "Ground Units", "Light armed ships"}. +-- @return #AUFTRAG self +function AUFTRAG:NewCAS(ZoneCAS, Altitude, Speed, Coordinate, Heading, Leg, TargetTypes) + + -- Ensure given TargetTypes parameter is a table. + if TargetTypes then + if type(TargetTypes)~="table" then + TargetTypes={TargetTypes} + end + end + + -- Create ORBIT first. + local mission=AUFTRAG:NewORBIT(Coordinate or ZoneCAS:GetCoordinate(), Altitude or 10000, Speed, Heading, Leg) + + -- Mission type CAS. + mission.type=AUFTRAG.Type.CAS + mission:_SetLogID() + + -- DCS Task options: + mission.engageZone=ZoneCAS + mission.engageTargetTypes=TargetTypes or {"Helicopters", "Ground Units", "Light armed ships"} + + -- Mission options: + mission.missionTask=ENUMS.MissionTask.CAS + mission.optionROE=ENUMS.ROE.OpenFire + mission.optionROT=ENUMS.ROT.EvadeFire + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- Create a FACA mission. +-- @param #AUFTRAG self +-- @param Wrapper.Group#GROUP Target Target group. Must be a GROUP object. +-- @param #string Designation Designation of target. See `AI.Task.Designation`. Default `AI.Task.Designation.AUTO`. +-- @param #boolean DataLink Enable data link. Default `true`. +-- @param #number Frequency Radio frequency in MHz the FAC uses for communication. Default is 133 MHz. +-- @param #number Modulation Radio modulation band. Default 0=AM. Use 1 for FM. See radio.modulation.AM or radio.modulaton.FM. +-- @return #AUFTRAG self +function AUFTRAG:NewFACA(Target, Designation, DataLink, Frequency, Modulation) + + local mission=AUFTRAG:New(AUFTRAG.Type.FACA) + + mission:_TargetFromObject(Target) + + -- TODO: check that target is really a group object! + + -- DCS Task options: + mission.facDesignation=Designation --or AI.Task.Designation.AUTO + mission.facDatalink=true + mission.facFreq=Frequency or 133 + mission.facModu=Modulation or radio.modulation.AM + + -- Mission options: + mission.missionTask=ENUMS.MissionTask.AFAC + mission.missionAltitude=nil + mission.missionFraction=0.5 + mission.optionROE=ENUMS.ROE.ReturnFire + mission.optionROT=ENUMS.ROT.PassiveDefense + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + + +--- Create a BAI mission. +-- @param #AUFTRAG self +-- @param Wrapper.Positionable#POSITIONABLE Target The target to attack. Can be a GROUP, UNIT or STATIC object. +-- @param #number Altitude Engage altitude in feet. Default 2000 ft. +-- @return #AUFTRAG self +function AUFTRAG:NewBAI(Target, Altitude) + + local mission=AUFTRAG:New(AUFTRAG.Type.BAI) + + mission:_TargetFromObject(Target) + + -- DCS Task options: + mission.engageWeaponType=ENUMS.WeaponFlag.AnyAG + mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL + mission.engageAltitude=UTILS.FeetToMeters(Altitude or 2000) + + -- Mission options: + mission.missionTask=ENUMS.MissionTask.GROUNDATTACK + mission.missionAltitude=mission.engageAltitude + mission.missionFraction=0.75 + mission.optionROE=ENUMS.ROE.OpenFire + mission.optionROT=ENUMS.ROT.PassiveDefense + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- Create a SEAD mission. +-- @param #AUFTRAG self +-- @param Wrapper.Positionable#POSITIONABLE Target The target to attack. Can be a GROUP or UNIT object. +-- @param #number Altitude Engage altitude in feet. Default 2000 ft. +-- @return #AUFTRAG self +function AUFTRAG:NewSEAD(Target, Altitude) + + local mission=AUFTRAG:New(AUFTRAG.Type.SEAD) + + mission:_TargetFromObject(Target) + + -- DCS Task options: + mission.engageWeaponType=ENUMS.WeaponFlag.AnyAG --ENUMS.WeaponFlag.Cannons + mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL + mission.engageAltitude=UTILS.FeetToMeters(Altitude or 2000) + + -- Mission options: + mission.missionTask=ENUMS.MissionTask.SEAD + mission.missionAltitude=mission.engageAltitude + mission.missionFraction=0.2 + mission.optionROE=ENUMS.ROE.OpenFire + mission.optionROT=ENUMS.ROT.EvadeFire + --mission.optionROT=ENUMS.ROT.AllowAbortMission + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- Create a STRIKE mission. Flight will attack the closest map object to the specified coordinate. +-- @param #AUFTRAG self +-- @param Core.Point#COORDINATE Target The target coordinate. Can also be given as a GROUP, UNIT or STATIC object. +-- @param #number Altitude Engage altitude in feet. Default 2000 ft. +-- @return #AUFTRAG self +function AUFTRAG:NewSTRIKE(Target, Altitude) + + local mission=AUFTRAG:New(AUFTRAG.Type.STRIKE) + + mission:_TargetFromObject(Target) + + -- DCS Task options: + mission.engageWeaponType=ENUMS.WeaponFlag.AnyAG + mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL + mission.engageAltitude=UTILS.FeetToMeters(Altitude or 2000) + + -- Mission options: + mission.missionTask=ENUMS.MissionTask.GROUNDATTACK + mission.missionAltitude=mission.engageAltitude + mission.missionFraction=0.75 + mission.optionROE=ENUMS.ROE.OpenFire + mission.optionROT=ENUMS.ROT.PassiveDefense + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- Create a BOMBING mission. Flight will drop bombs a specified coordinate. +-- @param #AUFTRAG self +-- @param Core.Point#COORDINATE Target Target coordinate. Can also be specified as a GROUP, UNIT or STATIC object. +-- @param #number Altitude Engage altitude in feet. Default 25000 ft. +-- @return #AUFTRAG self +function AUFTRAG:NewBOMBING(Target, Altitude) + + local mission=AUFTRAG:New(AUFTRAG.Type.BOMBING) + + mission:_TargetFromObject(Target) + + -- DCS task options: + mission.engageWeaponType=ENUMS.WeaponFlag.AnyBomb + mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL + mission.engageAltitude=UTILS.FeetToMeters(Altitude or 25000) + + -- Mission options: + mission.missionTask=ENUMS.MissionTask.GROUNDATTACK + mission.missionAltitude=mission.engageAltitude*0.8 + mission.missionFraction=0.5 + mission.optionROE=ENUMS.ROE.OpenFire + mission.optionROT=ENUMS.ROT.NoReaction -- No reaction is better. + + -- Evaluate result after 5 min. We might need time until the bombs have dropped and targets have been detroyed. + mission.dTevaluate=5*60 + + -- Get DCS task. + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- Create a BOMBRUNWAY mission. +-- @param #AUFTRAG self +-- @param Wrapper.Airbase#AIRBASE Airdrome The airbase to bomb. This must be an airdrome (not a FARP or ship) as these to not have a runway. +-- @param #number Altitude Engage altitude in feet. Default 25000 ft. +-- @return #AUFTRAG self +function AUFTRAG:NewBOMBRUNWAY(Airdrome, Altitude) + + if type(Airdrome)=="string" then + Airdrome=AIRBASE:FindByName(Airdrome) + end + + if Airdrome:IsInstanceOf("AIRBASE") then + + end + + local mission=AUFTRAG:New(AUFTRAG.Type.BOMBRUNWAY) + + mission:_TargetFromObject(Airdrome) + + -- DCS task options: + mission.engageWeaponType=ENUMS.WeaponFlag.AnyBomb + mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL + mission.engageAltitude=UTILS.FeetToMeters(Altitude or 25000) + + -- Mission options: + mission.missionTask=ENUMS.MissionTask.RUNWAYATTACK + mission.missionAltitude=mission.engageAltitude*0.8 + mission.missionFraction=0.75 + mission.optionROE=ENUMS.ROE.OpenFire + mission.optionROT=ENUMS.ROT.PassiveDefense + + -- Evaluate result after 5 min. + mission.dTevaluate=5*60 + + -- Get DCS task. + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- Create a CARPET BOMBING mission. +-- @param #AUFTRAG self +-- @param Core.Point#COORDINATE Target Target coordinate. Can also be specified as a GROUP, UNIT or STATIC object. +-- @param #number Altitude Engage altitude in feet. Default 25000 ft. +-- @param #number CarpetLength Length of bombing carpet in meters. Default 500 m. +-- @return #AUFTRAG self +function AUFTRAG:NewBOMBCARPET(Target, Altitude, CarpetLength) + + local mission=AUFTRAG:New(AUFTRAG.Type.BOMBCARPET) + + mission:_TargetFromObject(Target) + + -- DCS task options: + mission.engageWeaponType=ENUMS.WeaponFlag.AnyBomb + mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL + mission.engageAltitude=UTILS.FeetToMeters(Altitude or 25000) + mission.engageCarpetLength=CarpetLength or 500 + mission.engageAsGroup=false -- Looks like this must be false or the task is not executed. It is not available in the ME anyway but in the task of the mission file. + mission.engageDirection=nil -- This is also not available in the ME. + + -- Mission options: + mission.missionTask=ENUMS.MissionTask.GROUNDATTACK + mission.missionAltitude=mission.engageAltitude*0.8 + mission.missionFraction=0.5 + mission.optionROE=ENUMS.ROE.OpenFire + mission.optionROT=ENUMS.ROT.NoReaction + + -- Evaluate result after 5 min. + mission.dTevaluate=5*60 + + -- Get DCS task. + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + + +--- Create an ESCORT (or FOLLOW) mission. Flight will escort another group and automatically engage certain target types. +-- @param #AUFTRAG self +-- @param Wrapper.Group#GROUP EscortGroup The group to escort. +-- @param DCS#Vec3 OffsetVector A table with x, y and z components specifying the offset of the flight to the escorted group. Default {x=-100, y=0, z=200} for z=200 meters to the right, same alitude, x=100 meters behind. +-- @param #number EngageMaxDistance Max engage distance of targets in nautical miles. Default auto (*nil*). +-- @param #table TargetTypes Types of targets to engage automatically. Default is {"Air"}, i.e. all enemy airborne units. Use an empty set {} for a simple "FOLLOW" mission. +-- @return #AUFTRAG self +function AUFTRAG:NewESCORT(EscortGroup, OffsetVector, EngageMaxDistance, TargetTypes) + + local mission=AUFTRAG:New(AUFTRAG.Type.ESCORT) + + mission:_TargetFromObject(EscortGroup) + + -- DCS task parameters: + mission.escortVec3=OffsetVector or {x=-100, y=0, z=200} + mission.engageMaxDistance=EngageMaxDistance and UTILS.NMToMeters(EngageMaxDistance) or nil + mission.engageTargetTypes=TargetTypes or {"Air"} + + -- Mission options: + mission.missionTask=ENUMS.MissionTask.ESCORT + mission.missionFraction=0.1 + mission.missionAltitude=1000 + mission.optionROE=ENUMS.ROE.OpenFire -- TODO: what's the best ROE here? Make dependent on ESCORT or FOLLOW! + mission.optionROT=ENUMS.ROT.PassiveDefense + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- Create a RESCUE HELO mission. +-- @param #AUFTRAG self +-- @param Wrapper.Unit#UNIT Carrier The carrier unit. +-- @return #AUFTRAG self +function AUFTRAG:NewRESCUEHELO(Carrier) + + local mission=AUFTRAG:New(AUFTRAG.Type.RESCUEHELO) + + --mission.carrier=Carrier + + mission:_TargetFromObject(Carrier) + + -- Mission options: + mission.missionTask=ENUMS.MissionTask.NOTHING + mission.missionFraction=0.5 + mission.optionROE=ENUMS.ROE.WeaponHold + mission.optionROT=ENUMS.ROT.NoReaction + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + + +--- Create a TROOP TRANSPORT mission. +-- @param #AUFTRAG self +-- @param Core.Set#SET_GROUP TransportGroupSet The set group(s) to be transported. +-- @param Core.Point#COORDINATE DropoffCoordinate Coordinate where the helo will land drop off the the troops. +-- @param Core.Point#COORDINATE PickupCoordinate Coordinate where the helo will land to pick up the the cargo. Default is the fist transport group. +-- @return #AUFTRAG self +function AUFTRAG:NewTROOPTRANSPORT(TransportGroupSet, DropoffCoordinate, PickupCoordinate) + + local mission=AUFTRAG:New(AUFTRAG.Type.TROOPTRANSPORT) + + if TransportGroupSet:IsInstanceOf("GROUP") then + mission.transportGroupSet=SET_GROUP:New() + mission.transportGroupSet:AddGroup(TransportGroupSet) + elseif TransportGroupSet:IsInstanceOf("SET_GROUP") then + mission.transportGroupSet=TransportGroupSet + else + mission:E(mission.lid.."ERROR: TransportGroupSet must be a GROUP or SET_GROUP object!") + return nil + end + + mission:_TargetFromObject(mission.transportGroupSet) + + mission.transportPickup=PickupCoordinate or mission:GetTargetCoordinate() + mission.transportDropoff=DropoffCoordinate + + -- Debug. + mission.transportPickup:MarkToAll("Pickup") + mission.transportDropoff:MarkToAll("Drop off") + + -- TODO: what's the best ROE here? + mission.optionROE=ENUMS.ROE.ReturnFire + mission.optionROT=ENUMS.ROT.PassiveDefense + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- Create an ARTY mission. +-- @param #AUFTRAG self +-- @param Core.Point#COORDINATE Target Center of the firing solution. +-- @param #number Nshots Number of shots to be fired. Default 3. +-- @param #number Radius Radius of the shells in meters. Default 100 meters. +-- @return #AUFTRAG self +function AUFTRAG:NewARTY(Target, Nshots, Radius) + + local mission=AUFTRAG:New(AUFTRAG.Type.ARTY) + + mission:_TargetFromObject(Target) + + mission.artyShots=Nshots or 3 + mission.artyRadius=Radius or 100 + + mission.engageWeaponType=ENUMS.WeaponFlag.Auto + + mission.optionROE=ENUMS.ROE.OpenFire -- Ground/naval need open fire! + mission.optionAlarm=0 + + mission.missionFraction=0.0 + + -- Evaluate after 8 min. + mission.dTevaluate=8*60 + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- Create a PATROLZONE mission. Group(s) will go to the zone and patrol it randomly. +-- @param #AUFTRAG self +-- @param Core.Zone#ZONE Zone The patrol zone. +-- @param #number Speed Speed in knots. +-- @param #number Altitude Altitude in feet. Only for airborne units. Default 2000 feet ASL. +-- @return #AUFTRAG self +function AUFTRAG:NewPATROLZONE(Zone, Speed, Altitude) + + local mission=AUFTRAG:New(AUFTRAG.Type.PATROLZONE) + + mission:_TargetFromObject(Zone) + + mission.optionROE=ENUMS.ROE.OpenFire + mission.optionROT=ENUMS.ROT.PassiveDefense + mission.optionAlarm=ENUMS.AlarmState.Auto + + mission.missionFraction=1.0 + mission.missionSpeed=Speed and UTILS.KnotsToKmph(Speed) or nil + mission.missionAltitude=Altitude and UTILS.FeetToMeters(Altitude) or nil + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + + +--- Create a mission to attack a group. Mission type is automatically chosen from the group category. +-- @param #AUFTRAG self +-- @param Ops.Target#TARGET Target The target. +-- @return #AUFTRAG self +function AUFTRAG:NewTargetAir(Target) + + local mission=nil --#AUFTRAG + + self.engageTarget=Target + + local target=self.engageTarget:GetObject() + + local mission=self:NewAUTO(target) + + if mission then + mission:SetPriority(10, true) + end + + return mission +end + + +--- Create a mission to attack a group. Mission type is automatically chosen from the group category. +-- @param #AUFTRAG self +-- @param Wrapper.Positionable#POSITIONABLE Target Target object. +-- @return #string Auftrag type, e.g. `AUFTRAG.Type.BAI` (="BAI"). +function AUFTRAG:_DetermineAuftragType(Target) + + local group=nil --Wrapper.Group#GROUP + local airbase=nil --Wrapper.Airbase#AIRBASE + local scenery=nil --Wrapper.Scenery#SCENERY + local coordinate=nil --Core.Point#COORDINATE + local auftrag=nil + + if Target:IsInstanceOf("GROUP") then + group=Target --Target is already a group. + elseif Target:IsInstanceOf("UNIT") then + group=Target:GetGroup() + elseif Target:IsInstanceOf("AIRBASE") then + airbase=Target + elseif Target:IsInstanceOf("SCENERY") then + scenery=Target + end + + if group then + + local category=group:GetCategory() + local attribute=group:GetAttribute() + + if category==Group.Category.AIRPLANE or category==Group.Category.HELICOPTER then + + --- + -- A2A: Intercept + --- + + auftrag=AUFTRAG.Type.INTERCEPT + + elseif category==Group.Category.GROUND or category==Group.Category.TRAIN then + + --- + -- GROUND + --- + + if attribute==GROUP.Attribute.GROUND_SAM then + + -- SEAD/DEAD + + auftrag=AUFTRAG.Type.SEAD + + elseif attribute==GROUP.Attribute.GROUND_AAA then + + auftrag=AUFTRAG.Type.BAI + + elseif attribute==GROUP.Attribute.GROUND_ARTILLERY then + + auftrag=AUFTRAG.Type.BAI + + elseif attribute==GROUP.Attribute.GROUND_INFANTRY then + + auftrag=AUFTRAG.Type.BAI + + else + + auftrag=AUFTRAG.Type.BAI + + end + + + elseif category==Group.Category.SHIP then + + --- + -- NAVAL + --- + + auftrag=AUFTRAG.Type.ANTISHIP + + else + self:E(self.lid.."ERROR: Unknown Group category!") + end + + elseif airbase then + auftrag=AUFTRAG.Type.BOMBRUNWAY + elseif scenery then + auftrag=AUFTRAG.Type.STRIKE + elseif coordinate then + auftrag=AUFTRAG.Type.BOMBING + end + + return auftrag +end + +--- Create a mission to attack a group. Mission type is automatically chosen from the group category. +-- @param #AUFTRAG self +-- @param Wrapper.Group#GROUP EngageGroup Group to be engaged. +-- @return #AUFTRAG self +function AUFTRAG:NewAUTO(EngageGroup) + + local mission=nil --#AUFTRAG + + local Target=EngageGroup + + local auftrag=self:_DetermineAuftragType(EngageGroup) + + if auftrag==AUFTRAG.Type.ANTISHIP then + mission=AUFTRAG:NewANTISHIP(Target) + elseif auftrag==AUFTRAG.Type.ARTY then + mission=AUFTRAG:NewARTY(Target) + elseif auftrag==AUFTRAG.Type.AWACS then + mission=AUFTRAG:NewAWACS(Coordinate, Altitude,Speed,Heading,Leg) + elseif auftrag==AUFTRAG.Type.BAI then + mission=AUFTRAG:NewBAI(Target,Altitude) + elseif auftrag==AUFTRAG.Type.BOMBING then + mission=AUFTRAG:NewBOMBING(Target,Altitude) + elseif auftrag==AUFTRAG.Type.BOMBRUNWAY then + mission=AUFTRAG:NewBOMBRUNWAY(Airdrome,Altitude) + elseif auftrag==AUFTRAG.Type.BOMBCARPET then + mission=AUFTRAG:NewBOMBCARPET(Target,Altitude,CarpetLength) + elseif auftrag==AUFTRAG.Type.CAP then + mission=AUFTRAG:NewCAP(ZoneCAP,Altitude,Speed,Coordinate,Heading,Leg,TargetTypes) + elseif auftrag==AUFTRAG.Type.CAS then + mission=AUFTRAG:NewCAS(ZoneCAS,Altitude,Speed,Coordinate,Heading,Leg,TargetTypes) + elseif auftrag==AUFTRAG.Type.ESCORT then + mission=AUFTRAG:NewESCORT(EscortGroup,OffsetVector,EngageMaxDistance,TargetTypes) + elseif auftrag==AUFTRAG.Type.FACA then + mission=AUFTRAG:NewFACA(Target,Designation,DataLink,Frequency,Modulation) + elseif auftrag==AUFTRAG.Type.FERRY then + -- Not implemented yet. + elseif auftrag==AUFTRAG.Type.GCICAP then + mission=AUFTRAG:NewGCICAP(Coordinate,Altitude,Speed,Heading,Leg) + elseif auftrag==AUFTRAG.Type.INTERCEPT then + mission=AUFTRAG:NewINTERCEPT(Target) + elseif auftrag==AUFTRAG.Type.ORBIT then + mission=AUFTRAG:NewORBIT(Coordinate,Altitude,Speed,Heading,Leg) + elseif auftrag==AUFTRAG.Type.RECON then + -- Not implemented yet. + elseif auftrag==AUFTRAG.Type.RESCUEHELO then + mission=AUFTRAG:NewRESCUEHELO(Carrier) + elseif auftrag==AUFTRAG.Type.SEAD then + mission=AUFTRAG:NewSEAD(Target,Altitude) + elseif auftrag==AUFTRAG.Type.STRIKE then + mission=AUFTRAG:NewSTRIKE(Target,Altitude) + elseif auftrag==AUFTRAG.Type.TANKER then + mission=AUFTRAG:NewTANKER(Coordinate,Altitude,Speed,Heading,Leg,RefuelSystem) + elseif auftrag==AUFTRAG.Type.TROOPTRANSPORT then + mission=AUFTRAG:NewTROOPTRANSPORT(TransportGroupSet,DropoffCoordinate,PickupCoordinate) + else + + end + + if mission then + mission:SetPriority(10, true) + end + + return mission +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User API Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set mission start and stop time. +-- @param #AUFTRAG self +-- @param #string ClockStart Time the mission is started, e.g. "05:00" for 5 am. If specified as a #number, it will be relative (in seconds) to the current mission time. Default is 5 seconds after mission was added. +-- @param #string ClockStop (Optional) Time the mission is stopped, e.g. "13:00" for 1 pm. If mission could not be started at that time, it will be removed from the queue. If specified as a #number it will be relative (in seconds) to the current mission time. +-- @return #AUFTRAG self +function AUFTRAG:SetTime(ClockStart, ClockStop) + + -- Current mission time. + local Tnow=timer.getAbsTime() + + -- Set start time. Default in 5 sec. + local Tstart=Tnow+5 + if ClockStart and type(ClockStart)=="number" then + Tstart=Tnow+ClockStart + elseif ClockStart and type(ClockStart)=="string" then + Tstart=UTILS.ClockToSeconds(ClockStart) + end + + -- Set stop time. Default nil. + local Tstop=nil + if ClockStop and type(ClockStop)=="number" then + Tstop=Tnow+ClockStop + elseif ClockStop and type(ClockStop)=="string" then + Tstop=UTILS.ClockToSeconds(ClockStop) + end + + self.Tstart=Tstart + self.Tstop=Tstop + + if Tstop then + self.duration=self.Tstop-self.Tstart + end + + return self +end + +--- Set mission priority and (optional) urgency. Urgent missions can cancel other running missions. +-- @param #AUFTRAG self +-- @param #number Prio Priority 1=high, 100=low. Default 50. +-- @param #boolean Urgent If *true*, another running mission might be cancelled if it has a lower priority. +-- @param #number Importance Number 1-10. If missions with lower value are in the queue, these have to be finished first. Default is `nil`. +-- @return #AUFTRAG self +function AUFTRAG:SetPriority(Prio, Urgent, Importance) + self.prio=Prio or 50 + self.urgent=Urgent + self.importance=Importance + return self +end + +--- Set how many times the mission is repeated. Only valid if the mission is handled by an AIRWING or higher level. +-- @param #AUFTRAG self +-- @param #number Nrepeat Number of repeats. Default 0. +-- @return #AUFTRAG self +function AUFTRAG:SetRepeat(Nrepeat) + self.Nrepeat=Nrepeat or 0 + return self +end + +--- Set how many times the mission is repeated if it fails. Only valid if the mission is handled by an AIRWING or higher level. +-- @param #AUFTRAG self +-- @param #number Nrepeat Number of repeats. Default 0. +-- @return #AUFTRAG self +function AUFTRAG:SetRepeatOnFailure(Nrepeat) + self.NrepeatFailure=Nrepeat or 0 + return self +end + +--- Set how many times the mission is repeated if it was successful. Only valid if the mission is handled by an AIRWING or higher level. +-- @param #AUFTRAG self +-- @param #number Nrepeat Number of repeats. Default 0. +-- @return #AUFTRAG self +function AUFTRAG:SetRepeatOnSuccess(Nrepeat) + self.NrepeatSuccess=Nrepeat or 0 + return self +end + +--- Define how many assets are required to do the job. Only valid if the mission is handled by an AIRWING or higher level. +-- @param #AUFTRAG self +-- @param #number Nassets Number of asset groups. Default 1. +-- @return #AUFTRAG self +function AUFTRAG:SetRequiredAssets(Nassets) + self.nassets=Nassets or 1 + return self +end + +--- Set mission name. +-- @param #AUFTRAG self +-- @param #string Name Name of the mission. Default is "Auftrag Nr. X", where X is a running number, which is automatically increased. +-- @return #AUFTRAG self +function AUFTRAG:SetName(Name) + self.name=Name or string.format("Auftrag Nr. %d", self.auftragsnummer) + return self +end + +--- Enable markers, which dispay the mission status on the F10 map. +-- @param #AUFTRAG self +-- @param #number Coalition The coaliton side to which the markers are dispayed. Default is to all. +-- @return #AUFTRAG self +function AUFTRAG:SetEnableMarkers(Coalition) + self.markerOn=true + self.markerCoaliton=Coalition or -1 + return self +end + +--- Set verbosity level. +-- @param #AUFTRAG self +-- @param #number VerbosityLevel Level of output (higher=more). Default 0. +-- @return #AUFTRAG self +function AUFTRAG:SetVerbosity(VerbosityLevel) + self.verbose=VerbosityLevel or 0 + return self +end + +--- Set weapon type used for the engagement. +-- @param #AUFTRAG self +-- @param #number WeaponType Weapon type. Default is `ENUMS.WeaponFlag.Auto`. +-- @return #AUFTRAG self +function AUFTRAG:SetWeaponType(WeaponType) + + self.engageWeaponType=WeaponType or ENUMS.WeaponFlag.Auto + + -- Update the DCS task parameter. + self.DCStask=self:GetDCSMissionTask() + + return self +end + +--- Set number of weapons to expend. +-- @param #AUFTRAG self +-- @param #number WeaponExpend How much of the weapon load is expended during the attack, e.g. `AI.Task.WeaponExpend.ALL`. Default "Auto". +-- @return #AUFTRAG self +function AUFTRAG:SetWeaponExpend(WeaponExpend) + + self.engageWeaponExpend=WeaponExpend or "Auto" + + -- Update the DCS task parameter. + self.DCStask=self:GetDCSMissionTask() + + return self +end + +--- Set whether target will be attack as group. +-- @param #AUFTRAG self +-- @param #boolean Switch If true or nil, engage as group. If false, not. +-- @return #AUFTRAG self +function AUFTRAG:SetEngageAsGroup(Switch) + + if Switch==nil then + Switch=true + end + + self.engageAsGroup=Switch + + -- Update the DCS task parameter. + self.DCStask=self:GetDCSMissionTask() + + return self +end + +--- Set engage altitude. This is the altitude passed to the DCS task. In the ME it is the tickbox ALTITUDE ABOVE. +-- @param #AUFTRAG self +-- @param #string Altitude Altitude in feet. Default 6000 ft. +-- @return #AUFTRAG self +function AUFTRAG:SetEngageAltitude(Altitude) + + self.engageAltitude=UTILS.FeetToMeters(Altitude or 6000) + + -- Update the DCS task parameter. + self.DCStask=self:GetDCSMissionTask() + + return self +end + +--- Set mission altitude. This is the altitude of the waypoint create where the DCS task is executed. +-- @param #AUFTRAG self +-- @param #string Altitude Altitude in feet. +-- @return #AUFTRAG self +function AUFTRAG:SetMissionAltitude(Altitude) + self.missionAltitude=UTILS.FeetToMeters(Altitude) + return self +end + +--- Set mission speed. That is the speed the group uses to get to the mission waypoint. +-- @param #AUFTRAG self +-- @param #string Speed Mission speed in knots. +-- @return #AUFTRAG self +function AUFTRAG:SetMissionSpeed(Speed) + self.missionSpeed=Speed and UTILS.KnotsToKmph(Speed) or nil + return self +end + +--- Set max mission range. Only applies if the AUFTRAG is handled by an AIRWING or CHIEF. This is the max allowed distance from the airbase to the target. +-- @param #AUFTRAG self +-- @param #number Range Max range in NM. Default 100 NM. +-- @return #AUFTRAG self +function AUFTRAG:SetMissionRange(Range) + self.engageRange=UTILS.NMToMeters(Range or 100) + return self +end + +--- Set Rules of Engagement (ROE) for this mission. +-- @param #AUFTRAG self +-- @param #string roe Mission ROE. +-- @return #AUFTRAG self +function AUFTRAG:SetROE(roe) + + self.optionROE=roe + + return self +end + + +--- Set Reaction on Threat (ROT) for this mission. +-- @param #AUFTRAG self +-- @param #string rot Mission ROT. +-- @return #AUFTRAG self +function AUFTRAG:SetROT(rot) + + self.optionROT=rot + + return self +end + +--- Set alarm state for this mission. +-- @param #AUFTRAG self +-- @param #number Alarmstate Alarm state 0=Auto, 1=Green, 2=Red. +-- @return #AUFTRAG self +function AUFTRAG:SetAlarmstate(Alarmstate) + + self.optionAlarm=Alarmstate + + return self +end + +--- Set formation for this mission. +-- @param #AUFTRAG self +-- @param #number Formation Formation. +-- @return #AUFTRAG self +function AUFTRAG:SetFormation(Formation) + + self.optionFormation=Formation + + return self +end + +--- Set radio frequency and modulation for this mission. +-- @param #AUFTRAG self +-- @param #number Frequency Frequency in MHz. +-- @param #number Modulation Radio modulation. Default 0=AM. +-- @return #AUFTRAG self +function AUFTRAG:SetRadio(Frequency, Modulation) + + self.radio={} + self.radio.Freq=Frequency + self.radio.Modu=Modulation + + return self +end + +--- Set TACAN beacon channel and Morse code for this mission. +-- @param #AUFTRAG self +-- @param #number Channel TACAN channel. +-- @param #string Morse Morse code. Default "XXX". +-- @param #string UnitName Name of the unit in the group for which acts as TACAN beacon. Default is the first unit in the group. +-- @param #string Band Tacan channel mode ("X" or "Y"). Default is "X" for ground/naval and "Y" for aircraft. +-- @return #AUFTRAG self +function AUFTRAG:SetTACAN(Channel, Morse, UnitName, Band) + + self.tacan={} + self.tacan.Channel=Channel + self.tacan.Morse=Morse or "XXX" + self.tacan.UnitName=UnitName + self.tacan.Band=Band + + return self +end + +--- Set ICLS beacon channel and Morse code for this mission. +-- @param #AUFTRAG self +-- @param #number Channel ICLS channel. +-- @param #string Morse Morse code. Default "XXX". +-- @param #string UnitName Name of the unit in the group for which acts as ICLS beacon. Default is the first unit in the group. +-- @return #AUFTRAG self +function AUFTRAG:SetICLS(Channel, Morse, UnitName) + + self.icls={} + self.icls.Channel=Channel + self.icls.Morse=Morse or "XXX" + self.icls.UnitName=UnitName + + return self +end + +--- Get mission type. +-- @param #AUFTRAG self +-- @return #string Mission type, e.g. "BAI". +function AUFTRAG:GetType() + return self.type +end + +--- Get mission name. +-- @param #AUFTRAG self +-- @return #string Mission name, e.g. "Auftrag Nr.1". +function AUFTRAG:GetName() + return self.name +end + +--- Get number of required assets. +-- @param #AUFTRAG self +-- @return #number Numer of required assets. +function AUFTRAG:GetNumberOfRequiredAssets() + return self.nassets +end + +--- Get mission priority. +-- @param #AUFTRAG self +-- @return #number Priority. Smaller is higher. +function AUFTRAG:GetPriority() + return self.prio +end + +--- Get casualties, i.e. number of units that died during this mission. +-- @param #AUFTRAG self +-- @return #number Number of dead units. +function AUFTRAG:GetCasualties() + return self.Ncasualties or 0 +end + +--- Get kills, i.e. number of units that were destroyed by assets of this mission. +-- @param #AUFTRAG self +-- @return #number Number of units destroyed. +function AUFTRAG:GetKills() + return self.Nkills or 0 +end + + +--- Check if mission is "urgent". +-- @param #AUFTRAG self +-- @return #boolean If `true`, mission is "urgent". +function AUFTRAG:IsUrgent() + return self.urgent +end + +--- Get mission importance. +-- @param #AUFTRAG self +-- @return #number Importance. Smaller is higher. +function AUFTRAG:GetImportance() + return self.importance +end + +--- Add start condition. +-- @param #AUFTRAG self +-- @param #function ConditionFunction Function that needs to be true before the mission can be started. Must return a #boolean. +-- @param ... Condition function arguments if any. +-- @return #AUFTRAG self +function AUFTRAG:AddConditionStart(ConditionFunction, ...) + + local condition={} --#AUFTRAG.Condition + + condition.func=ConditionFunction + condition.arg={} + if arg then + condition.arg=arg + end + + table.insert(self.conditionStart, condition) + + return self +end + +--- Add success condition. +-- @param #AUFTRAG self +-- @param #function ConditionFunction If this function returns `true`, the mission is cancelled. +-- @param ... Condition function arguments if any. +-- @return #AUFTRAG self +function AUFTRAG:AddConditionSuccess(ConditionFunction, ...) + + local condition={} --#AUFTRAG.Condition + + condition.func=ConditionFunction + condition.arg={} + if arg then + condition.arg=arg + end + + table.insert(self.conditionSuccess, condition) + + return self +end + +--- Add failure condition. +-- @param #AUFTRAG self +-- @param #function ConditionFunction If this function returns `true`, the mission is cancelled. +-- @param ... Condition function arguments if any. +-- @return #AUFTRAG self +function AUFTRAG:AddConditionFailure(ConditionFunction, ...) + + local condition={} --#AUFTRAG.Condition + + condition.func=ConditionFunction + condition.arg={} + if arg then + condition.arg=arg + end + + table.insert(self.conditionFailure, condition) + + return self +end + + +--- Assign airwing squadron(s) to the mission. Only these squads will be considered for the job. +-- @param #AUFTRAG self +-- @param #table Squadrons A table of SQUADRON(s). **Has to be a table {}** even if a single squad is given. +-- @return #AUFTRAG self +function AUFTRAG:AssignSquadrons(Squadrons) + + for _,_squad in pairs(Squadrons) do + local squadron=_squad --Ops.Squadron#SQUADRON + self:I(self.lid..string.format("Assigning squadron %s", tostring(squadron.name))) + end + + self.squadrons=Squadrons +end + +--- Add a required payload for this mission. Only these payloads will be used for this mission. If they are not available, the mission cannot start. Only available for use with an AIRWING. +-- @param #AUFTRAG self +-- @param Ops.AirWing#AIRWING.Payload Payload Required payload. +-- @return #AUFTRAG self +function AUFTRAG:AddRequiredPayload(Payload) + + self.payloads=self.payloads or {} + + table.insert(self.payloads, Payload) + +end + + +--- Add a Ops group to the mission. +-- @param #AUFTRAG self +-- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPSGROUP object. +function AUFTRAG:AddOpsGroup(OpsGroup) + self:T(self.lid..string.format("Adding Ops group %s", OpsGroup.groupname)) + + local groupdata={} --#AUFTRAG.GroupData + groupdata.opsgroup=OpsGroup + groupdata.status=AUFTRAG.GroupStatus.SCHEDULED + groupdata.waypointcoordinate=nil + groupdata.waypointindex=nil + groupdata.waypointtask=nil + + self.groupdata[OpsGroup.groupname]=groupdata + +end + +--- Remove an Ops group from the mission. +-- @param #AUFTRAG self +-- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPSGROUP object. +function AUFTRAG:DelOpsGroup(OpsGroup) + self:T(self.lid..string.format("Removing OPS group %s", OpsGroup and OpsGroup.groupname or "nil (ERROR)!")) + + if OpsGroup then + + -- Remove mission form queue. + OpsGroup:RemoveMission(self) + + self.groupdata[OpsGroup.groupname]=nil + + end + +end + +--- Check if mission is PLANNED. +-- @param #AUFTRAG self +-- @return #boolean If true, mission is in the planning state. +function AUFTRAG:IsPlanned() + return self.status==AUFTRAG.Status.PLANNED +end + +--- Check if mission is QUEUED at an AIRWING mission queue. +-- @param #AUFTRAG self +-- @return #boolean If true, mission is queued. +function AUFTRAG:IsQueued() + return self.status==AUFTRAG.Status.QUEUED +end + +--- Check if mission is REQUESTED, i.e. request for WAREHOUSE assets is done. +-- @param #AUFTRAG self +-- @return #boolean If true, mission is requested. +function AUFTRAG:IsRequested() + return self.status==AUFTRAG.Status.REQUESTED +end + +--- Check if mission is SCHEDULED, i.e. request for WAREHOUSE assets is done. +-- @param #AUFTRAG self +-- @return #boolean If true, mission is queued. +function AUFTRAG:IsScheduled() + return self.status==AUFTRAG.Status.SCHEDULED +end + +--- Check if mission is STARTED, i.e. group is on its way to the mission execution waypoint. +-- @param #AUFTRAG self +-- @return #boolean If true, mission is started. +function AUFTRAG:IsStarted() + return self.status==AUFTRAG.Status.STARTED +end + +--- Check if mission is executing. +-- @param #AUFTRAG self +-- @return #boolean If true, mission is currently executing. +function AUFTRAG:IsExecuting() + return self.status==AUFTRAG.Status.EXECUTING +end + +--- Check if mission was cancelled. +-- @param #AUFTRAG self +-- @return #boolean If true, mission was cancelled. +function AUFTRAG:IsCancelled() + return self.status==AUFTRAG.Status.CANCELLED +end + +--- Check if mission is done. +-- @param #AUFTRAG self +-- @return #boolean If true, mission is done. +function AUFTRAG:IsDone() + return self.status==AUFTRAG.Status.DONE +end + +--- Check if mission was a success. +-- @param #AUFTRAG self +-- @return #boolean If true, mission was successful. +function AUFTRAG:IsSuccess() + return self.status==AUFTRAG.Status.SUCCESS +end + +--- Check if mission is over. This could be state DONE or CANCELLED. +-- @param #AUFTRAG self +-- @return #boolean If true, mission is currently executing. +function AUFTRAG:IsOver() + local over = self.status==AUFTRAG.Status.DONE or self.status==AUFTRAG.Status.CANCELLED or self.status==AUFTRAG.Status.SUCCESS or self.status==AUFTRAG.Status.FAILED + return over +end + +--- Check if mission is NOT over. +-- @param #AUFTRAG self +-- @return #boolean If true, mission is NOT over yet. +function AUFTRAG:IsNotOver() + return not self:IsOver() +end + +--- Check if mission is ready to be started. +-- * Mission start time passed. +-- * Mission stop time did not pass already. +-- * All start conditions are true. +-- @param #AUFTRAG self +-- @return #boolean If true, mission can be started. +function AUFTRAG:IsReadyToGo() + + local Tnow=timer.getAbsTime() + + -- Start time did not pass yet. + if self.Tstart and Tnowself.Tstop or false then + return false + end + + -- All start conditions true? + local startme=self:EvalConditionsAll(self.conditionStart) + + if not startme then + return false + end + + + -- We're good to go! + return true +end + +--- Check if mission is ready to be started. +-- * Mission stop already passed. +-- * Any stop condition is true. +-- @param #AUFTRAG self +-- @return #boolean If true, mission should be cancelled. +function AUFTRAG:IsReadyToCancel() + + local Tnow=timer.getAbsTime() + + -- Stop time already passed. + if self.Tstop and Tnow>self.Tstop then + return true + end + + -- Evaluate failure condition. One is enough. + local failure=self:EvalConditionsAny(self.conditionFailure) + + if failure then + self.failurecondition=true + return true + end + + -- Evaluate success consitions. One is enough. + local success=self:EvalConditionsAny(self.conditionSuccess) + + if success then + self.successcondition=true + return true + end + + -- No criterion matched. + return false +end + +--- Check if all given condition are true. +-- @param #AUFTRAG self +-- @param #table Conditions Table of conditions. +-- @return #boolean If true, all conditions were true. Returns false if at least one condition returned false. +function AUFTRAG:EvalConditionsAll(Conditions) + + -- Any stop condition must be true. + for _,_condition in pairs(Conditions or {}) do + local condition=_condition --#AUFTRAG.Condition + + -- Call function. + local istrue=condition.func(unpack(condition.arg)) + + -- Any false will return false. + if not istrue then + return false + end + + end + + -- All conditions were true. + return true +end + + +--- Check if any of the given conditions is true. +-- @param #AUFTRAG self +-- @param #table Conditions Table of conditions. +-- @return #boolean If true, at least one condition is true. +function AUFTRAG:EvalConditionsAny(Conditions) + + -- Any stop condition must be true. + for _,_condition in pairs(Conditions or {}) do + local condition=_condition --#AUFTRAG.Condition + + -- Call function. + local istrue=condition.func(unpack(condition.arg)) + + -- Any true will return true. + if istrue then + return true + end + + end + + -- No condition was true. + return false +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Mission Status +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "Status" event. +-- @param #AUFTRAG self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AUFTRAG:onafterStatus(From, Event, To) + + -- Current abs. mission time. + local Tnow=timer.getAbsTime() + + -- Number of alive mission targets. + local Ntargets=self:CountMissionTargets() + local Ntargets0=self:GetTargetInitialNumber() + + -- Number of alive groups attached to this mission. + local Ngroups=self:CountOpsGroups() + + -- Check if mission is not OVER yet. + if self:IsNotOver() then + + if self:CheckGroupsDone() then + + -- All groups have reported MISSON DONE. + self:Done() + + elseif (self.Tstop and Tnow>self.Tstop+10) or (Ntargets0>0 and Ntargets==0) then + + -- Cancel mission if stop time passed. + self:Cancel() + + end + + end + + -- Current FSM state. + local fsmstate=self:GetState() + + -- Check for error. + if fsmstate~=self.status then + self:E(self.lid..string.format("ERROR: FSM state %s != %s mission status!", fsmstate, self.status)) + end + + -- General info. + if self.verbose>=1 then + + -- Mission start stop time. + local Cstart=UTILS.SecondsToClock(self.Tstart, true) + local Cstop=self.Tstop and UTILS.SecondsToClock(self.Tstop, true) or "INF" + + local targetname=self:GetTargetName() or "unknown" + + local airwing=self.airwing and self.airwing.alias or "N/A" + local commander=self.wingcommander and tostring(self.wingcommander.coalition) or "N/A" + + -- Info message. + self:I(self.lid..string.format("Status %s: Target=%s, T=%s-%s, assets=%d, groups=%d, targets=%d, wing=%s, commander=%s", self.status, targetname, Cstart, Cstop, #self.assets, Ngroups, Ntargets, airwing, commander)) + end + + -- Group info. + if self.verbose>=2 then + -- Data on assigned groups. + local text="Group data:" + for groupname,_groupdata in pairs(self.groupdata) do + local groupdata=_groupdata --#AUFTRAG.GroupData + text=text..string.format("\n- %s: status mission=%s opsgroup=%s", groupname, groupdata.status, groupdata.opsgroup and groupdata.opsgroup:GetState() or "N/A") + end + self:I(self.lid..text) + end + + -- Ready to evaluate mission outcome? + local ready2evaluate=self.Tover and Tnow-self.Tover>=self.dTevaluate or false + + --env.info("FF Tover="..tostring(self.Tover)) + --if self.Tover then + -- env.info("FF Tnow-Tover="..tostring(Tnow-self.Tover)) + --end + + -- Check if mission is OVER (done or cancelled) and enough time passed to evaluate the result. + if self:IsOver() and ready2evaluate then + -- Evaluate success or failure of the mission. + self:Evaluate() + else + self:__Status(-30) + end + + -- Update F10 marker. + if self.markerOn then + self:UpdateMarker() + end + +end + +--- Evaluate mission outcome - success or failure. +-- @param #AUFTRAG self +-- @return #AUFTRAG self +function AUFTRAG:Evaluate() + + -- Assume success and check if any failed condition applies. + local failed=false + + -- Target damage in %. + local targetdamage=self:GetTargetDamage() + + -- Own damage in %. + local owndamage=self.Ncasualties/self.Nelements*100 + + -- Current number of mission targets. + local Ntargets=self:CountMissionTargets() + local Ntargets0=self:GetTargetInitialNumber() + + local Life=self:GetTargetLife() + local Life0=self:GetTargetInitialLife() + + + if Ntargets0>0 then + + --- + -- Mission had targets + --- + + -- Check if failed. + if self.type==AUFTRAG.Type.TROOPTRANSPORT or self.type==AUFTRAG.Type.ESCORT then + + -- Transported or escorted groups have to survive. + if Ntargets0 then + failed=true + end + + end + + else + + --- + -- Mission had NO targets + --- + + -- No targets and everybody died ==> mission failed. Well, unless success condition is true. + if self.Nelements==self.Ncasualties then + failed=true + end + + end + + + -- Any success condition true? + local successCondition=self:EvalConditionsAny(self.conditionSuccess) + + -- Any failure condition true? + local failureCondition=self:EvalConditionsAny(self.conditionFailure) + + if failureCondition then + failed=true + elseif successCondition then + failed=false + end + + -- Debug text. + local text=string.format("Evaluating mission:\n") + text=text..string.format("Own casualties = %d/%d\n", self.Ncasualties, self.Nelements) + text=text..string.format("Own losses = %.1f %%\n", owndamage) + text=text..string.format("Killed units = %d\n", self.Nkills) + text=text..string.format("--------------------------\n") + text=text..string.format("Targets left = %d/%d\n", Ntargets, Ntargets0) + text=text..string.format("Targets life = %.1f/%.1f\n", Life, Life0) + text=text..string.format("Enemy losses = %.1f %%\n", targetdamage) + text=text..string.format("--------------------------\n") + text=text..string.format("Success Cond = %s\n", tostring(successCondition)) + text=text..string.format("Failure Cond = %s\n", tostring(failureCondition)) + text=text..string.format("--------------------------\n") + text=text..string.format("Final Success = %s\n", tostring(not failed)) + text=text..string.format("=========================") + self:I(self.lid..text) + + if failed then + self:Failed() + else + self:Success() + end + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Asset Data +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get all OPS groups. +-- @param #AUFTRAG self +-- @return #table Table of Ops.OpsGroup#OPSGROUP or {}. +function AUFTRAG:GetOpsGroups() + local opsgroups={} + for _,_groupdata in pairs(self.groupdata or {}) do + local groupdata=_groupdata --#AUFTRAG.GroupData + table.insert(opsgroups, groupdata.opsgroup) + end + return opsgroups +end + +--- Get asset data table. +-- @param #AUFTRAG self +-- @param #string AssetName Name of the asset. +-- @return #AUFTRAG.GroupData Group data or *nil* if OPS group does not exist. +function AUFTRAG:GetAssetDataByName(AssetName) + return self.groupdata[tostring(AssetName)] +end + +--- Get flight data table. +-- @param #AUFTRAG self +-- @param Ops.OpsGroup#OPSGROUP opsgroup The flight group. +-- @return #AUFTRAG.GroupData Flight data or nil if opsgroup does not exist. +function AUFTRAG:GetGroupData(opsgroup) + if opsgroup and self.groupdata then + return self.groupdata[opsgroup.groupname] + end + return nil +end + +--- Set opsgroup mission status. +-- @param #AUFTRAG self +-- @param Ops.OpsGroup#OPSGROUP opsgroup The flight group. +-- @param #string status New status. +function AUFTRAG:SetGroupStatus(opsgroup, status) + self:T(self.lid..string.format("Setting flight %s to status %s", opsgroup and opsgroup.groupname or "nil", tostring(status))) + + if self:GetGroupStatus(opsgroup)==AUFTRAG.GroupStatus.CANCELLED and status==AUFTRAG.GroupStatus.DONE then + -- Do not overwrite a CANCELLED status with a DONE status. + else + local groupdata=self:GetGroupData(opsgroup) + if groupdata then + groupdata.status=status + else + self:E(self.lid.."WARNING: Could not SET flight data for flight group. Setting status to DONE") + end + end + + -- Debug info. + self:T2(self.lid..string.format("Setting flight %s status to %s. IsNotOver=%s CheckGroupsDone=%s", opsgroup.groupname, self:GetGroupStatus(opsgroup), tostring(self:IsNotOver()), tostring(self:CheckGroupsDone()))) + + -- Check if ALL flights are done with their mission. + if self:IsNotOver() and self:CheckGroupsDone() then + self:T3(self.lid.."All flights done ==> mission DONE!") + self:Done() + else + self:T3(self.lid.."Mission NOT DONE yet!") + end + +end + +--- Get ops group mission status. +-- @param #AUFTRAG self +-- @param Ops.OpsGroup#OPSGROUP opsgroup The flight group. +function AUFTRAG:GetGroupStatus(opsgroup) + self:T3(self.lid..string.format("Trying to get Flight status for flight group %s", opsgroup and opsgroup.groupname or "nil")) + + local groupdata=self:GetGroupData(opsgroup) + + if groupdata then + return groupdata.status + else + + self:E(self.lid..string.format("WARNING: Could not GET groupdata for opsgroup %s. Returning status DONE.", opsgroup and opsgroup.groupname or "nil")) + return AUFTRAG.GroupStatus.DONE + + end +end + + +--- Set Ops group waypoint coordinate. +-- @param #AUFTRAG self +-- @param Ops.OpsGroup#OPSGROUP opsgroup The flight group. +-- @param Core.Point#COORDINATE coordinate Waypoint Coordinate. +function AUFTRAG:SetGroupWaypointCoordinate(opsgroup, coordinate) + local groupdata=self:GetGroupData(opsgroup) + if groupdata then + groupdata.waypointcoordinate=coordinate + end +end + +--- Get opsgroup waypoint coordinate. +-- @param #AUFTRAG self +-- @return Core.Point#COORDINATE Waypoint Coordinate. +function AUFTRAG:GetGroupWaypointCoordinate(opsgroup) + local groupdata=self:GetGroupData(opsgroup) + if groupdata then + return groupdata.waypointcoordinate + end +end + + +--- Set Ops group waypoint task. +-- @param #AUFTRAG self +-- @param Ops.OpsGroup#OPSGROUP opsgroup The flight group. +-- @param Ops.OpsGroup#OPSGROUP.Task task Waypoint task. +function AUFTRAG:SetGroupWaypointTask(opsgroup, task) + self:T2(self.lid..string.format("Setting waypoint task %s", task and task.description or "WTF")) + local groupdata=self:GetGroupData(opsgroup) + if groupdata then + groupdata.waypointtask=task + end +end + +--- Get opsgroup waypoint task. +-- @param #AUFTRAG self +-- @param Ops.OpsGroup#OPSGROUP opsgroup The flight group. +-- @return Ops.OpsGroup#OPSGROUP.Task task Waypoint task. Waypoint task. +function AUFTRAG:GetGroupWaypointTask(opsgroup) + local groupdata=self:GetGroupData(opsgroup) + if groupdata then + return groupdata.waypointtask + end +end + +--- Set opsgroup waypoint index. +-- @param #AUFTRAG self +-- @param Ops.OpsGroup#OPSGROUP opsgroup The flight group. +-- @param #number waypointindex Waypoint index. +function AUFTRAG:SetGroupWaypointIndex(opsgroup, waypointindex) + self:T2(self.lid..string.format("Setting waypoint index %d", waypointindex)) + local groupdata=self:GetGroupData(opsgroup) + if groupdata then + groupdata.waypointindex=waypointindex + end +end + +--- Get opsgroup waypoint index. +-- @param #AUFTRAG self +-- @param Ops.OpsGroup#OPSGROUP opsgroup The flight group. +-- @return #number Waypoint index +function AUFTRAG:GetGroupWaypointIndex(opsgroup) + local groupdata=self:GetGroupData(opsgroup) + if groupdata then + return groupdata.waypointindex + end +end + + +--- Check if all flights are done with their mission (or dead). +-- @param #AUFTRAG self +-- @return #boolean If true, all flights are done with the mission. +function AUFTRAG:CheckGroupsDone() + + -- These are early stages, where we might not even have a opsgroup defined to be checked. + if self:IsPlanned() or self:IsQueued() or self:IsRequested() then + return false + end + + -- It could be that all flights were destroyed on the way to the mission execution waypoint. + -- TODO: would be better to check if everybody is dead by now. + if self:IsStarted() and self:CountOpsGroups()==0 then + return true + end + + -- Check status of all flight groups. + for groupname,data in pairs(self.groupdata) do + local groupdata=data --#AUFTRAG.GroupData + if groupdata then + if groupdata.status==AUFTRAG.GroupStatus.DONE or groupdata.status==AUFTRAG.GroupStatus.CANCELLED then + -- This one is done or cancelled. + else + -- At least this flight is not DONE or CANCELLED. + return false + end + end + end + + return true +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- EVENT Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Unit lost event. +-- @param #AUFTRAG self +-- @param Core.Event#EVENTDATA EventData Event data. +function AUFTRAG:OnEventUnitLost(EventData) + + -- Check that this is the right group. + if EventData and EventData.IniGroup and EventData.IniUnit then + local unit=EventData.IniUnit + local group=EventData.IniGroup + local unitname=EventData.IniUnitName + + for _,_groupdata in pairs(self.groupdata) do + local groupdata=_groupdata --#AUFTRAG.GroupData + if groupdata and groupdata.opsgroup and groupdata.opsgroup.groupname==EventData.IniGroupName then + self:I(self.lid..string.format("UNIT LOST event for opsgroup %s unit %s", groupdata.opsgroup.groupname, EventData.IniUnitName)) + end + end + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + +--- On after "Planned" event. +-- @param #AUFTRAG self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AUFTRAG:onafterPlanned(From, Event, To) + self.status=AUFTRAG.Status.PLANNED + self:T(self.lid..string.format("New mission status=%s", self.status)) +end + +--- On after "Queue" event. Mission is added to the mission queue of an AIRWING. +-- @param #AUFTRAG self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.AirWing#AIRWING Airwing The airwing. +function AUFTRAG:onafterQueued(From, Event, To, Airwing) + self.status=AUFTRAG.Status.QUEUED + self.airwing=Airwing + self:T(self.lid..string.format("New mission status=%s at airwing %s", self.status, tostring(Airwing.alias))) +end + + +--- On after "Requested" event. +-- @param #AUFTRAG self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AUFTRAG:onafterRequested(From, Event, To) + self.status=AUFTRAG.Status.REQUESTED + self:T(self.lid..string.format("New mission status=%s", self.status)) +end + +--- On after "Assign" event. +-- @param #AUFTRAG self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AUFTRAG:onafterAssign(From, Event, To) + self.status=AUFTRAG.Status.ASSIGNED + self:T(self.lid..string.format("New mission status=%s", self.status)) +end + +--- On after "Schedule" event. Mission is added to the mission queue of a FLIGHTGROUP. +-- @param #AUFTRAG self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AUFTRAG:onafterScheduled(From, Event, To) + self.status=AUFTRAG.Status.SCHEDULED + self:T(self.lid..string.format("New mission status=%s", self.status)) +end + +--- On after "Start" event. +-- @param #AUFTRAG self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AUFTRAG:onafterStarted(From, Event, To) + self.status=AUFTRAG.Status.STARTED + self:T(self.lid..string.format("New mission status=%s", self.status)) +end + +--- On after "Execute" event. +-- @param #AUFTRAG self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AUFTRAG:onafterExecuting(From, Event, To) + self.status=AUFTRAG.Status.EXECUTING + self:T(self.lid..string.format("New mission status=%s", self.status)) +end + +--- On after "Done" event. +-- @param #AUFTRAG self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AUFTRAG:onafterDone(From, Event, To) + self.status=AUFTRAG.Status.DONE + self:T(self.lid..string.format("New mission status=%s", self.status)) + + -- Set time stamp. + self.Tover=timer.getAbsTime() + +end + +--- On after "ElementDestroyed" event. +-- @param #AUFTRAG self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsGroup#OPSGROUP OpsGroup The ops group that is dead now. +function AUFTRAG:onafterElementDestroyed(From, Event, To, OpsGroup, Element) + -- Increase number of own casualties. + self.Ncasualties=self.Ncasualties+1 +end + +--- On after "GroupDead" event. +-- @param #AUFTRAG self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsGroup#OPSGROUP OpsGroup The ops group that is dead now. +function AUFTRAG:onafterGroupDead(From, Event, To, OpsGroup) + + local asset=self:GetAssetByName(OpsGroup.groupname) + if asset then + self:AssetDead(asset) + end + +end + +--- On after "AssetDead" event. +-- @param #AUFTRAG self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.AirWing#AIRWING.SquadronAsset Asset The asset. +function AUFTRAG:onafterAssetDead(From, Event, To, Asset) + + -- Number of groups alive. + local N=self:CountOpsGroups() + + self:I(self.lid..string.format("Asset %s dead! Number of ops groups remaining %d", tostring(Asset.spawngroupname), N)) + + -- All assets dead? + if N==0 then + + if self:IsNotOver() then + + -- Cancel mission. Wait for next mission update to evaluate SUCCESS or FAILURE. + self:Cancel() + + else + + --self:E(self.lid.."ERROR: All assets are dead not but mission was already over... Investigate!") + -- Now this can happen, because when a opsgroup dies (sometimes!), the mission is DONE + + end + end + + -- Delete asset from mission. + self:DelAsset(Asset) + +end + +--- On after "Cancel" event. Cancells the mission. +-- @param #AUFTRAG self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AUFTRAG:onafterCancel(From, Event, To) + + -- Debug info. + self:I(self.lid..string.format("CANCELLING mission in status %s. Will wait for groups to report mission DONE before evaluation", self.status)) + + -- Time stamp. + self.Tover=timer.getAbsTime() + + -- No more repeats. + self.Nrepeat=self.repeated + self.NrepeatFailure=self.repeatedFailure + self.NrepeatSuccess=self.repeatedSuccess + + -- Not necessary to delay the evaluaton?! + self.dTevaluate=0 + + if self.wingcommander then + + self:T(self.lid..string.format("Wingcommander will cancel the mission. Will wait for mission DONE before evaluation!")) + + self.wingcommander:CancelMission(self) + + elseif self.airwing then + + self:T(self.lid..string.format("Airwing %s will cancel the mission. Will wait for mission DONE before evaluation!", self.airwing.alias)) + + -- Airwing will cancel all flight missions and remove queued request from warehouse queue. + self.airwing:MissionCancel(self) + + else + + self:T(self.lid..string.format("No airwing or wingcommander. Attached flights will cancel the mission on their own. Will wait for mission DONE before evaluation!")) + + for _,_groupdata in pairs(self.groupdata) do + local groupdata=_groupdata --#AUFTRAG.GroupData + groupdata.opsgroup:MissionCancel(self) + end + + end + + -- Special mission states. + if self.status==AUFTRAG.Status.PLANNED then + self:T(self.lid..string.format("Cancelled mission was in planned stage. Call it done!")) + self:Done() + end + +end + +--- On after "Success" event. +-- @param #AUFTRAG self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AUFTRAG:onafterSuccess(From, Event, To) + + self.status=AUFTRAG.Status.SUCCESS + self:T(self.lid..string.format("New mission status=%s", self.status)) + + local repeatme=self.repeatedSuccess Repeat mission!", self.repeated+1, N)) + self:Repeat() + + else + + -- Stop mission. + self:I(self.lid..string.format("Mission SUCCESS! Number of max repeats %d reached ==> Stopping mission!", self.repeated+1)) + self:Stop() + + end + +end + +--- On after "Failed" event. +-- @param #AUFTRAG self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AUFTRAG:onafterFailed(From, Event, To) + + self.status=AUFTRAG.Status.FAILED + self:T(self.lid..string.format("New mission status=%s", self.status)) + + local repeatme=self.repeatedFailure Repeat mission!", self.repeated+1, N)) + self:Repeat() + + else + + -- Stop mission. + self:I(self.lid..string.format("Mission FAILED! Number of max repeats %d reached ==> Stopping mission!", self.repeated+1)) + self:Stop() + + end + +end + + +--- On after "Repeat" event. +-- @param #AUFTRAG self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AUFTRAG:onafterRepeat(From, Event, To) + + -- Set mission status to PLANNED. + self.status=AUFTRAG.Status.PLANNED + + self:T(self.lid..string.format("New mission status=%s (on Repeat)", self.status)) + + -- Increase repeat counter. + self.repeated=self.repeated+1 + + if self.chief then + + --TODO + + elseif self.wingcommander then + + -- Remove mission from airwing because WC will assign it again but maybe to a different wing. + if self.airwing then + self.airwing:RemoveMission(self) + end + + elseif self.airwing then + + -- Already at the airwing ==> Queued() + self:Queued(self.airwing) + + else + self:E(self.lid.."ERROR: Mission can only be repeated by a CHIEF, WINGCOMMANDER or AIRWING! Stopping AUFTRAG") + self:Stop() + end + + + -- No mission assets. + self.assets={} + + for _,_groupdata in pairs(self.groupdata) do + local groupdata=_groupdata --#AUFTRAG.GroupData + local opsgroup=groupdata.opsgroup + if opsgroup then + self:DelOpsGroup(opsgroup) + end + + end + -- No flight data. + self.groupdata={} + + -- Reset casualties and units assigned. + self.Ncasualties=0 + self.Nelements=0 + + -- Call status again. + self:__Status(-30) + +end + +--- On after "Stop" event. Remove mission from AIRWING and FLIGHTGROUP mission queues. +-- @param #AUFTRAG self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AUFTRAG:onafterStop(From, Event, To) + + self:I(self.lid..string.format("STOPPED mission in status=%s. Removing missions from queues. Stopping CallScheduler!", self.status)) + + -- TODO: remove missions from queues in WINGCOMMANDER, AIRWING and FLIGHGROUPS! + -- TODO: Mission should be OVER! we dont want to remove running missions from any queues. + + if self.wingcommander then + self.wingcommander:RemoveMission(self) + end + + if self.airwing then + self.airwing:RemoveMission(self) + end + + for _,_groupdata in pairs(self.groupdata) do + local groupdata=_groupdata --#AUFTRAG.GroupData + groupdata.opsgroup:RemoveMission(self) + end + + -- No mission assets. + self.assets={} + + -- No flight data. + self.groupdata={} + + -- Clear pending scheduler calls. + self.CallScheduler:Clear() + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Target Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create target data from a given object. +-- @param #AUFTRAG self +-- @param Wrapper.Positionable#POSITIONABLE Object The target GROUP, UNIT, STATIC. +function AUFTRAG:_TargetFromObject(Object) + + if not self.engageTarget then + + if Object:IsInstanceOf("TARGET") then + + self.engageTarget=Object + + else + + self.engageTarget=TARGET:New(Object) + + end + + else + + -- Target was already specified elsewhere. + + end + + -- Debug info. + --self:T2(self.lid..string.format("Mission Target %s Type=%s, Ntargets=%d, Lifepoints=%d", self.engageTarget.lid, self.engageTarget.lid, self.engageTarget.N0, self.engageTarget:GetLife())) + + return self +end + + +--- Count alive mission targets. +-- @param #AUFTRAG self +-- @return #number Number of alive target units. +function AUFTRAG:CountMissionTargets() + + if self.engageTarget then + return self.engageTarget:CountTargets() + else + return 0 + end + +end + +--- Get initial number of targets. +-- @param #AUFTRAG self +-- @return #number Number of initial life points when mission was planned. +function AUFTRAG:GetTargetInitialNumber() + local target=self:GetTargetData() + if target then + return target.N0 + else + return 0 + end +end + + +--- Get target life points. +-- @param #AUFTRAG self +-- @return #number Number of initial life points when mission was planned. +function AUFTRAG:GetTargetInitialLife() + local target=self:GetTargetData() + if target then + return target.life0 + else + return 0 + end +end + +--- Get target damage. +-- @param #AUFTRAG self +-- @return #number Damage in percent. +function AUFTRAG:GetTargetDamage() + local target=self:GetTargetData() + if target then + return target:GetDamage() + else + return 0 + end +end + + +--- Get target life points. +-- @param #AUFTRAG self +-- @return #number Life points of target. +function AUFTRAG:GetTargetLife() + local target=self:GetTargetData() + if target then + return target:GetLife() + else + return 0 + end +end + +--- Get target. +-- @param #AUFTRAG self +-- @return Ops.Target#TARGET The target object. Could be many things. +function AUFTRAG:GetTargetData() + return self.engageTarget +end + +--- Get mission objective object. Could be many things depending on the mission type. +-- @param #AUFTRAG self +-- @return Wrapper.Positionable#POSITIONABLE The target object. Could be many things. +function AUFTRAG:GetObjective() + return self:GetTargetData():GetObject() +end + +--- Get type of target. +-- @param #AUFTRAG self +-- @return #string The target type. +function AUFTRAG:GetTargetType() + return self:GetTargetData().Type +end + +--- Get 2D vector of target. +-- @param #AUFTRAG self +-- @return DCS#VEC2 The target 2D vector or *nil*. +function AUFTRAG:GetTargetVec2() + local coord=self:GetTargetCoordinate() + if coord then + return coord:GetVec2() + end + return nil +end + +--- Get coordinate of target. +-- @param #AUFTRAG self +-- @return Core.Point#COORDINATE The target coordinate or *nil*. +function AUFTRAG:GetTargetCoordinate() + + if self.transportPickup then + + -- Special case where we defined a + return self.transportPickup + + elseif self.engageTarget then + + return self.engageTarget:GetCoordinate() + + else + self:E(self.lid.."ERROR: Cannot get target coordinate!") + end + + return nil +end + +--- Get name of the target. +-- @param #AUFTRAG self +-- @return #string Name of the target or "N/A". +function AUFTRAG:GetTargetName() + + if self.engageTarget then + return self.engageTarget:GetName() + end + + return "N/A" +end + + +--- Get distance to target. +-- @param #AUFTRAG self +-- @param Core.Point#COORDINATE FromCoord The coordinate from which the distance is measured. +-- @return #number Distance in meters or 0. +function AUFTRAG:GetTargetDistance(FromCoord) + + local TargetCoord=self:GetTargetCoordinate() + + if TargetCoord and FromCoord then + return TargetCoord:Get2DDistance(FromCoord) + else + self:E(self.lid.."ERROR: TargetCoord or FromCoord does not exist in AUFTRAG:GetTargetDistance() function! Returning 0") + end + + return 0 +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add asset to mission. +-- @param #AUFTRAG self +-- @param Ops.AirWing#AIRWING.SquadronAsset Asset The asset to be added to the mission. +-- @return #AUFTRAG self +function AUFTRAG:AddAsset(Asset) + + self.assets=self.assets or {} + + table.insert(self.assets, Asset) + + return self +end + +--- Delete asset from mission. +-- @param #AUFTRAG self +-- @param Ops.AirWing#AIRWING.SquadronAsset Asset The asset to be removed. +-- @return #AUFTRAG self +function AUFTRAG:DelAsset(Asset) + + for i,_asset in pairs(self.assets or {}) do + local asset=_asset --Ops.AirWing#AIRWING.SquadronAsset + + if asset.uid==Asset.uid then + self:T(self.lid..string.format("Removing asset \"%s\" from mission", tostring(asset.spawngroupname))) + table.remove(self.assets, i) + return self + end + + end + + return self +end + +--- Get asset by its spawn group name. +-- @param #AUFTRAG self +-- @param #string Name Asset spawn group name. +-- @return Ops.AirWing#AIRWING.SquadronAsset +function AUFTRAG:GetAssetByName(Name) + + for i,_asset in pairs(self.assets or {}) do + local asset=_asset --Ops.AirWing#AIRWING.SquadronAsset + + if asset.spawngroupname==Name then + return asset + end + + end + + return nil +end + +--- Count alive ops groups assigned for this mission. +-- @param #AUFTRAG self +-- @return #number Number of alive flight groups. +function AUFTRAG:CountOpsGroups() + local N=0 + for _,_groupdata in pairs(self.groupdata) do + local groupdata=_groupdata --#AUFTRAG.GroupData + if groupdata and groupdata.opsgroup and groupdata.opsgroup:IsAlive() and not groupdata.opsgroup:IsDead() then + N=N+1 + end + end + return N +end + + +--- Get coordinate of target. First unit/group of the set is used. +-- @param #AUFTRAG self +-- @param #table MissionTypes A table of mission types. +-- @return #string Comma separated list of mission types. +function AUFTRAG:GetMissionTypesText(MissionTypes) + + local text="" + for _,missiontype in pairs(MissionTypes) do + text=text..string.format("%s, ", missiontype) + end + + return text +end + +--- Set the mission waypoint coordinate where the mission is executed. +-- @param #AUFTRAG self +-- @return Core.Point#COORDINATE Coordinate where the mission is executed. +-- @return #AUFTRAG self +function AUFTRAG:SetMissionWaypointCoord(Coordinate) + self.missionWaypointCoord=Coordinate +end + +--- Get coordinate of target. First unit/group of the set is used. +-- @param #AUFTRAG self +-- @param Wrapper.Group#GROUP group Group. +-- @return Core.Point#COORDINATE Coordinate where the mission is executed. +function AUFTRAG:GetMissionWaypointCoord(group) + + -- Check if a coord has been explicitly set. + if self.missionWaypointCoord then + local coord=self.missionWaypointCoord + if self.missionAltitude then + coord.y=self.missionAltitude + end + return coord + end + + -- Create waypoint coordinate half way between us and the target. + local waypointcoord=group:GetCoordinate():GetIntermediateCoordinate(self:GetTargetCoordinate(), self.missionFraction) + local alt=waypointcoord.y + + -- Add some randomization. + waypointcoord=ZONE_RADIUS:New("Temp", waypointcoord:GetVec2(), 1000):GetRandomCoordinate():SetAltitude(alt, false) + + -- Set altitude of mission waypoint. + if self.missionAltitude then + waypointcoord:SetAltitude(self.missionAltitude, true) + end + + return waypointcoord +end + + +--- Set log ID string. +-- @param #AUFTRAG self +-- @return #AUFTRAG self +function AUFTRAG:_SetLogID() + self.lid=string.format("Auftrag #%d %s | ", self.auftragsnummer, tostring(self.type)) + return self +end + +--- Update mission F10 map marker. +-- @param #AUFTRAG self +-- @return #AUFTRAG self +function AUFTRAG:UpdateMarker() + + -- Marker text. + local text=string.format("%s %s: %s", self.name, self.type:upper(), self.status:upper()) + text=text..string.format("\n%s", self:GetTargetName()) + text=text..string.format("\nTargets %d/%d, Life Points=%d/%d", self:CountMissionTargets(), self:GetTargetInitialNumber(), self:GetTargetLife(), self:GetTargetInitialLife()) + text=text..string.format("\nFlights %d/%d", self:CountOpsGroups(), self.nassets) + + if not self.marker then + + -- Get target coordinates. Can be nil! + local targetcoord=self:GetTargetCoordinate() + + if self.markerCoaliton and self.markerCoaliton>=0 then + self.marker=MARKER:New(targetcoord, text):ReadOnly():ToCoalition(self.markerCoaliton) + else + self.marker=MARKER:New(targetcoord, text):ReadOnly():ToAll() + end + + else + + if self.marker:GetText()~=text then + self.marker:UpdateText(text) + end + + end + + return self +end + +--- Get DCS task table for the given mission. +-- @param #AUFTRAG self +-- @param Wrapper.Controllable#CONTROLLABLE TaskControllable The controllable for which this task is set. Most tasks don't need it. +-- @return DCS#Task The DCS task table. If multiple tasks are necessary, this is returned as a combo task. +function AUFTRAG:GetDCSMissionTask(TaskControllable) + + local DCStasks={} + + -- Create DCS task based on current self. + if self.type==AUFTRAG.Type.ANTISHIP then + + ---------------------- + -- ANTISHIP Mission -- + ---------------------- + + self:_GetDCSAttackTask(self.engageTarget, DCStasks) + + elseif self.type==AUFTRAG.Type.AWACS then + + ------------------- + -- AWACS Mission -- + ------------------- + + local DCStask=CONTROLLABLE.EnRouteTaskAWACS(nil) + + table.insert(self.enrouteTasks, DCStask) + + elseif self.type==AUFTRAG.Type.BAI then + + ----------------- + -- BAI Mission -- + ----------------- + + self:_GetDCSAttackTask(self.engageTarget, DCStasks) + + elseif self.type==AUFTRAG.Type.BOMBING then + + --------------------- + -- BOMBING Mission -- + --------------------- + + local DCStask=CONTROLLABLE.TaskBombing(nil, self:GetTargetVec2(), self.engageAsGroup, self.engageWeaponExpend, self.engageQuantity, self.engageDirection, self.engageAltitude, self.engageWeaponType, Divebomb) + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.BOMBRUNWAY then + + ------------------------ + -- BOMBRUNWAY Mission -- + ------------------------ + + local DCStask=CONTROLLABLE.TaskBombingRunway(nil, self.engageTarget:GetObject(), self.engageWeaponType, self.engageWeaponExpend, self.engageQuantity, self.engageDirection, self.engageAsGroup) + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.BOMBCARPET then + + ------------------------ + -- BOMBCARPET Mission -- + ------------------------ + + local DCStask=CONTROLLABLE.TaskCarpetBombing(nil, self:GetTargetVec2(), self.engageAsGroup, self.engageWeaponExpend, self.engageQuantity, self.engageDirection, self.engageAltitude, self.engageWeaponType, self.engageCarpetLength) + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.CAP then + + ----------------- + -- CAP Mission -- + ----------------- + + local DCStask=CONTROLLABLE.EnRouteTaskEngageTargetsInZone(nil, self.engageZone:GetVec2(), self.engageZone:GetRadius(), self.engageTargetTypes, Priority) + + table.insert(self.enrouteTasks, DCStask) + + elseif self.type==AUFTRAG.Type.CAS then + + ----------------- + -- CAS Mission -- + ----------------- + + local DCStask=CONTROLLABLE.EnRouteTaskEngageTargetsInZone(nil, self.engageZone:GetVec2(), self.engageZone:GetRadius(), self.engageTargetTypes, Priority) + + table.insert(self.enrouteTasks, DCStask) + + elseif self.type==AUFTRAG.Type.ESCORT then + + -------------------- + -- ESCORT Mission -- + -------------------- + + local DCStask=CONTROLLABLE.TaskEscort(nil, self.engageTarget:GetObject(), self.escortVec3, LastWaypointIndex, self.engageMaxDistance, self.engageTargetTypes) + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.FACA then + + ----------------- + -- FAC Mission -- + ----------------- + + local DCStask=CONTROLLABLE.TaskFAC_AttackGroup(nil, self.engageTarget:GetObject(), self.engageWeaponType, self.facDesignation, self.facDatalink, self.facFreq, self.facModu, CallsignName, CallsignNumber) + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.FERRY then + + ------------------- + -- FERRY Mission -- + ------------------- + + -- TODO: Ferry mission type. How? + + elseif self.type==AUFTRAG.Type.INTERCEPT then + + ----------------------- + -- INTERCEPT Mission -- + ----------------------- + + self:_GetDCSAttackTask(self.engageTarget, DCStasks) + + elseif self.type==AUFTRAG.Type.ORBIT then + + ------------------- + -- ORBIT Mission -- + ------------------- + + -- Done below as also other mission types use the orbit task. + + elseif self.type==AUFTRAG.Type.GCICAP then + + -------------------- + -- GCICAP Mission -- + -------------------- + + -- Done below as also other mission types use the orbit task. + + elseif self.type==AUFTRAG.Type.RECON then + + ------------------- + -- RECON Mission -- + ------------------- + + -- TODO: What? Table of coordinates? + + elseif self.type==AUFTRAG.Type.SEAD then + + ------------------ + -- SEAD Mission -- + ------------------ + + --[[ + local DCStask=CONTROLLABLE.EnRouteTaskEngageTargets(nil, nil ,{"Air Defence"} , 0) + table.insert(self.enrouteTasks, DCStask) + DCStask.key="SEAD" + ]] + + self:_GetDCSAttackTask(self.engageTarget, DCStasks) + + elseif self.type==AUFTRAG.Type.STRIKE then + + -------------------- + -- STRIKE Mission -- + -------------------- + + local DCStask=CONTROLLABLE.TaskAttackMapObject(nil, self:GetTargetVec2(), self.engageAsGroup, self.engageWeaponExpend, self.engageQuantity, self.engageDirection, self.engageAltitude, self.engageWeaponType) + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.TANKER then + + -------------------- + -- TANKER Mission -- + -------------------- + + local DCStask=CONTROLLABLE.EnRouteTaskTanker(nil) + + table.insert(self.enrouteTasks, DCStask) + + elseif self.type==AUFTRAG.Type.TROOPTRANSPORT then + + ---------------------------- + -- TROOPTRANSPORT Mission -- + ---------------------------- + + -- Task to embark the troops at the pick up point. + local TaskEmbark=CONTROLLABLE.TaskEmbarking(TaskControllable, self.transportPickup, self.transportGroupSet, self.transportWaitForCargo) + + -- Task to disembark the troops at the drop off point. + local TaskDisEmbark=CONTROLLABLE.TaskDisembarking(TaskControllable, self.transportDropoff, self.transportGroupSet) + + table.insert(DCStasks, TaskEmbark) + table.insert(DCStasks, TaskDisEmbark) + + elseif self.type==AUFTRAG.Type.RESCUEHELO then + + ------------------------- + -- RESCUE HELO Mission -- + ------------------------- + + local DCStask={} + + DCStask.id="Formation" + + -- We create a "fake" DCS task and pass the parameters to the FLIGHTGROUP. + local param={} + param.unitname=self:GetTargetName() --self.carrier:GetName() + param.offsetX=200 + param.offsetZ=240 + param.altitude=70 + param.dtFollow=1.0 + + DCStask.params=param + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.ARTY then + + ------------------ + -- ARTY Mission -- + ------------------ + + local DCStask=CONTROLLABLE.TaskFireAtPoint(nil, self:GetTargetVec2(), self.artyRadius, self.artyShots, self.engageWeaponType) + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.PATROLZONE then + + ------------------------- + -- PATROL ZONE Mission -- + ------------------------- + + local DCStask={} + + DCStask.id="PatrolZone" + + -- We create a "fake" DCS task and pass the parameters to the FLIGHTGROUP. + local param={} + param.zone=self:GetObjective() + param.altitude=self.missionAltitude + param.speed=self.missionSpeed + + DCStask.params=param + + table.insert(DCStasks, DCStask) + + else + self:E(self.lid..string.format("ERROR: Unknown mission task!")) + return nil + end + + + -- Set ORBIT task. Also applies to other missions: AWACS, TANKER, CAP, CAS. + if self.type==AUFTRAG.Type.ORBIT or + self.type==AUFTRAG.Type.CAP or + self.type==AUFTRAG.Type.CAS or + self.type==AUFTRAG.Type.GCICAP or + self.type==AUFTRAG.Type.AWACS or + self.type==AUFTRAG.Type.TANKER then + + ------------------- + -- ORBIT Mission -- + ------------------- + + local Coordinate=self:GetTargetCoordinate() + + local DCStask=CONTROLLABLE.TaskOrbit(nil, Coordinate, self.orbitAltitude, self.orbitSpeed, self.orbitRaceTrack) + + table.insert(DCStasks, DCStask) + + end + + -- Debug info. + self:T3({missiontask=DCStasks}) + + -- Return the task. + if #DCStasks==1 then + return DCStasks[1] + else + return CONTROLLABLE.TaskCombo(nil, DCStasks) + end + +end + +--- Get DCS task table for an attack group or unit task. +-- @param #AUFTRAG self +-- @param Ops.Target#TARGET Target Target data. +-- @param #table DCStasks DCS DCS tasks table to which the task is added. +-- @return DCS#Task The DCS task table. +function AUFTRAG:_GetDCSAttackTask(Target, DCStasks) + + DCStasks=DCStasks or {} + + for _,_target in pairs(Target.targets) do + local target=_target --Ops.Target#TARGET.Object + + if target.Type==TARGET.ObjectType.GROUP then + + local DCStask=CONTROLLABLE.TaskAttackGroup(nil, target.Object, self.engageWeaponType, self.engageWeaponExpend, self.engageQuantity, self.engageDirection, self.engageAltitude, self.engageAsGroup) + + table.insert(DCStasks, DCStask) + + elseif target.Type==TARGET.ObjectType.UNIT or target.Type==TARGET.ObjectType.STATIC then + + local DCStask=CONTROLLABLE.TaskAttackUnit(nil, target.Object, self.engageAsGroup, self.WeaponExpend, self.engageQuantity, self.engageDirection, self.engageAltitude, self.engageWeaponType) + + table.insert(DCStasks, DCStask) + + end + + end + + return DCStasks +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- **Ops** - Target. +-- +-- **Main Features:** +-- +-- * Manages target, number alive, life points, damage etc. +-- * Events when targets are damaged or destroyed +-- * Various target objects: UNIT, GROUP, STATIC, AIRBASE, COORDINATE, SET_GROUP, SET_UNIT +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Ops.Target +-- @image OPS_Target.png + + +--- TARGET class. +-- @type TARGET +-- @field #string ClassName Name of the class. +-- @field #number verbose Verbosity level. +-- @field #string lid Class id string for output to DCS log file. +-- @field #table targets Table of target objects. +-- @field #number targetcounter Running number to generate target object IDs. +-- @field #number life Total life points on last status update. +-- @field #number life0 Total life points of completely healthy targets. +-- @field #number threatlevel0 Initial threat level. +-- @field #number category Target category (Ground, Air, Sea). +-- @field #number N0 Number of initial target elements/units. +-- @field #number Ntargets0 Number of initial target objects. +-- @field #number Ndestroyed Number of target elements/units that were destroyed. +-- @field #number Ndead Number of target elements/units that are dead (destroyed or despawned). +-- @field #table elements Table of target elements/units. +-- @field #table casualties Table of dead element names. +-- @extends Core.Fsm#FSM + +--- **It is far more important to be able to hit the target than it is to haggle over who makes a weapon or who pulls a trigger** -- Dwight D. Eisenhower +-- +-- === +-- +-- ![Banner Image](..\Presentations\OPS\Target\_Main.pngs) +-- +-- # The TARGET Concept +-- +-- Define a target of your mission and monitor its status. Events are triggered when the target is damaged or destroyed. +-- +-- A target can consist of one or multiple "objects". +-- +-- +-- @field #TARGET +TARGET = { + ClassName = "TARGET", + verbose = 0, + lid = nil, + targets = {}, + targetcounter = 0, + life = 0, + life0 = 0, + N0 = 0, + Ntargets0 = 0, + Ndestroyed = 0, + Ndead = 0, + elements = {}, + casualties = {}, + threatlevel0 = 0 +} + + +--- Type. +-- @type TARGET.ObjectType +-- @field #string GROUP Target is a GROUP object. +-- @field #string UNIT Target is a UNIT object. +-- @field #string STATIC Target is a STATIC object. +-- @field #string SCENERY Target is a SCENERY object. +-- @field #string COORDINATE Target is a COORDINATE. +-- @field #string AIRBASE Target is an AIRBASE. +-- @field #string ZONE Target is a ZONE object. +TARGET.ObjectType={ + GROUP="Group", + UNIT="Unit", + STATIC="Static", + SCENERY="Scenery", + COORDINATE="Coordinate", + AIRBASE="Airbase", + ZONE="Zone", +} + + +--- Category. +-- @type TARGET.Category +-- @field #string AIRCRAFT +-- @field #string GROUND +-- @field #string NAVAL +-- @field #string AIRBASE +-- @field #string COORDINATE +-- @field #string ZONE +TARGET.Category={ + AIRCRAFT="Aircraft", + GROUND="Ground", + NAVAL="Naval", + AIRBASE="Airbase", + COORDINATE="Coordinate", + ZONE="Zone", +} + +--- Object status. +-- @type TARGET.ObjectStatus +-- @field #string ALIVE Object is alive. +-- @field #string DEAD Object is dead. +TARGET.ObjectStatus={ + ALIVE="Alive", + DEAD="Dead", +} +--- Target object. +-- @type TARGET.Object +-- @field #number ID Target unique ID. +-- @field #string Name Target name. +-- @field #string Type Target type. +-- @field Wrapper.Positionable#POSITIONABLE Object The object, which can be many things, e.g. a UNIT, GROUP, STATIC, SCENERY, AIRBASE or COORDINATE object. +-- @field #number Life Life points on last status update. +-- @field #number Life0 Life points of completely healthy target. +-- @field #number N0 Number of initial elements. +-- @field #number Ndead Number of dead elements. +-- @field #number Ndestroyed Number of destroyed elements. +-- @field #string Status Status "Alive" or "Dead". +-- @field Core.Point#COORDINATE Coordinate of the target object. + +--- Global target ID counter. +_TARGETID=0 + +--- TARGET class version. +-- @field #string version +TARGET.version="0.3.1" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: A lot. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new TARGET object and start the FSM. +-- @param #TARGET self +-- @param #table TargetObject Target object. +-- @return #TARGET self +function TARGET:New(TargetObject) + + -- Inherit everything from INTEL class. + local self=BASE:Inherit(self, FSM:New()) --#TARGET + + -- Increase counter. + _TARGETID=_TARGETID+1 + + -- Add object. + self:AddObject(TargetObject) + + -- Get first target. + local Target=self.targets[1] --#TARGET.Object + + if not Target then + self:E("ERROR: No valid TARGET!") + return nil + end + + -- Target Name. + self.name=self:GetTargetName(Target) + + -- Target category. + self.category=self:GetTargetCategory(Target) + + -- Log ID. + self.lid=string.format("TARGET #%03d [%s] | ", _TARGETID, tostring(self.category)) + + -- Start state. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Alive") -- Start FSM. + self:AddTransition("*", "Status", "*") -- Status update. + self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. + + self:AddTransition("*", "ObjectDamaged", "*") -- A target object was damaged. + self:AddTransition("*", "ObjectDestroyed", "*") -- A target object was destroyed. + self:AddTransition("*", "ObjectDead", "*") -- A target object is dead (destroyed or despawned). + + self:AddTransition("*", "Damaged", "*") -- Target was damaged. + self:AddTransition("*", "Destroyed", "Dead") -- Target was completely destroyed. + self:AddTransition("*", "Dead", "Dead") -- Target was completely destroyed. + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the TARGET. Initializes parameters and starts event handlers. + -- @function [parent=#TARGET] Start + -- @param #TARGET self + + --- Triggers the FSM event "Start" after a delay. Starts the TARGET. Initializes parameters and starts event handlers. + -- @function [parent=#TARGET] __Start + -- @param #TARGET self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the TARGET and all its event handlers. + -- @param #TARGET self + + --- Triggers the FSM event "Stop" after a delay. Stops the TARGET and all its event handlers. + -- @function [parent=#TARGET] __Stop + -- @param #TARGET self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Status". + -- @function [parent=#TARGET] Status + -- @param #TARGET self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#TARGET] __Status + -- @param #TARGET self + -- @param #number delay Delay in seconds. + + + + -- Start. + self:__Start(-1) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create target data from a given object. +-- @param #TARGET self +-- @param Wrapper.Positionable#POSITIONABLE Object The target GROUP, UNIT, STATIC, AIRBASE or COORDINATE. +function TARGET:AddObject(Object) + + if Object:IsInstanceOf("SET_GROUP") or Object:IsInstanceOf("SET_UNIT") then + + --- + -- Sets + --- + + local set=Object --Core.Set#SET_GROUP + + for _,object in pairs(set.Set) do + self:AddObject(object) + end + + else + + --- + -- Groups, Units, Statics, Airbases, Coordinates + --- + + self:_AddObject(Object) + + end + +end + +--- Check if TARGET is alive. +-- @param #TARGET self +-- @return #boolean If true, target is alive. +function TARGET:IsAlive() + return self:Is("Alive") +end + +--- Check if TARGET is dead. +-- @param #TARGET self +-- @return #boolean If true, target is dead. +function TARGET:IsDead() + return self:Is("Dead") +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Start & Status +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after Start event. Starts the FLIGHTGROUP FSM and event handlers. +-- @param #TARGET self +-- @param Wrapper.Group#GROUP Group Flight group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function TARGET:onafterStart(From, Event, To) + + -- Short info. + local text=string.format("Starting Target") + self:T(self.lid..text) + + self:HandleEvent(EVENTS.Dead, self.OnEventUnitDeadOrLost) + self:HandleEvent(EVENTS.UnitLost, self.OnEventUnitDeadOrLost) + self:HandleEvent(EVENTS.RemoveUnit, self.OnEventUnitDeadOrLost) + + self:__Status(-1) +end + +--- On after "Status" event. +-- @param #TARGET self +-- @param Wrapper.Group#GROUP Group Flight group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function TARGET:onafterStatus(From, Event, To) + + -- FSM state. + local fsmstate=self:GetState() + + -- Update damage. + local damaged=false + for i,_target in pairs(self.targets) do + local target=_target --#TARGET.Object + + local life=target.Life + + target.Life=self:GetTargetLife(target) + + if target.Life=1 then + local text=string.format("%s: Targets=%d/%d Life=%.1f/%.1f Damage=%.1f", fsmstate, self:CountTargets(), self.N0, self:GetLife(), self:GetLife0(), self:GetDamage()) + if damaged then + text=text.." Damaged!" + end + self:I(self.lid..text) + end + + -- Log output verbose=2. + if self.verbose>=2 then + local text="Target:" + for i,_target in pairs(self.targets) do + local target=_target --#TARGET.Object + local damage=(1-target.Life/target.Life0)*100 + text=text..string.format("\n[%d] %s %s %s: Life=%.1f/%.1f, Damage=%.1f", i, target.Type, target.Name, target.Status, target.Life, target.Life0, damage) + end + self:I(self.lid..text) + end + + -- Update status again in 30 sec. + if self:IsAlive() then + self:__Status(-30) + end +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Events +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "ObjectDamaged" event. +-- @param #TARGET self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #TARGET.Object Target Target object. +function TARGET:onafterObjectDamaged(From, Event, To, Target) + + -- Debug info. + self:T(self.lid..string.format("Object %s damaged", Target.Name)) + +end + +--- On after "ObjectDestroyed" event. +-- @param #TARGET self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #TARGET.Object Target Target object. +function TARGET:onafterObjectDestroyed(From, Event, To, Target) + + -- Debug message. + self:T(self.lid..string.format("Object %s destroyed", Target.Name)) + + -- Increase destroyed counter. + self.Ndestroyed=self.Ndestroyed+1 + + -- Call object dead event. + self:ObjectDead(Target) + +end + +--- On after "ObjectDead" event. +-- @param #TARGET self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #TARGET.Object Target Target object. +function TARGET:onafterObjectDead(From, Event, To, Target) + + -- Debug message. + self:T(self.lid..string.format("Object %s dead", Target.Name)) + + -- Set target status. + Target.Status=TARGET.ObjectStatus.DEAD + + -- Increase dead counter. + self.Ndead=self.Ndead+1 + + -- Check if anyone is alive? + local dead=true + for _,_target in pairs(self.targets) do + local target=_target --#TARGET.Object + if target.Status==TARGET.ObjectStatus.ALIVE then + dead=false + end + end + + -- All dead ==> Trigger destroyed event. + if dead then + + if self.Ndestroyed==self.Ntargets0 then + + self:Destroyed() + + else + + self:Dead() + + end + end + +end + +--- On after "Damaged" event. +-- @param #TARGET self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function TARGET:onafterDamaged(From, Event, To) + + self:T(self.lid..string.format("TARGET damaged")) + +end + +--- On after "Destroyed" event. +-- @param #TARGET self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function TARGET:onafterDestroyed(From, Event, To) + + self:T(self.lid..string.format("TARGET destroyed")) + + self:Dead() + +end + +--- On after "Dead" event. +-- @param #TARGET self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function TARGET:onafterDead(From, Event, To) + + self:T(self.lid..string.format("TARGET dead")) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Event Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Event function handling the loss of a unit. +-- @param #TARGET self +-- @param Core.Event#EVENTDATA EventData Event data. +function TARGET:OnEventUnitDeadOrLost(EventData) + + local Name=EventData and EventData.IniUnitName or nil + + -- Check that this is the right group. + if self:IsElement(Name) and not self:IsCasualty(Name) then + + -- Debug info. + self:T3(self.lid..string.format("EVENT ID=%d: Unit %s dead or lost!", EventData.id, tostring(Name))) + + -- Add to the list of casualties. + table.insert(self.casualties, Name) + + -- Try to get target Group. + local target=self:GetTargetByName(EventData.IniGroupName) + + -- Try unit target. + if not target then + target=self:GetTargetByName(EventData.IniUnitName) + end + + -- Check if we could find a target object. + if target then + + if EventData.id==EVENTS.RemoveUnit then + target.Ndead=target.Ndead+1 + else + target.Ndestroyed=target.Ndestroyed+1 + target.Ndead=target.Ndead+1 + end + + if target.Ndead==target.N0 then + + if target.Ndestroyed>=target.N0 then + + -- Debug message. + self:T2(self.lid..string.format("EVENT ID=%d: target %s dead/lost ==> destroyed", EventData.id, tostring(target.Name))) + + -- Trigger object destroyed event. + self:ObjectDestroyed(target) + + else + + -- Debug message. + self:T2(self.lid..string.format("EVENT ID=%d: target %s removed ==> dead", EventData.id, tostring(target.Name))) + + -- Trigger object dead event. + self:ObjectDead(target) + + end + + end + + end -- Event belongs to this TARGET + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Adding and Removing Targets +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create target data from a given object. +-- @param #TARGET self +-- @param Wrapper.Positionable#POSITIONABLE Object The target GROUP, UNIT, STATIC, AIRBASE or COORDINATE. +function TARGET:_AddObject(Object) + + local target={} --#TARGET.Object + + target.N0=0 + target.Ndead=0 + target.Ndestroyed=0 + + if Object:IsInstanceOf("GROUP") then + + local group=Object --Wrapper.Group#GROUP + + target.Type=TARGET.ObjectType.GROUP + target.Name=group:GetName() + + target.Coordinate=group:GetCoordinate() + + local units=group:GetUnits() + + target.Life=0 ; target.Life0=0 + for _,_unit in pairs(units or {}) do + local unit=_unit --Wrapper.Unit#UNIT + + local life=unit:GetLife() + + target.Life=target.Life+life + target.Life0=target.Life0+math.max(unit:GetLife0(), life) -- There was an issue with ships that life is greater life0, which cannot be! + + self.threatlevel0=self.threatlevel0+unit:GetThreatLevel() + + table.insert(self.elements, unit:GetName()) + + target.N0=target.N0+1 + end + + elseif Object:IsInstanceOf("UNIT") then + + local unit=Object --Wrapper.Unit#UNIT + + target.Type=TARGET.ObjectType.UNIT + target.Name=unit:GetName() + + target.Coordinate=unit:GetCoordinate() + + if unit then + target.Life=unit:GetLife() + target.Life0=math.max(unit:GetLife0(), target.Life) -- There was an issue with ships that life is greater life0! + + self.threatlevel0=self.threatlevel0+unit:GetThreatLevel() + + table.insert(self.elements, unit:GetName()) + + target.N0=target.N0+1 + end + + elseif Object:IsInstanceOf("STATIC") then + + local static=Object --Wrapper.Static#STATIC + + target.Type=TARGET.ObjectType.STATIC + target.Name=static:GetName() + + target.Coordinate=static:GetCoordinate() + + if static and static:IsAlive() then + + target.Life0=1 + target.Life=1 + target.N0=target.N0+1 + + table.insert(self.elements, target.Name) + + end + + elseif Object:IsInstanceOf("SCENERY") then + + local scenery=Object --Wrapper.Scenery#SCENERY + + target.Type=TARGET.ObjectType.SCENERY + target.Name=scenery:GetName() + + target.Coordinate=scenery:GetCoordinate() + + target.Life0=1 + target.Life=1 + + target.N0=target.N0+1 + + table.insert(self.elements, target.Name) + + elseif Object:IsInstanceOf("AIRBASE") then + + local airbase=Object --Wrapper.Airbase#AIRBASE + + target.Type=TARGET.ObjectType.AIRBASE + target.Name=airbase:GetName() + + target.Coordinate=airbase:GetCoordinate() + + target.Life0=1 + target.Life=1 + + target.N0=target.N0+1 + + table.insert(self.elements, target.Name) + + elseif Object:IsInstanceOf("COORDINATE") then + + local coord=UTILS.DeepCopy(Object) --Core.Point#COORDINATE + + target.Type=TARGET.ObjectType.COORDINATE + target.Name=coord:ToStringMGRS() + + target.Coordinate=coord + + target.Life0=1 + target.Life=1 + + elseif Object:IsInstanceOf("ZONE_BASE") then + + local zone=Object --Core.Zone#ZONE_BASE + Object=zone --:GetCoordinate() + + target.Type=TARGET.ObjectType.ZONE + target.Name=zone:GetName() + + target.Coordinate=zone:GetCoordinate() + + target.Life0=1 + target.Life=1 + + else + self:E(self.lid.."ERROR: Unknown object type!") + return nil + end + + self.life=self.life+target.Life + self.life0=self.life0+target.Life0 + + self.N0=self.N0+target.N0 + self.Ntargets0=self.Ntargets0+1 + + -- Increase counter. + self.targetcounter=self.targetcounter+1 + + target.ID=self.targetcounter + target.Status=TARGET.ObjectStatus.ALIVE + target.Object=Object + + table.insert(self.targets, target) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Life and Damage Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get target life points. +-- @param #TARGET self +-- @return #number Number of initial life points when mission was planned. +function TARGET:GetLife0() + return self.life0 +end + +--- Get current damage. +-- @param #TARGET self +-- @return #number Damage in percent. +function TARGET:GetDamage() + local life=self:GetLife()/self:GetLife0() + local damage=1-life + return damage*100 +end + +--- Get target life points. +-- @param #TARGET self +-- @param #TARGET.Object Target Target object. +-- @return #number Life points of target. +function TARGET:GetTargetLife(Target) + + if Target.Type==TARGET.ObjectType.GROUP then + + if Target.Object and Target.Object:IsAlive() then + + local units=Target.Object:GetUnits() + + local life=0 + for _,_unit in pairs(units or {}) do + local unit=_unit --Wrapper.Unit#UNIT + life=life+unit:GetLife() + end + + return life + else + return 0 + end + + elseif Target.Type==TARGET.ObjectType.UNIT then + + local unit=Target.Object --Wrapper.Unit#UNIT + + if unit and unit:IsAlive() then + + -- Note! According to the profiler, there is a big difference if we "return unit:GetLife()" or "local life=unit:GetLife(); return life"! + local life=unit:GetLife() + return life + else + return 0 + end + + elseif Target.Type==TARGET.ObjectType.STATIC then + + if Target.Object and Target.Object:IsAlive() then + return 1 + else + return 0 + end + + elseif Target.Type==TARGET.ObjectType.SCENERY then + + if Target.Status==TARGET.ObjectStatus.ALIVE then + return 1 + else + return 0 + end + + elseif Target.Type==TARGET.ObjectType.AIRBASE then + + if Target.Status==TARGET.ObjectStatus.ALIVE then + return 1 + else + return 0 + end + + elseif Target.Type==TARGET.ObjectType.COORDINATE then + + return 1 + + elseif Target.Type==TARGET.ObjectType.ZONE then + + return 1 + + else + self:E("ERROR: unknown target object type in GetTargetLife!") + end + +end + +--- Get current life points. +-- @param #TARGET self +-- @return #number Life points of target. +function TARGET:GetLife() + + local N=0 + + for _,_target in pairs(self.targets) do + local Target=_target --#TARGET.Object + + N=N+self:GetTargetLife(Target) + + end + + return N +end + +--- Get target 3D position vector. +-- @param #TARGET self +-- @param #TARGET.Object Target Target object. +-- @return DCS#Vec3 Vector with x,y,z components +function TARGET:GetTargetVec3(Target) + + if Target.Type==TARGET.ObjectType.GROUP then + + local object=Target.Object --Wrapper.Group#GROUP + + if object and object:IsAlive() then + local vec3=object:GetVec3() + return vec3 + + else + + return nil + + end + + elseif Target.Type==TARGET.ObjectType.UNIT then + + local object=Target.Object --Wrapper.Unit#UNIT + + if object and object:IsAlive() then + local vec3=object:GetVec3() + return vec3 + else + return nil + end + + elseif Target.Type==TARGET.ObjectType.STATIC then + + local object=Target.Object --Wrapper.Static#STATIC + + if object and object:IsAlive() then + local vec3=object:GetVec3() + return vec3 + else + return nil + end + + elseif Target.Type==TARGET.ObjectType.SCENERY then + + local object=Target.Object --Wrapper.Scenery#SCENERY + + if object then + local vec3=object:GetVec3() + return vec3 + else + return nil + end + + elseif Target.Type==TARGET.ObjectType.AIRBASE then + + local object=Target.Object --Wrapper.Airbase#AIRBASE + + local vec3=object:GetVec3() + return vec3 + + --if Target.Status==TARGET.ObjectStatus.ALIVE then + --end + + elseif Target.Type==TARGET.ObjectType.COORDINATE then + + local object=Target.Object --Core.Point#COORDINATE + + local vec3={x=object.x, y=object.y, z=object.z} + return vec3 + + elseif Target.Type==TARGET.ObjectType.ZONE then + + local object=Target.Object --Core.Zone#ZONE + + local vec3=object:GetVec3() + return vec3 + + end + + self:E(self.lid.."ERROR: Unknown TARGET type! Cannot get Vec3") +end + + +--- Get target coordinate. +-- @param #TARGET self +-- @param #TARGET.Object Target Target object. +-- @return Core.Point#COORDINATE Coordinate of the target. +function TARGET:GetTargetCoordinate(Target) + + if Target.Type==TARGET.ObjectType.COORDINATE then + + -- Coordinate is the object itself. + return Target.Object + + else + + -- Get updated position vector. + local vec3=self:GetTargetVec3(Target) + + -- Update position. This saves us to create a new COORDINATE object each time. + if vec3 then + Target.Coordinate.x=vec3.x + Target.Coordinate.y=vec3.y + Target.Coordinate.z=vec3.z + end + + return Target.Coordinate + + end + + return nil +end + +--- Get target name. +-- @param #TARGET self +-- @param #TARGET.Object Target Target object. +-- @return #string Name of the target object. +function TARGET:GetTargetName(Target) + + if Target.Type==TARGET.ObjectType.GROUP then + + if Target.Object and Target.Object:IsAlive() then + + return Target.Object:GetName() + + end + + elseif Target.Type==TARGET.ObjectType.UNIT then + + if Target.Object and Target.Object:IsAlive() then + return Target.Object:GetName() + end + + elseif Target.Type==TARGET.ObjectType.STATIC then + + if Target.Object and Target.Object:IsAlive() then + return Target.Object:GetName() + end + + elseif Target.Type==TARGET.ObjectType.AIRBASE then + + if Target.Status==TARGET.ObjectStatus.ALIVE then + return Target.Object:GetName() + end + + elseif Target.Type==TARGET.ObjectType.COORDINATE then + + local coord=Target.Object --Core.Point#COORDINATE + + return coord:ToStringMGRS() + + end + + return "Unknown" +end + +--- Get name. +-- @param #TARGET self +-- @return #string Name of the target usually the first object. +function TARGET:GetName() + return self.name +end + +--- Get coordinate. +-- @param #TARGET self +-- @return Core.Point#COORDINATE Coordinate of the target. +function TARGET:GetCoordinate() + + for _,_target in pairs(self.targets) do + local Target=_target --#TARGET.Object + + local coordinate=self:GetTargetCoordinate(Target) + + if coordinate then + return coordinate + end + + end + + self:E(self.lid..string.format("ERROR: Cannot get coordinate of target %s", self.name)) + return nil +end + + +--- Get target category. +-- @param #TARGET self +-- @param #TARGET.Object Target Target object. +-- @return #TARGET.Category Target category. +function TARGET:GetTargetCategory(Target) + + local category=nil + + if Target.Type==TARGET.ObjectType.GROUP then + + if Target.Object and Target.Object:IsAlive()~=nil then + + local group=Target.Object --Wrapper.Group#GROUP + + local cat=group:GetCategory() + + if cat==Group.Category.AIRPLANE or cat==Group.Category.HELICOPTER then + category=TARGET.Category.AIRCRAFT + elseif cat==Group.Category.GROUND or cat==Group.Category.TRAIN then + category=TARGET.Category.GROUND + elseif cat==Group.Category.SHIP then + category=TARGET.Category.NAVAL + end + + end + + elseif Target.Type==TARGET.ObjectType.UNIT then + + if Target.Object and Target.Object:IsAlive()~=nil then + local unit=Target.Object --Wrapper.Unit#UNIT + + local group=unit:GetGroup() + + local cat=group:GetCategory() + + if cat==Group.Category.AIRPLANE or cat==Group.Category.HELICOPTER then + category=TARGET.Category.AIRCRAFT + elseif cat==Group.Category.GROUND or cat==Group.Category.TRAIN then + category=TARGET.Category.GROUND + elseif cat==Group.Category.SHIP then + category=TARGET.Category.NAVAL + end + + end + + elseif Target.Type==TARGET.ObjectType.STATIC then + + return TARGET.Category.GROUND + + elseif Target.Type==TARGET.ObjectType.SCENERY then + + return TARGET.Category.GROUND + + elseif Target.Type==TARGET.ObjectType.AIRBASE then + + return TARGET.Category.AIRBASE + + elseif Target.Type==TARGET.ObjectType.COORDINATE then + + return TARGET.Category.COORDINATE + + elseif Target.Type==TARGET.ObjectType.ZONE then + + return TARGET.Category.ZONE + + else + self:E("ERROR: unknown target category!") + end + + return category +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get a target object by its name. +-- @param #TARGET self +-- @param #string ObjectName Object name. +-- @return #TARGET.Object The target object table or nil. +function TARGET:GetTargetByName(ObjectName) + + for _,_target in pairs(self.targets) do + local target=_target --#TARGET.Object + if ObjectName==target.Name then + return target + end + end + + return nil +end + + +--- Get the first target objective alive. +-- @param #TARGET self +-- @return #TARGET.Object The target objective. +function TARGET:GetObjective() + + for _,_target in pairs(self.targets) do + local target=_target --#TARGET.Object + if target.Status==TARGET.ObjectStatus.ALIVE then + return target + end + end + + return nil +end + +--- Get the first target object alive. +-- @param #TARGET self +-- @return Wrapper.Positionable#POSITIONABLE The target object or nil. +function TARGET:GetObject() + + local target=self:GetObjective() + if target then + return target.Object + end + + return nil +end + +--- Count alive objects. +-- @param #TARGET self +-- @param #TARGET.Object Target Target objective. +-- @return #number Number of alive target objects. +function TARGET:CountObjectives(Target) + + local N=0 + + if Target.Type==TARGET.ObjectType.GROUP then + + local target=Target.Object --Wrapper.Group#GROUP + + local units=target:GetUnits() + + for _,_unit in pairs(units or {}) do + local unit=_unit --Wrapper.Unit#UNIT + if unit and unit:IsAlive()~=nil and unit:GetLife()>1 then + N=N+1 + end + end + + elseif Target.Type==TARGET.ObjectType.UNIT then + + local target=Target.Object --Wrapper.Unit#UNIT + + if target and target:IsAlive()~=nil and target:GetLife()>1 then + N=N+1 + end + + elseif Target.Type==TARGET.ObjectType.STATIC then + + local target=Target.Object --Wrapper.Static#STATIC + + if target and target:IsAlive() then + N=N+1 + end + + elseif Target.Type==TARGET.ObjectType.SCENERY then + + if Target.Status==TARGET.ObjectStatus.ALIVE then + N=N+1 + end + + elseif Target.Type==TARGET.ObjectType.AIRBASE then + + if Target.Status==TARGET.ObjectStatus.ALIVE then + N=N+1 + end + + elseif Target.Type==TARGET.ObjectType.COORDINATE then + + -- No target we can check! + + elseif Target.Type==TARGET.ObjectType.ZONE then + + -- No target we can check! + + else + self:E(self.lid.."ERROR: Unknown target type! Cannot count targets") + end + + return N +end + +--- Count alive targets. +-- @param #TARGET self +-- @return #number Number of alive target objects. +function TARGET:CountTargets() + + local N=0 + + for _,_target in pairs(self.targets) do + local Target=_target --#TARGET.Object + + N=N+self:CountObjectives(Target) + + end + + return N +end + +--- Check if something is an element of the TARGET. +-- @param #TARGET self +-- @param #string Name The name of the potential element. +-- @return #boolean If `true`, this name is part of this TARGET. +function TARGET:IsElement(Name) + + if Name==nil then + return false + end + + for _,name in pairs(self.elements) do + if name==Name then + return true + end + end + + return false +end + +--- Check if something is a a casualty of this TARGET. +-- @param #TARGET self +-- @param #string Name The name of the potential element. +-- @return #boolean If `true`, this name is a casualty of this TARGET. +function TARGET:IsCasualty(Name) + + if Name==nil then + return false + end + + for _,name in pairs(self.casualties) do + if name==Name then + return true + end + end + + return false +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- **Ops** - Generic group enhancement. +-- +-- This class is **not** meant to be used itself by the end user. It contains common functionalities of derived classes for air, ground and sea. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- === +-- @module Ops.OpsGroup +-- @image OPS_OpsGroup.png + + +--- OPSGROUP class. +-- @type OPSGROUP +-- @field #string ClassName Name of the class. +-- @field #boolean Debug Debug mode. Messages to all about status. +-- @field #number verbose Verbosity level. 0=silent. +-- @field #string lid Class id string for output to DCS log file. +-- @field #string groupname Name of the group. +-- @field Wrapper.Group#GROUP group Group object. +-- @field #table template Template of the group. +-- @field #boolean isLateActivated Is the group late activated. +-- @field #boolean isUncontrolled Is the group uncontrolled. +-- @field #boolean isFlightgroup Is a FLIGHTGROUP. +-- @field #boolean isArmygroup Is an ARMYGROUP. +-- @field #boolean isNavygroup Is a NAVYGROUP. +-- @field #table elements Table of elements, i.e. units of the group. +-- @field #boolean isAI If true, group is purely AI. +-- @field #boolean isAircraft If true, group is airplane or helicopter. +-- @field #boolean isNaval If true, group is ships or submarine. +-- @field #boolean isGround If true, group is some ground unit. +-- @field #table waypoints Table of waypoints. +-- @field #table waypoints0 Table of initial waypoints. +-- @field #number currentwp Current waypoint index. This is the index of the last passed waypoint. +-- @field #boolean adinfinitum Resume route at first waypoint when final waypoint is reached. +-- @field #table taskqueue Queue of tasks. +-- @field #number taskcounter Running number of task ids. +-- @field #number taskcurrent ID of current task. If 0, there is no current task assigned. +-- @field #table taskenroute Enroute task of the group. +-- @field #table taskpaused Paused tasks. +-- @field #table missionqueue Queue of missions. +-- @field #number currentmission The ID (auftragsnummer) of the currently assigned AUFTRAG. +-- @field Core.Set#SET_UNIT detectedunits Set of detected units. +-- @field Core.Set#SET_GROUP detectedgroups Set of detected groups. +-- @field #string attribute Generalized attribute. +-- @field #number speedMax Max speed in km/h. +-- @field #number speedCruise Cruising speed in km/h. +-- @field #number speedWp Speed to the next waypoint in m/s. +-- @field #boolean passedfinalwp Group has passed the final waypoint. +-- @field #number wpcounter Running number counting waypoints. +-- @field #boolean respawning Group is being respawned. +-- @field Core.Set#SET_ZONE checkzones Set of zones. +-- @field Core.Set#SET_ZONE inzones Set of zones in which the group is currently in. +-- @field Core.Timer#TIMER timerCheckZone Timer for check zones. +-- @field Core.Timer#TIMER timerQueueUpdate Timer for queue updates. +-- @field #boolean groupinitialized If true, group parameters were initialized. +-- @field #boolean detectionOn If true, detected units of the group are analyzed. +-- @field Ops.Auftrag#AUFTRAG missionpaused Paused mission. +-- @field #number Ndestroyed Number of destroyed units. +-- @field #number Nkills Number kills of this groups. +-- +-- @field Core.Point#COORDINATE coordinate Current coordinate. +-- +-- @field DCS#Vec3 position Position of the group at last status check. +-- @field DCS#Vec3 positionLast Backup of last position vec to monitor changes. +-- @field #number heading Heading of the group at last status check. +-- @field #number headingLast Backup of last heading to monitor changes. +-- @field DCS#Vec3 orientX Orientation at last status check. +-- @field DCS#Vec3 orientXLast Backup of last orientation to monitor changes. +-- @field #number traveldist Distance traveled in meters. This is a lower bound. +-- @field #number traveltime Time. +-- +-- @field Core.Astar#ASTAR Astar path finding. +-- @field #boolean ispathfinding If true, group is on pathfinding route. +-- +-- @field #OPSGROUP.Radio radio Current radio settings. +-- @field #OPSGROUP.Radio radioDefault Default radio settings. +-- @field Core.RadioQueue#RADIOQUEUE radioQueue Radio queue. +-- +-- @field #OPSGROUP.Beacon tacan Current TACAN settings. +-- @field #OPSGROUP.Beacon tacanDefault Default TACAN settings. +-- +-- @field #OPSGROUP.Beacon icls Current ICLS settings. +-- @field #OPSGROUP.Beacon iclsDefault Default ICLS settings. +-- +-- @field #OPSGROUP.Option option Current optional settings. +-- @field #OPSGROUP.Option optionDefault Default option settings. +-- +-- @field #OPSGROUP.Callsign callsign Current callsign settings. +-- @field #OPSGROUP.Callsign callsignDefault Default callsign settings. +-- +-- @field #OPSGROUP.Spot spot Laser and IR spot. +-- +-- @field #OPSGROUP.Ammo ammo Initial ammount of ammo. +-- @field #OPSGROUP.WeaponData weaponData Weapon data table with key=BitType. +-- +-- @extends Core.Fsm#FSM + +--- *A small group of determined and like-minded people can change the course of history.* --- Mahatma Gandhi +-- +-- === +-- +-- ![Banner Image](..\Presentations\OPS\OpsGroup\_Main.png) +-- +-- # The OPSGROUP Concept +-- +-- The OPSGROUP class contains common functions used by other classes such as FLIGHGROUP, NAVYGROUP and ARMYGROUP. +-- Those classes inherit everything of this class and extend it with features specific to their unit category. +-- +-- This class is **NOT** meant to be used by the end user itself. +-- +-- +-- @field #OPSGROUP +OPSGROUP = { + ClassName = "OPSGROUP", + Debug = false, + verbose = 0, + lid = nil, + groupname = nil, + group = nil, + template = nil, + isLateActivated = nil, + waypoints = nil, + waypoints0 = nil, + currentwp = 1, + elements = {}, + taskqueue = {}, + taskcounter = nil, + taskcurrent = nil, + taskenroute = nil, + taskpaused = {}, + missionqueue = {}, + currentmission = nil, + detectedunits = {}, + detectedgroups = {}, + attribute = nil, + checkzones = nil, + inzones = nil, + groupinitialized = nil, + respawning = nil, + wpcounter = 1, + radio = {}, + option = {}, + optionDefault = {}, + tacan = {}, + icls = {}, + callsign = {}, + Ndestroyed = 0, + Nkills = 0, + weaponData = {}, +} + + +--- OPS group element. +-- @type OPSGROUP.Element +-- @field #string name Name of the element, i.e. the unit. +-- @field Wrapper.Unit#UNIT unit The UNIT object. +-- @field #string status The element status. +-- @field #string typename Type name. +-- @field #number length Length of element in meters. +-- @field #number width Width of element in meters. +-- @field #number height Height of element in meters. +-- @field #number life0 Initial life points. +-- @field #number life Life points when last updated. + +--- Status of group element. +-- @type OPSGROUP.ElementStatus +-- @field #string INUTERO Element was not spawned yet or its status is unknown so far. +-- @field #string SPAWNED Element was spawned into the world. +-- @field #string PARKING Element is parking after spawned on ramp. +-- @field #string ENGINEON Element started its engines. +-- @field #string TAXIING Element is taxiing after engine startup. +-- @field #string TAKEOFF Element took of after takeoff event. +-- @field #string AIRBORNE Element is airborne. Either after takeoff or after air start. +-- @field #string LANDING Element is landing. +-- @field #string LANDED Element landed and is taxiing to its parking spot. +-- @field #string ARRIVED Element arrived at its parking spot and shut down its engines. +-- @field #string DEAD Element is dead after it crashed, pilot ejected or pilot dead events. +OPSGROUP.ElementStatus={ + INUTERO="inutero", + SPAWNED="spawned", + PARKING="parking", + ENGINEON="engineon", + TAXIING="taxiing", + TAKEOFF="takeoff", + AIRBORNE="airborne", + LANDING="landing", + LANDED="landed", + ARRIVED="arrived", + DEAD="dead", +} + +--- Ops group task status. +-- @type OPSGROUP.TaskStatus +-- @field #string SCHEDULED Task is scheduled. +-- @field #string EXECUTING Task is being executed. +-- @field #string PAUSED Task is paused. +-- @field #string DONE Task is done. +OPSGROUP.TaskStatus={ + SCHEDULED="scheduled", + EXECUTING="executing", + PAUSED="paused", + DONE="done", +} + +--- Ops group task status. +-- @type OPSGROUP.TaskType +-- @field #string SCHEDULED Task is scheduled and will be executed at a given time. +-- @field #string WAYPOINT Task is executed at a specific waypoint. +OPSGROUP.TaskType={ + SCHEDULED="scheduled", + WAYPOINT="waypoint", +} + +--- Task structure. +-- @type OPSGROUP.Task +-- @field #string type Type of task: either SCHEDULED or WAYPOINT. +-- @field #number id Task ID. Running number to get the task. +-- @field #number prio Priority. +-- @field #number time Abs. mission time when to execute the task. +-- @field #table dcstask DCS task structure. +-- @field #string description Brief text which describes the task. +-- @field #string status Task status. +-- @field #number duration Duration before task is cancelled in seconds. Default never. +-- @field #number timestamp Abs. mission time, when task was started. +-- @field #number waypoint Waypoint index if task is a waypoint task. +-- @field Core.UserFlag#USERFLAG stopflag If flag is set to 1 (=true), the task is stopped. +-- @field #number backupROE Rules of engagement that are restored once the task is over. + +--- Enroute task. +-- @type OPSGROUP.EnrouteTask +-- @field DCS#Task DCStask DCS task structure table. +-- @field #number WaypointIndex Waypoint number at which the enroute task is added. + +--- Beacon data. +-- @type OPSGROUP.Beacon +-- @field #number Channel Channel. +-- @field #number Morse Morse Code. +-- @field #string Band Band "X" or "Y" for TACAN beacon. +-- @field #string BeaconName Name of the unit acting as beacon. +-- @field Wrapper.Unit#UNIT BeaconUnit Unit object acting as beacon. +-- @field #boolean On If true, beacon is on, if false, beacon is turned off. If nil, has not been used yet. + +--- Radio data. +-- @type OPSGROUP.Radio +-- @field #number Freq Frequency +-- @field #number Modu Modulation. +-- @field #boolean On If true, radio is on, if false, radio is turned off. If nil, has not been used yet. + +--- Callsign data. +-- @type OPSGROUP.Callsign +-- @field #number NumberSquad Squadron number corresponding to a name like "Uzi". +-- @field #number NumberGroup Group number. First number after name, e.g. "Uzi-**1**-1". +-- @field #number NumberElement Element number.Second number after name, e.g. "Uzi-1-**1**" +-- @field #string NameSquad Name of the squad, e.g. "Uzi". +-- @field #string NameElement Name of group element, e.g. Uzi 11. + +--- Option data. +-- @type OPSGROUP.Option +-- @field #number ROE Rule of engagement. +-- @field #number ROT Reaction on threat. +-- @field #number Alarm Alarm state. +-- @field #number Formation Formation. +-- @field #boolean EPLRS data link. +-- @field #boolean Disperse Disperse under fire. + +--- Weapon range data. +-- @type OPSGROUP.WeaponData +-- @field #number BitType Type of weapon. +-- @field #number RangeMin Min range in meters. +-- @field #number RangeMax Max range in meters. +-- @field #number ReloadTime Time to reload in seconds. + +--- Laser and IR spot data. +-- @type OPSGROUP.Spot +-- @field #boolean CheckLOS If true, check LOS to target. +-- @field #boolean IRon If true, turn IR pointer on. +-- @field #number dt Update time interval in seconds. +-- @field DCS#Spot Laser Laser spot. +-- @field DCS#Spot IR Infra-red spot. +-- @field #number Code Laser code. +-- @field Wrapper.Group#GROUP TargetGroup The target group. +-- @field Wrapper.Positionable#POSITIONABLE TargetUnit The current target unit. +-- @field Core.Point#COORDINATE Coordinate where the spot is pointing. +-- @field #number TargetType Type of target: 0=coordinate, 1=static, 2=unit, 3=group. +-- @field #boolean On If true, the laser is on. +-- @field #boolean Paused If true, laser is paused. +-- @field #boolean lostLOS If true, laser lost LOS. +-- @field #OPSGROUP.Element element The element of the group that is lasing. +-- @field DCS#Vec3 vec3 The 3D positon vector of the laser (and IR) spot. +-- @field DCS#Vec3 offset Local offset of the laser source. +-- @field DCS#Vec3 offsetTarget Offset of the target. +-- @field Core.Timer#TIMER timer Spot timer. + +--- Ammo data. +-- @type OPSGROUP.Ammo +-- @field #number Total Total amount of ammo. +-- @field #number Guns Amount of gun shells. +-- @field #number Bombs Amount of bombs. +-- @field #number Rockets Amount of rockets. +-- @field #number Torpedos Amount of torpedos. +-- @field #number Missiles Amount of missiles. +-- @field #number MissilesAA Amount of air-to-air missiles. +-- @field #number MissilesAG Amount of air-to-ground missiles. +-- @field #number MissilesAS Amount of anti-ship missiles. +-- @field #number MissilesCR Amount of cruise missiles. +-- @field #number MissilesBM Amount of ballistic missiles. + +--- Waypoint data. +-- @type OPSGROUP.Waypoint +-- @field #number uid Waypoint's unit id, which is a running number. +-- @field #number speed Speed in m/s. +-- @field #number alt Altitude in meters. For submaries use negative sign for depth. +-- @field #string action Waypoint action (turning point, etc.). Ground groups have the formation here. +-- @field #table task Waypoint DCS task combo. +-- @field #string type Waypoint type. +-- @field #string name Waypoint description. Shown in the F10 map. +-- @field #number x Waypoint x-coordinate. +-- @field #number y Waypoint y-coordinate. +-- @field #boolean detour If true, this waypoint is not part of the normal route. +-- @field #boolean intowind If true, this waypoint is a turn into wind route point. +-- @field #boolean astar If true, this waypint was found by A* pathfinding algorithm. +-- @field #number npassed Number of times a groups passed this waypoint. +-- @field Core.Point#COORDINATE coordinate Waypoint coordinate. +-- @field Core.Point#COORDINATE roadcoord Closest point to road. +-- @field #number roaddist Distance to closest point on road. +-- @field Wrapper.Marker#MARKER marker Marker on the F10 map. +-- @field #string formation Ground formation. Similar to action but on/off road. + +--- NavyGroup version. +-- @field #string version +OPSGROUP.version="0.7.1" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: AI on/off. +-- TODO: Invisible/immortal. +-- TODO: F10 menu. +-- TODO: Add pseudo function. +-- TODO: EPLRS datalink. +-- TODO: Emission on/off. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new OPSGROUP class object. +-- @param #OPSGROUP self +-- @param Wrapper.Group#GROUP Group The group object. Can also be given by its group name as `#string`. +-- @return #OPSGROUP self +function OPSGROUP:New(Group) + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #OPSGROUP + + -- Get group and group name. + if type(Group)=="string" then + self.groupname=Group + self.group=GROUP:FindByName(self.groupname) + else + self.group=Group + self.groupname=Group:GetName() + end + + -- Set some string id for output to DCS.log file. + self.lid=string.format("OPSGROUP %s | ", tostring(self.groupname)) + + if self.group then + if not self:IsExist() then + self:E(self.lid.."ERROR: GROUP does not exist! Returning nil") + return nil + end + end + + -- Init set of detected units. + self.detectedunits=SET_UNIT:New() + + -- Init set of detected groups. + self.detectedgroups=SET_GROUP:New() + + -- Init inzone set. + self.inzones=SET_ZONE:New() + + -- Laser. + self.spot={} + self.spot.On=false + self.spot.timer=TIMER:New(self._UpdateLaser, self) + self.spot.Coordinate=COORDINATE:New(0, 0, 0) + self:SetLaser(1688, true, false, 0.5) + + -- Init task counter. + self.taskcurrent=0 + self.taskcounter=0 + + -- Start state. + self:SetStartState("InUtero") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("InUtero", "Spawned", "Spawned") -- The whole group was spawned. + self:AddTransition("*", "Dead", "Dead") -- The whole group is dead. + self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. + + self:AddTransition("*", "Status", "*") -- Status update. + + self:AddTransition("*", "Destroyed", "*") -- The whole group is dead. + self:AddTransition("*", "Damaged", "*") -- Someone in the group took damage. + + self:AddTransition("*", "UpdateRoute", "*") -- Update route of group. Only if airborne. + self:AddTransition("*", "Respawn", "*") -- Respawn group. + self:AddTransition("*", "PassingWaypoint", "*") -- Passing waypoint. + + self:AddTransition("*", "DetectedUnit", "*") -- Unit was detected (again) in this detection cycle. + self:AddTransition("*", "DetectedUnitNew", "*") -- Add a newly detected unit to the detected units set. + self:AddTransition("*", "DetectedUnitKnown", "*") -- A known unit is still detected. + self:AddTransition("*", "DetectedUnitLost", "*") -- Group lost a detected target. + + self:AddTransition("*", "DetectedGroup", "*") -- Unit was detected (again) in this detection cycle. + self:AddTransition("*", "DetectedGroupNew", "*") -- Add a newly detected unit to the detected units set. + self:AddTransition("*", "DetectedGroupKnown", "*") -- A known unit is still detected. + self:AddTransition("*", "DetectedGroupLost", "*") -- Group lost a detected target group. + + self:AddTransition("*", "PassingWaypoint", "*") -- Group passed a waypoint. + self:AddTransition("*", "GotoWaypoint", "*") -- Group switches to a specific waypoint. + + self:AddTransition("*", "OutOfAmmo", "*") -- Group is completely out of ammo. + self:AddTransition("*", "OutOfGuns", "*") -- Group is out of gun shells. + self:AddTransition("*", "OutOfRockets", "*") -- Group is out of rockets. + self:AddTransition("*", "OutOfBombs", "*") -- Group is out of bombs. + self:AddTransition("*", "OutOfMissiles", "*") -- Group is out of missiles. + + self:AddTransition("*", "EnterZone", "*") -- Group entered a certain zone. + self:AddTransition("*", "LeaveZone", "*") -- Group leaves a certain zone. + + self:AddTransition("*", "LaserOn", "*") -- Turn laser on. + self:AddTransition("*", "LaserOff", "*") -- Turn laser off. + self:AddTransition("*", "LaserCode", "*") -- Switch laser code. + self:AddTransition("*", "LaserPause", "*") -- Turn laser off temporarily. + self:AddTransition("*", "LaserResume", "*") -- Turn laser back on again if it was paused. + self:AddTransition("*", "LaserLostLOS", "*") -- Lasing element lost line of sight. + self:AddTransition("*", "LaserGotLOS", "*") -- Lasing element got line of sight. + + self:AddTransition("*", "TaskExecute", "*") -- Group will execute a task. + self:AddTransition("*", "TaskPause", "*") -- Pause current task. Not implemented yet! + self:AddTransition("*", "TaskCancel", "*") -- Cancel current task. + self:AddTransition("*", "TaskDone", "*") -- Task is over. + + self:AddTransition("*", "MissionStart", "*") -- Mission is started. + self:AddTransition("*", "MissionExecute", "*") -- Mission execution began. + self:AddTransition("*", "MissionCancel", "*") -- Cancel current mission. + self:AddTransition("*", "PauseMission", "*") -- Pause the current mission. + self:AddTransition("*", "UnpauseMission", "*") -- Unpause the the paused mission. + self:AddTransition("*", "MissionDone", "*") -- Mission is over. + + self:AddTransition("*", "ElementSpawned", "*") -- An element was spawned. + self:AddTransition("*", "ElementDestroyed", "*") -- An element was destroyed. + self:AddTransition("*", "ElementDead", "*") -- An element is dead. + self:AddTransition("*", "ElementDamaged", "*") -- An element was damaged. + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Stop". Stops the OPSGROUP and all its event handlers. + -- @param #OPSGROUP self + + --- Triggers the FSM event "Stop" after a delay. Stops the OPSGROUP and all its event handlers. + -- @function [parent=#OPSGROUP] __Stop + -- @param #OPSGROUP self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Status". + -- @function [parent=#OPSGROUP] Status + -- @param #OPSGROUP self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#OPSGROUP] __Status + -- @param #OPSGROUP self + -- @param #number delay Delay in seconds. + + -- TODO: Add pseudo functions. + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get coalition. +-- @param #OPSGROUP self +-- @return #number Coalition side of carrier. +function OPSGROUP:GetCoalition() + return self.group:GetCoalition() +end + +--- Returns the absolute (average) life points of the group. +-- @param #OPSGROUP self +-- @return #number Life points. If group contains more than one element, the average is given. +-- @return #number Initial life points. +function OPSGROUP:GetLifePoints() + if self.group then + return self.group:GetLife(), self.group:GetLife0() + end +end + + +--- Set verbosity level. +-- @param #OPSGROUP self +-- @param #number VerbosityLevel Level of output (higher=more). Default 0. +-- @return #OPSGROUP self +function OPSGROUP:SetVerbosity(VerbosityLevel) + self.verbose=VerbosityLevel or 0 + return self +end + +--- Set default cruise speed. +-- @param #OPSGROUP self +-- @param #number Speed Speed in knots. +-- @return #OPSGROUP self +function OPSGROUP:SetDefaultSpeed(Speed) + if Speed then + self.speedCruise=UTILS.KnotsToKmph(Speed) + end + return self +end + +--- Get default cruise speed. +-- @param #OPSGROUP self +-- @return #number Cruise speed (>0) in knots. +function OPSGROUP:GetSpeedCruise() + return UTILS.KmphToKnots(self.speedCruise or self.speedMax*0.7) +end + +--- Set detection on or off. +-- If detection is on, detected targets of the group will be evaluated and FSM events triggered. +-- @param #OPSGROUP self +-- @param #boolean Switch If `true`, detection is on. If `false` or `nil`, detection is off. Default is off. +-- @return #OPSGROUP self +function OPSGROUP:SetDetection(Switch) + self.detectionOn=Switch + return self +end + +--- Set LASER parameters. +-- @param #OPSGROUP self +-- @param #number Code Laser code. Default 1688. +-- @param #boolean CheckLOS Check if lasing unit has line of sight to target coordinate. Default is `true`. +-- @param #boolean IROff If true, then dont switch on the additional IR pointer. +-- @param #number UpdateTime Time interval in seconds the beam gets up for moving targets. Default every 0.5 sec. +-- @return #OPSGROUP self +function OPSGROUP:SetLaser(Code, CheckLOS, IROff, UpdateTime) + self.spot.Code=Code or 1688 + if CheckLOS~=nil then + self.spot.CheckLOS=CheckLOS + else + self.spot.CheckLOS=true + end + self.spot.IRon=not IROff + self.spot.dt=UpdateTime or 0.5 + return self +end + +--- Get LASER code. +-- @param #OPSGROUP self +-- @return #number Current Laser code. +function OPSGROUP:GetLaserCode() + return self.spot.Code +end + +--- Get current LASER coordinate, i.e. where the beam is pointing at if the LASER is on. +-- @param #OPSGROUP self +-- @return Core.Point#COORDINATE Current position where the LASER is pointing at. +function OPSGROUP:GetLaserCoordinate() + return self.spot.Coordinate +end + +--- Get current target of the LASER. This can be a STATIC or UNIT object. +-- @param #OPSGROUP self +-- @return Wrapper.Positionable#POSITIONABLE Current target object. +function OPSGROUP:GetLaserTarget() + return self.spot.TargetUnit +end + +--- Define a SET of zones that trigger and event if the group enters or leaves any of the zones. +-- @param #OPSGROUP self +-- @param Core.Set#SET_ZONE CheckZonesSet Set of zones. +-- @return #OPSGROUP self +function OPSGROUP:SetCheckZones(CheckZonesSet) + self.checkzones=CheckZonesSet + return self +end + +--- Add a zone that triggers and event if the group enters or leaves any of the zones. +-- @param #OPSGROUP self +-- @param Core.Zone#ZONE CheckZone Zone to check. +-- @return #OPSGROUP self +function OPSGROUP:AddCheckZone(CheckZone) + if not self.checkzones then + self.checkzones=SET_ZONE:New() + end + self.checkzones:AddZone(CheckZone) + return self +end + + +--- Add a weapon range for ARTY auftrag. +-- @param #OPSGROUP self +-- @param #number RangeMin Minimum range in nautical miles. Default 0 NM. +-- @param #number RangeMax Maximum range in nautical miles. Default 10 NM. +-- @param #number BitType Bit mask of weapon type for which the given min/max ranges apply. Default is `ENUMS.WeaponFlag.Auto`, i.e. for all weapon types. +-- @return #OPSGROUP self +function OPSGROUP:AddWeaponRange(RangeMin, RangeMax, BitType) + + RangeMin=UTILS.NMToMeters(RangeMin or 0) + RangeMax=UTILS.NMToMeters(RangeMax or 10) + + local weapon={} --#OPSGROUP.WeaponData + + weapon.BitType=BitType or ENUMS.WeaponFlag.Auto + weapon.RangeMax=RangeMax + weapon.RangeMin=RangeMin + + self.weaponData=self.weaponData or {} + self.weaponData[weapon.BitType]=weapon + + return self +end + +--- Get weapon data. +-- @param #OPSGROUP self +-- @param #number BitType Type of weapon. +-- @return #OPSGROUP.WeaponData Weapon range data. +function OPSGROUP:GetWeaponData(BitType) + + BitType=BitType or ENUMS.WeaponFlag.Auto + + if self.weaponData[BitType] then + return self.weaponData[BitType] + else + return self.weaponData[ENUMS.WeaponFlag.Auto] + end + +end + +--- Get set of detected units. +-- @param #OPSGROUP self +-- @return Core.Set#SET_UNIT Set of detected units. +function OPSGROUP:GetDetectedUnits() + return self.detectedunits or {} +end + +--- Get set of detected groups. +-- @param #OPSGROUP self +-- @return Core.Set#SET_GROUP Set of detected groups. +function OPSGROUP:GetDetectedGroups() + return self.detectedgroups or {} +end + +--- Get inital amount of ammunition. +-- @param #OPSGROUP self +-- @return #OPSGROUP.Ammo Initial ammo table. +function OPSGROUP:GetAmmo0() + return self.ammo +end + +--- Get highest detected threat. Detection must be turned on. The threat level is a number between 0 and 10, where 0 is the lowest, e.g. unarmed units. +-- @param #OPSGROUP self +-- @param #number ThreatLevelMin Only consider threats with level greater or equal to this number. Default 1 (so unarmed units wont be considered). +-- @param #number ThreatLevelMax Only consider threats with level smaller or queal to this number. Default 10. +-- @return Wrapper.Unit#UNIT Highest threat unit detected by the group or `nil` if no threat is currently detected. +-- @return #number Threat level. +function OPSGROUP:GetThreat(ThreatLevelMin, ThreatLevelMax) + + ThreatLevelMin=ThreatLevelMin or 1 + ThreatLevelMax=ThreatLevelMax or 10 + + local threat=nil --Wrapper.Unit#UNIT + local level=0 + for _,_unit in pairs(self.detectedunits:GetSet()) do + local unit=_unit --Wrapper.Unit#UNIT + + -- Get threatlevel of unit. + local threatlevel=unit:GetThreatLevel() + + -- Check if withing threasholds. + if threatlevel>=ThreatLevelMin and threatlevel<=ThreatLevelMax then + + if threatlevellevelmax then + threat=unit + levelmax=threatlevel + end + + end + + return threat, levelmax +end + +--- Check if an element of the group has line of sight to a coordinate. +-- @param #OPSGROUP self +-- @param Core.Point#COORDINATE Coordinate The position to which we check the LoS. +-- @param #OPSGROUP.Element Element The (optinal) element. If not given, all elements are checked. +-- @param DCS#Vec3 OffsetElement Offset vector of the element. +-- @param DCS#Vec3 OffsetCoordinate Offset vector of the coordinate. +-- @return #boolean If `true`, there is line of sight to the specified coordinate. +function OPSGROUP:HasLoS(Coordinate, Element, OffsetElement, OffsetCoordinate) + + -- Target vector. + local Vec3=Coordinate:GetVec3() + + -- Optional offset. + if OffsetCoordinate then + Vec3=UTILS.VecAdd(Vec3, OffsetCoordinate) + end + + --- Function to check LoS for an element of the group. + local function checklos(element) + local vec3=element.unit:GetVec3() + if OffsetElement then + vec3=UTILS.VecAdd(vec3, OffsetElement) + end + local _los=land.isVisible(vec3, Vec3) + --self:I({los=_los, source=vec3, target=Vec3}) + return _los + end + + if Element then + local los=checklos(Element) + return los + else + + for _,element in pairs(self.elements) do + -- Get LoS of this element. + local los=checklos(element) + if los then + return true + end + end + + return false + end + + return nil +end + +--- Get MOOSE GROUP object. +-- @param #OPSGROUP self +-- @return Wrapper.Group#GROUP Moose group object. +function OPSGROUP:GetGroup() + return self.group +end + +--- Get the group name. +-- @param #OPSGROUP self +-- @return #string Group name. +function OPSGROUP:GetName() + return self.groupname +end + +--- Get DCS GROUP object. +-- @param #OPSGROUP self +-- @return DCS#Group DCS group object. +function OPSGROUP:GetDCSGroup() + local DCSGroup=Group.getByName(self.groupname) + return DCSGroup +end + +--- Get MOOSE UNIT object. +-- @param #OPSGROUP self +-- @param #number UnitNumber Number of the unit in the group. Default first unit. +-- @return Wrapper.Unit#UNIT The MOOSE UNIT object. +function OPSGROUP:GetUnit(UnitNumber) + + local DCSUnit=self:GetDCSUnit(UnitNumber) + + if DCSUnit then + local unit=UNIT:Find(DCSUnit) + return unit + end + + return nil +end + +--- Get DCS GROUP object. +-- @param #OPSGROUP self +-- @param #number UnitNumber Number of the unit in the group. Default first unit. +-- @return DCS#Unit DCS group object. +function OPSGROUP:GetDCSUnit(UnitNumber) + + local DCSGroup=self:GetDCSGroup() + + if DCSGroup then + local unit=DCSGroup:getUnit(UnitNumber or 1) + return unit + end + + return nil +end + +--- Get DCS units. +-- @param #OPSGROUP self +-- @return #list DCS units. +function OPSGROUP:GetDCSUnits() + + local DCSGroup=self:GetDCSGroup() + + if DCSGroup then + local units=DCSGroup:getUnits() + return units + end + + return nil +end + +--- Despawn the group. The whole group is despawned and (optionally) a "Remove Unit" event is generated for all current units of the group. +-- @param #OPSGROUP self +-- @param #number Delay Delay in seconds before the group will be despawned. Default immediately. +-- @param #boolean NoEventRemoveUnit If true, no event "Remove Unit" is generated. +-- @return #OPSGROUP self +function OPSGROUP:Despawn(Delay, NoEventRemoveUnit) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, OPSGROUP.Despawn, self, 0, NoEventRemoveUnit) + else + + local DCSGroup=self:GetDCSGroup() + + if DCSGroup then + + -- Destroy DCS group. + DCSGroup:destroy() + + if not NoEventRemoveUnit then + + -- Get all units. + local units=self:GetDCSUnits() + + -- Create a "Remove Unit" event. + local EventTime=timer.getTime() + for i=1,#units do + self:CreateEventRemoveUnit(EventTime, units[i]) + end + + end + end + end + + return self +end + +--- Destroy group. The whole group is despawned and a *Unit Lost* for aircraft or *Dead* event for ground/naval units is generated for all current units. +-- @param #OPSGROUP self +-- @param #number Delay Delay in seconds before the group will be destroyed. Default immediately. +-- @return #OPSGROUP self +function OPSGROUP:Destroy(Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, OPSGROUP.Destroy, self) + else + + local DCSGroup=self:GetDCSGroup() + + if DCSGroup then + + self:T(self.lid.."Destroying group") + + -- Destroy DCS group. + DCSGroup:destroy() + + -- Get all units. + local units=self:GetDCSUnits() + + -- Create a "Unit Lost" event. + local EventTime=timer.getTime() + for i=1,#units do + if self.isAircraft then + self:CreateEventUnitLost(EventTime, units[i]) + else + self:CreateEventDead(EventTime, units[i]) + end + end + end + + end + + return self +end + +--- Despawn an element/unit of the group. +-- @param #OPSGROUP self +-- @param #OPSGROUP.Element Element The element that will be despawned. +-- @param #number Delay Delay in seconds before the element will be despawned. Default immediately. +-- @param #boolean NoEventRemoveUnit If true, no event "Remove Unit" is generated. +-- @return #OPSGROUP self +function OPSGROUP:DespawnElement(Element, Delay, NoEventRemoveUnit) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, OPSGROUP.DespawnElement, self, Element, 0, NoEventRemoveUnit) + else + + if Element then + + -- Get DCS unit object. + local DCSunit=Unit.getByName(Element.name) + + if DCSunit then + + -- Destroy object. + DCSunit:destroy() + + -- Create a remove unit event. + if not NoEventRemoveUnit then + self:CreateEventRemoveUnit(timer.getTime(), DCSunit) + end + + end + + end + + end + + return self +end + +--- Get current 2D position vector of the group. +-- @param #OPSGROUP self +-- @return DCS#Vec2 Vector with x,y components. +function OPSGROUP:GetVec2() + + local vec3=self:GetVec3() + + if vec3 then + local vec2={x=vec3.x, y=vec3.z} + return vec2 + end + + return nil +end + + +--- Get current 3D position vector of the group. +-- @param #OPSGROUP self +-- @return DCS#Vec3 Vector with x,y,z components. +function OPSGROUP:GetVec3() + if self:IsExist() then + + local unit=self:GetDCSUnit() + + if unit then + local vec3=unit:getPoint() + + return vec3 + end + + end + return nil +end + +--- Get current coordinate of the group. +-- @param #OPSGROUP self +-- @param #boolean NewObject Create a new coordiante object. +-- @return Core.Point#COORDINATE The coordinate (of the first unit) of the group. +function OPSGROUP:GetCoordinate(NewObject) + + local vec3=self:GetVec3() + + if vec3 then + + self.coordinate=self.coordinate or COORDINATE:New(0,0,0) + + self.coordinate.x=vec3.x + self.coordinate.y=vec3.y + self.coordinate.z=vec3.z + + if NewObject then + local coord=COORDINATE:NewFromCoordinate(self.coordinate) + return coord + else + return self.coordinate + end + else + self:E(self.lid.."WARNING: Group is not alive. Cannot get coordinate!") + end + + return nil +end + +--- Get current velocity of the group. +-- @param #OPSGROUP self +-- @return #number Velocity in m/s. +function OPSGROUP:GetVelocity() + if self:IsExist() then + + local unit=self:GetDCSUnit(1) + + if unit then + + local velvec3=unit:getVelocity() + + local vel=UTILS.VecNorm(velvec3) + + return vel + + end + else + self:E(self.lid.."WARNING: Group does not exist. Cannot get velocity!") + end + return nil +end + +--- Get current heading of the group. +-- @param #OPSGROUP self +-- @return #number Current heading of the group in degrees. +function OPSGROUP:GetHeading() + + if self:IsExist() then + + local unit=self:GetDCSUnit() + + if unit then + + local pos=unit:getPosition() + + local heading=math.atan2(pos.x.z, pos.x.x) + + if heading<0 then + heading=heading+ 2*math.pi + end + + heading=math.deg(heading) + + return heading + end + + else + self:E(self.lid.."WARNING: Group does not exist. Cannot get heading!") + end + + return nil +end + +--- Get current orientation of the first unit in the group. +-- @param #OPSGROUP self +-- @return DCS#Vec3 Orientation X parallel to where the "nose" is pointing. +-- @return DCS#Vec3 Orientation Y pointing "upwards". +-- @return DCS#Vec3 Orientation Z perpendicular to the "nose". +function OPSGROUP:GetOrientation() + + if self:IsExist() then + + local unit=self:GetDCSUnit() + + if unit then + + local pos=unit:getPosition() + + return pos.x, pos.y, pos.z + end + + else + self:E(self.lid.."WARNING: Group does not exist. Cannot get orientation!") + end + + return nil +end + +--- Get current orientation of the first unit in the group. +-- @param #OPSGROUP self +-- @return DCS#Vec3 Orientation X parallel to where the "nose" is pointing. +function OPSGROUP:GetOrientationX() + + local X,Y,Z=self:GetOrientation() + + return X +end + + + +--- Check if task description is unique. +-- @param #OPSGROUP self +-- @param #string description Task destription +-- @return #boolean If true, no other task has the same description. +function OPSGROUP:CheckTaskDescriptionUnique(description) + + -- Loop over tasks in queue + for _,_task in pairs(self.taskqueue) do + local task=_task --#OPSGROUP.Task + if task.description==description then + return false + end + end + + return true +end + + +--- Activate a *late activated* group. +-- @param #OPSGROUP self +-- @param #number delay (Optional) Delay in seconds before the group is activated. Default is immediately. +-- @return #OPSGROUP self +function OPSGROUP:Activate(delay) + + if delay and delay>0 then + self:T2(self.lid..string.format("Activating late activated group in %d seconds", delay)) + self:ScheduleOnce(delay, OPSGROUP.Activate, self) + else + + if self:IsAlive()==false then + + self:T(self.lid.."Activating late activated group") + self.group:Activate() + self.isLateActivated=false + + elseif self:IsAlive()==true then + self:E(self.lid.."WARNING: Activating group that is already activated") + else + self:E(self.lid.."ERROR: Activating group that is does not exist!") + end + + end + + return self +end + +--- Self destruction of group. An explosion is created at the position of each element. +-- @param #OPSGROUP self +-- @param #number Delay Delay in seconds. Default now. +-- @param #number ExplosionPower (Optional) Explosion power in kg TNT. Default 500 kg. +-- @return #number Relative fuel in percent. +function OPSGROUP:SelfDestruction(Delay, ExplosionPower) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, OPSGROUP.SelfDestruction, self, 0, ExplosionPower) + else + + -- Loop over all elements. + for i,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + + local unit=element.unit + + if unit and unit:IsAlive() then + unit:Explode(ExplosionPower) + end + end + end + +end + + +--- Check if group is exists. +-- @param #OPSGROUP self +-- @return #boolean If true, the group exists or false if the group does not exist. If nil, the DCS group could not be found. +function OPSGROUP:IsExist() + + local DCSGroup=self:GetDCSGroup() + + if DCSGroup then + local exists=DCSGroup:isExist() + return exists + end + + return nil +end + +--- Check if group is activated. +-- @param #OPSGROUP self +-- @return #boolean If true, the group exists or false if the group does not exist. If nil, the DCS group could not be found. +function OPSGROUP:IsActive() + +end + +--- Check if group is alive. +-- @param #OPSGROUP self +-- @return #boolean *true* if group is exists and is activated, *false* if group is exist but is NOT activated. *nil* otherwise, e.g. the GROUP object is *nil* or the group is not spawned yet. +function OPSGROUP:IsAlive() + + if self.group then + local alive=self.group:IsAlive() + return alive + end + + return nil +end + +--- Check if this group is currently "late activated" and needs to be "activated" to appear in the mission. +-- @param #OPSGROUP self +-- @return #boolean Is this the group late activated? +function OPSGROUP:IsLateActivated() + return self.isLateActivated +end + +--- Check if group is in state in utero. +-- @param #OPSGROUP self +-- @return #boolean If true, group is not spawned yet. +function OPSGROUP:IsInUtero() + return self:Is("InUtero") +end + +--- Check if group is in state spawned. +-- @param #OPSGROUP self +-- @return #boolean If true, group is spawned. +function OPSGROUP:IsSpawned() + return self:Is("Spawned") +end + +--- Check if group is dead. +-- @param #OPSGROUP self +-- @return #boolean If true, all units/elements of the group are dead. +function OPSGROUP:IsDead() + return self:Is("Dead") +end + +--- Check if FSM is stopped. +-- @param #OPSGROUP self +-- @return #boolean If true, FSM state is stopped. +function OPSGROUP:IsStopped() + return self:Is("Stopped") +end + +--- Check if this group is currently "uncontrolled" and needs to be "started" to begin its route. +-- @param #OPSGROUP self +-- @return #boolean If true, this group uncontrolled. +function OPSGROUP:IsUncontrolled() + return self.isUncontrolled +end + +--- Check if this group has passed its final waypoint. +-- @param #OPSGROUP self +-- @return #boolean If true, this group has passed the final waypoint. +function OPSGROUP:HasPassedFinalWaypoint() + return self.passedfinalwp +end + +--- Check if the group is currently rearming. +-- @param #OPSGROUP self +-- @return #boolean If true, group is rearming. +function OPSGROUP:IsRearming() + local rearming=self:Is("Rearming") or self:Is("Rearm") + return rearming +end + +--- Check if the group has currently switched a LASER on. +-- @param #OPSGROUP self +-- @return #boolean If true, LASER of the group is on. +function OPSGROUP:IsLasing() + return self.spot.On +end + +--- Check if the group is currently retreating. +-- @param #OPSGROUP self +-- @return #boolean If true, group is retreating. +function OPSGROUP:IsRetreating() + return self:is("Retreating") +end + +--- Check if the group is engaging another unit or group. +-- @param #OPSGROUP self +-- @return #boolean If true, group is engaging. +function OPSGROUP:IsEngaging() + return self:is("Engaging") +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Waypoint Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get the waypoints. +-- @param #OPSGROUP self +-- @return #table Table of all waypoints. +function OPSGROUP:GetWaypoints() + return self.waypoints +end + +--- Mark waypoints on F10 map. +-- @param #OPSGROUP self +-- @param #number Duration Duration in seconds how long the waypoints are displayed before they are automatically removed. Default is that they are never removed. +-- @return #OPSGROUP self +function OPSGROUP:MarkWaypoints(Duration) + + for i,_waypoint in pairs(self.waypoints or {}) do + local waypoint=_waypoint --#OPSGROUP.Waypoint + + local text=string.format("Waypoint ID=%d of %s", waypoint.uid, self.groupname) + text=text..string.format("\nSpeed=%.1f kts, Alt=%d ft (%s)", UTILS.MpsToKnots(waypoint.speed), UTILS.MetersToFeet(waypoint.alt), "BARO") + + if waypoint.marker then + if waypoint.marker.text~=text then + waypoint.marker.text=text + end + + else + waypoint.marker=MARKER:New(waypoint.coordinate, text):ToCoalition(self:GetCoalition()) + end + end + + + if Duration then + self:RemoveWaypointMarkers(Duration) + end + + return self +end + +--- Remove waypoints markers on the F10 map. +-- @param #OPSGROUP self +-- @param #number Delay Delay in seconds before the markers are removed. Default is immediately. +-- @return #OPSGROUP self +function OPSGROUP:RemoveWaypointMarkers(Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, OPSGROUP.RemoveWaypointMarkers, self) + else + + for i,_waypoint in pairs(self.waypoints or {}) do + local waypoint=_waypoint --#OPSGROUP.Waypoint + + if waypoint.marker then + waypoint.marker:Remove() + end + end + + end + + return self +end + + +--- Get the waypoint from its unique ID. +-- @param #OPSGROUP self +-- @param #number uid Waypoint unique ID. +-- @return #OPSGROUP.Waypoint Waypoint data. +function OPSGROUP:GetWaypointByID(uid) + + for _,_waypoint in pairs(self.waypoints or {}) do + local waypoint=_waypoint --#OPSGROUP.Waypoint + if waypoint.uid==uid then + return waypoint + end + end + + return nil +end + +--- Get the waypoint from its index. +-- @param #OPSGROUP self +-- @param #number index Waypoint index. +-- @return #OPSGROUP.Waypoint Waypoint data. +function OPSGROUP:GetWaypointByIndex(index) + + for i,_waypoint in pairs(self.waypoints) do + local waypoint=_waypoint --#OPSGROUP.Waypoint + if i==index then + return waypoint + end + end + + return nil +end + +--- Get the waypoint UID from its index, i.e. its current position in the waypoints table. +-- @param #OPSGROUP self +-- @param #number index Waypoint index. +-- @return #number Unique waypoint ID. +function OPSGROUP:GetWaypointUIDFromIndex(index) + + for i,_waypoint in pairs(self.waypoints) do + local waypoint=_waypoint --#OPSGROUP.Waypoint + if i==index then + return waypoint.uid + end + end + + return nil +end + +--- Get the waypoint index (its position in the current waypoints table). +-- @param #OPSGROUP self +-- @param #number uid Waypoint unique ID. +-- @return #OPSGROUP.Waypoint Waypoint data. +function OPSGROUP:GetWaypointIndex(uid) + + if uid then + for i,_waypoint in pairs(self.waypoints or {}) do + local waypoint=_waypoint --#OPSGROUP.Waypoint + if waypoint.uid==uid then + return i + end + end + end + + return nil +end + +--- Get next waypoint index. +-- @param #OPSGROUP self +-- @param #boolean cyclic If true, return first waypoint if last waypoint was reached. Default is patrol ad infinitum value set. +-- @param #number i Waypoint index from which the next index is returned. Default is the last waypoint passed. +-- @return #number Next waypoint index. +function OPSGROUP:GetWaypointIndexNext(cyclic, i) + + if cyclic==nil then + cyclic=self.adinfinitum + end + + local N=#self.waypoints + + i=i or self.currentwp + + local n=math.min(i+1, N) + + if cyclic and i==N then + n=1 + end + + return n +end + +--- Get current waypoint index. This is the index of the last passed waypoint. +-- @param #OPSGROUP self +-- @return #number Current waypoint index. +function OPSGROUP:GetWaypointIndexCurrent() + return self.currentwp or 1 +end + +--- Get waypoint index after waypoint with given ID. So if the waypoint has index 3 it will return 4. +-- @param #OPSGROUP self +-- @param #number uid Unique ID of the waypoint. Default is new waypoint index after the last current one. +-- @return #number Index after waypoint with given ID. +function OPSGROUP:GetWaypointIndexAfterID(uid) + + local index=self:GetWaypointIndex(uid) + if index then + return index+1 + else + return #self.waypoints+1 + end + +end + +--- Get waypoint. +-- @param #OPSGROUP self +-- @param #number indx Waypoint index. +-- @return #OPSGROUP.Waypoint Waypoint table. +function OPSGROUP:GetWaypoint(indx) + return self.waypoints[indx] +end + +--- Get final waypoint. +-- @param #OPSGROUP self +-- @return #OPSGROUP.Waypoint Final waypoint table. +function OPSGROUP:GetWaypointFinal() + return self.waypoints[#self.waypoints] +end + +--- Get next waypoint. +-- @param #OPSGROUP self +-- @param #boolean cyclic If true, return first waypoint if last waypoint was reached. +-- @return #OPSGROUP.Waypoint Next waypoint table. +function OPSGROUP:GetWaypointNext(cyclic) + + local n=self:GetWaypointIndexNext(cyclic) + + return self.waypoints[n] +end + +--- Get current waypoint. +-- @param #OPSGROUP self +-- @return #OPSGROUP.Waypoint Current waypoint table. +function OPSGROUP:GetWaypointCurrent() + return self.waypoints[self.currentwp] +end + +--- Get coordinate of next waypoint of the group. +-- @param #OPSGROUP self +-- @param #boolean cyclic If true, return first waypoint if last waypoint was reached. +-- @return Core.Point#COORDINATE Coordinate of the next waypoint. +function OPSGROUP:GetNextWaypointCoordinate(cyclic) + + -- Get next waypoint + local waypoint=self:GetWaypointNext(cyclic) + + return waypoint.coordinate +end + +--- Get waypoint coordinates. +-- @param #OPSGROUP self +-- @param #number index Waypoint index. +-- @return Core.Point#COORDINATE Coordinate of the next waypoint. +function OPSGROUP:GetWaypointCoordinate(index) + local waypoint=self:GetWaypoint(index) + if waypoint then + return waypoint.coordinate + end + return nil +end + +--- Get waypoint speed. +-- @param #OPSGROUP self +-- @param #number indx Waypoint index. +-- @return #number Speed set at waypoint in knots. +function OPSGROUP:GetWaypointSpeed(indx) + + local waypoint=self:GetWaypoint(indx) + + if waypoint then + return UTILS.MpsToKnots(waypoint.speed) + end + + return nil +end + +--- Get unique ID of waypoint. +-- @param #OPSGROUP self +-- @param #OPSGROUP.Waypoint waypoint The waypoint data table. +-- @return #number Unique ID. +function OPSGROUP:GetWaypointUID(waypoint) + return waypoint.uid +end + +--- Get unique ID of waypoint given its index. +-- @param #OPSGROUP self +-- @param #number indx Waypoint index. +-- @return #number Unique ID. +function OPSGROUP:GetWaypointID(indx) + + local waypoint=self:GetWaypoint(indx) + + if waypoint then + return waypoint.uid + end + + return nil + +end + +--- Returns a non-zero speed to the next waypoint (even if the waypoint speed is zero). +-- @param #OPSGROUP self +-- @param #number indx Waypoint index. +-- @return #number Speed to next waypoint (>0) in knots. +function OPSGROUP:GetSpeedToWaypoint(indx) + + local speed=self:GetWaypointSpeed(indx) + + if speed<=0.1 then + speed=self:GetSpeedCruise() + end + + return speed +end + +--- Get distance to waypoint. +-- @param #OPSGROUP self +-- @param #number indx Waypoint index. Default is the next waypoint. +-- @return #number Distance in meters. +function OPSGROUP:GetDistanceToWaypoint(indx) + local dist=0 + + if #self.waypoints>0 then + + indx=indx or self:GetWaypointIndexNext() + + local wp=self:GetWaypoint(indx) + + if wp then + + local coord=self:GetCoordinate() + + dist=coord:Get2DDistance(wp.coordinate) + end + + end + + return dist +end + +--- Get time to waypoint based on current velocity. +-- @param #OPSGROUP self +-- @param #number indx Waypoint index. Default is the next waypoint. +-- @return #number Time in seconds. If velocity is 0 +function OPSGROUP:GetTimeToWaypoint(indx) + + local s=self:GetDistanceToWaypoint(indx) + + local v=self:GetVelocity() + + local t=s/v + + if t==math.inf then + return 365*24*60*60 + elseif t==math.nan then + return 0 + else + return t + end + +end + +--- Returns the currently expected speed. +-- @param #OPSGROUP self +-- @return #number Expected speed in m/s. +function OPSGROUP:GetExpectedSpeed() + + if self:IsHolding() then + return 0 + else + return self.speedWp or 0 + end + +end + +--- Remove a waypoint with a ceratin UID. +-- @param #OPSGROUP self +-- @param #number uid Waypoint UID. +-- @return #OPSGROUP self +function OPSGROUP:RemoveWaypointByID(uid) + + local index=self:GetWaypointIndex(uid) + + if index then + self:RemoveWaypoint(index) + end + + return self +end + +--- Remove a waypoint. +-- @param #OPSGROUP self +-- @param #number wpindex Waypoint number. +-- @return #OPSGROUP self +function OPSGROUP:RemoveWaypoint(wpindex) + + if self.waypoints then + + -- Number of waypoints before delete. + local N=#self.waypoints + + -- Remove waypoint marker. + local wp=self:GetWaypoint(wpindex) + if wp and wp.marker then + wp.marker:Remove() + end + + -- Remove waypoint. + table.remove(self.waypoints, wpindex) + + -- Number of waypoints after delete. + local n=#self.waypoints + + -- Debug info. + self:T(self.lid..string.format("Removing waypoint index %d, current wp index %d. N %d-->%d", wpindex, self.currentwp, N, n)) + + -- Waypoint was not reached yet. + if wpindex > self.currentwp then + + --- + -- Removed a FUTURE waypoint + --- + + -- TODO: patrol adinfinitum. + + if self.currentwp>=n then + self.passedfinalwp=true + end + + self:_CheckGroupDone(1) + + else + + --- + -- Removed a waypoint ALREADY PASSED + --- + + -- If an already passed waypoint was deleted, we do not need to update the route. + + -- If current wp = 1 it stays 1. Otherwise decrease current wp. + + if self.currentwp==1 then + + if self.adinfinitum then + self.currentwp=#self.waypoints + else + self.currentwp=1 + end + + else + self.currentwp=self.currentwp-1 + end + + end + + end + + return self +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Task Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set DCS task. Enroute tasks are injected automatically. +-- @param #OPSGROUP self +-- @param #table DCSTask DCS task structure. +-- @return #OPSGROUP self +function OPSGROUP:SetTask(DCSTask) + + if self:IsAlive() then + + if self.taskcurrent>0 then + + -- TODO: Why the hell did I do this? It breaks scheduled tasks. I comment it out for now to see where it fails. + --local task=self:GetTaskCurrent() + --self:RemoveTask(task) + --self.taskcurrent=0 + + end + + -- Inject enroute tasks. + if self.taskenroute and #self.taskenroute>0 then + if tostring(DCSTask.id)=="ComboTask" then + for _,task in pairs(self.taskenroute) do + table.insert(DCSTask.params.tasks, 1, task) + end + else + local tasks=UTILS.DeepCopy(self.taskenroute) + table.insert(tasks, DCSTask) + + DCSTask=self.group.TaskCombo(self, tasks) + end + end + + -- Set task. + self.group:SetTask(DCSTask) + + -- Debug info. + local text=string.format("SETTING Task %s", tostring(DCSTask.id)) + if tostring(DCSTask.id)=="ComboTask" then + for i,task in pairs(DCSTask.params.tasks) do + text=text..string.format("\n[%d] %s", i, tostring(task.id)) + end + end + self:T(self.lid..text) + end + + return self +end + +--- Push DCS task. +-- @param #OPSGROUP self +-- @param #table DCSTask DCS task structure. +-- @return #OPSGROUP self +function OPSGROUP:PushTask(DCSTask) + + if self:IsAlive() then + + -- Push task. + self.group:PushTask(DCSTask) + + -- Debug info. + local text=string.format("PUSHING Task %s", tostring(DCSTask.id)) + if tostring(DCSTask.id)=="ComboTask" then + for i,task in pairs(DCSTask.params.tasks) do + text=text..string.format("\n[%d] %s", i, tostring(task.id)) + end + end + self:T(self.lid..text) + end + + return self +end + +--- Clear DCS tasks. +-- @param #OPSGROUP self +-- @param #table DCSTask DCS task structure. +-- @return #OPSGROUP self +function OPSGROUP:ClearTasks() + if self:IsAlive() then + self.group:ClearTasks() + self:I(self.lid..string.format("CLEARING Tasks")) + end + return self +end + +--- Add a *scheduled* task. +-- @param #OPSGROUP self +-- @param #table task DCS task table structure. +-- @param #string clock Mission time when task is executed. Default in 5 seconds. If argument passed as #number, it defines a relative delay in seconds. +-- @param #string description Brief text describing the task, e.g. "Attack SAM". +-- @param #number prio Priority of the task. +-- @param #number duration Duration before task is cancelled in seconds counted after task started. Default never. +-- @return #OPSGROUP.Task The task structure. +function OPSGROUP:AddTask(task, clock, description, prio, duration) + + local newtask=self:NewTaskScheduled(task, clock, description, prio, duration) + + -- Add to table. + table.insert(self.taskqueue, newtask) + + -- Info. + self:T(self.lid..string.format("Adding SCHEDULED task %s starting at %s", newtask.description, UTILS.SecondsToClock(newtask.time, true))) + self:T3({newtask=newtask}) + + return newtask +end + +--- Create a *scheduled* task. +-- @param #OPSGROUP self +-- @param #table task DCS task table structure. +-- @param #string clock Mission time when task is executed. Default in 5 seconds. If argument passed as #number, it defines a relative delay in seconds. +-- @param #string description Brief text describing the task, e.g. "Attack SAM". +-- @param #number prio Priority of the task. +-- @param #number duration Duration before task is cancelled in seconds counted after task started. Default never. +-- @return #OPSGROUP.Task The task structure. +function OPSGROUP:NewTaskScheduled(task, clock, description, prio, duration) + + -- Increase counter. + self.taskcounter=self.taskcounter+1 + + -- Set time. + local time=timer.getAbsTime()+5 + if clock then + if type(clock)=="string" then + time=UTILS.ClockToSeconds(clock) + elseif type(clock)=="number" then + time=timer.getAbsTime()+clock + end + end + + -- Task data structure. + local newtask={} --#OPSGROUP.Task + newtask.status=OPSGROUP.TaskStatus.SCHEDULED + newtask.dcstask=task + newtask.description=description or task.id + newtask.prio=prio or 50 + newtask.time=time + newtask.id=self.taskcounter + newtask.duration=duration + newtask.waypoint=-1 + newtask.type=OPSGROUP.TaskType.SCHEDULED + newtask.stopflag=USERFLAG:New(string.format("%s StopTaskFlag %d", self.groupname, newtask.id)) + newtask.stopflag:Set(0) + + return newtask +end + +--- Add a *waypoint* task. +-- @param #OPSGROUP self +-- @param #table task DCS task table structure. +-- @param #OPSGROUP.Waypoint Waypoint where the task is executed. Default is the at *next* waypoint. +-- @param #string description Brief text describing the task, e.g. "Attack SAM". +-- @param #number prio Priority of the task. Number between 1 and 100. Default is 50. +-- @param #number duration Duration before task is cancelled in seconds counted after task started. Default never. +-- @return #OPSGROUP.Task The task structure. +function OPSGROUP:AddTaskWaypoint(task, Waypoint, description, prio, duration) + + -- Waypoint of task. + Waypoint=Waypoint or self:GetWaypointNext() + + if Waypoint then + + -- Increase counter. + self.taskcounter=self.taskcounter+1 + + -- Task data structure. + local newtask={} --#OPSGROUP.Task + newtask.description=description or string.format("Task #%d", self.taskcounter) + newtask.status=OPSGROUP.TaskStatus.SCHEDULED + newtask.dcstask=task + newtask.prio=prio or 50 + newtask.id=self.taskcounter + newtask.duration=duration + newtask.time=0 + newtask.waypoint=Waypoint.uid + newtask.type=OPSGROUP.TaskType.WAYPOINT + newtask.stopflag=USERFLAG:New(string.format("%s StopTaskFlag %d", self.groupname, newtask.id)) + newtask.stopflag:Set(0) + + -- Add to table. + table.insert(self.taskqueue, newtask) + + -- Info. + self:T(self.lid..string.format("Adding WAYPOINT task %s at WP ID=%d", newtask.description, newtask.waypoint)) + self:T3({newtask=newtask}) + + -- Update route. + self:__UpdateRoute(-1) + + return newtask + end + + return nil +end + +--- Add an *enroute* task. +-- @param #OPSGROUP self +-- @param #table task DCS task table structure. +function OPSGROUP:AddTaskEnroute(task) + + if not self.taskenroute then + self.taskenroute={} + end + + -- Check not to add the same task twice! + local gotit=false + for _,Task in pairs(self.taskenroute) do + if Task.id==task.id then + gotit=true + break + end + end + + if not gotit then + table.insert(self.taskenroute, task) + end + +end + +--- Get the unfinished waypoint tasks +-- @param #OPSGROUP self +-- @param #number id Unique waypoint ID. +-- @return #table Table of tasks. Table could also be empty {}. +function OPSGROUP:GetTasksWaypoint(id) + + -- Tasks table. + local tasks={} + + -- Sort queue. + self:_SortTaskQueue() + + -- Look for first task that SCHEDULED. + for _,_task in pairs(self.taskqueue) do + local task=_task --#OPSGROUP.Task + if task.type==OPSGROUP.TaskType.WAYPOINT and task.status==OPSGROUP.TaskStatus.SCHEDULED and task.waypoint==id then + table.insert(tasks, task) + end + end + + return tasks +end + +--- Count remaining waypoint tasks. +-- @param #OPSGROUP self +-- @param #number uid Unique waypoint ID. +-- @return #number Number of waypoint tasks. +function OPSGROUP:CountTasksWaypoint(id) + + -- Tasks table. + local n=0 + + -- Look for first task that SCHEDULED. + for _,_task in pairs(self.taskqueue) do + local task=_task --#OPSGROUP.Task + if task.type==OPSGROUP.TaskType.WAYPOINT and task.status==OPSGROUP.TaskStatus.SCHEDULED and task.waypoint==id then + n=n+1 + end + end + + return n +end + +--- Sort task queue. +-- @param #OPSGROUP self +function OPSGROUP:_SortTaskQueue() + + -- Sort results table wrt prio and then start time. + local function _sort(a, b) + local taskA=a --#OPSGROUP.Task + local taskB=b --#OPSGROUP.Task + return (taskA.prio=task.time then + return task + end + end + + return nil +end + +--- Get the currently executed task if there is any. +-- @param #OPSGROUP self +-- @return #OPSGROUP.Task Current task or nil. +function OPSGROUP:GetTaskCurrent() + local task=self:GetTaskByID(self.taskcurrent, OPSGROUP.TaskStatus.EXECUTING) + return task +end + +--- Get task by its id. +-- @param #OPSGROUP self +-- @param #number id Task id. +-- @param #string status (Optional) Only return tasks with this status, e.g. OPSGROUP.TaskStatus.SCHEDULED. +-- @return #OPSGROUP.Task The task or nil. +function OPSGROUP:GetTaskByID(id, status) + + for _,_task in pairs(self.taskqueue) do + local task=_task --#OPSGROUP.Task + + if task.id==id then + if status==nil or status==task.status then + return task + end + end + + end + + return nil +end + +--- On after "TaskExecute" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsGroup#OPSGROUP.Task Task The task. +function OPSGROUP:onafterTaskExecute(From, Event, To, Task) + + -- Debug message. + local text=string.format("Task %s ID=%d execute", tostring(Task.description), Task.id) + self:T(self.lid..text) + + -- Cancel current task if there is any. + if self.taskcurrent>0 then + self:TaskCancel() + end + + -- Set current task. + self.taskcurrent=Task.id + + -- Set time stamp. + Task.timestamp=timer.getAbsTime() + + -- Task status executing. + Task.status=OPSGROUP.TaskStatus.EXECUTING + + if Task.dcstask.id=="Formation" then + + -- Set of group(s) to follow Mother. + local followSet=SET_GROUP:New():AddGroup(self.group) + + local param=Task.dcstask.params + + local followUnit=UNIT:FindByName(param.unitname) + + -- Define AI Formation object. + Task.formation=AI_FORMATION:New(followUnit, followSet, "Formation", "Follow X at given parameters.") + + -- Formation parameters. + Task.formation:FormationCenterWing(-param.offsetX, 50, math.abs(param.altitude), 50, param.offsetZ, 50) + + -- Set follow time interval. + Task.formation:SetFollowTimeInterval(param.dtFollow) + + -- Formation mode. + Task.formation:SetFlightModeFormation(self.group) + + -- Start formation FSM. + Task.formation:Start() + + elseif Task.dcstask.id=="PatrolZone" then + + --- + -- Task patrol zone. + --- + + -- Parameters. + local zone=Task.dcstask.params.zone --Core.Zone#ZONE + local Coordinate=zone:GetRandomCoordinate() + local Speed=UTILS.KmphToKnots(Task.dcstask.params.speed or self.speedCruise) + local Altitude=Task.dcstask.params.altitude and UTILS.MetersToFeet(Task.dcstask.params.altitude) or nil + + -- New waypoint. + if self.isFlightgroup then + FLIGHTGROUP.AddWaypoint(self, Coordinate, Speed, AfterWaypointWithID, Altitude) + elseif self.isNavygroup then + ARMYGROUP.AddWaypoint(self, Coordinate, Speed, AfterWaypointWithID, Formation) + elseif self.isArmygroup then + NAVYGROUP.AddWaypoint(self, Coordinate, Speed, AfterWaypointWithID, Altitude) + end + + else + + -- If task is scheduled (not waypoint) set task. + if Task.type==OPSGROUP.TaskType.SCHEDULED then + + local DCStasks={} + if Task.dcstask.id=='ComboTask' then + -- Loop over all combo tasks. + for TaskID, Task in ipairs(Task.dcstask.params.tasks) do + table.insert(DCStasks, Task) + end + else + table.insert(DCStasks, Task.dcstask) + end + + -- Combo task. + local TaskCombo=self.group:TaskCombo(DCStasks) + + -- Stop condition! + local TaskCondition=self.group:TaskCondition(nil, Task.stopflag:GetName(), 1, nil, Task.duration) + + -- Controlled task. + local TaskControlled=self.group:TaskControlled(TaskCombo, TaskCondition) + + -- Task done. + local TaskDone=self.group:TaskFunction("OPSGROUP._TaskDone", self, Task) + + -- Final task. + local TaskFinal=self.group:TaskCombo({TaskControlled, TaskDone}) + + -- Set task for group. + self:SetTask(TaskFinal) + + end + + end + + -- Get mission of this task (if any). + local Mission=self:GetMissionByTaskID(self.taskcurrent) + if Mission then + -- Set AUFTRAG status. + self:MissionExecute(Mission) + end + +end + +--- On after "TaskCancel" event. Cancels the current task or simply sets the status to DONE if the task is not the current one. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPSGROUP.Task Task The task to cancel. Default is the current task (if any). +function OPSGROUP:onafterTaskCancel(From, Event, To, Task) + + -- Get current task. + local currenttask=self:GetTaskCurrent() + + -- If no task, we take the current task. But this could also be *nil*! + Task=Task or currenttask + + if Task then + + -- Check if the task is the current task? + if currenttask and Task.id==currenttask.id then + + -- Current stop flag value. I noticed cases, where setting the flag to 1 would not cancel the task, e.g. when firing HARMS on a dead ship. + local stopflag=Task.stopflag:Get() + + -- Debug info. + local text=string.format("Current task %s ID=%d cancelled (flag %s=%d)", Task.description, Task.id, Task.stopflag:GetName(), stopflag) + self:T(self.lid..text) + + -- Set stop flag. When the flag is true, the _TaskDone function is executed and calls :TaskDone() + Task.stopflag:Set(1) + + local done=false + if Task.dcstask.id=="Formation" then + Task.formation:Stop() + done=true + elseif Task.dcstask.id=="PatrolZone" then + done=true + elseif stopflag==1 or (not self:IsAlive()) or self:IsDead() or self:IsStopped() then + -- Manual call TaskDone if setting flag to one was not successful. + done=true + end + + if done then + self:TaskDone(Task) + end + + else + + -- Debug info. + self:T(self.lid..string.format("TaskCancel: Setting task %s ID=%d to DONE", Task.description, Task.id)) + + -- Call task done function. + self:TaskDone(Task) + + end + + else + + local text=string.format("WARNING: No (current) task to cancel!") + self:E(self.lid..text) + + end + +end + +--- On before "TaskDone" event. Deny transition if task status is PAUSED. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPSGROUP.Task Task +function OPSGROUP:onbeforeTaskDone(From, Event, To, Task) + + local allowed=true + + if Task.status==OPSGROUP.TaskStatus.PAUSED then + allowed=false + end + + return allowed +end + +--- On after "TaskDone" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPSGROUP.Task Task +function OPSGROUP:onafterTaskDone(From, Event, To, Task) + + -- Debug message. + local text=string.format("Task done: %s ID=%d", Task.description, Task.id) + self:T(self.lid..text) + + -- No current task. + if Task.id==self.taskcurrent then + self.taskcurrent=0 + end + + -- Task status done. + Task.status=OPSGROUP.TaskStatus.DONE + + -- Restore old ROE. + if Task.backupROE then + self:SwitchROE(Task.backupROE) + end + + -- Check if this task was the task of the current mission ==> Mission Done! + local Mission=self:GetMissionByTaskID(Task.id) + + if Mission and Mission:IsNotOver() then + + local status=Mission:GetGroupStatus(self) + + if status~=AUFTRAG.GroupStatus.PAUSED then + self:T(self.lid.."Task Done ==> Mission Done!") + self:MissionDone(Mission) + else + --Mission paused. Do nothing! + end + else + + if Task.description=="Engage_Target" then + self:Disengage() + end + + self:T(self.lid.."Task Done but NO mission found ==> _CheckGroupDone in 1 sec") + self:_CheckGroupDone(1) + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Mission Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add mission to queue. +-- @param #OPSGROUP self +-- @param Ops.Auftrag#AUFTRAG Mission Mission for this group. +-- @return #OPSGROUP self +function OPSGROUP:AddMission(Mission) + + -- Add group to mission. + Mission:AddOpsGroup(self) + + -- Set group status to SCHEDULED.. + Mission:SetGroupStatus(self, AUFTRAG.GroupStatus.SCHEDULED) + + -- Set mission status to SCHEDULED. + Mission:Scheduled() + + -- Add elements. + Mission.Nelements=Mission.Nelements+#self.elements + + -- Add mission to queue. + table.insert(self.missionqueue, Mission) + + -- Info text. + local text=string.format("Added %s mission %s starting at %s, stopping at %s", + tostring(Mission.type), tostring(Mission.name), UTILS.SecondsToClock(Mission.Tstart, true), Mission.Tstop and UTILS.SecondsToClock(Mission.Tstop, true) or "INF") + self:T(self.lid..text) + + return self +end + +--- Remove mission from queue. +-- @param #OPSGROUP self +-- @param Ops.Auftrag#AUFTRAG Mission Mission to be removed. +-- @return #OPSGROUP self +function OPSGROUP:RemoveMission(Mission) + + for i,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + if mission.auftragsnummer==Mission.auftragsnummer then + + -- Remove mission waypoint task. + local Task=Mission:GetGroupWaypointTask(self) + + if Task then + self:RemoveTask(Task) + end + + -- Remove mission from queue. + table.remove(self.missionqueue, i) + + return self + end + + end + + return self +end + +--- Count remaining missons. +-- @param #OPSGROUP self +-- @return #number Number of missions to be done. +function OPSGROUP:CountRemainingMissison() + + local N=0 + + -- Loop over mission queue. + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + if mission and mission:IsNotOver() then + + -- Get group status. + local status=mission:GetGroupStatus(self) + + if status~=AUFTRAG.GroupStatus.DONE and status~=AUFTRAG.GroupStatus.CANCELLED then + N=N+1 + end + + end + end + + return N +end + +--- Get next mission. +-- @param #OPSGROUP self +-- @return Ops.Auftrag#AUFTRAG Next mission or *nil*. +function OPSGROUP:_GetNextMission() + + -- Number of missions. + local Nmissions=#self.missionqueue + + -- Treat special cases. + if Nmissions==0 then + return nil + end + + -- Sort results table wrt times they have already been engaged. + local function _sort(a, b) + local taskA=a --Ops.Auftrag#AUFTRAG + local taskB=b --Ops.Auftrag#AUFTRAG + return (taskA.prio0 then + -- Delayed call. + self:ScheduleOnce(delay, OPSGROUP.RouteToMission, self, mission) + else + + if self:IsDead() then + return + end + + -- ID of current waypoint. + local uid=self:GetWaypointCurrent().uid + + -- Get coordinate where the mission is executed. + local waypointcoord=mission:GetMissionWaypointCoord(self.group) + + -- Add enroute tasks. + for _,task in pairs(mission.enrouteTasks) do + self:AddTaskEnroute(task) + end + + -- Speed to mission waypoint. + local SpeedToMission=UTILS.KmphToKnots(self.speedCruise) + + -- Special for Troop transport. + if mission.type==AUFTRAG.Type.TROOPTRANSPORT then + + -- Refresh DCS task with the known controllable. + mission.DCStask=mission:GetDCSMissionTask(self.group) + + -- Add task to embark for the troops. + for _,_group in pairs(mission.transportGroupSet.Set) do + local group=_group --Wrapper.Group#GROUP + + if group and group:IsAlive() then + local DCSTask=group:TaskEmbarkToTransport(mission.transportPickup, 500) + group:SetTask(DCSTask, 5) + end + + end + + elseif mission.type==AUFTRAG.Type.ARTY then + + -- Get weapon range. + local weapondata=self:GetWeaponData(mission.engageWeaponType) + + if weapondata then + + -- Get target coordinate. + local targetcoord=mission:GetTargetCoordinate() + + -- Heading to target. + local heading=self:GetCoordinate():HeadingTo(targetcoord) + + -- Distance to target. + local dist=self:GetCoordinate():Get2DDistance(targetcoord) + + -- Check if we are within range. + if dist>weapondata.RangeMax then + + local d=(dist-weapondata.RangeMax)*1.1 + + -- New waypoint coord. + waypointcoord=self:GetCoordinate():Translate(d, heading) + + self:T(self.lid..string.format("Out of max range = %.1f km for weapon %d", weapondata.RangeMax/1000, mission.engageWeaponType)) + elseif dist0 then + for i,_task in pairs(tasks) do + local task=_task --#OPSGROUP.Task + text=text..string.format("\n[%d] %s", i, task.description) + end + else + text=text.." None" + end + self:T(self.lid..text) + + + -- Tasks at this waypoints. + local taskswp={} + + -- TODO: maybe set waypoint enroute tasks? + + for _,task in pairs(tasks) do + local Task=task --Ops.OpsGroup#OPSGROUP.Task + + -- Task execute. + table.insert(taskswp, self.group:TaskFunction("OPSGROUP._TaskExecute", self, Task)) + + -- Stop condition if userflag is set to 1 or task duration over. + local TaskCondition=self.group:TaskCondition(nil, Task.stopflag:GetName(), 1, nil, Task.duration) + + -- Controlled task. + table.insert(taskswp, self.group:TaskControlled(Task.dcstask, TaskCondition)) + + -- Task done. + table.insert(taskswp, self.group:TaskFunction("OPSGROUP._TaskDone", self, Task)) + + end + + -- Execute waypoint tasks. + if #taskswp>0 then + self:SetTask(self.group:TaskCombo(taskswp)) + end + + return #taskswp +end + +--- On after "GotoWaypoint" event. Group will got to the given waypoint and execute its route from there. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number UID The goto waypoint unique ID. +function OPSGROUP:onafterGotoWaypoint(From, Event, To, UID) + + local n=self:GetWaypointIndex(UID) + + if n then + + -- TODO: switch to re-enable waypoint tasks. + if false then + local tasks=self:GetTasksWaypoint(n) + + for _,_task in pairs(tasks) do + local task=_task --#OPSGROUP.Task + task.status=OPSGROUP.TaskStatus.SCHEDULED + end + + end + + local Speed=self:GetSpeedToWaypoint(n) + + -- Update the route. + self:__UpdateRoute(-1, n, Speed) + + end + +end + +--- On after "DetectedUnit" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Unit#UNIT Unit The detected unit. +function OPSGROUP:onafterDetectedUnit(From, Event, To, Unit) + + -- Get unit name. + local unitname=Unit and Unit:GetName() or "unknown" + + -- Debug. + self:T2(self.lid..string.format("Detected unit %s", unitname)) + + if self.detectedunits:FindUnit(unitname) then + -- Unit is already in the detected unit set ==> Trigger "DetectedUnitKnown" event. + self:DetectedUnitKnown(Unit) + else + -- Unit is was not detected ==> Trigger "DetectedUnitNew" event. + self:DetectedUnitNew(Unit) + end + +end + +--- On after "DetectedUnitNew" event. Add newly detected unit to detected unit set. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Unit#UNIT Unit The detected unit. +function OPSGROUP:onafterDetectedUnitNew(From, Event, To, Unit) + + -- Debug info. + self:T(self.lid..string.format("Detected New unit %s", Unit:GetName())) + + -- Add unit to detected unit set. + self.detectedunits:AddUnit(Unit) +end + +--- On after "DetectedGroup" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Group#GROUP Group The detected Group. +function OPSGROUP:onafterDetectedGroup(From, Event, To, Group) + + -- Get group name. + local groupname=Group and Group:GetName() or "unknown" + + -- Debug info. + self:T(self.lid..string.format("Detected group %s", groupname)) + + if self.detectedgroups:FindGroup(groupname) then + -- Group is already in the detected set ==> Trigger "DetectedGroupKnown" event. + self:DetectedGroupKnown(Group) + else + -- Group is was not detected ==> Trigger "DetectedGroupNew" event. + self:DetectedGroupNew(Group) + end + +end + +--- On after "DetectedGroupNew" event. Add newly detected group to detected group set. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Group#GROUP Group The detected group. +function OPSGROUP:onafterDetectedGroupNew(From, Event, To, Group) + + -- Debug info. + self:T(self.lid..string.format("Detected New group %s", Group:GetName())) + + -- Add unit to detected unit set. + self.detectedgroups:AddGroup(Group) +end + +--- On after "EnterZone" event. Sets self.inzones[zonename]=true. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Zone#ZONE Zone The zone that the group entered. +function OPSGROUP:onafterEnterZone(From, Event, To, Zone) + local zonename=Zone and Zone:GetName() or "unknown" + self:T2(self.lid..string.format("Entered Zone %s", zonename)) + self.inzones:Add(Zone:GetName(), Zone) +end + +--- On after "LeaveZone" event. Sets self.inzones[zonename]=false. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Zone#ZONE Zone The zone that the group entered. +function OPSGROUP:onafterLeaveZone(From, Event, To, Zone) + local zonename=Zone and Zone:GetName() or "unknown" + self:T2(self.lid..string.format("Left Zone %s", zonename)) + self.inzones:Remove(zonename, true) +end + +--- On before "LaserOn" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Point#COORDINATE Target Target Coordinate. Target can also be any POSITIONABLE from which we can obtain its coordinates. +function OPSGROUP:onbeforeLaserOn(From, Event, To, Target) + + -- Check if LASER is already on. + if self.spot.On then + return false + end + + if Target then + + -- Target specified ==> set target. + self:SetLaserTarget(Target) + + else + -- No target specified. + self:E(self.lid.."ERROR: No target provided for LASER!") + return false + end + + -- Get the first element alive. + local element=self:GetElementAlive() + + if element then + + -- Set element. + self.spot.element=element + + -- Height offset. No offset for aircraft. We take the height for ground or naval. + local offsetY=0 + if self.isGround or self.isNaval then + offsetY=element.height + end + + -- Local offset of the LASER source. + self.spot.offset={x=0, y=offsetY, z=0} + + -- Check LOS. + if self.spot.CheckLOS then + + -- Check LOS. + local los=self:HasLoS(self.spot.Coordinate, self.spot.element, self.spot.offset) + + --self:I({los=los, coord=self.spot.Coordinate, offset=self.spot.offset}) + + if los then + self:LaserGotLOS() + else + -- Try to switch laser on again in 10 sec. + self:I(self.lid.."LASER got no LOS currently. Trying to switch the laser on again in 10 sec") + self:__LaserOn(-10, Target) + return false + end + + end + + else + self:E(self.lid.."ERROR: No element alive for lasing") + return false + end + + return true +end + +--- On after "LaserOn" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Point#COORDINATE Target Target Coordinate. Target can also be any POSITIONABLE from which we can obtain its coordinates. +function OPSGROUP:onafterLaserOn(From, Event, To, Target) + + -- Start timer that calls the update twice per sec by default. + if not self.spot.timer:IsRunning() then + self.spot.timer:Start(nil, self.spot.dt) + end + + -- Get DCS unit. + local DCSunit=self.spot.element.unit:GetDCSObject() + + -- Create laser and IR beams. + self.spot.Laser=Spot.createLaser(DCSunit, self.spot.offset, self.spot.vec3, self.spot.Code or 1688) + if self.spot.IRon then + self.spot.IR=Spot.createInfraRed(DCSunit, self.spot.offset, self.spot.vec3) + end + + -- Laser is on. + self.spot.On=true + + -- No paused in case it was. + self.spot.Paused=false + + -- Debug message. + self:T(self.lid.."Switching LASER on") + +end + +--- On before "LaserOff" event. Check if LASER is on. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onbeforeLaserOff(From, Event, To) + return self.spot.On or self.spot.Paused +end + +--- On after "LaserOff" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterLaserOff(From, Event, To) + + -- Debug message. + self:T(self.lid.."Switching LASER off") + + -- "Destroy" the laser beam. + if self.spot.On then + self.spot.Laser:destroy() + self.spot.IR:destroy() + + -- Set to nil. + self.spot.Laser=nil + self.spot.IR=nil + end + + -- Stop update timer. + self.spot.timer:Stop() + + -- No target unit. + self.spot.TargetUnit=nil + + -- Laser is off. + self.spot.On=false + + -- Not paused if it was. + self.spot.Paused=false +end + +--- On after "LaserPause" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterLaserPause(From, Event, To) + + -- Debug message. + self:T(self.lid.."Switching LASER off temporarily") + + -- "Destroy" the laser beam. + self.spot.Laser:destroy() + self.spot.IR:destroy() + + -- Set to nil. + self.spot.Laser=nil + self.spot.IR=nil + + -- Laser is off. + self.spot.On=false + + -- Laser is paused. + self.spot.Paused=true + +end + +--- On before "LaserResume" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onbeforeLaserResume(From, Event, To) + return self.spot.Paused +end + +--- On after "LaserResume" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterLaserResume(From, Event, To) + + -- Debug info. + self:T(self.lid.."Resuming LASER") + + -- Unset paused. + self.spot.Paused=false + + -- Set target. + local target=nil + if self.spot.TargetType==0 then + target=self.spot.Coordinate + elseif self.spot.TargetType==1 or self.spot.TargetType==2 then + target=self.spot.TargetUnit + elseif self.spot.TargetType==3 then + target=self.spot.TargetGroup + end + + -- Switch laser back on. + if target then + + -- Debug message. + self:T(self.lid.."Switching LASER on again") + + self:LaserOn(target) + end + +end + +--- On after "LaserCode" event. Changes the LASER code. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number Code Laser code. Default is 1688. +function OPSGROUP:onafterLaserCode(From, Event, To, Code) + + -- Default is 1688. + self.spot.Code=Code or 1688 + + -- Debug message. + self:T2(self.lid..string.format("Setting LASER Code to %d", self.spot.Code)) + + if self.spot.On then + + -- Debug info. + self:T(self.lid..string.format("New LASER Code is %d", self.spot.Code)) + + -- Set LASER code. + self.spot.Laser:setCode(self.spot.Code) + end + +end + +--- On after "LaserLostLOS" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterLaserLostLOS(From, Event, To) + + --env.info("FF lost LOS") + + -- No of sight. + self.spot.LOS=false + + -- Lost line of sight. + self.spot.lostLOS=true + + if self.spot.On then + + --env.info("FF lost LOS ==> pause laser") + + -- Switch laser off. + self:LaserPause() + + end + +end + +--- On after "LaserGotLOS" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterLaserGotLOS(From, Event, To) + + -- Has line of sight. + self.spot.LOS=true + + --env.info("FF Laser Got LOS") + + if self.spot.lostLOS then + + -- Did not loose LOS anymore. + self.spot.lostLOS=false + + --env.info("FF had lost LOS and regained it") + + -- Resume laser if currently paused. + if self.spot.Paused then + --env.info("FF laser was paused ==> resume") + self:LaserResume() + end + + end + +end + +--- Set LASER target. +-- @param #OPSGROUP self +-- @param Wrapper.Positionable#POSITIONABLE Target The target to lase. Can also be a COORDINATE object. +function OPSGROUP:SetLaserTarget(Target) + + if Target then + + -- Check object type. + if Target:IsInstanceOf("SCENERY") then + + -- Scenery as target. Treat it like a coordinate. Set offset to 1 meter above ground. + self.spot.TargetType=0 + self.spot.offsetTarget={x=0, y=1, z=0} + + elseif Target:IsInstanceOf("POSITIONABLE") then + + local target=Target --Wrapper.Positionable#POSITIONABLE + + if target:IsAlive() then + + if target:IsInstanceOf("GROUP") then + -- We got a GROUP as target. + self.spot.TargetGroup=target + self.spot.TargetUnit=target:GetHighestThreat() + self.spot.TargetType=3 + else + -- We got a UNIT or STATIC as target. + self.spot.TargetUnit=target + if target:IsInstanceOf("STATIC") then + self.spot.TargetType=1 + elseif target:IsInstanceOf("UNIT") then + self.spot.TargetType=2 + end + end + + -- Get object size. + local size,x,y,z=self.spot.TargetUnit:GetObjectSize() + + if y then + self.spot.offsetTarget={x=0, y=y*0.75, z=0} + else + self.spot.offsetTarget={x=0, 2, z=0} + end + + --env.info(string.format("Target offset %.3f", y)) + + else + self:E("WARNING: LASER target is not alive!") + return + end + + elseif Target:IsInstanceOf("COORDINATE") then + + -- Coordinate as target. + self.spot.TargetType=0 + self.spot.offsetTarget={x=0, y=0, z=0} + + else + self:E(self.lid.."ERROR: LASER target should be a POSITIONABLE (GROUP, UNIT or STATIC) or a COORDINATE object!") + return + end + + -- Set vec3 and account for target offset. + self.spot.vec3=UTILS.VecAdd(Target:GetVec3(), self.spot.offsetTarget) + + -- Set coordinate. + self.spot.Coordinate:UpdateFromVec3(self.spot.vec3) + end + +end + +--- Update laser point. +-- @param #OPSGROUP self +function OPSGROUP:_UpdateLaser() + + -- Check if we have a POSITIONABLE to lase. + if self.spot.TargetUnit then + + --- + -- Lasing a possibly moving target + --- + + if self.spot.TargetUnit:IsAlive() then + + -- Get current target position. + local vec3=self.spot.TargetUnit:GetVec3() + + -- Add target offset. + vec3=UTILS.VecAdd(vec3, self.spot.offsetTarget) + + -- Calculate distance + local dist=UTILS.VecDist3D(vec3, self.spot.vec3) + + -- Store current position. + self.spot.vec3=vec3 + + -- Update beam coordinate. + self.spot.Coordinate:UpdateFromVec3(vec3) + + -- Update laser if target moved more than one meter. + if dist>1 then + + -- If the laser is ON, set the new laser target point. + if self.spot.On then + self.spot.Laser:setPoint(vec3) + if self.spot.IRon then + self.spot.IR:setPoint(vec3) + end + end + + end + + else + + if self.spot.TargetGroup and self.spot.TargetGroup:IsAlive() then + + -- Get first alive unit in the group. + local unit=self.spot.TargetGroup:GetHighestThreat() + + if unit then + self:T(self.lid..string.format("Switching to target unit %s in the group", unit:GetName())) + self.spot.TargetUnit=unit + -- We update the laser position in the next update cycle and then check the LOS. + return + else + -- Switch laser off. + self:T(self.lid.."Target is not alive any more ==> switching LASER off") + self:LaserOff() + return + end + + else + + -- Switch laser off. + self:T(self.lid.."Target is not alive any more ==> switching LASER off") + self:LaserOff() + return + end + + end + end + + -- Check LOS. + if self.spot.CheckLOS then + + -- Check current LOS. + local los=self:HasLoS(self.spot.Coordinate, self.spot.element, self.spot.offset) + + --env.info(string.format("FF check LOS current=%s previous=%s", tostring(los), tostring(self.spot.LOS))) + + if los then + -- Got LOS + if self.spot.lostLOS then + --self:I({los=self.spot.LOS, coord=self.spot.Coordinate, offset=self.spot.offset}) + self:LaserGotLOS() + end + + else + -- No LOS currently + if not self.spot.lostLOS then + self:LaserLostLOS() + end + + end + + end + +end + + +--- On after "ElementDestroyed" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPSGROUP.Element Element The flight group element. +function OPSGROUP:onafterElementDestroyed(From, Event, To, Element) + self:T(self.lid..string.format("Element destroyed %s", Element.name)) + + -- Cancel all missions. + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + mission:ElementDestroyed(self, Element) + + end + + -- Increase counter. + self.Ndestroyed=self.Ndestroyed+1 + + -- Set element status. + self:_UpdateStatus(Element, OPSGROUP.ElementStatus.DEAD) + +end + +--- On after "ElementDead" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPSGROUP.Element Element The flight group element. +function OPSGROUP:onafterElementDead(From, Event, To, Element) + self:T(self.lid..string.format("Element dead %s at t=%.3f", Element.name, timer.getTime())) + + -- Set element status. + self:_UpdateStatus(Element, OPSGROUP.ElementStatus.DEAD) + + -- Check if element was lasing and if so, switch to another unit alive to lase. + if self.spot.On and self.spot.element.name==Element.name then + + -- Switch laser off. + self:LaserOff() + + -- If there is another element alive, switch laser on again. + if self:GetNelements()>0 then + + -- New target if any. + local target=nil + + if self.spot.TargetType==0 then + -- Coordinate + target=self.spot.Coordinate + elseif self.spot.TargetType==1 or self.spot.TargetType==2 then + -- Static or unit + if self.spot.TargetUnit and self.spot.TargetUnit:IsAlive() then + target=self.spot.TargetUnit + end + elseif self.spot.TargetType==3 then + -- Group + if self.spot.TargetGroup and self.spot.TargetGroup:IsAlive() then + target=self.spot.TargetGroup + end + end + + -- Switch laser on again. + if target then + self:__LaserOn(-1, target) + end + end + end + +end + +--- On before "Dead" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onbeforeDead(From, Event, To) + if self.Ndestroyed==#self.elements then + self:Destroyed() + end +end + +--- On after "Dead" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterDead(From, Event, To) + self:T(self.lid..string.format("Group dead at t=%.3f", timer.getTime())) + + -- Delete waypoints so they are re-initialized at the next spawn. + self.waypoints=nil + self.groupinitialized=false + + -- Cancel all missions. + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + self:T(self.lid.."Cancelling mission because group is dead! Mission name "..tostring(mission:GetName())) + + self:MissionCancel(mission) + mission:GroupDead(self) + + end + + -- Stop in a sec. + self:__Stop(-5) +end + +--- On after "Stop" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterStop(From, Event, To) + + -- Stop check timers. + self.timerCheckZone:Stop() + self.timerQueueUpdate:Stop() + + -- Stop FSM scheduler. + self.CallScheduler:Clear() + + if self:IsAlive() and not (self:IsDead() or self:IsStopped()) then + local life, life0=self:GetLifePoints() + local state=self:GetState() + local text=string.format("WARNING: Group is still alive! Current state=%s. Life points=%d/%d. Use OPSGROUP:Destroy() or OPSGROUP:Despawn() for a clean stop", state, life, life0) + self:E(self.lid..text) + end + + -- Debug output. + self:I(self.lid.."STOPPED! Unhandled events, cleared scheduler and removed from database.") +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Internal Check Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check if group is in zones. +-- @param #OPSGROUP self +function OPSGROUP:_CheckInZones() + + if self.checkzones and self:IsAlive() then + + local Ncheck=self.checkzones:Count() + local Ninside=self.inzones:Count() + + -- Debug info. + self:T(self.lid..string.format("Check if group is in %d zones. Currently it is in %d zones.", self.checkzones:Count(), self.inzones:Count())) + + -- Firstly, check if group is still inside zone it was already in. If not, remove zones and trigger LeaveZone() event. + local leftzones={} + for inzonename, inzone in pairs(self.inzones:GetSet()) do + + -- Check if group is still inside the zone. + local isstillinzone=self.group:IsInZone(inzone) --:IsPartlyOrCompletelyInZone(inzone) + + -- If not, trigger, LeaveZone event. + if not isstillinzone then + table.insert(leftzones, inzone) + end + end + + -- Trigger leave zone event. + for _,leftzone in pairs(leftzones) do + self:LeaveZone(leftzone) + end + + + -- Now, run of all check zones and see if the group entered a zone. + local enterzones={} + for checkzonename,_checkzone in pairs(self.checkzones:GetSet()) do + local checkzone=_checkzone --Core.Zone#ZONE + + -- Is group currtently in this check zone? + local isincheckzone=self.group:IsInZone(checkzone) --:IsPartlyOrCompletelyInZone(checkzone) + + if isincheckzone and not self.inzones:_Find(checkzonename) then + table.insert(enterzones, checkzone) + end + end + + -- Trigger enter zone event. + for _,enterzone in pairs(enterzones) do + self:EnterZone(enterzone) + end + + end + +end + +--- Check detected units. +-- @param #OPSGROUP self +function OPSGROUP:_CheckDetectedUnits() + + if self.group and not self:IsDead() then + + -- Get detected DCS units. + local detectedtargets=self.group:GetDetectedTargets() + + local detected={} + local groups={} + for DetectionObjectID, Detection in pairs(detectedtargets or {}) do + local DetectedObject=Detection.object -- DCS#Object + + if DetectedObject and DetectedObject:isExist() and DetectedObject.id_<50000000 then + + -- Unit. + local unit=UNIT:Find(DetectedObject) + + if unit and unit:IsAlive() then + + -- Name of detected unit + local unitname=unit:GetName() + + -- Add unit to detected table of this run. + table.insert(detected, unit) + + -- Trigger detected unit event ==> This also triggers the DetectedUnitNew and DetectedUnitKnown events. + self:DetectedUnit(unit) + + -- Get group of unit. + local group=unit:GetGroup() + + -- Add group to table. + if group then + groups[group:GetName()]=group + end + + end + end + end + + -- Call detected group event. + for groupname, group in pairs(groups) do + self:DetectedGroup(group) + end + + -- Loop over units in detected set. + local lost={} + for _,_unit in pairs(self.detectedunits:GetSet()) do + local unit=_unit --Wrapper.Unit#UNIT + + -- Loop over detected units + local gotit=false + for _,_du in pairs(detected) do + local du=_du --Wrapper.Unit#UNIT + if unit:GetName()==du:GetName() then + gotit=true + end + end + + if not gotit then + table.insert(lost, unit:GetName()) + self:DetectedUnitLost(unit) + end + + end + + -- Remove lost units from detected set. + self.detectedunits:RemoveUnitsByName(lost) + + + -- Loop over groups in detected set. + local lost={} + for _,_group in pairs(self.detectedgroups:GetSet()) do + local group=_group --Wrapper.Group#GROUP + + -- Loop over detected units + local gotit=false + for _,_du in pairs(groups) do + local du=_du --Wrapper.Group#GROUP + if group:GetName()==du:GetName() then + gotit=true + end + end + + if not gotit then + table.insert(lost, group:GetName()) + self:DetectedGroupLost(group) + end + + end + + -- Remove lost units from detected set. + self.detectedgroups:RemoveGroupsByName(lost) + + end + +end + +--- Check if passed the final waypoint and, if necessary, update route. +-- @param #OPSGROUP self +-- @param #number delay Delay in seconds. +function OPSGROUP:_CheckGroupDone(delay) + + if self:IsAlive() and self.isAI then + + if delay and delay>0 then + -- Delayed call. + self:ScheduleOnce(delay, self._CheckGroupDone, self) + else + + if self:IsEngaging() then + self:UpdateRoute() + return + end + + -- Get current waypoint. + local waypoint=self:GetWaypoint(self.currentwp) + + --env.info("FF CheckGroupDone") + + if waypoint then + + -- Number of tasks remaining for this waypoint. + local ntasks=self:CountTasksWaypoint(waypoint.uid) + + -- We only want to update the route if there are no more tasks to be done. + if ntasks>0 then + self:T(self.lid..string.format("Still got %d tasks for the current waypoint UID=%d ==> RETURN (no action)", ntasks, waypoint.uid)) + return + end + end + + if self.adinfinitum then + + --- + -- Parol Ad Infinitum + --- + + if #self.waypoints>0 then + + -- Next waypoint index. + local i=self:GetWaypointIndexNext(true) + + -- Get positive speed to first waypoint. + local speed=self:GetSpeedToWaypoint(i) + + -- Start route at first waypoint. + self:UpdateRoute(i, speed) + + self:T(self.lid..string.format("Adinfinitum=TRUE ==> Goto WP index=%d at speed=%d knots", i, speed)) + + else + self:E(self.lid..string.format("WARNING: No waypoints left! Commanding a Full Stop")) + self:__FullStop(-1) + end + + else + + --- + -- Finite Patrol + --- + + if self.passedfinalwp then + + --- + -- Passed FINAL waypoint + --- + + -- No further waypoints. Command a full stop. + self:__FullStop(-1) + + self:T(self.lid..string.format("Passed final WP, adinfinitum=FALSE ==> Full Stop")) + + else + + --- + -- Final waypoint NOT passed yet + --- + + if #self.waypoints>0 then + self:T(self.lid..string.format("NOT Passed final WP, #WP>0 ==> Update Route")) + self:UpdateRoute() + else + self:E(self.lid..string.format("WARNING: No waypoints left! Commanding a Full Stop")) + self:__FullStop(-1) + end + + end + + end + + end + end + +end + +--- Check if group got stuck. +-- @param #OPSGROUP self +function OPSGROUP:_CheckStuck() + + -- Holding means we are not stuck. + if self:IsHolding() or self:Is("Rearming") then + return + end + + -- Current time. + local Tnow=timer.getTime() + + -- Expected speed in m/s. + local ExpectedSpeed=self:GetExpectedSpeed() + + -- Current speed in m/s. + local speed=self:GetVelocity() + + -- Check speed. + if speed<0.5 then + + if ExpectedSpeed>0 and not self.stuckTimestamp then + self:T2(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected", speed, ExpectedSpeed)) + self.stuckTimestamp=Tnow + self.stuckVec3=self:GetVec3() + end + + else + -- Moving (again). + self.stuckTimestamp=nil + end + + -- Somehow we are not moving... + if self.stuckTimestamp then + + -- Time we are holding. + local holdtime=Tnow-self.stuckTimestamp + + if holdtime>=10*60 then + + self:E(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected for %d sec", speed, ExpectedSpeed, holdtime)) + + --TODO: Stuck event! + + end + + end + +end + + +--- Check damage. +-- @param #OPSGROUP self +-- @return #OPSGROUP self +function OPSGROUP:_CheckDamage() + + self.life=0 + local damaged=false + for _,_element in pairs(self.elements) do + local element=_element --Ops.OpsGroup#OPSGROUP + + -- Current life points. + local life=element.unit:GetLife() + + self.life=self.life+life + + if life0 then + + -- Get current ammo. + local ammo=self:GetAmmoTot() + + -- Check if rearming is completed. + if self:IsRearming() then + if ammo.Total==self.ammo.Total then + self:Rearmed() + end + end + + -- Total. + if self.outofAmmo and ammo.Total>0 then + self.outofAmmo=false + end + if ammo.Total==0 and not self.outofAmmo then + self.outofAmmo=true + self:OutOfAmmo() + end + + -- Guns. + if self.outofGuns and ammo.Guns>0 then + self.outoffGuns=false + end + if ammo.Guns==0 and self.ammo.Guns>0 and not self.outofGuns then + self.outofGuns=true + self:OutOfGuns() + end + + -- Rockets. + if self.outofRockets and ammo.Rockets>0 then + self.outoffRockets=false + end + if ammo.Rockets==0 and self.ammo.Rockets>0 and not self.outofRockets then + self.outofRockets=true + self:OutOfRockets() + end + + -- Bombs. + if self.outofBombs and ammo.Bombs>0 then + self.outoffBombs=false + end + if ammo.Bombs==0 and self.ammo.Bombs>0 and not self.outofBombs then + self.outofBombs=true + self:OutOfBombs() + end + + -- Missiles. + if self.outofMissiles and ammo.Missiles>0 then + self.outoffMissiles=false + end + if ammo.Missiles==0 and self.ammo.Missiles>0 and not self.outofMissiles then + self.outofMissiles=true + self:OutOfMissiles() + end + + -- Check if group is engaging. + if self:IsEngaging() and ammo.Total==0 then + self:Disengage() + end + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Status Info Common to Air, Land and Sea +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Print info on mission and task status to DCS log file. +-- @param #OPSGROUP self +function OPSGROUP:_PrintTaskAndMissionStatus() + + --- + -- Tasks: verbose >= 3 + --- + + -- Task queue. + if self.verbose>=3 and #self.taskqueue>0 then + local text=string.format("Tasks #%d", #self.taskqueue) + for i,_task in pairs(self.taskqueue) do + local task=_task --Ops.OpsGroup#OPSGROUP.Task + local name=task.description + local taskid=task.dcstask.id or "unknown" + local status=task.status + local clock=UTILS.SecondsToClock(task.time, true) + local eta=task.time-timer.getAbsTime() + local started=task.timestamp and UTILS.SecondsToClock(task.timestamp, true) or "N/A" + local duration=-1 + if task.duration then + duration=task.duration + if task.timestamp then + -- Time the task is running. + duration=task.duration-(timer.getAbsTime()-task.timestamp) + else + -- Time the task is supposed to run. + duration=task.duration + end + end + -- Output text for element. + if task.type==OPSGROUP.TaskType.SCHEDULED then + text=text..string.format("\n[%d] %s (%s): status=%s, scheduled=%s (%d sec), started=%s, duration=%d", i, taskid, name, status, clock, eta, started, duration) + elseif task.type==OPSGROUP.TaskType.WAYPOINT then + text=text..string.format("\n[%d] %s (%s): status=%s, waypoint=%d, started=%s, duration=%d, stopflag=%d", i, taskid, name, status, task.waypoint, started, duration, task.stopflag:Get()) + end + end + self:I(self.lid..text) + end + + --- + -- Missions: verbose>=2 + --- + + -- Current mission name. + if self.verbose>=2 then + local Mission=self:GetMissionByID(self.currentmission) + + -- Current status. + local text=string.format("Missions %d, Current: %s", self:CountRemainingMissison(), Mission and Mission.name or "none") + for i,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + local Cstart= UTILS.SecondsToClock(mission.Tstart, true) + local Cstop = mission.Tstop and UTILS.SecondsToClock(mission.Tstop, true) or "INF" + text=text..string.format("\n[%d] %s (%s) status=%s (%s), Time=%s-%s, prio=%d wp=%s targets=%d", + i, tostring(mission.name), mission.type, mission:GetGroupStatus(self), tostring(mission.status), Cstart, Cstop, mission.prio, tostring(mission:GetGroupWaypointIndex(self)), mission:CountMissionTargets()) + end + self:I(self.lid..text) + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Waypoints & Routing +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Enhance waypoint table. +-- @param #OPSGROUP self +-- @param #OPSGROUP.Waypoint Waypoint data. +-- @return #OPSGROUP.Waypoint Modified waypoint data. +function OPSGROUP:_CreateWaypoint(waypoint) + + -- Set uid. + waypoint.uid=self.wpcounter + + -- Waypoint has not been passed yet. + waypoint.npassed=0 + + -- Coordinate. + waypoint.coordinate=COORDINATE:New(waypoint.x, waypoint.alt, waypoint.y) + + -- Set waypoint name. + waypoint.name=string.format("Waypoint UID=%d", waypoint.uid) + + -- Set types. + waypoint.patrol=false + waypoint.detour=false + waypoint.astar=false + + -- Increase UID counter. + self.wpcounter=self.wpcounter+1 + + return waypoint +end + +--- Initialize Mission Editor waypoints. +-- @param #OPSGROUP self +-- @param #OPSGROUP.Waypoint waypoint Waypoint data. +-- @param #number wpnumber Waypoint index/number. Default is as last waypoint. +function OPSGROUP:_AddWaypoint(waypoint, wpnumber) + + -- Index. + wpnumber=wpnumber or #self.waypoints+1 + + -- Add waypoint to table. + table.insert(self.waypoints, wpnumber, waypoint) + + -- Debug info. + self:T(self.lid..string.format("Adding waypoint at index=%d id=%d", wpnumber, waypoint.uid)) + + -- Now we obviously did not pass the final waypoint. + self.passedfinalwp=false + + -- Switch to cruise mode. + if self:IsHolding() then + self:Cruise() + end +end + +--- Initialize Mission Editor waypoints. +-- @param #OPSGROUP self +-- @return #OPSGROUP self +function OPSGROUP:InitWaypoints() + + -- Template waypoints. + self.waypoints0=self.group:GetTemplateRoutePoints() + + -- Waypoints + self.waypoints={} + + for index,wp in pairs(self.waypoints0) do + + -- Coordinate of the waypoint. + local coordinate=COORDINATE:New(wp.x, wp.alt, wp.y) + + -- Strange! + wp.speed=wp.speed or 0 + + -- Speed at the waypoint. + local speedknots=UTILS.MpsToKnots(wp.speed) + + if index==1 then + self.speedWp=wp.speed + end + + -- Add waypoint. + self:AddWaypoint(coordinate, speedknots, index-1, nil, false) + + end + + -- Debug info. + self:T(self.lid..string.format("Initializing %d waypoints", #self.waypoints)) + + -- Update route. + if #self.waypoints>0 then + + -- Check if only 1 wp? + if #self.waypoints==1 then + self.passedfinalwp=true + end + + end + + return self +end + +--- Route group along waypoints. +-- @param #OPSGROUP self +-- @param #table waypoints Table of waypoints. +-- @param #number delay Delay in seconds. +-- @return #OPSGROUP self +function OPSGROUP:Route(waypoints, delay) + + if delay and delay>0 then + self:ScheduleOnce(delay, OPSGROUP.Route, self, waypoints) + else + + if self:IsAlive() then + + -- DCS task combo. + local Tasks={} + + -- Route (Mission) task. + local TaskRoute=self.group:TaskRoute(waypoints) + table.insert(Tasks, TaskRoute) + + -- TaskCombo of enroute and mission tasks. + local TaskCombo=self.group:TaskCombo(Tasks) + + -- Set tasks. + if #Tasks>1 then + self:SetTask(TaskCombo) + else + self:SetTask(TaskRoute) + end + + else + self:E(self.lid.."ERROR: Group is not alive! Cannot route group.") + end + end + + return self +end + + + +--- Initialize Mission Editor waypoints. +-- @param #OPSGROUP self +-- @param #number n Waypoint +function OPSGROUP:_UpdateWaypointTasks(n) + + local waypoints=self.waypoints or {} + local nwaypoints=#waypoints + + for i,_wp in pairs(waypoints) do + local wp=_wp --Ops.OpsGroup#OPSGROUP.Waypoint + + if i>=n or nwaypoints==1 then + + -- Debug info. + self:T2(self.lid..string.format("Updating waypoint task for waypoint %d/%d ID=%d. Last waypoint passed %d", i, nwaypoints, wp.uid, self.currentwp)) + + -- Tasks of this waypoint + local taskswp={} + + -- At each waypoint report passing. + local TaskPassingWaypoint=self.group:TaskFunction("OPSGROUP._PassingWaypoint", self, wp.uid) + table.insert(taskswp, TaskPassingWaypoint) + + -- Waypoint task combo. + wp.task=self.group:TaskCombo(taskswp) + + end + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Global Task Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Function called when a group is passing a waypoint. +--@param Wrapper.Group#GROUP group Group that passed the waypoint. +--@param #OPSGROUP opsgroup Ops group object. +--@param #number uid Waypoint UID. +function OPSGROUP._PassingWaypoint(group, opsgroup, uid) + + -- Get waypoint data. + local waypoint=opsgroup:GetWaypointByID(uid) + + if waypoint then + + -- Current wp. + local currentwp=opsgroup.currentwp + + -- Get the current waypoint index. + opsgroup.currentwp=opsgroup:GetWaypointIndex(uid) + + -- Set expected speed and formation from the next WP. + local wpnext=opsgroup:GetWaypointNext() + if wpnext then + + -- Set formation. + if opsgroup.isGround then + opsgroup.formation=wpnext.action + end + + -- Set speed. + opsgroup.speed=wpnext.speed + + end + + -- Debug message. + local text=string.format("Group passing waypoint uid=%d", uid) + opsgroup:T(opsgroup.lid..text) + + -- Trigger PassingWaypoint event. + if waypoint.astar then + + -- Remove Astar waypoint. + opsgroup:RemoveWaypointByID(uid) + + -- Cruise. + opsgroup:Cruise() + + elseif waypoint.detour then + + -- Remove detour waypoint. + opsgroup:RemoveWaypointByID(uid) + + if opsgroup:IsRearming() then + + -- Trigger Rearming event. + opsgroup:Rearming() + + elseif opsgroup:IsRetreating() then + + -- Trigger Retreated event. + opsgroup:Retreated() + + elseif opsgroup:IsEngaging() then + + -- Nothing to do really. + + else + + -- Trigger DetourReached event. + opsgroup:DetourReached() + + if waypoint.detour==0 then + opsgroup:FullStop() + elseif waypoint.detour==1 then + opsgroup:Cruise() + else + opsgroup:E("ERROR: waypoint.detour should be 0 or 1") + end + + end + + else + + -- Check if the group is still pathfinding. + if opsgroup.ispathfinding then + opsgroup.ispathfinding=false + end + + -- Increase passing counter. + waypoint.npassed=waypoint.npassed+1 + + -- Call event function. + opsgroup:PassingWaypoint(waypoint) + end + + end + +end + +--- Function called when a task is executed. +--@param Wrapper.Group#GROUP group Group which should execute the task. +--@param #OPSGROUP opsgroup Ops group. +--@param #OPSGROUP.Task task Task. +function OPSGROUP._TaskExecute(group, opsgroup, task) + + -- Debug message. + local text=string.format("_TaskExecute %s", task.description) + opsgroup:T3(opsgroup.lid..text) + + -- Set current task to nil so that the next in line can be executed. + if opsgroup then + opsgroup:TaskExecute(task) + end +end + +--- Function called when a task is done. +--@param Wrapper.Group#GROUP group Group for which the task is done. +--@param #OPSGROUP opsgroup Ops group. +--@param #OPSGROUP.Task task Task. +function OPSGROUP._TaskDone(group, opsgroup, task) + + -- Debug message. + local text=string.format("_TaskDone %s", task.description) + opsgroup:T3(opsgroup.lid..text) + + -- Set current task to nil so that the next in line can be executed. + if opsgroup then + opsgroup:TaskDone(task) + end +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- OPTION FUNCTIONS +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set the default ROE for the group. This is the ROE state gets when the group is spawned or to which it defaults back after a mission. +-- @param #OPSGROUP self +-- @param #number roe ROE of group. Default is `ENUMS.ROE.ReturnFire`. +-- @return #OPSGROUP self +function OPSGROUP:SetDefaultROE(roe) + self.optionDefault.ROE=roe or ENUMS.ROE.ReturnFire + return self +end + +--- Set current ROE for the group. +-- @param #OPSGROUP self +-- @param #string roe ROE of group. Default is value set in `SetDefaultROE` (usually `ENUMS.ROE.ReturnFire`). +-- @return #OPSGROUP self +function OPSGROUP:SwitchROE(roe) + + if self:IsAlive() or self:IsInUtero() then + + self.option.ROE=roe or self.optionDefault.ROE + + if self:IsInUtero() then + self:T2(self.lid..string.format("Setting current ROE=%d when GROUP is SPAWNED", self.option.ROE)) + else + + self.group:OptionROE(self.option.ROE) + + self:T(self.lid..string.format("Setting current ROE=%d (%s)", self.option.ROE, self:_GetROEName(self.option.ROE))) + end + + + else + self:E(self.lid.."WARNING: Cannot switch ROE! Group is not alive") + end + + return self +end + +--- Get name of ROE corresponding to the numerical value. +-- @param #OPSGROUP self +-- @return #string Name of ROE. +function OPSGROUP:_GetROEName(roe) + local name="unknown" + if roe==0 then + name="Weapon Free" + elseif roe==1 then + name="Open Fire/Weapon Free" + elseif roe==2 then + name="Open Fire" + elseif roe==3 then + name="Return Fire" + elseif roe==4 then + name="Weapon Hold" + end + return name +end + +--- Get current ROE of the group. +-- @param #OPSGROUP self +-- @return #number Current ROE. +function OPSGROUP:GetROE() + return self.option.ROE or self.optionDefault.ROE +end + +--- Set the default ROT for the group. This is the ROT state gets when the group is spawned or to which it defaults back after a mission. +-- @param #OPSGROUP self +-- @param #number rot ROT of group. Default is `ENUMS.ROT.PassiveDefense`. +-- @return #OPSGROUP self +function OPSGROUP:SetDefaultROT(rot) + self.optionDefault.ROT=rot or ENUMS.ROT.PassiveDefense + return self +end + +--- Set ROT for the group. +-- @param #OPSGROUP self +-- @param #string rot ROT of group. Default is value set in `:SetDefaultROT` (usually `ENUMS.ROT.PassiveDefense`). +-- @return #OPSGROUP self +function OPSGROUP:SwitchROT(rot) + + if self:IsAlive() or self:IsInUtero() then + + self.option.ROT=rot or self.optionDefault.ROT + + if self:IsInUtero() then + self:T2(self.lid..string.format("Setting current ROT=%d when GROUP is SPAWNED", self.option.ROT)) + else + + self.group:OptionROT(self.option.ROT) + + self:T(self.lid..string.format("Setting current ROT=%d (0=NoReaction, 1=Passive, 2=Evade, 3=ByPass, 4=AllowAbort)", self.option.ROT)) + end + + + else + self:E(self.lid.."WARNING: Cannot switch ROT! Group is not alive") + end + + return self +end + +--- Get current ROT of the group. +-- @param #OPSGROUP self +-- @return #number Current ROT. +function OPSGROUP:GetROT() + return self.option.ROT or self.optionDefault.ROT +end + + +--- Set the default Alarm State for the group. This is the state gets when the group is spawned or to which it defaults back after a mission. +-- @param #OPSGROUP self +-- @param #number alarmstate Alarm state of group. Default is `AI.Option.Ground.val.ALARM_STATE.AUTO` (0). +-- @return #OPSGROUP self +function OPSGROUP:SetDefaultAlarmstate(alarmstate) + self.optionDefault.Alarm=alarmstate or 0 + return self +end + +--- Set current Alarm State of the group. +-- +-- * 0 = "Auto" +-- * 1 = "Green" +-- * 2 = "Red" +-- +-- @param #OPSGROUP self +-- @param #number alarmstate Alarm state of group. Default is 0="Auto". +-- @return #OPSGROUP self +function OPSGROUP:SwitchAlarmstate(alarmstate) + + if self:IsAlive() or self:IsInUtero() then + + if self.isArmygroup or self.isNavygroup then + + self.option.Alarm=alarmstate or self.optionDefault.Alarm + + if self:IsInUtero() then + self:T2(self.lid..string.format("Setting current Alarm State=%d when GROUP is SPAWNED", self.option.Alarm)) + else + + if self.option.Alarm==0 then + self.group:OptionAlarmStateAuto() + elseif self.option.Alarm==1 then + self.group:OptionAlarmStateGreen() + elseif self.option.Alarm==2 then + self.group:OptionAlarmStateRed() + else + self:E("ERROR: Unknown Alarm State! Setting to AUTO") + self.group:OptionAlarmStateAuto() + self.option.Alarm=0 + end + + self:T(self.lid..string.format("Setting current Alarm State=%d (0=Auto, 1=Green, 2=Red)", self.option.Alarm)) + + end + + end + else + self:E(self.lid.."WARNING: Cannot switch Alarm State! Group is not alive.") + end + + return self +end + +--- Get current Alarm State of the group. +-- @param #OPSGROUP self +-- @return #number Current Alarm State. +function OPSGROUP:GetAlarmstate() + return self.option.Alarm or self.optionDefault.Alarm +end + +--- Set default TACAN parameters. +-- @param #OPSGROUP self +-- @param #number Channel TACAN channel. Default is 74. +-- @param #string Morse Morse code. Default "XXX". +-- @param #string UnitName Name of the unit acting as beacon. +-- @param #string Band TACAN mode. Default is "X" for ground and "Y" for airborne units. +-- @param #boolean OffSwitch If true, TACAN is off by default. +-- @return #OPSGROUP self +function OPSGROUP:SetDefaultTACAN(Channel, Morse, UnitName, Band, OffSwitch) + + self.tacanDefault={} + self.tacanDefault.Channel=Channel or 74 + self.tacanDefault.Morse=Morse or "XXX" + self.tacanDefault.BeaconName=UnitName + + if self.isAircraft then + Band=Band or "Y" + else + Band=Band or "X" + end + self.tacanDefault.Band=Band + + + if OffSwitch then + self.tacanDefault.On=false + else + self.tacanDefault.On=true + end + + return self +end + + +--- Activate/switch TACAN beacon settings. +-- @param #OPSGROUP self +-- @param #OPSGROUP.Beacon Tacan TACAN data table. Default is the default TACAN settings. +-- @return #OPSGROUP self +function OPSGROUP:_SwitchTACAN(Tacan) + + if Tacan then + + self:SwitchTACAN(Tacan.Channel, Tacan.Morse, Tacan.BeaconName, Tacan.Band) + + else + + if self.tacanDefault.On then + self:SwitchTACAN() + else + self:TurnOffTACAN() + end + + end + +end + +--- Activate/switch TACAN beacon settings. +-- @param #OPSGROUP self +-- @param #number Channel TACAN Channel. +-- @param #string Morse TACAN morse code. Default is the value set in @{#OPSGROUP.SetDefaultTACAN} or if not set "XXX". +-- @param #string UnitName Name of the unit in the group which should activate the TACAN beacon. Can also be given as #number to specify the unit number. Default is the first unit of the group. +-- @param #string Band TACAN channel mode "X" or "Y". Default is "Y" for aircraft and "X" for ground and naval groups. +-- @return #OPSGROUP self +function OPSGROUP:SwitchTACAN(Channel, Morse, UnitName, Band) + + if self:IsInUtero() then + + self:T(self.lid..string.format("Switching TACAN to DEFAULT when group is spawned")) + self:SetDefaultTACAN(Channel, Morse, UnitName, Band) + + elseif self:IsAlive() then + + Channel=Channel or self.tacanDefault.Channel + Morse=Morse or self.tacanDefault.Morse + Band=Band or self.tacanDefault.Band + UnitName=UnitName or self.tacanDefault.BeaconName + local unit=self:GetUnit(1) --Wrapper.Unit#UNIT + + if UnitName then + if type(UnitName)=="number" then + unit=self.group:GetUnit(UnitName) + else + unit=UNIT:FindByName(UnitName) + end + end + + if not unit then + self:T(self.lid.."WARNING: Could not get TACAN unit. Trying first unit in the group") + unit=self:GetUnit(1) + end + + if unit and unit:IsAlive() then + + -- Unit ID. + local UnitID=unit:GetID() + + -- Type + local Type=BEACON.Type.TACAN + + -- System + local System=BEACON.System.TACAN + if self.isAircraft then + System=BEACON.System.TACAN_TANKER_Y + end + + -- Tacan frequency. + local Frequency=UTILS.TACANToFrequency(Channel, Band) + + -- Activate beacon. + unit:CommandActivateBeacon(Type, System, Frequency, UnitID, Channel, Band, true, Morse, true) + + -- Update info. + self.tacan.Channel=Channel + self.tacan.Morse=Morse + self.tacan.Band=Band + self.tacan.BeaconName=unit:GetName() + self.tacan.BeaconUnit=unit + self.tacan.On=true + + -- Debug info. + self:T(self.lid..string.format("Switching TACAN to Channel %d%s Morse %s on unit %s", self.tacan.Channel, self.tacan.Band, tostring(self.tacan.Morse), self.tacan.BeaconName)) + + else + self:E(self.lid.."ERROR: Cound not set TACAN! Unit is not alive") + end + + else + self:E(self.lid.."ERROR: Cound not set TACAN! Group is not alive and not in utero any more") + end + + return self +end + +--- Deactivate TACAN beacon. +-- @param #OPSGROUP self +-- @return #OPSGROUP self +function OPSGROUP:TurnOffTACAN() + + if self.tacan.BeaconUnit and self.tacan.BeaconUnit:IsAlive() then + self.tacan.BeaconUnit:CommandDeactivateBeacon() + end + + self:T(self.lid..string.format("Switching TACAN OFF")) + self.tacan.On=false + +end + +--- Get current TACAN parameters. +-- @param #OPSGROUP self +-- @return #number TACAN channel. +-- @return #string TACAN Morse code. +-- @return #string TACAN band ("X" or "Y"). +-- @return #boolean TACAN is On (true) or Off (false). +-- @return #string UnitName Name of the unit acting as beacon. +function OPSGROUP:GetTACAN() + return self.tacan.Channel, self.tacan.Morse, self.tacan.Band, self.tacan.On, self.tacan.BeaconName +end + + + +--- Set default ICLS parameters. +-- @param #OPSGROUP self +-- @param #number Channel ICLS channel. Default is 1. +-- @param #string Morse Morse code. Default "XXX". +-- @param #string UnitName Name of the unit acting as beacon. +-- @param #boolean OffSwitch If true, TACAN is off by default. +-- @return #OPSGROUP self +function OPSGROUP:SetDefaultICLS(Channel, Morse, UnitName, OffSwitch) + + self.iclsDefault={} + self.iclsDefault.Channel=Channel or 1 + self.iclsDefault.Morse=Morse or "XXX" + self.iclsDefault.BeaconName=UnitName + + if OffSwitch then + self.iclsDefault.On=false + else + self.iclsDefault.On=true + end + + return self +end + + +--- Activate/switch ICLS beacon settings. +-- @param #OPSGROUP self +-- @param #OPSGROUP.Beacon Icls ICLS data table. +-- @return #OPSGROUP self +function OPSGROUP:_SwitchICLS(Icls) + + if Icls then + + self:SwitchICLS(Icls.Channel, Icls.Morse, Icls.BeaconName) + + else + + if self.iclsDefault.On then + self:SwitchICLS() + else + self:TurnOffICLS() + end + + end + +end + +--- Activate/switch ICLS beacon settings. +-- @param #OPSGROUP self +-- @param #number Channel ICLS Channel. Default is what is set in `SetDefaultICLS()` so usually channel 1. +-- @param #string Morse ICLS morse code. Default is what is set in `SetDefaultICLS()` so usually "XXX". +-- @param #string UnitName Name of the unit in the group which should activate the ICLS beacon. Can also be given as #number to specify the unit number. Default is the first unit of the group. +-- @return #OPSGROUP self +function OPSGROUP:SwitchICLS(Channel, Morse, UnitName) + + if self:IsInUtero() then + + self:SetDefaultICLS(Channel,Morse,UnitName) + + self:T2(self.lid..string.format("Switching ICLS to Channel %d Morse %s on unit %s when GROUP is SPAWNED", self.iclsDefault.Channel, tostring(self.iclsDefault.Morse), tostring(self.iclsDefault.BeaconName))) + + elseif self:IsAlive() then + + Channel=Channel or self.iclsDefault.Channel + Morse=Morse or self.iclsDefault.Morse + local unit=self:GetUnit(1) --Wrapper.Unit#UNIT + + if UnitName then + if type(UnitName)=="number" then + unit=self:GetUnit(UnitName) + else + unit=UNIT:FindByName(UnitName) + end + end + + if not unit then + self:T(self.lid.."WARNING: Could not get ICLS unit. Trying first unit in the group") + unit=self:GetUnit(1) + end + + if unit and unit:IsAlive() then + + -- Unit ID. + local UnitID=unit:GetID() + + -- Activate beacon. + unit:CommandActivateICLS(Channel, UnitID, Morse) + + -- Update info. + self.icls.Channel=Channel + self.icls.Morse=Morse + self.icls.Band=nil + self.icls.BeaconName=unit:GetName() + self.icls.BeaconUnit=unit + self.icls.On=true + + -- Debug info. + self:T(self.lid..string.format("Switching ICLS to Channel %d Morse %s on unit %s", self.icls.Channel, tostring(self.icls.Morse), self.icls.BeaconName)) + + else + self:E(self.lid.."ERROR: Cound not set ICLS! Unit is not alive.") + end + + end + + return self +end + +--- Deactivate ICLS beacon. +-- @param #OPSGROUP self +-- @return #OPSGROUP self +function OPSGROUP:TurnOffICLS() + + if self.icls.BeaconUnit and self.icls.BeaconUnit:IsAlive() then + self.icls.BeaconUnit:CommandDeactivateICLS() + end + + self:T(self.lid..string.format("Switching ICLS OFF")) + self.icls.On=false + +end + + +--- Set default Radio frequency and modulation. +-- @param #OPSGROUP self +-- @param #number Frequency Radio frequency in MHz. Default 251 MHz. +-- @param #number Modulation Radio modulation. Default `radio.Modulation.AM`. +-- @param #boolean OffSwitch If true, radio is OFF by default. +-- @return #OPSGROUP self +function OPSGROUP:SetDefaultRadio(Frequency, Modulation, OffSwitch) + + self.radioDefault={} + self.radioDefault.Freq=Frequency or 251 + self.radioDefault.Modu=Modulation or radio.modulation.AM + if OffSwitch then + self.radioDefault.On=false + else + self.radioDefault.On=true + end + + return self +end + +--- Get current Radio frequency and modulation. +-- @param #OPSGROUP self +-- @return #number Radio frequency in MHz or nil. +-- @return #number Radio modulation or nil. +-- @return #boolean If true, the radio is on. Otherwise, radio is turned off. +function OPSGROUP:GetRadio() + return self.radio.Freq, self.radio.Modu, self.radio.On +end + +--- Turn radio on or switch frequency/modulation. +-- @param #OPSGROUP self +-- @param #number Frequency Radio frequency in MHz. Default is value set in `SetDefaultRadio` (usually 251 MHz). +-- @param #number Modulation Radio modulation. Default is value set in `SetDefaultRadio` (usually `radio.Modulation.AM`). +-- @return #OPSGROUP self +function OPSGROUP:SwitchRadio(Frequency, Modulation) + + if self:IsInUtero() then + + -- Set default radio. + self:SetDefaultRadio(Frequency, Modulation) + + -- Debug info. + self:T2(self.lid..string.format("Switching radio to frequency %.3f MHz %s when GROUP is SPAWNED", self.radioDefault.Freq, UTILS.GetModulationName(self.radioDefault.Modu))) + + elseif self:IsAlive() then + + Frequency=Frequency or self.radioDefault.Freq + Modulation=Modulation or self.radioDefault.Modu + + if self.isAircraft and not self.radio.On then + self.group:SetOption(AI.Option.Air.id.SILENCE, false) + end + + -- Give command + self.group:CommandSetFrequency(Frequency, Modulation) + + -- Update current settings. + self.radio.Freq=Frequency + self.radio.Modu=Modulation + self.radio.On=true + + -- Debug info. + self:T(self.lid..string.format("Switching radio to frequency %.3f MHz %s", self.radio.Freq, UTILS.GetModulationName(self.radio.Modu))) + + else + self:E(self.lid.."ERROR: Cound not set Radio! Group is not alive or not in utero any more") + end + + return self +end + +--- Turn radio off. +-- @param #OPSGROUP self +-- @return #OPSGROUP self +function OPSGROUP:TurnOffRadio() + + if self:IsAlive() then + + if self.isAircraft then + + -- Set group to be silient. + self.group:SetOption(AI.Option.Air.id.SILENCE, true) + + -- Radio is off. + self.radio.On=false + + self:T(self.lid..string.format("Switching radio OFF")) + else + self:E(self.lid.."ERROR: Radio can only be turned off for aircraft!") + end + + end + + return self +end + + + +--- Set default formation. +-- @param #OPSGROUP self +-- @param #number Formation The formation the groups flies in. +-- @return #OPSGROUP self +function OPSGROUP:SetDefaultFormation(Formation) + + self.optionDefault.Formation=Formation + + return self +end + +--- Switch to a specific formation. +-- @param #OPSGROUP self +-- @param #number Formation New formation the group will fly in. Default is the setting of `SetDefaultFormation()`. +-- @return #OPSGROUP self +function OPSGROUP:SwitchFormation(Formation) + + if self:IsAlive() then + + Formation=Formation or self.optionDefault.Formation + + if self.isAircraft then + + self.group:SetOption(AI.Option.Air.id.FORMATION, Formation) + + elseif self.isGround then + + -- Polymorphic and overwritten in ARMYGROUP. + + else + self:E(self.lid.."ERROR: Formation can only be set for aircraft or ground units!") + return self + end + + -- Set current formation. + self.option.Formation=Formation + + -- Debug info. + self:T(self.lid..string.format("Switching formation to %d", self.option.Formation)) + + end + + return self +end + + + +--- Set default callsign. +-- @param #OPSGROUP self +-- @param #number CallsignName Callsign name. +-- @param #number CallsignNumber Callsign number. +-- @return #OPSGROUP self +function OPSGROUP:SetDefaultCallsign(CallsignName, CallsignNumber) + + self.callsignDefault={} + self.callsignDefault.NumberSquad=CallsignName + self.callsignDefault.NumberGroup=CallsignNumber or 1 + + return self +end + +--- Switch to a specific callsign. +-- @param #OPSGROUP self +-- @param #number CallsignName Callsign name. +-- @param #number CallsignNumber Callsign number. +-- @return #OPSGROUP self +function OPSGROUP:SwitchCallsign(CallsignName, CallsignNumber) + + if self:IsInUtero() then + + -- Set default callsign. We switch to this when group is spawned. + self:SetDefaultCallsign(CallsignName, CallsignNumber) + + elseif self:IsAlive() then + + CallsignName=CallsignName or self.callsignDefault.NumberSquad + CallsignNumber=CallsignNumber or self.callsignDefault.NumberGroup + + -- Set current callsign. + self.callsign.NumberSquad=CallsignName + self.callsign.NumberGroup=CallsignNumber + + -- Debug. + self:T(self.lid..string.format("Switching callsign to %d-%d", self.callsign.NumberSquad, self.callsign.NumberGroup)) + + -- Give command to change the callsign. + self.group:CommandSetCallsign(self.callsign.NumberSquad, self.callsign.NumberGroup) + + else + --TODO: Error + end + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Element and Group Status Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check if all elements of the group have the same status (or are dead). +-- @param #OPSGROUP self +-- @return #OPSGROUP self +function OPSGROUP:_UpdatePosition() + + if self:IsAlive() then + + -- Backup last state to monitor differences. + self.positionLast=self.position or self:GetVec3() + self.headingLast=self.heading or self:GetHeading() + self.orientXLast=self.orientX or self:GetOrientationX() + self.velocityLast=self.velocity or self.group:GetVelocityMPS() + + -- Current state. + self.position=self:GetVec3() + self.heading=self:GetHeading() + self.orientX=self:GetOrientationX() + self.velocity=self:GetVelocity() + + -- Update time. + local Tnow=timer.getTime() + self.dTpositionUpdate=self.TpositionUpdate and Tnow-self.TpositionUpdate or 0 + self.TpositionUpdate=Tnow + + if not self.traveldist then + self.traveldist=0 + end + + self.travelds=UTILS.VecNorm(UTILS.VecSubstract(self.position, self.positionLast)) + + -- Add up travelled distance. + + self.traveldist=self.traveldist+self.travelds + + -- Debug info. + --env.info(string.format("FF Traveled %.1f m", self.traveldist)) + + end + + return self +end + +--- Check if all elements of the group have the same status (or are dead). +-- @param #OPSGROUP self +-- @param #string unitname Name of unit. +function OPSGROUP:_AllSameStatus(status) + + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + + if element.status==OPSGROUP.ElementStatus.DEAD then + -- Do nothing. Element is already dead and does not count. + elseif element.status~=status then + -- At least this element has a different status. + return false + end + + end + + return true +end + +--- Check if all elements of the group have the same status (or are dead). +-- @param #OPSGROUP self +-- @param #string status Status to check. +-- @return #boolean If true, all elements have a similar status. +function OPSGROUP:_AllSimilarStatus(status) + + -- Check if all are dead. + if status==OPSGROUP.ElementStatus.DEAD then + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + if element.status~=OPSGROUP.ElementStatus.DEAD then + -- At least one is still alive. + return false + end + end + return true + end + + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + + self:T2(self.lid..string.format("Status=%s, element %s status=%s", status, element.name, element.status)) + + -- Dead units dont count ==> We wont return false for those. + if element.status~=OPSGROUP.ElementStatus.DEAD then + + ---------- + -- ALIVE + ---------- + + if status==OPSGROUP.ElementStatus.SPAWNED then + + -- Element SPAWNED: Check that others are not still IN UTERO + if element.status~=status and + element.status==OPSGROUP.ElementStatus.INUTERO then + return false + end + + elseif status==OPSGROUP.ElementStatus.PARKING then + + -- Element PARKING: Check that the other are not still SPAWNED + if element.status~=status or + (element.status==OPSGROUP.ElementStatus.INUTERO or + element.status==OPSGROUP.ElementStatus.SPAWNED) then + return false + end + + elseif status==OPSGROUP.ElementStatus.ENGINEON then + + -- Element TAXIING: Check that the other are not still SPAWNED or PARKING + if element.status~=status and + (element.status==OPSGROUP.ElementStatus.INUTERO or + element.status==OPSGROUP.ElementStatus.SPAWNED or + element.status==OPSGROUP.ElementStatus.PARKING) then + return false + end + + elseif status==OPSGROUP.ElementStatus.TAXIING then + + -- Element TAXIING: Check that the other are not still SPAWNED or PARKING + if element.status~=status and + (element.status==OPSGROUP.ElementStatus.INUTERO or + element.status==OPSGROUP.ElementStatus.SPAWNED or + element.status==OPSGROUP.ElementStatus.PARKING or + element.status==OPSGROUP.ElementStatus.ENGINEON) then + return false + end + + elseif status==OPSGROUP.ElementStatus.TAKEOFF then + + -- Element TAKEOFF: Check that the other are not still SPAWNED, PARKING or TAXIING + if element.status~=status and + (element.status==OPSGROUP.ElementStatus.INUTERO or + element.status==OPSGROUP.ElementStatus.SPAWNED or + element.status==OPSGROUP.ElementStatus.PARKING or + element.status==OPSGROUP.ElementStatus.ENGINEON or + element.status==OPSGROUP.ElementStatus.TAXIING) then + return false + end + + elseif status==OPSGROUP.ElementStatus.AIRBORNE then + + -- Element AIRBORNE: Check that the other are not still SPAWNED, PARKING, TAXIING or TAKEOFF + if element.status~=status and + (element.status==OPSGROUP.ElementStatus.INUTERO or + element.status==OPSGROUP.ElementStatus.SPAWNED or + element.status==OPSGROUP.ElementStatus.PARKING or + element.status==OPSGROUP.ElementStatus.ENGINEON or + element.status==OPSGROUP.ElementStatus.TAXIING or + element.status==OPSGROUP.ElementStatus.TAKEOFF) then + return false + end + + elseif status==OPSGROUP.ElementStatus.LANDED then + + -- Element LANDED: check that the others are not still AIRBORNE or LANDING + if element.status~=status and + (element.status==OPSGROUP.ElementStatus.AIRBORNE or + element.status==OPSGROUP.ElementStatus.LANDING) then + return false + end + + elseif status==OPSGROUP.ElementStatus.ARRIVED then + + -- Element ARRIVED: check that the others are not still AIRBORNE, LANDING, or LANDED (taxiing). + if element.status~=status and + (element.status==OPSGROUP.ElementStatus.AIRBORNE or + element.status==OPSGROUP.ElementStatus.LANDING or + element.status==OPSGROUP.ElementStatus.LANDED) then + return false + end + + end + + else + -- Element is dead. We don't care unless all are dead. + end --DEAD + + end + + -- Debug info. + self:T2(self.lid..string.format("All %d elements have similar status %s ==> returning TRUE", #self.elements, status)) + + return true +end + +--- Check if all elements of the group have the same status or are dead. +-- @param #OPSGROUP self +-- @param #OPSGROUP.Element element Element. +-- @param #string newstatus New status of element +-- @param Wrapper.Airbase#AIRBASE airbase Airbase if applicable. +function OPSGROUP:_UpdateStatus(element, newstatus, airbase) + + -- Old status. + local oldstatus=element.status + + -- Update status of element. + element.status=newstatus + + -- Debug + self:T3(self.lid..string.format("UpdateStatus element=%s: %s --> %s", element.name, oldstatus, newstatus)) + for _,_element in pairs(self.elements) do + local Element=_element -- #OPSGROUP.Element + self:T3(self.lid..string.format("Element %s: %s", Element.name, Element.status)) + end + + if newstatus==OPSGROUP.ElementStatus.SPAWNED then + --- + -- SPAWNED + --- + + if self:_AllSimilarStatus(newstatus) then + self:__Spawned(-0.5) + end + + elseif newstatus==OPSGROUP.ElementStatus.PARKING then + --- + -- PARKING + --- + + if self:_AllSimilarStatus(newstatus) then + self:__Parking(-0.5) + end + + elseif newstatus==OPSGROUP.ElementStatus.ENGINEON then + --- + -- ENGINEON + --- + + -- No FLIGHT status. Waiting for taxiing. + + elseif newstatus==OPSGROUP.ElementStatus.TAXIING then + --- + -- TAXIING + --- + + if self:_AllSimilarStatus(newstatus) then + self:__Taxiing(-0.5) + end + + elseif newstatus==OPSGROUP.ElementStatus.TAKEOFF then + --- + -- TAKEOFF + --- + + if self:_AllSimilarStatus(newstatus) then + -- Trigger takeoff event. Also triggers airborne event. + self:__Takeoff(-0.5, airbase) + end + + elseif newstatus==OPSGROUP.ElementStatus.AIRBORNE then + --- + -- AIRBORNE + --- + + if self:_AllSimilarStatus(newstatus) then + self:__Airborne(-0.5) + end + + elseif newstatus==OPSGROUP.ElementStatus.LANDED then + --- + -- LANDED + --- + + if self:_AllSimilarStatus(newstatus) then + if self:IsLandingAt() then + self:LandedAt() + else + self:Landed(airbase) + end + end + + elseif newstatus==OPSGROUP.ElementStatus.ARRIVED then + --- + -- ARRIVED + --- + + if self:_AllSimilarStatus(newstatus) then + + if self:IsLanded() then + self:Arrived() + elseif self:IsAirborne() then + self:Landed() + self:Arrived() + end + + end + + elseif newstatus==OPSGROUP.ElementStatus.DEAD then + --- + -- DEAD + --- + + if self:_AllSimilarStatus(newstatus) then + self:__Dead(-1) + end + + end +end + +--- Set status for all elements (except dead ones). +-- @param #OPSGROUP self +-- @param #string status Element status. +function OPSGROUP:_SetElementStatusAll(status) + + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + if element.status~=OPSGROUP.ElementStatus.DEAD then + element.status=status + end + end + +end + +--- Get the element of a group. +-- @param #OPSGROUP self +-- @param #string unitname Name of unit. +-- @return #OPSGROUP.Element The element. +function OPSGROUP:GetElementByName(unitname) + + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + + if element.name==unitname then + return element + end + + end + + return nil +end + +--- Get the first element of a group, which is alive. +-- @param #OPSGROUP self +-- @return #OPSGROUP.Element The element or `#nil` if no element is alive any more. +function OPSGROUP:GetElementAlive() + + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + if element.status~=OPSGROUP.ElementStatus.DEAD then + if element.unit and element.unit:IsAlive() then + return element + end + end + end + + return nil +end + +--- Get number of elements alive. +-- @param #OPSGROUP self +-- @param #string status (Optional) Only count number, which are in a special status. +-- @return #number Number of elements. +function OPSGROUP:GetNelements(status) + + local n=0 + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + if element.status~=OPSGROUP.ElementStatus.DEAD then + if element.unit and element.unit:IsAlive() then + if status==nil or element.status==status then + n=n+1 + end + end + end + end + + + return n +end + +--- Get the number of shells a unit or group currently has. For a group the ammo count of all units is summed up. +-- @param #OPSGROUP self +-- @param #OPSGROUP.Element element The element. +-- @return #OPSGROUP.Ammo Ammo data. +function OPSGROUP:GetAmmoElement(element) + return self:GetAmmoUnit(element.unit) +end + +--- Get total amount of ammunition of the whole group. +-- @param #OPSGROUP self +-- @return #OPSGROUP.Ammo Ammo data. +function OPSGROUP:GetAmmoTot() + + local units=self.group:GetUnits() + + local Ammo={} --#OPSGROUP.Ammo + Ammo.Total=0 + Ammo.Guns=0 + Ammo.Rockets=0 + Ammo.Bombs=0 + Ammo.Torpedos=0 + Ammo.Missiles=0 + Ammo.MissilesAA=0 + Ammo.MissilesAG=0 + Ammo.MissilesAS=0 + Ammo.MissilesCR=0 + Ammo.MissilesSA=0 + + for _,_unit in pairs(units) do + local unit=_unit --Wrapper.Unit#UNIT + + if unit and unit:IsAlive()~=nil then + + -- Get ammo of the unit. + local ammo=self:GetAmmoUnit(unit) + + -- Add up total. + Ammo.Total=Ammo.Total+ammo.Total + Ammo.Guns=Ammo.Guns+ammo.Guns + Ammo.Rockets=Ammo.Rockets+ammo.Rockets + Ammo.Bombs=Ammo.Bombs+ammo.Bombs + Ammo.Torpedos=Ammo.Torpedos+ammo.Torpedos + Ammo.Missiles=Ammo.Missiles+ammo.Missiles + Ammo.MissilesAA=Ammo.MissilesAA+ammo.MissilesAA + Ammo.MissilesAG=Ammo.MissilesAG+ammo.MissilesAG + Ammo.MissilesAS=Ammo.MissilesAS+ammo.MissilesAS + Ammo.MissilesCR=Ammo.MissilesCR+ammo.MissilesCR + Ammo.MissilesSA=Ammo.MissilesSA+ammo.MissilesSA + + end + + end + + return Ammo +end + +--- Get the number of shells a unit or group currently has. For a group the ammo count of all units is summed up. +-- @param #OPSGROUP self +-- @param Wrapper.Unit#UNIT unit The unit object. +-- @param #boolean display Display ammo table as message to all. Default false. +-- @return #OPSGROUP.Ammo Ammo data. +function OPSGROUP:GetAmmoUnit(unit, display) + + -- Default is display false. + if display==nil then + display=false + end + + -- Init counter. + local nammo=0 + local nshells=0 + local nrockets=0 + local nmissiles=0 + local nmissilesAA=0 + local nmissilesAG=0 + local nmissilesAS=0 + local nmissilesSA=0 + local nmissilesBM=0 + local nmissilesCR=0 + local ntorps=0 + local nbombs=0 + + -- Output. + local text=string.format("OPSGROUP group %s - unit %s:\n", self.groupname, unit:GetName()) + + -- Get ammo table. + local ammotable=unit:GetAmmo() + + if ammotable then + + local weapons=#ammotable + + -- Loop over all weapons. + for w=1,weapons do + + -- Number of current weapon. + local Nammo=ammotable[w]["count"] + + -- Type name of current weapon. + local Tammo=ammotable[w]["desc"]["typeName"] + + local _weaponString = UTILS.Split(Tammo,"%.") + local _weaponName = _weaponString[#_weaponString] + + -- Get the weapon category: shell=0, missile=1, rocket=2, bomb=3, torpedo=4 + local Category=ammotable[w].desc.category + + -- Get missile category: Weapon.MissileCategory AAM=1, SAM=2, BM=3, ANTI_SHIP=4, CRUISE=5, OTHER=6 + local MissileCategory=nil + if Category==Weapon.Category.MISSILE then + MissileCategory=ammotable[w].desc.missileCategory + end + + -- We are specifically looking for shells or rockets here. + if Category==Weapon.Category.SHELL then + + -- Add up all shells. + nshells=nshells+Nammo + + -- Debug info. + text=text..string.format("- %d shells of type %s\n", Nammo, _weaponName) + + elseif Category==Weapon.Category.ROCKET then + + -- Add up all rockets. + nrockets=nrockets+Nammo + + -- Debug info. + text=text..string.format("- %d rockets of type %s\n", Nammo, _weaponName) + + elseif Category==Weapon.Category.BOMB then + + -- Add up all rockets. + nbombs=nbombs+Nammo + + -- Debug info. + text=text..string.format("- %d bombs of type %s\n", Nammo, _weaponName) + + elseif Category==Weapon.Category.MISSILE then + + -- Add up all cruise missiles (category 5) + if MissileCategory==Weapon.MissileCategory.AAM then + nmissiles=nmissiles+Nammo + nmissilesAA=nmissilesAA+Nammo + elseif MissileCategory==Weapon.MissileCategory.SAM then + nmissiles=nmissiles+Nammo + nmissilesSA=nmissilesSA+Nammo + elseif MissileCategory==Weapon.MissileCategory.ANTI_SHIP then + nmissiles=nmissiles+Nammo + nmissilesAS=nmissilesAS+Nammo + elseif MissileCategory==Weapon.MissileCategory.BM then + nmissiles=nmissiles+Nammo + nmissilesAG=nmissilesAG+Nammo + elseif MissileCategory==Weapon.MissileCategory.CRUISE then + nmissiles=nmissiles+Nammo + nmissilesCR=nmissilesCR+Nammo + elseif MissileCategory==Weapon.MissileCategory.OTHER then + nmissiles=nmissiles+Nammo + nmissilesAG=nmissilesAG+Nammo + end + + -- Debug info. + text=text..string.format("- %d %s missiles of type %s\n", Nammo, self:_MissileCategoryName(MissileCategory), _weaponName) + + elseif Category==Weapon.Category.TORPEDO then + + -- Add up all rockets. + ntorps=ntorps+Nammo + + -- Debug info. + text=text..string.format("- %d torpedos of type %s\n", Nammo, _weaponName) + + else + + -- Debug info. + text=text..string.format("- %d unknown ammo of type %s (category=%d, missile category=%s)\n", Nammo, Tammo, Category, tostring(MissileCategory)) + + end + + end + end + + -- Debug text and send message. + if display then + self:I(self.lid..text) + else + self:T3(self.lid..text) + end + + -- Total amount of ammunition. + nammo=nshells+nrockets+nmissiles+nbombs+ntorps + + local ammo={} --#OPSGROUP.Ammo + ammo.Total=nammo + ammo.Guns=nshells + ammo.Rockets=nrockets + ammo.Bombs=nbombs + ammo.Torpedos=ntorps + ammo.Missiles=nmissiles + ammo.MissilesAA=nmissilesAA + ammo.MissilesAG=nmissilesAG + ammo.MissilesAS=nmissilesAS + ammo.MissilesCR=nmissilesCR + ammo.MissilesBM=nmissilesBM + ammo.MissilesSA=nmissilesSA + + return ammo +end + +--- Returns a name of a missile category. +-- @param #OPSGROUP self +-- @param #number categorynumber Number of missile category from weapon missile category enumerator. See https://wiki.hoggitworld.com/view/DCS_Class_Weapon +-- @return #string Missile category name. +function OPSGROUP:_MissileCategoryName(categorynumber) + local cat="unknown" + if categorynumber==Weapon.MissileCategory.AAM then + cat="air-to-air" + elseif categorynumber==Weapon.MissileCategory.SAM then + cat="surface-to-air" + elseif categorynumber==Weapon.MissileCategory.BM then + cat="ballistic" + elseif categorynumber==Weapon.MissileCategory.ANTI_SHIP then + cat="anti-ship" + elseif categorynumber==Weapon.MissileCategory.CRUISE then + cat="cruise" + elseif categorynumber==Weapon.MissileCategory.OTHER then + cat="other" + end + return cat +end + +--- Get coordinate from an object. +-- @param #OPSGROUP self +-- @param Wrapper.Object#OBJECT Object The object. +-- @return Core.Point#COORDINATE The coordinate of the object. +function OPSGROUP:_CoordinateFromObject(Object) + + if Object:IsInstanceOf("COORDINATE") then + return Object + else + if Object:IsInstanceOf("POSITIONABLE") or Object:IsInstanceOf("ZONE_BASE") then + self:T(self.lid.."WARNING: Coordinate is not a COORDINATE but a POSITIONABLE or ZONE. Trying to get coordinate") + return Object:GetCoordinate() + else + self:E(self.lid.."ERROR: Coordinate is neither a COORDINATE nor any POSITIONABLE or ZONE!") + end + end + + return nil +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- **Ops** - Enhanced Airborne Group. +-- +-- ## Main Features: +-- +-- * Monitor flight status of elements and/or the entire group +-- * Monitor fuel and ammo status +-- * Conveniently set radio freqencies, TACAN, ROE etc +-- * Order helos to land at specifc coordinates +-- * Dynamically add and remove waypoints +-- * Sophisticated task queueing system (know when DCS tasks start and end) +-- * Convenient checks when the group enters or leaves a zone +-- * Detection events for new, known and lost units +-- * Simple LASER and IR-pointer setup +-- * Compatible with AUFTRAG class +-- * Many additional events that the mission designer can hook into +-- +-- === +-- +-- ## Example Missions: +-- +-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/OPS%20-%20Flightgroup). +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- === +-- @module Ops.FlightGroup +-- @image OPS_FlightGroup.png + + +--- FLIGHTGROUP class. +-- @type FLIGHTGROUP +-- @field Wrapper.Airbase#AIRBASE homebase The home base of the flight group. +-- @field Wrapper.Airbase#AIRBASE destbase The destination base of the flight group. +-- @field Core.Zone#ZONE homezone The home zone of the flight group. Set when spawn happens in air. +-- @field Core.Zone#ZONE destzone The destination zone of the flight group. Set when final waypoint is in air. +-- @field #string actype Type name of the aircraft. +-- @field #number rangemax Max range in km. +-- @field #number ceiling Max altitude the aircraft can fly at in meters. +-- @field #number tankertype The refueling system type (0=boom, 1=probe), if the group is a tanker. +-- @field #number refueltype The refueling system type (0=boom, 1=probe), if the group can refuel from a tanker. +-- @field Ops.OpsGroup#OPSGROUP.Ammo ammo Ammunition data. Number of Guns, Rockets, Bombs, Missiles. +-- @field #boolean ai If true, flight is purely AI. If false, flight contains at least one human player. +-- @field #boolean fuellow Fuel low switch. +-- @field #number fuellowthresh Low fuel threshold in percent. +-- @field #boolean fuellowrtb RTB on low fuel switch. +-- @field #boolean fuelcritical Fuel critical switch. +-- @field #number fuelcriticalthresh Critical fuel threshold in percent. +-- @field #boolean fuelcriticalrtb RTB on critical fuel switch. +-- @field Ops.Squadron#SQUADRON squadron The squadron of this flight group. +-- @field Ops.AirWing#AIRWING airwing The airwing the flight group belongs to. +-- @field Ops.FlightControl#FLIGHTCONTROL flightcontrol The flightcontrol handling this group. +-- @field Ops.Airboss#AIRBOSS airboss The airboss handling this group. +-- @field Core.UserFlag#USERFLAG flaghold Flag for holding. +-- @field #number Tholding Abs. mission time stamp when the group reached the holding point. +-- @field #number Tparking Abs. mission time stamp when the group was spawned uncontrolled and is parking. +-- @field #table menu F10 radio menu. +-- @field #string controlstatus Flight control status. +-- @field #boolean ishelo If true, the is a helicopter group. +-- @field #number callsignName Callsign name. +-- @field #number callsignNumber Callsign number. +-- @field #boolean despawnAfterLanding If true, group is despawned after landed at an airbase. +-- +-- @extends Ops.OpsGroup#OPSGROUP + +--- *To invent an airplane is nothing; to build one is something; to fly is everything.* -- Otto Lilienthal +-- +-- === +-- +-- ![Banner Image](..\Presentations\OPS\FlightGroup\_Main.png) +-- +-- # The FLIGHTGROUP Concept +-- +-- # Events +-- +-- This class introduces a lot of additional events that will be handy in many situations. +-- Certain events like landing, takeoff etc. are triggered for each element and also have a corresponding event when the whole group reaches this state. +-- +-- ## Spawning +-- +-- ## Parking +-- +-- ## Taxiing +-- +-- ## Takeoff +-- +-- ## Airborne +-- +-- ## Landed +-- +-- ## Arrived +-- +-- ## Dead +-- +-- ## Fuel +-- +-- ## Ammo +-- +-- ## Detected Units +-- +-- ## Check In Zone +-- +-- ## Passing Waypoint +-- +-- +-- # Tasking +-- +-- The FLIGHTGROUP class significantly simplifies the monitoring of DCS tasks. Two types of tasks can be set +-- +-- * **Scheduled Tasks** +-- * **Waypoint Tasks** +-- +-- ## Scheduled Tasks +-- +-- ## Waypoint Tasks +-- +-- # Examples +-- +-- Here are some examples to show how things are done. +-- +-- ## 1. Spawn +-- +-- +-- +-- @field #FLIGHTGROUP +FLIGHTGROUP = { + ClassName = "FLIGHTGROUP", + homebase = nil, + destbase = nil, + homezone = nil, + destzone = nil, + actype = nil, + speedMax = nil, + rangemax = nil, + ceiling = nil, + fuellow = false, + fuellowthresh = nil, + fuellowrtb = nil, + fuelcritical = nil, + fuelcriticalthresh = nil, + fuelcriticalrtb = false, + outofAAMrtb = true, + outofAGMrtb = true, + squadron = nil, + flightcontrol = nil, + flaghold = nil, + Tholding = nil, + Tparking = nil, + menu = nil, + ishelo = nil, + RTBRecallCount = 0, +} + + +--- Generalized attribute. See [DCS attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes) on hoggit. +-- @type FLIGHTGROUP.Attribute +-- @field #string TRANSPORTPLANE Airplane with transport capability. This can be used to transport other assets. +-- @field #string AWACS Airborne Early Warning and Control System. +-- @field #string FIGHTER Fighter, interceptor, ... airplane. +-- @field #string BOMBER Aircraft which can be used for strategic bombing. +-- @field #string TANKER Airplane which can refuel other aircraft. +-- @field #string TRANSPORTHELO Helicopter with transport capability. This can be used to transport other assets. +-- @field #string ATTACKHELO Attack helicopter. +-- @field #string UAV Unpiloted Aerial Vehicle, e.g. drones. +-- @field #string OTHER Other aircraft type. +FLIGHTGROUP.Attribute = { + TRANSPORTPLANE="TransportPlane", + AWACS="AWACS", + FIGHTER="Fighter", + BOMBER="Bomber", + TANKER="Tanker", + TRANSPORTHELO="TransportHelo", + ATTACKHELO="AttackHelo", + UAV="UAV", + OTHER="Other", +} + +--- Flight group element. +-- @type FLIGHTGROUP.Element +-- @field #string name Name of the element, i.e. the unit/client. +-- @field Wrapper.Unit#UNIT unit Element unit object. +-- @field Wrapper.Group#GROUP group Group object of the element. +-- @field #string modex Tail number. +-- @field #string skill Skill level. +-- @field #boolean ai If true, element is AI. +-- @field Wrapper.Client#CLIENT client The client if element is occupied by a human player. +-- @field #table pylons Table of pylons. +-- @field #number fuelmass Mass of fuel in kg. +-- @field #number category Aircraft category. +-- @field #string categoryname Aircraft category name. +-- @field #string callsign Call sign, e.g. "Uzi 1-1". +-- @field #string status Status, i.e. born, parking, taxiing. See @{#OPSGROUP.ElementStatus}. +-- @field #number damage Damage of element in percent. +-- @field Wrapper.Airbase#AIRBASE.ParkingSpot parking The parking spot table the element is parking on. + + +--- FLIGHTGROUP class version. +-- @field #string version +FLIGHTGROUP.version="0.6.1" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: VTOL aircraft. +-- TODO: Use new UnitLost event instead of crash/dead. +-- TODO: Options EPLRS, Afterburner restrict etc. +-- DONE: Add TACAN beacon. +-- TODO: Damage? +-- TODO: shot events? +-- TODO: Marks to add waypoints/tasks on-the-fly. +-- TODO: Mark assigned parking spot on F10 map. +-- TODO: Let user request a parking spot via F10 marker :) +-- TODO: Monitor traveled distance in air ==> calculate fuel consumption ==> calculate range remaining. Will this give half way accurate results? +-- DONE: Out of AG/AA missiles. Safe state of out-of-ammo. +-- DONE: Add tasks. +-- DONE: Waypoints, read, add, insert, detour. +-- DONE: Get ammo. +-- DONE: Get pylons. +-- DONE: Fuel threshhold ==> RTB. +-- DONE: ROE +-- NOGO: Respawn? With correct loadout, fuelstate. Solved in DCS 2.5.6! + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new FLIGHTGROUP object and start the FSM. +-- @param #FLIGHTGROUP self +-- @param Wrapper.Group#GROUP group The group object. Can also be given by its group name as `#string`. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:New(group) + + -- First check if we already have a flight group for this group. + local fg=_DATABASE:GetFlightGroup(group) + if fg then + fg:I(fg.lid..string.format("WARNING: Flight group already exists in data base!")) + return fg + end + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, OPSGROUP:New(group)) -- #FLIGHTGROUP + + -- Set some string id for output to DCS.log file. + self.lid=string.format("FLIGHTGROUP %s | ", self.groupname) + + -- Defaults + --self:SetVerbosity(0) + self:SetFuelLowThreshold() + self:SetFuelLowRTB() + self:SetFuelCriticalThreshold() + self:SetFuelCriticalRTB() + self:SetDefaultROE() + self:SetDefaultROT() + self:SetDetection() + self.isFlightgroup=true + + -- Holding flag. + self.flaghold=USERFLAG:New(string.format("%s_FlagHold", self.groupname)) + self.flaghold:Set(0) + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("*", "RTB", "Inbound") -- Group is returning to destination base. + self:AddTransition("*", "RTZ", "Inbound") -- Group is returning to destination zone. Not implemented yet! + self:AddTransition("Inbound", "Holding", "Holding") -- Group is in holding pattern. + + self:AddTransition("*", "Refuel", "Going4Fuel") -- Group is send to refuel at a tanker. + self:AddTransition("Going4Fuel", "Refueled", "Airborne") -- Group finished refueling. + + self:AddTransition("*", "LandAt", "LandingAt") -- Helo group is ordered to land at a specific point. + self:AddTransition("LandingAt", "LandedAt", "LandedAt") -- Helo group landed landed at a specific point. + + self:AddTransition("*", "Wait", "*") -- Group is orbiting. + + self:AddTransition("*", "FuelLow", "*") -- Fuel state of group is low. Default ~25%. + self:AddTransition("*", "FuelCritical", "*") -- Fuel state of group is critical. Default ~10%. + + self:AddTransition("*", "OutOfMissilesAA", "*") -- Group is out of A2A missiles. + self:AddTransition("*", "OutOfMissilesAG", "*") -- Group is out of A2G missiles. + self:AddTransition("*", "OutOfMissilesAS", "*") -- Group is out of A2S(ship) missiles. Not implemented yet! + + self:AddTransition("Airborne", "EngageTarget", "Engaging") -- Engage targets. + self:AddTransition("Engaging", "Disengage", "Airborne") -- Engagement over. + + self:AddTransition("*", "ElementParking", "*") -- An element is parking. + self:AddTransition("*", "ElementEngineOn", "*") -- An element spooled up the engines. + self:AddTransition("*", "ElementTaxiing", "*") -- An element is taxiing to the runway. + self:AddTransition("*", "ElementTakeoff", "*") -- An element took off. + self:AddTransition("*", "ElementAirborne", "*") -- An element is airborne. + self:AddTransition("*", "ElementLanded", "*") -- An element landed. + self:AddTransition("*", "ElementArrived", "*") -- An element arrived. + + self:AddTransition("*", "ElementOutOfAmmo", "*") -- An element is completely out of ammo. + + self:AddTransition("*", "Parking", "Parking") -- The whole flight group is parking. + self:AddTransition("*", "Taxiing", "Taxiing") -- The whole flight group is taxiing. + self:AddTransition("*", "Takeoff", "Airborne") -- The whole flight group is airborne. + self:AddTransition("*", "Airborne", "Airborne") -- The whole flight group is airborne. + self:AddTransition("*", "Landing", "Landing") -- The whole flight group is landing. + self:AddTransition("*", "Landed", "Landed") -- The whole flight group has landed. + self:AddTransition("*", "Arrived", "Arrived") -- The whole flight group has arrived. + + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Stop". Stops the FLIGHTGROUP and all its event handlers. + -- @param #FLIGHTGROUP self + + --- Triggers the FSM event "Stop" after a delay. Stops the FLIGHTGROUP and all its event handlers. + -- @function [parent=#FLIGHTGROUP] __Stop + -- @param #FLIGHTGROUP self + -- @param #number delay Delay in seconds. + + -- TODO: Add pseudo functions. + + -- Debug trace. + if false then + BASE:TraceOnOff(true) + BASE:TraceClass(self.ClassName) + BASE:TraceLevel(1) + end + + -- Add to data base. + _DATABASE:AddFlightGroup(self) + + -- Handle events: + self:HandleEvent(EVENTS.Birth, self.OnEventBirth) + self:HandleEvent(EVENTS.EngineStartup, self.OnEventEngineStartup) + self:HandleEvent(EVENTS.Takeoff, self.OnEventTakeOff) + self:HandleEvent(EVENTS.Land, self.OnEventLanding) + self:HandleEvent(EVENTS.EngineShutdown, self.OnEventEngineShutdown) + self:HandleEvent(EVENTS.PilotDead, self.OnEventPilotDead) + self:HandleEvent(EVENTS.Ejection, self.OnEventEjection) + self:HandleEvent(EVENTS.Crash, self.OnEventCrash) + self:HandleEvent(EVENTS.RemoveUnit, self.OnEventRemoveUnit) + self:HandleEvent(EVENTS.UnitLost, self.OnEventUnitLost) + self:HandleEvent(EVENTS.Kill, self.OnEventKill) + + -- Init waypoints. + self:InitWaypoints() + + -- Initialize group. + self:_InitGroup() + + -- Start the status monitoring. + self:__Status(-1) + + -- Start queue update timer. + self.timerQueueUpdate=TIMER:New(self._QueueUpdate, self):Start(2, 5) + + -- Start check zone timer. + self.timerCheckZone=TIMER:New(self._CheckInZones, self):Start(3, 10) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add an *enroute* task to attack targets in a certain **circular** zone. +-- @param #FLIGHTGROUP self +-- @param Core.Zone#ZONE_RADIUS ZoneRadius The circular zone, where to engage targets. +-- @param #table TargetTypes (Optional) The target types, passed as a table, i.e. mind the curly brackets {}. Default {"Air"}. +-- @param #number Priority (Optional) Priority. Default 0. +function FLIGHTGROUP:AddTaskEnrouteEngageTargetsInZone(ZoneRadius, TargetTypes, Priority) + local Task=self.group:EnRouteTaskEngageTargetsInZone(ZoneRadius:GetVec2(), ZoneRadius:GetRadius(), TargetTypes, Priority) + self:AddTaskEnroute(Task) +end + +--- Set AIRWING the flight group belongs to. +-- @param #FLIGHTGROUP self +-- @param Ops.AirWing#AIRWING airwing The AIRWING object. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetAirwing(airwing) + self:T(self.lid..string.format("Add flight to AIRWING %s", airwing.alias)) + self.airwing=airwing + return self +end + +--- Get airwing the flight group belongs to. +-- @param #FLIGHTGROUP self +-- @return Ops.AirWing#AIRWING The AIRWING object. +function FLIGHTGROUP:GetAirWing() + return self.airwing +end + +--- Set the FLIGHTCONTROL controlling this flight group. +-- @param #FLIGHTGROUP self +-- @param Ops.FlightControl#FLIGHTCONTROL flightcontrol The FLIGHTCONTROL object. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetFlightControl(flightcontrol) + + -- Check if there is already a FC. + if self.flightcontrol then + if self.flightcontrol.airbasename==flightcontrol.airbasename then + -- Flight control is already controlling this flight! + return + else + -- Remove flight from previous FC. + self.flightcontrol:_RemoveFlight(self) + end + end + + -- Set FC. + self:I(self.lid..string.format("Setting FLIGHTCONTROL to airbase %s", flightcontrol.airbasename)) + self.flightcontrol=flightcontrol + + -- Add flight to all flights. + table.insert(flightcontrol.flights, self) + + -- Update flight's F10 menu. + if self.isAI==false then + self:_UpdateMenu(0.5) + end + + return self +end + +--- Get the FLIGHTCONTROL controlling this flight group. +-- @param #FLIGHTGROUP self +-- @return Ops.FlightControl#FLIGHTCONTROL The FLIGHTCONTROL object. +function FLIGHTGROUP:GetFlightControl() + return self.flightcontrol +end + + +--- Set the homebase. +-- @param #FLIGHTGROUP self +-- @param Wrapper.Airbase#AIRBASE HomeAirbase The home airbase. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetHomebase(HomeAirbase) + self.homebase=HomeAirbase + return self +end + +--- Set the destination airbase. This is where the flight will go, when the final waypoint is reached. +-- @param #FLIGHTGROUP self +-- @param Wrapper.Airbase#AIRBASE DestinationAirbase The destination airbase. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetDestinationbase(DestinationAirbase) + self.destbase=DestinationAirbase + return self +end + + +--- Set the AIRBOSS controlling this flight group. +-- @param #FLIGHTGROUP self +-- @param Ops.Airboss#AIRBOSS airboss The AIRBOSS object. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetAirboss(airboss) + self.airboss=airboss + return self +end + +--- Set low fuel threshold. Triggers event "FuelLow" and calls event function "OnAfterFuelLow". +-- @param #FLIGHTGROUP self +-- @param #number threshold Fuel threshold in percent. Default 25 %. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetFuelLowThreshold(threshold) + self.fuellowthresh=threshold or 25 + return self +end + +--- Set if low fuel threshold is reached, flight goes RTB. +-- @param #FLIGHTGROUP self +-- @param #boolean switch If true or nil, flight goes RTB. If false, turn this off. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetFuelLowRTB(switch) + if switch==false then + self.fuellowrtb=false + else + self.fuellowrtb=true + end + return self +end + +--- Set if flight is out of Air-Air-Missiles, flight goes RTB. +-- @param #FLIGHTGROUP self +-- @param #boolean switch If true or nil, flight goes RTB. If false, turn this off. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetOutOfAAMRTB(switch) + if switch==false then + self.outofAAMrtb=false + else + self.outofAAMrtb=true + end + return self +end + +--- Set if flight is out of Air-Ground-Missiles, flight goes RTB. +-- @param #FLIGHTGROUP self +-- @param #boolean switch If true or nil, flight goes RTB. If false, turn this off. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetOutOfAGMRTB(switch) + if switch==false then + self.outofAGMrtb=false + else + self.outofAGMrtb=true + end + return self +end + +--- Set if low fuel threshold is reached, flight tries to refuel at the neares tanker. +-- @param #FLIGHTGROUP self +-- @param #boolean switch If true or nil, flight goes for refuelling. If false, turn this off. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetFuelLowRefuel(switch) + if switch==false then + self.fuellowrefuel=false + else + self.fuellowrefuel=true + end + return self +end + +--- Set fuel critical threshold. Triggers event "FuelCritical" and event function "OnAfterFuelCritical". +-- @param #FLIGHTGROUP self +-- @param #number threshold Fuel threshold in percent. Default 10 %. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetFuelCriticalThreshold(threshold) + self.fuelcriticalthresh=threshold or 10 + return self +end + +--- Set if critical fuel threshold is reached, flight goes RTB. +-- @param #FLIGHTGROUP self +-- @param #boolean switch If true or nil, flight goes RTB. If false, turn this off. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetFuelCriticalRTB(switch) + if switch==false then + self.fuelcriticalrtb=false + else + self.fuelcriticalrtb=true + end + return self +end + +--- Enable to automatically engage detected targets. +-- @param #FLIGHTGROUP self +-- @param #number RangeMax Max range in NM. Only detected targets within this radius from the group will be engaged. Default is 25 NM. +-- @param #table TargetTypes Types of target attributes that will be engaged. See [DCS enum attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes). Default "All". +-- @param Core.Set#SET_ZONE EngageZoneSet Set of zones in which targets are engaged. Default is anywhere. +-- @param Core.Set#SET_ZONE NoEngageZoneSet Set of zones in which targets are *not* engaged. Default is nowhere. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetEngageDetectedOn(RangeMax, TargetTypes, EngageZoneSet, NoEngageZoneSet) + + -- Ensure table. + if TargetTypes then + if type(TargetTypes)~="table" then + TargetTypes={TargetTypes} + end + else + TargetTypes={"All"} + end + + -- Ensure SET_ZONE if ZONE is provided. + if EngageZoneSet and EngageZoneSet:IsInstanceOf("ZONE_BASE") then + local zoneset=SET_ZONE:New():AddZone(EngageZoneSet) + EngageZoneSet=zoneset + end + if NoEngageZoneSet and NoEngageZoneSet:IsInstanceOf("ZONE_BASE") then + local zoneset=SET_ZONE:New():AddZone(NoEngageZoneSet) + NoEngageZoneSet=zoneset + end + + -- Set parameters. + self.engagedetectedOn=true + self.engagedetectedRmax=UTILS.NMToMeters(RangeMax or 25) + self.engagedetectedTypes=TargetTypes + self.engagedetectedEngageZones=EngageZoneSet + self.engagedetectedNoEngageZones=NoEngageZoneSet + + -- Ensure detection is ON or it does not make any sense. + self:SetDetection(true) + + return self +end + +--- Disable to automatically engage detected targets. +-- @param #FLIGHTGROUP self +-- @return #OPSGROUP self +function FLIGHTGROUP:SetEngageDetectedOff() + self.engagedetectedOn=false + return self +end + + +--- Enable that the group is despawned after landing. This can be useful to avoid DCS taxi issues with other AI or players or jamming taxiways. +-- @param #FLIGHTGROUP self +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetDespawnAfterLanding() + self.despawnAfterLanding=true + return self +end + + +--- Check if flight is parking. +-- @param #FLIGHTGROUP self +-- @return #boolean If true, flight is parking after spawned. +function FLIGHTGROUP:IsParking() + return self:Is("Parking") +end + +--- Check if flight is parking. +-- @param #FLIGHTGROUP self +-- @return #boolean If true, flight is taxiing after engine start up. +function FLIGHTGROUP:IsTaxiing() + return self:Is("Taxiing") +end + +--- Check if flight is airborne. +-- @param #FLIGHTGROUP self +-- @return #boolean If true, flight is airborne. +function FLIGHTGROUP:IsAirborne() + return self:Is("Airborne") +end + +--- Check if flight is waiting after passing final waypoint. +-- @param #FLIGHTGROUP self +-- @return #boolean If true, flight is waiting. +function FLIGHTGROUP:IsWaiting() + return self:Is("Waiting") +end + +--- Check if flight is landing. +-- @param #FLIGHTGROUP self +-- @return #boolean If true, flight is landing, i.e. on final approach. +function FLIGHTGROUP:IsLanding() + return self:Is("Landing") +end + +--- Check if flight has landed and is now taxiing to its parking spot. +-- @param #FLIGHTGROUP self +-- @return #boolean If true, flight has landed +function FLIGHTGROUP:IsLanded() + return self:Is("Landed") +end + +--- Check if flight has arrived at its destination parking spot. +-- @param #FLIGHTGROUP self +-- @return #boolean If true, flight has arrived at its destination and is parking. +function FLIGHTGROUP:IsArrived() + return self:Is("Arrived") +end + +--- Check if flight is inbound and traveling to holding pattern. +-- @param #FLIGHTGROUP self +-- @return #boolean If true, flight is holding. +function FLIGHTGROUP:IsInbound() + return self:Is("Inbound") +end + +--- Check if flight is holding and waiting for landing clearance. +-- @param #FLIGHTGROUP self +-- @return #boolean If true, flight is holding. +function FLIGHTGROUP:IsHolding() + return self:Is("Holding") +end + +--- Check if flight is going for fuel. +-- @param #FLIGHTGROUP self +-- @return #boolean If true, flight is refueling. +function FLIGHTGROUP:IsGoing4Fuel() + return self:Is("Going4Fuel") +end + +--- Check if helo(!) flight is ordered to land at a specific point. +-- @param #FLIGHTGROUP self +-- @return #boolean If true, group has task to land somewhere. +function FLIGHTGROUP:IsLandingAt() + return self:Is("LandingAt") +end + +--- Check if helo(!) flight is currently landed at a specific point. +-- @param #FLIGHTGROUP self +-- @return #boolean If true, group is currently landed at the assigned position and waiting until task is complete. +function FLIGHTGROUP:IsLandedAt() + return self:Is("LandedAt") +end + +--- Check if flight is low on fuel. +-- @param #FLIGHTGROUP self +-- @return #boolean If true, flight is low on fuel. +function FLIGHTGROUP:IsFuelLow() + return self.fuellow +end + +--- Check if flight is critical on fuel. +-- @param #FLIGHTGROUP self +-- @return #boolean If true, flight is critical on fuel. +function FLIGHTGROUP:IsFuelCritical() + return self.fuelcritical +end + +--- Check if flight can do air-to-ground tasks. +-- @param #FLIGHTGROUP self +-- @param #boolean ExcludeGuns If true, exclude gun +-- @return #boolean *true* if has air-to-ground weapons. +function FLIGHTGROUP:CanAirToGround(ExcludeGuns) + local ammo=self:GetAmmoTot() + if ExcludeGuns then + return ammo.MissilesAG+ammo.Rockets+ammo.Bombs>0 + else + return ammo.MissilesAG+ammo.Rockets+ammo.Bombs+ammo.Guns>0 + end +end + +--- Check if flight can do air-to-air attacks. +-- @param #FLIGHTGROUP self +-- @param #boolean ExcludeGuns If true, exclude available gun shells. +-- @return #boolean *true* if has air-to-ground weapons. +function FLIGHTGROUP:CanAirToAir(ExcludeGuns) + local ammo=self:GetAmmoTot() + if ExcludeGuns then + return ammo.MissilesAA>0 + else + return ammo.MissilesAA+ammo.Guns>0 + end +end + + + +--- Start an *uncontrolled* group. +-- @param #FLIGHTGROUP self +-- @param #number delay (Optional) Delay in seconds before the group is started. Default is immediately. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:StartUncontrolled(delay) + + if delay and delay>0 then + self:T2(self.lid..string.format("Starting uncontrolled group in %d seconds", delay)) + self:ScheduleOnce(delay, FLIGHTGROUP.StartUncontrolled, self) + else + + if self:IsAlive() then + --TODO: check Alive==true and Alive==false ==> Activate first + self:T(self.lid.."Starting uncontrolled group") + self.group:StartUncontrolled(delay) + self.isUncontrolled=true + else + self:E(self.lid.."ERROR: Could not start uncontrolled group as it is NOT alive!") + end + + end + + return self +end + +--- Clear the group for landing when it is holding. +-- @param #FLIGHTGROUP self +-- @param #number Delay Delay in seconds before landing clearance is given. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:ClearToLand(Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, FLIGHTGROUP.ClearToLand, self) + else + + if self:IsHolding() then + self:T(self.lid..string.format("Clear to land ==> setting holding flag to 1 (true)")) + self.flaghold:Set(1) + end + + end + return self +end + +--- Get min fuel of group. This returns the relative fuel amount of the element lowest fuel in the group. +-- @param #FLIGHTGROUP self +-- @return #number Relative fuel in percent. +function FLIGHTGROUP:GetFuelMin() + + local fuelmin=math.huge + for i,_element in pairs(self.elements) do + local element=_element --#FLIGHTGROUP.Element + + local unit=element.unit + + local life=unit:GetLife() + + if unit and unit:IsAlive() and life>1 then + local fuel=unit:GetFuel() + if fuel false")) + return false + elseif self:IsStopped() then + self:T(self.lid..string.format("Onbefore Status STOPPED ==> false")) + return false + end + + return true +end + +--- On after "Status" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTGROUP:onafterStatus(From, Event, To) + + -- FSM state. + local fsmstate=self:GetState() + + -- Update position. + self:_UpdatePosition() + + --- + -- Detection + --- + + -- Check if group has detected any units. + if self.detectionOn then + self:_CheckDetectedUnits() + end + + --- + -- Parking + --- + + -- Check if flight began to taxi (if it was parking). + if self:IsParking() then + for _,_element in pairs(self.elements) do + local element=_element --#FLIGHTGROUP.Element + if element.parking then + + -- Get distance to assigned parking spot. + local dist=element.unit:GetCoordinate():Get2DDistance(element.parking.Coordinate) + + -- If distance >10 meters, we consider the unit as taxiing. + -- TODO: Check distance threshold! If element is taxiing, the parking spot is free again. + -- When the next plane is spawned on this spot, collisions should be avoided! + if dist>10 then + if element.status==OPSGROUP.ElementStatus.ENGINEON then + self:ElementTaxiing(element) + end + end + + else + --self:E(self.lid..string.format("Element %s is in PARKING queue but has no parking spot assigned!", element.name)) + end + end + end + + --- + -- Group + --- + + -- Short info. + if self.verbose>=1 then + + local nTaskTot, nTaskSched, nTaskWP=self:CountRemainingTasks() + local nMissions=self:CountRemainingMissison() + + + local text=string.format("Status %s [%d/%d]: Tasks=%d (%d,%d) Curr=%d, Missions=%s, Waypoint=%d/%d, Detected=%d, Home=%s, Destination=%s", + fsmstate, #self.elements, #self.elements, nTaskTot, nTaskSched, nTaskWP, self.taskcurrent, nMissions, self.currentwp or 0, self.waypoints and #self.waypoints or 0, + self.detectedunits:Count(), self.homebase and self.homebase:GetName() or "unknown", self.destbase and self.destbase:GetName() or "unknown") + self:I(self.lid..text) + + end + + --- + -- Elements + --- + + if self.verbose>=2 then + local text="Elements:" + for i,_element in pairs(self.elements) do + local element=_element --#FLIGHTGROUP.Element + + local name=element.name + local status=element.status + local unit=element.unit + local fuel=unit:GetFuel() or 0 + local life=unit:GetLifeRelative() or 0 + local parking=element.parking and tostring(element.parking.TerminalID) or "X" + + -- Check if element is not dead and we missed an event. + --if life<=0 and element.status~=OPSGROUP.ElementStatus.DEAD and element.status~=OPSGROUP.ElementStatus.INUTERO then + -- self:ElementDead(element) + --end + + -- Get ammo. + local ammo=self:GetAmmoElement(element) + + -- Output text for element. + text=text..string.format("\n[%d] %s: status=%s, fuel=%.1f, life=%.1f, guns=%d, rockets=%d, bombs=%d, missiles=%d (AA=%d, AG=%d, AS=%s), parking=%s", + i, name, status, fuel*100, life*100, ammo.Guns, ammo.Rockets, ammo.Bombs, ammo.Missiles, ammo.MissilesAA, ammo.MissilesAG, ammo.MissilesAS, parking) + end + if #self.elements==0 then + text=text.." none!" + end + self:I(self.lid..text) + end + + --- + -- Distance travelled + --- + + if self.verbose>=4 and self:IsAlive() then + + -- Travelled distance since last check. + local ds=self.travelds + + -- Time interval. + local dt=self.dTpositionUpdate + + -- Speed. + local v=ds/dt + + + -- Max fuel time remaining. + local TmaxFuel=math.huge + + for _,_element in pairs(self.elements) do + local element=_element --#FLIGHTGROUP.Element + + -- Get relative fuel of element. + local fuel=element.unit:GetFuel() or 0 + + -- Relative fuel used since last check. + local dFrel=element.fuelrel-fuel + + -- Relative fuel used per second. + local dFreldt=dFrel/dt + + -- Fuel remaining in seconds. + local Tfuel=fuel/dFreldt + + if Tfuel Tfuel=%.1f min", element.name, fuel*100, dFrel*100, dFreldt*100*60, Tfuel/60)) + + -- Store rel fuel. + element.fuelrel=fuel + end + + -- Log outut. + self:I(self.lid..string.format("Travelled ds=%.1f km dt=%.1f s ==> v=%.1f knots. Fuel left for %.1f min", self.traveldist/1000, dt, UTILS.MpsToKnots(v), TmaxFuel/60)) + + end + + --- + -- Tasks & Missions + --- + + self:_PrintTaskAndMissionStatus() + + --- + -- Fuel State + --- + + -- Only if group is in air. + if self:IsAlive() and self.group:IsAirborne(true) then + + local fuelmin=self:GetFuelMin() + + if fuelmin>=self.fuellowthresh then + self.fuellow=false + end + + if fuelmin>=self.fuelcriticalthresh then + self.fuelcritical=false + end + + + -- Low fuel? + if fuelmin1 groups to have passed. + -- TODO: Can I do this more rigorously? + self:ScheduleOnce(1, reset) + + else + + -- Set homebase if not already set. + if EventData.Place then + self.homebase=self.homebase or EventData.Place + end + + if self.homebase and not self.destbase then + self.destbase=self.homebase + end + + -- Get element. + local element=self:GetElementByName(unitname) + + -- Create element spawned event if not already present. + if not self:_IsElement(unitname) then + element=self:AddElementByName(unitname) + end + + -- Set element to spawned state. + self:T(self.lid..string.format("EVENT: Element %s born at airbase %s==> spawned", element.name, self.homebase and self.homebase:GetName() or "unknown")) + -- This is delayed by a millisec because inAir check for units spawned in air failed (returned false even though the unit was spawned in air). + self:__ElementSpawned(0.0, element) + + end + + end + +end + +--- Flightgroup event function handling the crash of a unit. +-- @param #FLIGHTGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function FLIGHTGROUP:OnEventEngineStartup(EventData) + + -- Check that this is the right group. + if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then + local unit=EventData.IniUnit + local group=EventData.IniGroup + local unitname=EventData.IniUnitName + + -- Get element. + local element=self:GetElementByName(unitname) + + if element then + + if self:IsAirborne() or self:IsInbound() or self:IsHolding() then + -- TODO: what? + else + self:T3(self.lid..string.format("EVENT: Element %s started engines ==> taxiing (if AI)", element.name)) + -- TODO: could be that this element is part of a human flight group. + -- Problem: when player starts hot, the AI does too and starts to taxi immidiately :( + -- when player starts cold, ? + if self.isAI then + self:ElementEngineOn(element) + else + if element.ai then + -- AI wingmen will start taxiing even if the player/client is still starting up his engines :( + self:ElementEngineOn(element) + end + end + end + + end + + end + +end + +--- Flightgroup event function handling the crash of a unit. +-- @param #FLIGHTGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function FLIGHTGROUP:OnEventTakeOff(EventData) + + -- Check that this is the right group. + if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then + local unit=EventData.IniUnit + local group=EventData.IniGroup + local unitname=EventData.IniUnitName + + -- Get element. + local element=self:GetElementByName(unitname) + + if element then + self:T3(self.lid..string.format("EVENT: Element %s took off ==> airborne", element.name)) + self:ElementTakeoff(element, EventData.Place) + end + + end + +end + +--- Flightgroup event function handling the crash of a unit. +-- @param #FLIGHTGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function FLIGHTGROUP:OnEventLanding(EventData) + + -- Check that this is the right group. + if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then + local unit=EventData.IniUnit + local group=EventData.IniGroup + local unitname=EventData.IniUnitName + + -- Get element. + local element=self:GetElementByName(unitname) + + local airbase=EventData.Place + + local airbasename="unknown" + if airbase then + airbasename=tostring(airbase:GetName()) + end + + if element then + self:T3(self.lid..string.format("EVENT: Element %s landed at %s ==> landed", element.name, airbasename)) + self:ElementLanded(element, airbase) + end + + end + +end + +--- Flightgroup event function handling the crash of a unit. +-- @param #FLIGHTGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function FLIGHTGROUP:OnEventEngineShutdown(EventData) + + -- Check that this is the right group. + if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then + local unit=EventData.IniUnit + local group=EventData.IniGroup + local unitname=EventData.IniUnitName + + -- Get element. + local element=self:GetElementByName(unitname) + + if element then + + if element.unit and element.unit:IsAlive() then + + local airbase=self:GetClosestAirbase() + local parking=self:GetParkingSpot(element, 10, airbase) + + if airbase and parking then + self:ElementArrived(element, airbase, parking) + self:T3(self.lid..string.format("EVENT: Element %s shut down engines ==> arrived", element.name)) + else + self:T3(self.lid..string.format("EVENT: Element %s shut down engines but is not parking. Is it dead?", element.name)) + end + + else + --self:I(self.lid..string.format("EVENT: Element %s shut down engines but is NOT alive ==> waiting for crash event (==> dead)", element.name)) + end + + end -- element nil? + + end + +end + + +--- Flightgroup event function handling the crash of a unit. +-- @param #FLIGHTGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function FLIGHTGROUP:OnEventCrash(EventData) + + -- Check that this is the right group. + if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then + local unit=EventData.IniUnit + local group=EventData.IniGroup + local unitname=EventData.IniUnitName + + -- Get element. + local element=self:GetElementByName(unitname) + + if element and element.status~=OPSGROUP.ElementStatus.DEAD then + self:T(self.lid..string.format("EVENT: Element %s crashed ==> destroyed", element.name)) + self:ElementDestroyed(element) + end + + end + +end + +--- Flightgroup event function handling the crash of a unit. +-- @param #FLIGHTGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function FLIGHTGROUP:OnEventUnitLost(EventData) + + -- Check that this is the right group. + if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then + self:T2(self.lid..string.format("EVENT: Unit %s lost at t=%.3f", EventData.IniUnitName, timer.getTime())) + + local unit=EventData.IniUnit + local group=EventData.IniGroup + local unitname=EventData.IniUnitName + + -- Get element. + local element=self:GetElementByName(unitname) + + if element and element.status~=OPSGROUP.ElementStatus.DEAD then + self:T(self.lid..string.format("EVENT: Element %s unit lost ==> destroyed t=%.3f", element.name, timer.getTime())) + self:ElementDestroyed(element) + end + + end + +end + +--- Flightgroup event function handling the crash of a unit. +-- @param #FLIGHTGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function FLIGHTGROUP:OnEventKill(EventData) + + -- Check that this is the right group. + if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then + + -- Target name + local targetname=tostring(EventData.TgtUnitName) + + -- Debug info. + self:T2(self.lid..string.format("EVENT: Unit %s killed object %s!", tostring(EventData.IniUnitName), targetname)) + + -- Check if this was a UNIT or STATIC object. + local target=UNIT:FindByName(targetname) + if not target then + target=STATIC:FindByName(targetname, false) + end + + -- Only count UNITS and STATICs (not SCENERY) + if target then + + -- Debug info. + self:T(self.lid..string.format("EVENT: Unit %s killed unit/static %s!", tostring(EventData.IniUnitName), targetname)) + + -- Kill counter. + self.Nkills=self.Nkills+1 + + -- Check if on a mission. + local mission=self:GetMissionCurrent() + if mission then + mission.Nkills=mission.Nkills+1 -- Increase mission kill counter. + end + + end + + end + +end + +--- Flightgroup event function handling the crash of a unit. +-- @param #FLIGHTGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function FLIGHTGROUP:OnEventRemoveUnit(EventData) + + -- Check that this is the right group. + if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then + local unit=EventData.IniUnit + local group=EventData.IniGroup + local unitname=EventData.IniUnitName + + -- Get element. + local element=self:GetElementByName(unitname) + + if element then + self:T3(self.lid..string.format("EVENT: Element %s removed ==> dead", element.name)) + self:ElementDead(element) + end + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "ElementSpawned" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #FLIGHTGROUP.Element Element The flight group element. +function FLIGHTGROUP:onafterElementSpawned(From, Event, To, Element) + self:T(self.lid..string.format("Element spawned %s", Element.name)) + + -- Set element status. + self:_UpdateStatus(Element, OPSGROUP.ElementStatus.SPAWNED) + + if Element.unit:InAir(true) then + -- Trigger ElementAirborne event. Add a little delay because spawn is also delayed! + self:__ElementAirborne(0.11, Element) + else + + -- Get parking spot. + local spot=self:GetParkingSpot(Element, 10) + + if spot then + + -- Trigger ElementParking event. Add a little delay because spawn is also delayed! + self:__ElementParking(0.11, Element, spot) + + else + -- TODO: This can happen if spawned on deck of a carrier! + self:T(self.lid..string.format("Element spawned not in air but not on any parking spot.")) + self:__ElementParking(0.11, Element) + end + end +end + +--- On after "ElementParking" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #FLIGHTGROUP.Element Element The flight group element. +-- @param Wrapper.Airbase#AIRBASE.ParkingSpot Spot Parking Spot. +function FLIGHTGROUP:onafterElementParking(From, Event, To, Element, Spot) + self:T(self.lid..string.format("Element parking %s at spot %s", Element.name, Element.parking and tostring(Element.parking.TerminalID) or "N/A")) + + -- Set element status. + self:_UpdateStatus(Element, OPSGROUP.ElementStatus.PARKING) + + if Spot then + self:_SetElementParkingAt(Element, Spot) + end + + if self:IsTakeoffCold() then + -- Wait for engine startup event. + elseif self:IsTakeoffHot() then + self:__ElementEngineOn(0.5, Element) -- delay a bit to allow all elements + elseif self:IsTakeoffRunway() then + self:__ElementEngineOn(0.5, Element) + end +end + +--- On after "ElementEngineOn" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #FLIGHTGROUP.Element Element The flight group element. +function FLIGHTGROUP:onafterElementEngineOn(From, Event, To, Element) + + -- Debug info. + self:T(self.lid..string.format("Element %s started engines", Element.name)) + + -- Set element status. + self:_UpdateStatus(Element, OPSGROUP.ElementStatus.ENGINEON) +end + +--- On after "ElementTaxiing" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #FLIGHTGROUP.Element Element The flight group element. +function FLIGHTGROUP:onafterElementTaxiing(From, Event, To, Element) + + -- Get terminal ID. + local TerminalID=Element.parking and tostring(Element.parking.TerminalID) or "N/A" + + -- Debug info. + self:T(self.lid..string.format("Element taxiing %s. Parking spot %s is now free", Element.name, TerminalID)) + + -- Set parking spot to free. Also for FC. + self:_SetElementParkingFree(Element) + + -- Set element status. + self:_UpdateStatus(Element, OPSGROUP.ElementStatus.TAXIING) +end + +--- On after "ElementTakeoff" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #FLIGHTGROUP.Element Element The flight group element. +-- @param Wrapper.Airbase#AIRBASE airbase The airbase if applicable or nil. +function FLIGHTGROUP:onafterElementTakeoff(From, Event, To, Element, airbase) + self:T(self.lid..string.format("Element takeoff %s at %s airbase.", Element.name, airbase and airbase:GetName() or "unknown")) + + -- Helos with skids just take off without taxiing! + if Element.parking then + self:_SetElementParkingFree(Element) + end + + -- Set element status. + self:_UpdateStatus(Element, OPSGROUP.ElementStatus.TAKEOFF, airbase) + + -- Trigger element airborne event. + self:__ElementAirborne(2, Element) +end + +--- On after "ElementAirborne" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #FLIGHTGROUP.Element Element The flight group element. +function FLIGHTGROUP:onafterElementAirborne(From, Event, To, Element) + self:T2(self.lid..string.format("Element airborne %s", Element.name)) + + -- Set element status. + self:_UpdateStatus(Element, OPSGROUP.ElementStatus.AIRBORNE) +end + +--- On after "ElementLanded" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #FLIGHTGROUP.Element Element The flight group element. +-- @param Wrapper.Airbase#AIRBASE airbase The airbase if applicable or nil. +function FLIGHTGROUP:onafterElementLanded(From, Event, To, Element, airbase) + self:T2(self.lid..string.format("Element landed %s at %s airbase", Element.name, airbase and airbase:GetName() or "unknown")) + + if self.despawnAfterLanding then + + -- Despawn the element. + self:DespawnElement(Element) + + else + + -- Helos with skids land directly on parking spots. + if self.ishelo then + + local Spot=self:GetParkingSpot(Element, 10, airbase) + + self:_SetElementParkingAt(Element, Spot) + + end + + -- Set element status. + self:_UpdateStatus(Element, OPSGROUP.ElementStatus.LANDED, airbase) + + end +end + +--- On after "ElementArrived" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #FLIGHTGROUP.Element Element The flight group element. +-- @param Wrapper.Airbase#AIRBASE airbase The airbase, where the element arrived. +-- @param Wrapper.Airbase#AIRBASE.ParkingSpot Parking The Parking spot the element has. +function FLIGHTGROUP:onafterElementArrived(From, Event, To, Element, airbase, Parking) + self:T(self.lid..string.format("Element arrived %s at %s airbase using parking spot %d", Element.name, airbase and airbase:GetName() or "unknown", Parking and Parking.TerminalID or -99)) + + self:_SetElementParkingAt(Element, Parking) + + -- Set element status. + self:_UpdateStatus(Element, OPSGROUP.ElementStatus.ARRIVED) +end + +--- On after "ElementDestroyed" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #FLIGHTGROUP.Element Element The flight group element. +function FLIGHTGROUP:onafterElementDestroyed(From, Event, To, Element) + + -- Call OPSGROUP function. + self:GetParent(self).onafterElementDestroyed(self, From, Event, To, Element) + +end + +--- On after "ElementDead" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #FLIGHTGROUP.Element Element The flight group element. +function FLIGHTGROUP:onafterElementDead(From, Event, To, Element) + + -- Call OPSGROUP function. + self:GetParent(self).onafterElementDead(self, From, Event, To, Element) + + if self.flightcontrol and Element.parking then + self.flightcontrol:SetParkingFree(Element.parking) + end + + -- Not parking any more. + Element.parking=nil + +end + + +--- On after "Spawned" event. Sets the template, initializes the waypoints. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTGROUP:onafterSpawned(From, Event, To) + self:T(self.lid..string.format("Flight spawned")) + + -- Update position. + self:_UpdatePosition() + + if self.isAI then + + -- Set ROE. + self:SwitchROE(self.option.ROE) + + -- Set ROT. + self:SwitchROT(self.option.ROT) + + -- Set Formation + self:SwitchFormation(self.option.Formation) + + -- Set TACAN beacon. + self:_SwitchTACAN() + + -- Set radio freq and modu. + if self.radioDefault then + self:SwitchRadio() + else + self:SetDefaultRadio(self.radio.Freq, self.radio.Modu, self.radio.On) + end + + -- Set callsign. + if self.callsignDefault then + self:SwitchCallsign(self.callsignDefault.NumberSquad, self.callsignDefault.NumberGroup) + else + self:SetDefaultCallsign(self.callsign.NumberSquad, self.callsign.NumberGroup) + end + + -- TODO: make this input. + self:GetGroup():SetOption(AI.Option.Air.id.PROHIBIT_JETT, true) + self:GetGroup():SetOption(AI.Option.Air.id.PROHIBIT_AB, true) -- Does not seem to work. AI still used the after burner. + self:GetGroup():SetOption(AI.Option.Air.id.RTB_ON_BINGO, false) + --self.group:SetOption(AI.Option.Air.id.RADAR_USING, AI.Option.Air.val.RADAR_USING.FOR_CONTINUOUS_SEARCH) + + -- Update route. + self:__UpdateRoute(-0.5) + + else + + -- F10 other menu. + self:_UpdateMenu() + + end + +end + +--- On after "Parking" event. Add flight to flightcontrol of airbase. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTGROUP:onafterParking(From, Event, To) + self:T(self.lid..string.format("Flight is parking")) + + local airbase=self:GetClosestAirbase() --self.group:GetCoordinate():GetClosestAirbase() + + local airbasename=airbase:GetName() or "unknown" + + -- Parking time stamp. + self.Tparking=timer.getAbsTime() + + -- Get FC of this airbase. + local flightcontrol=_DATABASE:GetFlightControl(airbasename) + + if flightcontrol then + + -- Set FC for this flight + self:SetFlightControl(flightcontrol) + + if self.flightcontrol then + + -- Set flight status. + self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.PARKING) + + -- Update player menu. + if not self.isAI then + self:_UpdateMenu(0.5) + end + + end + end +end + +--- On after "Taxiing" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTGROUP:onafterTaxiing(From, Event, To) + self:T(self.lid..string.format("Flight is taxiing")) + + -- Parking over. + self.Tparking=nil + + -- TODO: need a better check for the airbase. + local airbase=self:GetClosestAirbase() --self.group:GetCoordinate():GetClosestAirbase(nil, self.group:GetCoalition()) + + if self.flightcontrol and airbase and self.flightcontrol.airbasename==airbase:GetName() then + + -- Add AI flight to takeoff queue. + if self.isAI then + -- AI flights go directly to TAKEOFF as we don't know when they finished taxiing. + self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.TAKEOFF) + else + -- Human flights go to TAXI OUT queue. They will go to the ready for takeoff queue when they request it. + self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.TAXIOUT) + -- Update menu. + self:_UpdateMenu() + end + + end + +end + +--- On after "Takeoff" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Airbase#AIRBASE airbase The airbase the flight landed. +function FLIGHTGROUP:onafterTakeoff(From, Event, To, airbase) + self:T(self.lid..string.format("Flight takeoff from %s", airbase and airbase:GetName() or "unknown airbase")) + + -- Remove flight from all FC queues. + if self.flightcontrol and airbase and self.flightcontrol.airbasename==airbase:GetName() then + self.flightcontrol:_RemoveFlight(self) + self.flightcontrol=nil + end + +end + +--- On after "Airborne" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTGROUP:onafterAirborne(From, Event, To) + self:T(self.lid..string.format("Flight airborne")) + + if self.isAI then + self:_CheckGroupDone(1) + else + self:_UpdateMenu() + end +end + +--- On after "Landing" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTGROUP:onafterLanding(From, Event, To) + self:T(self.lid..string.format("Flight is landing")) + + self:_SetElementStatusAll(OPSGROUP.ElementStatus.LANDING) + +end + + +--- On after "Landed" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Airbase#AIRBASE airbase The airbase the flight landed. +function FLIGHTGROUP:onafterLanded(From, Event, To, airbase) + self:T(self.lid..string.format("Flight landed at %s", airbase and airbase:GetName() or "unknown place")) + + if self.flightcontrol and airbase and self.flightcontrol.airbasename==airbase:GetName() then + -- Add flight to taxiinb queue. + self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.TAXIINB) + end + +end + +--- On after "LandedAt" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTGROUP:onafterLandedAt(From, Event, To) + self:T(self.lid..string.format("Flight landed at")) +end + + +--- On after "Arrived" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTGROUP:onafterArrived(From, Event, To) + self:T(self.lid..string.format("Flight arrived")) + + -- Flight Control + if self.flightcontrol then + -- Add flight to arrived queue. + self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.ARRIVED) + end + + -- Despawn in 5 min. + if not self.airwing then + self:Despawn(5*60) + end +end + +--- On after "Dead" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTGROUP:onafterDead(From, Event, To) + + -- Remove flight from all FC queues. + if self.flightcontrol then + self.flightcontrol:_RemoveFlight(self) + self.flightcontrol=nil + end + + if self.Ndestroyed==#self.elements then + if self.squadron then + -- All elements were destroyed ==> Asset group is gone. + self.squadron:DelGroup(self.groupname) + end + else + if self.airwing then + -- Not all assets were destroyed (despawn) ==> Add asset back to airwing. + self.airwing:AddAsset(self.group, 1) + end + end + + -- Call OPSGROUP function. + self:GetParent(self).onafterDead(self, From, Event, To) + +end + + +--- On before "UpdateRoute" event. Update route of group, e.g after new waypoints and/or waypoint tasks have been added. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number n Waypoint number. +-- @return #boolean Transision allowed? +function FLIGHTGROUP:onbeforeUpdateRoute(From, Event, To, n) + + -- Is transition allowed? We assume yes until proven otherwise. + local allowed=true + local trepeat=nil + + if self:IsAlive() then -- and (self:IsAirborne() or self:IsWaiting() or self:IsInbound() or self:IsHolding()) then + -- Alive & Airborne ==> Update route possible. + self:T3(self.lid.."Update route possible. Group is ALIVE") + elseif self:IsDead() then + -- Group is dead! No more updates. + self:E(self.lid.."Update route denied. Group is DEAD!") + allowed=false + else + -- Not airborne yet. Try again in 5 sec. + self:T(self.lid.."Update route denied ==> checking back in 5 sec") + trepeat=-5 + allowed=false + end + + if n and n<1 then + self:E(self.lid.."Update route denied because waypoint n<1!") + allowed=false + end + + if not self.currentwp then + self:E(self.lid.."Update route denied because self.currentwp=nil!") + allowed=false + end + + local N=n or self.currentwp+1 + if not N or N<1 then + self:E(self.lid.."Update route denied because N=nil or N<1") + trepeat=-5 + allowed=false + end + + if self.taskcurrent>0 then + + --local task=self:GetTaskCurrent() + local task=self:GetTaskByID(self.taskcurrent) + + if task then + if task.dcstask.id=="PatrolZone" then + -- For patrol zone, we need to allow the update. + else + local taskname=task and task.description or "No description" + self:E(self.lid..string.format("WARNING: Update route denied because taskcurrent=%d>0! Task description = %s", self.taskcurrent, tostring(taskname))) + allowed=false + end + else + -- Now this can happen, if we directly use TaskExecute as the task is not in the task queue and cannot be removed. + self:T(self.lid..string.format("WARNING: before update route taskcurrent=%d>0 but no task?!", self.taskcurrent)) + -- Anyhow, a task is running so we do not allow to update the route! + allowed=false + end + end + + -- Not good, because mission will never start. Better only check if there is a current task! + --if self.currentmission then + --end + + -- Only AI flights. + if not self.isAI then + allowed=false + end + + -- Debug info. + self:T2(self.lid..string.format("Onbefore Updateroute allowed=%s state=%s repeat in %s", tostring(allowed), self:GetState(), tostring(trepeat))) + + if trepeat then + self:__UpdateRoute(trepeat, n) + end + + return allowed +end + +--- On after "UpdateRoute" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number n Waypoint number. Default is next waypoint. +function FLIGHTGROUP:onafterUpdateRoute(From, Event, To, n) + + -- Update route from this waypoint number onwards. + n=n or self.currentwp+1 + + -- Update waypoint tasks, i.e. inject WP tasks into waypoint table. + self:_UpdateWaypointTasks(n) + + -- Waypoints. + local wp={} + + -- Current velocity. + local speed=self.group and self.group:GetVelocityKMH() or 100 + + -- Set current waypoint or we get problem that the _PassingWaypoint function is triggered too early, i.e. right now and not when passing the next WP. + local current=self.group:GetCoordinate():WaypointAir(COORDINATE.WaypointAltType.BARO, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, speed, true, nil, {}, "Current") + table.insert(wp, current) + + local Nwp=self.waypoints and #self.waypoints or 0 + + -- Add remaining waypoints to route. + for i=n, Nwp do + table.insert(wp, self.waypoints[i]) + end + + -- Debug info. + local hb=self.homebase and self.homebase:GetName() or "unknown" + local db=self.destbase and self.destbase:GetName() or "unknown" + self:T(self.lid..string.format("Updating route for WP #%d-%d homebase=%s destination=%s", n, #wp, hb, db)) + + + if #wp>1 then + + -- Route group to all defined waypoints remaining. + self:Route(wp) + + else + + --- + -- No waypoints left + --- + + if self:IsAirborne() then + self:T(self.lid.."No waypoints left ==> CheckGroupDone") + self:_CheckGroupDone() + end + + end + +end + +--- On after "Respawn" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #table Template The template used to respawn the group. +function FLIGHTGROUP:onafterRespawn(From, Event, To, Template) + + self:T(self.lid.."Respawning group!") + + local template=UTILS.DeepCopy(Template or self.template) + + if self.group and self.group:InAir() then + template.lateActivation=false + self.respawning=true + self.group=self.group:Respawn(template) + end + +end + +--- On after "OutOfMissilesAA" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTGROUP:onafterOutOfMissilesAA(From, Event, To) + self:I(self.lid.."Group is out of AA Missiles!") + if self.outofAAMrtb then + -- Back to destination or home. + local airbase=self.destbase or self.homebase + self:__RTB(-5,airbase) + end +end + +--- On after "OutOfMissilesAG" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTGROUP:onafterOutOfMissilesAG(From, Event, To) + self:I(self.lid.."Group is out of AG Missiles!") + if self.outofAGMrtb then + -- Back to destination or home. + local airbase=self.destbase or self.homebase + self:__RTB(-5,airbase) + end +end + +--- Check if flight is done, i.e. +-- +-- * passed the final waypoint, +-- * no current task +-- * no current mission +-- * number of remaining tasks is zero +-- * number of remaining missions is zero +-- +-- @param #FLIGHTGROUP self +-- @param #number delay Delay in seconds. +function FLIGHTGROUP:_CheckGroupDone(delay) + + if self:IsAlive() and self.isAI then + + if delay and delay>0 then + -- Delayed call. + self:ScheduleOnce(delay, FLIGHTGROUP._CheckGroupDone, self) + else + + -- First check if there is a paused mission that + if self.missionpaused then + self:UnpauseMission() + return + end + + -- Group is currently engaging. + if self:IsEngaging() then + return + end + + -- Number of tasks remaining. + local nTasks=self:CountRemainingTasks() + + -- Number of mission remaining. + local nMissions=self:CountRemainingMissison() + + -- Final waypoint passed? + if self.passedfinalwp then + + -- Got current mission or task? + if self.currentmission==nil and self.taskcurrent==0 then + + -- Number of remaining tasks/missions? + if nTasks==0 and nMissions==0 then + + local destbase=self.destbase or self.homebase + local destzone=self.destzone or self.homezone + + -- Send flight to destination. + if destbase then + self:T(self.lid.."Passed Final WP and No current and/or future missions/task ==> RTB!") + self:__RTB(-3, destbase) + elseif destzone then + self:T(self.lid.."Passed Final WP and No current and/or future missions/task ==> RTZ!") + self:__RTZ(-3, destzone) + else + self:T(self.lid.."Passed Final WP and NO Tasks/Missions left. No DestBase or DestZone ==> Wait!") + self:__Wait(-1) + end + + else + self:T(self.lid..string.format("Passed Final WP but Tasks=%d or Missions=%d left in the queue. Wait!", nTasks, nMissions)) + self:__Wait(-1) + end + else + self:T(self.lid..string.format("Passed Final WP but still have current Task (#%s) or Mission (#%s) left to do", tostring(self.taskcurrent), tostring(self.currentmission))) + end + else + self:T(self.lid..string.format("Flight (status=%s) did NOT pass the final waypoint yet ==> update route", self:GetState())) + self:__UpdateRoute(-1) + end + end + + end + +end + +--- On before "RTB" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Airbase#AIRBASE airbase The airbase to hold at. +-- @param #number SpeedTo Speed used for travelling from current position to holding point in knots. +-- @param #number SpeedHold Holding speed in knots. +function FLIGHTGROUP:onbeforeRTB(From, Event, To, airbase, SpeedTo, SpeedHold) + + if self:IsAlive() then + + local allowed=true + local Tsuspend=nil + + if airbase==nil then + self:E(self.lid.."ERROR: Airbase is nil in RTB() call!") + allowed=false + end + + -- Check that coaliton is okay. We allow same (blue=blue, red=red) or landing on neutral bases. + if airbase and airbase:GetCoalition()~=self.group:GetCoalition() and airbase:GetCoalition()>0 then + self:E(self.lid..string.format("ERROR: Wrong airbase coalition %d in RTB() call! We allow only same as group %d or neutral airbases 0.", airbase:GetCoalition(), self.group:GetCoalition())) + allowed=false + end + + if not self.group:IsAirborne(true) then + -- this should really not happen, either the AUFTRAG is cancelled before the group was airborne or it is stuck at the ground for some reason + self:I(self.lid..string.format("WARNING: Group is not AIRBORNE ==> RTB event is suspended for 20 sec.")) + allowed=false + Tsuspend=-20 + local groupspeed = self.group:GetVelocityMPS() + if groupspeed <= 1 then self.RTBRecallCount = self.RTBRecallCount+1 end + if self.RTBRecallCount > 6 then + self:Despawn(5) + end + end + + -- Only if fuel is not low or critical. + if not (self:IsFuelLow() or self:IsFuelCritical()) then + + -- Check if there are remaining tasks. + local Ntot,Nsched, Nwp=self:CountRemainingTasks() + + if self.taskcurrent>0 then + self:I(self.lid..string.format("WARNING: Got current task ==> RTB event is suspended for 10 sec.")) + Tsuspend=-10 + allowed=false + end + + if Nsched>0 then + self:I(self.lid..string.format("WARNING: Still got %d SCHEDULED tasks in the queue ==> RTB event is suspended for 10 sec.", Nsched)) + Tsuspend=-10 + allowed=false + end + + if Nwp>0 then + self:I(self.lid..string.format("WARNING: Still got %d WAYPOINT tasks in the queue ==> RTB event is suspended for 10 sec.", Nwp)) + Tsuspend=-10 + allowed=false + end + + end + + if Tsuspend and not allowed then + self:__RTB(Tsuspend, airbase, SpeedTo, SpeedHold) + end + + return allowed + + else + self:E(self.lid.."WARNING: Group is not alive! RTB call not allowed.") + return false + end + +end + +--- On after "RTB" event. Order flight to hold at an airbase and wait for signal to land. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Airbase#AIRBASE airbase The airbase to hold at. +-- @param #number SpeedTo Speed used for traveling from current position to holding point in knots. Default 75% of max speed. +-- @param #number SpeedHold Holding speed in knots. Default 250 kts. +-- @param #number SpeedLand Landing speed in knots. Default 170 kts. +function FLIGHTGROUP:onafterRTB(From, Event, To, airbase, SpeedTo, SpeedHold, SpeedLand) + + -- Debug info. + self:T(self.lid..string.format("RTB: event=%s: %s --> %s to %s", Event, From, To, airbase:GetName())) + + -- Set the destination base. + self.destbase=airbase + + -- Clear holding time in any case. + self.Tholding=nil + + -- Cancel all missions. + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + local mystatus=mission:GetGroupStatus(self) + + -- Check if mission is already over! + if not (mystatus==AUFTRAG.GroupStatus.DONE or mystatus==AUFTRAG.GroupStatus.CANCELLED) then + local text=string.format("Canceling mission %s in state=%s", mission.name, mission.status) + self:T(self.lid..text) + self:MissionCancel(mission) + end + + end + + -- Defaults: + SpeedTo=SpeedTo or UTILS.KmphToKnots(self.speedCruise) + SpeedHold=SpeedHold or (self.ishelo and 80 or 250) + SpeedLand=SpeedLand or (self.ishelo and 40 or 170) + + -- Debug message. + local text=string.format("Flight group set to hold at airbase %s. SpeedTo=%d, SpeedHold=%d, SpeedLand=%d", airbase:GetName(), SpeedTo, SpeedHold, SpeedLand) + self:T(self.lid..text) + + local althold=self.ishelo and 1000+math.random(10)*100 or math.random(4,10)*1000 + + -- Holding points. + local c0=self.group:GetCoordinate() + local p0=airbase:GetZone():GetRandomCoordinate():SetAltitude(UTILS.FeetToMeters(althold)) + local p1=nil + local wpap=nil + + -- Do we have a flight control? + local fc=_DATABASE:GetFlightControl(airbase:GetName()) + if fc then + -- Get holding point from flight control. + local HoldingPoint=fc:_GetHoldingpoint(self) + p0=HoldingPoint.pos0 + p1=HoldingPoint.pos1 + + -- Debug marks. + if self.Debug then + p0:MarkToAll("Holding point P0") + p1:MarkToAll("Holding point P1") + end + + -- Set flightcontrol for this flight. + self:SetFlightControl(fc) + + -- Add flight to inbound queue. + self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.INBOUND) + end + + -- Altitude above ground for a glide slope of 3 degrees. + local x1=self.ishelo and UTILS.NMToMeters(5.0) or UTILS.NMToMeters(10) + local x2=self.ishelo and UTILS.NMToMeters(2.5) or UTILS.NMToMeters(5) + local alpha=math.rad(3) + local h1=x1*math.tan(alpha) + local h2=x2*math.tan(alpha) + + local runway=airbase:GetActiveRunway() + + -- Set holding flag to 0=false. + self.flaghold:Set(0) + + local holdtime=5*60 + if fc or self.airboss then + holdtime=nil + end + + -- Task fuction when reached holding point. + local TaskArrived=self.group:TaskFunction("FLIGHTGROUP._ReachedHolding", self) + + -- Orbit until flaghold=1 (true) but max 5 min if no FC is giving the landing clearance. + local TaskOrbit = self.group:TaskOrbit(p0, nil, UTILS.KnotsToMps(SpeedHold), p1) + local TaskLand = self.group:TaskCondition(nil, self.flaghold.UserFlagName, 1, nil, holdtime) + local TaskHold = self.group:TaskControlled(TaskOrbit, TaskLand) + local TaskKlar = self.group:TaskFunction("FLIGHTGROUP._ClearedToLand", self) -- Once the holding flag becomes true, set trigger FLIGHTLANDING, i.e. set flight STATUS to LANDING. + + -- Waypoints from current position to holding point. + local wp={} + wp[#wp+1]=c0:WaypointAir(nil, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, UTILS.KnotsToKmph(SpeedTo), true , nil, {}, "Current Pos") + wp[#wp+1]=p0:WaypointAir(nil, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, UTILS.KnotsToKmph(SpeedTo), true , nil, {TaskArrived, TaskHold, TaskKlar}, "Holding Point") + + -- Approach point: 10 NN in direction of runway. + if airbase:GetAirbaseCategory()==Airbase.Category.AIRDROME then + + --- + -- Airdrome + --- + + local papp=airbase:GetCoordinate():Translate(x1, runway.heading-180):SetAltitude(h1) + wp[#wp+1]=papp:WaypointAirTurningPoint(nil, UTILS.KnotsToKmph(SpeedLand), {}, "Final Approach") + + -- Okay, it looks like it's best to specify the coordinates not at the airbase but a bit away. This causes a more direct landing approach. + local pland=airbase:GetCoordinate():Translate(x2, runway.heading-180):SetAltitude(h2) + wp[#wp+1]=pland:WaypointAirLanding(UTILS.KnotsToKmph(SpeedLand), airbase, {}, "Landing") + + elseif airbase:GetAirbaseCategory()==Airbase.Category.SHIP then + + --- + -- Ship + --- + + local pland=airbase:GetCoordinate() + wp[#wp+1]=pland:WaypointAirLanding(UTILS.KnotsToKmph(SpeedLand), airbase, {}, "Landing") + + end + + if self.isAI then + + local routeto=false + if fc or world.event.S_EVENT_KILL then + routeto=true + end + + -- Clear all tasks. + -- Warning, looks like this can make DCS CRASH! Had this after calling RTB once passed the final waypoint. + --self:ClearTasks() + + -- Respawn? + if routeto then + + -- Just route the group. Respawn might happen when going from holding to final. + self:Route(wp, 1) + + else + + -- Get group template. + local Template=self.group:GetTemplate() + + -- Set route points. + Template.route.points=wp + + --Respawn the group with new waypoints. + self:Respawn(Template) + + end + + end + +end + +--- On before "Wait" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Point#COORDINATE Coord Coordinate where to orbit. Default current position. +-- @param #number Altitude Altitude in feet. Default 10000 ft. +-- @param #number Speed Speed in knots. Default 250 kts. +function FLIGHTGROUP:onbeforeWait(From, Event, To, Coord, Altitude, Speed) + + local allowed=true + local Tsuspend=nil + + -- Check if there are remaining tasks. + local Ntot,Nsched, Nwp=self:CountRemainingTasks() + + if self.taskcurrent>0 then + self:I(self.lid..string.format("WARNING: Got current task ==> WAIT event is suspended for 10 sec.")) + Tsuspend=-10 + allowed=false + end + + if Nsched>0 then + self:I(self.lid..string.format("WARNING: Still got %d SCHEDULED tasks in the queue ==> WAIT event is suspended for 10 sec.", Nsched)) + Tsuspend=-10 + allowed=false + end + + if Nwp>0 then + self:I(self.lid..string.format("WARNING: Still got %d WAYPOINT tasks in the queue ==> WAIT event is suspended for 10 sec.", Nwp)) + Tsuspend=-10 + allowed=false + end + + if Tsuspend and not allowed then + self:__Wait(Tsuspend, Coord, Altitude, Speed) + end + + return allowed +end + + +--- On after "Wait" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Point#COORDINATE Coord Coordinate where to orbit. Default current position. +-- @param #number Altitude Altitude in feet. Default 10000 ft. +-- @param #number Speed Speed in knots. Default 250 kts. +function FLIGHTGROUP:onafterWait(From, Event, To, Coord, Altitude, Speed) + + Coord=Coord or self.group:GetCoordinate() + Altitude=Altitude or (self.ishelo and 1000 or 10000) + Speed=Speed or (self.ishelo and 80 or 250) + + -- Debug message. + local text=string.format("Flight group set to wait/orbit at altitude %d m and speed %.1f km/h", Altitude, Speed) + self:T(self.lid..text) + + --TODO: set ROE passive. introduce roe event/state/variable. + + -- Orbit task. + local TaskOrbit=self.group:TaskOrbit(Coord, UTILS.FeetToMeters(Altitude), UTILS.KnotsToMps(Speed)) + + -- Set task. + self:SetTask(TaskOrbit) + +end + + +--- On after "Refuel" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Point#COORDINATE Coordinate The coordinate. +function FLIGHTGROUP:onafterRefuel(From, Event, To, Coordinate) + + -- Debug message. + local text=string.format("Flight group set to refuel at the nearest tanker") + self:I(self.lid..text) + + --TODO: set ROE passive. introduce roe event/state/variable. + --TODO: cancel current task + + -- Pause current mission if there is any. + self:PauseMission() + + -- Refueling task. + local TaskRefuel=self.group:TaskRefueling() + local TaskFunction=self.group:TaskFunction("FLIGHTGROUP._FinishedRefuelling", self) + local DCSTasks={TaskRefuel, TaskFunction} + + local Speed=self.speedCruise + + local coordinate=self.group:GetCoordinate() + + Coordinate=Coordinate or coordinate:Translate(UTILS.NMToMeters(5), self.group:GetHeading(), true) + + local wp0=coordinate:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, Speed, true) + local wp9=Coordinate:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, Speed, true, nil, DCSTasks, "Refuel") + + self:Route({wp0, wp9}, 1) + +end + +--- On after "Refueled" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTGROUP:onafterRefueled(From, Event, To) + + -- Debug message. + local text=string.format("Flight group finished refuelling") + self:I(self.lid..text) + + -- Check if flight is done. + self:_CheckGroupDone(1) + +end + + +--- On after "Holding" event. Flight arrived at the holding point. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTGROUP:onafterHolding(From, Event, To) + + -- Set holding flag to 0 (just in case). + self.flaghold:Set(0) + + -- Holding time stamp. + self.Tholding=timer.getAbsTime() + + local text=string.format("Flight group %s is HOLDING now", self.groupname) + self:T(self.lid..text) + + -- Add flight to waiting/holding queue. + if self.flightcontrol then + + -- Set flight status to holding + self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.HOLDING) + + if not self.isAI then + self:_UpdateMenu() + end + + elseif self.airboss then + + if self.ishelo then + + local carrierpos=self.airboss:GetCoordinate() + local carrierheading=self.airboss:GetHeading() + + local Distance=UTILS.NMToMeters(5) + local Angle=carrierheading+90 + local altitude=math.random(12, 25)*100 + local oc=carrierpos:Translate(Distance,Angle):SetAltitude(altitude, true) + + -- Orbit until flaghold=1 (true) but max 5 min if no FC is giving the landing clearance. + local TaskOrbit=self.group:TaskOrbit(oc, nil, UTILS.KnotsToMps(50)) + local TaskLand=self.group:TaskCondition(nil, self.flaghold.UserFlagName, 1) + local TaskHold=self.group:TaskControlled(TaskOrbit, TaskLand) + local TaskKlar=self.group:TaskFunction("FLIGHTGROUP._ClearedToLand", self) -- Once the holding flag becomes true, set trigger FLIGHTLANDING, i.e. set flight STATUS to LANDING. + + local DCSTask=self.group:TaskCombo({TaskOrbit, TaskHold, TaskKlar}) + + self:SetTask(DCSTask) + end + + end + +end + +--- On after "EngageTarget" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #table Target Target object. Can be a UNIT, STATIC, GROUP, SET_UNIT or SET_GROUP object. +function FLIGHTGROUP:onafterEngageTarget(From, Event, To, Target) + + -- DCS task. + local DCStask=nil + + -- Check target object. + if Target:IsInstanceOf("UNIT") or Target:IsInstanceOf("STATIC") then + + DCStask=self:GetGroup():TaskAttackUnit(Target, true) + + elseif Target:IsInstanceOf("GROUP") then + + DCStask=self:GetGroup():TaskAttackGroup(Target, nil, nil, nil, nil, nil, nil, true) + + elseif Target:IsInstanceOf("SET_UNIT") then + + local DCSTasks={} + + for _,_unit in pairs(Target:GetSet()) do --detected by =HRP= Zero + local unit=_unit --Wrapper.Unit#UNIT + local task=self:GetGroup():TaskAttackUnit(unit, true) + table.insert(DCSTasks) + end + + -- Task combo. + DCStask=self:GetGroup():TaskCombo(DCSTasks) + + elseif Target:IsInstanceOf("SET_GROUP") then + + local DCSTasks={} + + for _,_unit in pairs(Target:GetSet()) do --detected by =HRP= Zero + local unit=_unit --Wrapper.Unit#UNIT + local task=self:GetGroup():TaskAttackGroup(Target, nil, nil, nil, nil, nil, nil, true) + table.insert(DCSTasks) + end + + -- Task combo. + DCStask=self:GetGroup():TaskCombo(DCSTasks) + + else + self:E("ERROR: unknown Target in EngageTarget! Needs to be a UNIT, STATIC, GROUP, SET_UNIT or SET_GROUP") + return + end + + -- Create new task.The description "Engage_Target" is checked so do not change that lightly. + local Task=self:NewTaskScheduled(DCStask, 1, "Engage_Target", 0) + + -- Backup ROE setting. + Task.backupROE=self:GetROE() + + -- Switch ROE to open fire + self:SwitchROE(ENUMS.ROE.OpenFire) + + -- Pause current mission. + local mission=self:GetMissionCurrent() + if mission then + self:PauseMission() + end + + -- Execute task. + self:TaskExecute(Task) + +end + +--- On after "Disengage" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Set#SET_UNIT TargetUnitSet +function FLIGHTGROUP:onafterDisengage(From, Event, To) + self:T(self.lid.."Disengage target") +end + +--- On before "LandAt" event. Check we have a helo group. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Point#COORDINATE Coordinate The coordinate where to land. Default is current position. +-- @param #number Duration The duration in seconds to remain on ground. Default 600 sec (10 min). +function FLIGHTGROUP:onbeforeLandAt(From, Event, To, Coordinate, Duration) + return self.ishelo +end + +--- On after "LandAt" event. Order helicopter to land at a specific point. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Point#COORDINATE Coordinate The coordinate where to land. Default is current position. +-- @param #number Duration The duration in seconds to remain on ground. Default 600 sec (10 min). +function FLIGHTGROUP:onafterLandAt(From, Event, To, Coordinate, Duration) + + -- Duration. + Duration=Duration or 600 + + Coordinate=Coordinate or self:GetCoordinate() + + local DCStask=self.group:TaskLandAtVec2(Coordinate:GetVec2(), Duration) + + local Task=self:NewTaskScheduled(DCStask, 1, "Task_Land_At", 0) + + -- Add task with high priority. + --self:AddTask(task, 1, "Task_Land_At", 0) + + self:TaskExecute(Task) + +end + +--- On after "FuelLow" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTGROUP:onafterFuelLow(From, Event, To) + + -- Debug message. + local text=string.format("Low fuel for flight group %s", self.groupname) + self:I(self.lid..text) + + -- Set switch to true. + self.fuellow=true + + -- Back to destination or home. + local airbase=self.destbase or self.homebase + + if self.airwing then + + -- Get closest tanker from airwing that can refuel this flight. + local tanker=self.airwing:GetTankerForFlight(self) + + if tanker and self.fuellowrefuel then + + -- Debug message. + self:I(self.lid..string.format("Send to refuel at tanker %s", tanker.flightgroup:GetName())) + + -- Get a coordinate towards the tanker. + local coordinate=self:GetCoordinate():GetIntermediateCoordinate(tanker.flightgroup:GetCoordinate(), 0.75) + + -- Send flight to tanker with refueling task. + self:Refuel(coordinate) + + else + + if airbase and self.fuellowrtb then + self:RTB(airbase) + --TODO: RTZ + end + + end + + else + + if self.fuellowrefuel and self.refueltype then + + local tanker=self:FindNearestTanker(50) + + if tanker then + + self:I(self.lid..string.format("Send to refuel at tanker %s", tanker:GetName())) + + -- Get a coordinate towards the tanker. + local coordinate=self:GetCoordinate():GetIntermediateCoordinate(tanker:GetCoordinate(), 0.75) + + self:Refuel(coordinate) + + return + end + end + + if airbase and self.fuellowrtb then + self:RTB(airbase) + --TODO: RTZ + end + + end + +end + +--- On after "FuelCritical" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTGROUP:onafterFuelCritical(From, Event, To) + + -- Debug message. + local text=string.format("Critical fuel for flight group %s", self.groupname) + self:I(self.lid..text) + + -- Set switch to true. + self.fuelcritical=true + + -- Airbase. + local airbase=self.destbase or self.homebase + + if airbase and self.fuelcriticalrtb and not self:IsGoing4Fuel() then + self:RTB(airbase) + --TODO: RTZ + end +end + +--- On after "Stop" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTGROUP:onafterStop(From, Event, To) + + -- Check if group is still alive. + if self:IsAlive() then + + -- Set element parking spot to FREE (after arrived for example). + if self.flightcontrol then + for _,_element in pairs(self.elements) do + local element=_element --#FLIGHTGROUP.Element + self:_SetElementParkingFree(element) + end + end + + end + + -- Handle events: + self:UnHandleEvent(EVENTS.Birth) + self:UnHandleEvent(EVENTS.EngineStartup) + self:UnHandleEvent(EVENTS.Takeoff) + self:UnHandleEvent(EVENTS.Land) + self:UnHandleEvent(EVENTS.EngineShutdown) + self:UnHandleEvent(EVENTS.PilotDead) + self:UnHandleEvent(EVENTS.Ejection) + self:UnHandleEvent(EVENTS.Crash) + self:UnHandleEvent(EVENTS.RemoveUnit) + + -- Call OPSGROUP function. + self:GetParent(self).onafterStop(self, From, Event, To) + + -- Remove flight from data base. + _DATABASE.FLIGHTGROUPS[self.groupname]=nil +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Task functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Mission functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Special Task Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Function called when flight has reached the holding point. +-- @param Wrapper.Group#GROUP group Group object. +-- @param #FLIGHTGROUP flightgroup Flight group object. +function FLIGHTGROUP._ReachedHolding(group, flightgroup) + flightgroup:T2(flightgroup.lid..string.format("Group reached holding point")) + + -- Trigger Holding event. + flightgroup:__Holding(-1) +end + +--- Function called when flight has reached the holding point. +-- @param Wrapper.Group#GROUP group Group object. +-- @param #FLIGHTGROUP flightgroup Flight group object. +function FLIGHTGROUP._ClearedToLand(group, flightgroup) + flightgroup:T2(flightgroup.lid..string.format("Group was cleared to land")) + + -- Trigger Landing event. + flightgroup:__Landing(-1) +end + +--- Function called when flight finished refuelling. +-- @param Wrapper.Group#GROUP group Group object. +-- @param #FLIGHTGROUP flightgroup Flight group object. +function FLIGHTGROUP._FinishedRefuelling(group, flightgroup) + flightgroup:T2(flightgroup.lid..string.format("Group finished refueling")) + + -- Trigger Holding event. + flightgroup:__Refueled(-1) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Initialize group parameters. Also initializes waypoints if self.waypoints is nil. +-- @param #FLIGHTGROUP self +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:_InitGroup() + + -- First check if group was already initialized. + if self.groupinitialized then + self:E(self.lid.."WARNING: Group was already initialized!") + return + end + + -- Group object. + local group=self.group --Wrapper.Group#GROUP + + -- Get template of group. + self.template=group:GetTemplate() + + -- Define category. + self.isAircraft=true + self.isNaval=false + self.isGround=false + + -- Helo group. + self.ishelo=group:IsHelicopter() + + -- Is (template) group uncontrolled. + self.isUncontrolled=self.template.uncontrolled + + -- Is (template) group late activated. + self.isLateActivated=self.template.lateActivation + + -- Max speed in km/h. + self.speedMax=group:GetSpeedMax() + + -- Cruise speed limit 350 kts for fixed and 80 knots for rotary wings. + local speedCruiseLimit=self.ishelo and UTILS.KnotsToKmph(80) or UTILS.KnotsToKmph(350) + + -- Cruise speed: 70% of max speed but within limit. + self.speedCruise=math.min(self.speedMax*0.7, speedCruiseLimit) + + -- Group ammo. + self.ammo=self:GetAmmoTot() + + -- Radio parameters from template. Default is set on spawn if not modified by user. + self.radio.Freq=tonumber(self.template.frequency) + self.radio.Modu=tonumber(self.template.modulation) + self.radio.On=self.template.communication + + -- Set callsign. Default is set on spawn if not modified by user. + local callsign=self.template.units[1].callsign + if type(callsign)=="number" then -- Sometimes callsign is just "101". + local cs=tostring(callsign) + callsign={} + callsign[1]=cs:sub(1,1) + callsign[2]=cs:sub(2,2) + callsign[3]=cs:sub(3,3) + end + self.callsign.NumberSquad=callsign[1] + self.callsign.NumberGroup=callsign[2] + self.callsign.NumberElement=callsign[3] -- First element only + self.callsign.NameSquad=UTILS.GetCallsignName(self.callsign.NumberSquad) + + -- Set default formation. + if self.ishelo then + self.optionDefault.Formation=ENUMS.Formation.RotaryWing.EchelonLeft.D300 + else + self.optionDefault.Formation=ENUMS.Formation.FixedWing.EchelonLeft.Group + end + + -- Default TACAN off. + self:SetDefaultTACAN(nil, nil, nil, nil, true) + self.tacan=UTILS.DeepCopy(self.tacanDefault) + + -- Is this purely AI? + self.isAI=not self:_IsHuman(group) + + -- Create Menu. + if not self.isAI then + self.menu=self.menu or {} + self.menu.atc=self.menu.atc or {} + self.menu.atc.root=self.menu.atc.root or MENU_GROUP:New(self.group, "ATC") + end + + -- Add elemets. + for _,unit in pairs(self.group:GetUnits()) do + local element=self:AddElementByName(unit:GetName()) + end + + -- Get first unit. This is used to extract other parameters. + local unit=self.group:GetUnit(1) + + if unit then + + self.rangemax=unit:GetRange() + + self.descriptors=unit:GetDesc() + + self.actype=unit:GetTypeName() + + self.ceiling=self.descriptors.Hmax + + self.tankertype=select(2, unit:IsTanker()) + self.refueltype=select(2, unit:IsRefuelable()) + + -- Debug info. + if self.verbose>=1 then + local text=string.format("Initialized Flight Group %s:\n", self.groupname) + text=text..string.format("Unit type = %s\n", self.actype) + text=text..string.format("Speed max = %.1f Knots\n", UTILS.KmphToKnots(self.speedMax)) + text=text..string.format("Range max = %.1f km\n", self.rangemax/1000) + text=text..string.format("Ceiling = %.1f feet\n", UTILS.MetersToFeet(self.ceiling)) + text=text..string.format("Tanker type = %s\n", tostring(self.tankertype)) + text=text..string.format("Refuel type = %s\n", tostring(self.refueltype)) + text=text..string.format("AI = %s\n", tostring(self.isAI)) + text=text..string.format("Helicopter = %s\n", tostring(self.group:IsHelicopter())) + text=text..string.format("Elements = %d\n", #self.elements) + text=text..string.format("Waypoints = %d\n", #self.waypoints) + text=text..string.format("Radio = %.1f MHz %s %s\n", self.radio.Freq, UTILS.GetModulationName(self.radio.Modu), tostring(self.radio.On)) + text=text..string.format("Ammo = %d (G=%d/R=%d/B=%d/M=%d)\n", self.ammo.Total, self.ammo.Guns, self.ammo.Rockets, self.ammo.Bombs, self.ammo.Missiles) + text=text..string.format("FSM state = %s\n", self:GetState()) + text=text..string.format("Is alive = %s\n", tostring(self.group:IsAlive())) + text=text..string.format("LateActivate = %s\n", tostring(self:IsLateActivated())) + text=text..string.format("Uncontrolled = %s\n", tostring(self:IsUncontrolled())) + text=text..string.format("Start Air = %s\n", tostring(self:IsTakeoffAir())) + text=text..string.format("Start Cold = %s\n", tostring(self:IsTakeoffCold())) + text=text..string.format("Start Hot = %s\n", tostring(self:IsTakeoffHot())) + text=text..string.format("Start Rwy = %s\n", tostring(self:IsTakeoffRunway())) + self:I(self.lid..text) + end + + -- Init done. + self.groupinitialized=true + + end + + return self +end + +--- Add an element to the flight group. +-- @param #FLIGHTGROUP self +-- @param #string unitname Name of unit. +-- @return #FLIGHTGROUP.Element The element or nil. +function FLIGHTGROUP:AddElementByName(unitname) + + local unit=UNIT:FindByName(unitname) + + if unit then + + local element={} --#FLIGHTGROUP.Element + + element.name=unitname + element.unit=unit + element.status=OPSGROUP.ElementStatus.INUTERO + element.group=unit:GetGroup() + + -- TODO: this is wrong when grouping is used! + local unittemplate=element.unit:GetTemplate() + + element.modex=unittemplate.onboard_num + element.skill=unittemplate.skill + element.payload=unittemplate.payload + element.pylons=unittemplate.payload and unittemplate.payload.pylons or nil --element.unit:GetTemplatePylons() + element.fuelmass0=unittemplate.payload and unittemplate.payload.fuel or 0 --element.unit:GetTemplatePayload().fuel + element.fuelmass=element.fuelmass0 + element.fuelrel=element.unit:GetFuel() + element.category=element.unit:GetUnitCategory() + element.categoryname=element.unit:GetCategoryName() + element.callsign=element.unit:GetCallsign() + element.size=element.unit:GetObjectSize() + + if element.skill=="Client" or element.skill=="Player" then + element.ai=false + element.client=CLIENT:FindByName(unitname) + else + element.ai=true + end + + -- Debug text. + local text=string.format("Adding element %s: status=%s, skill=%s, modex=%s, fuelmass=%.1f (%d), category=%d, categoryname=%s, callsign=%s, ai=%s", + element.name, element.status, element.skill, element.modex, element.fuelmass, element.fuelrel*100, element.category, element.categoryname, element.callsign, tostring(element.ai)) + self:T(self.lid..text) + + -- Add element to table. + table.insert(self.elements, element) + + if unit:IsAlive() then + self:ElementSpawned(element) + end + + return element + end + + return nil +end + + +--- Check if a unit is and element of the flightgroup. +-- @param #FLIGHTGROUP self +-- @return Wrapper.Airbase#AIRBASE Final destination airbase or #nil. +function FLIGHTGROUP:GetHomebaseFromWaypoints() + + local wp=self:GetWaypoint(1) + + if wp then + + if wp and wp.action and wp.action==COORDINATE.WaypointAction.FromParkingArea + or wp.action==COORDINATE.WaypointAction.FromParkingAreaHot + or wp.action==COORDINATE.WaypointAction.FromRunway then + + -- Get airbase ID depending on airbase category. + local airbaseID=nil + + if wp.airdromeId then + airbaseID=wp.airdromeId + else + airbaseID=-wp.helipadId + end + + local airbase=AIRBASE:FindByID(airbaseID) + + return airbase + end + + --TODO: Handle case where e.g. only one WP but that is not landing. + --TODO: Probably other cases need to be taken care of. + + end + + return nil +end + +--- Find the nearest friendly airbase (same or neutral coalition). +-- @param #FLIGHTGROUP self +-- @param #number Radius Search radius in NM. Default 50 NM. +-- @return Wrapper.Airbase#AIRBASE Closest tanker group #nil. +function FLIGHTGROUP:FindNearestAirbase(Radius) + + local coord=self:GetCoordinate() + + local dmin=math.huge + local airbase=nil --Wrapper.Airbase#AIRBASE + for _,_airbase in pairs(AIRBASE.GetAllAirbases()) do + local ab=_airbase --Wrapper.Airbase#AIRBASE + + local coalitionAB=ab:GetCoalition() + + if coalitionAB==self:GetCoalition() or coalitionAB==coalition.side.NEUTRAL then + + if airbase then + local d=ab:GetCoordinate():Get2DDistance(coord) + + if d %s Destination", #self.waypoints, self.homebase and self.homebase:GetName() or "unknown", self.destbase and self.destbase:GetName() or "uknown")) + + -- Update route. + if #self.waypoints>0 then + + -- Check if only 1 wp? + if #self.waypoints==1 then + self.passedfinalwp=true + end + + end + + return self +end + +--- Add an AIR waypoint to the flight plan. +-- @param #FLIGHTGROUP self +-- @param Core.Point#COORDINATE Coordinate The coordinate of the waypoint. Use COORDINATE:SetAltitude(altitude) to define the altitude. +-- @param #number Speed Speed in knots. Default 350 kts. +-- @param #number AfterWaypointWithID Insert waypoint after waypoint given ID. Default is to insert as last waypoint. +-- @param #number Altitude Altitude in feet. Default is y-component of Coordinate. Note that these altitudes are wrt to sea level (barometric altitude). +-- @param #boolean Updateroute If true or nil, call UpdateRoute. If false, no call. +-- @return Ops.OpsGroup#OPSGROUP.Waypoint Waypoint table. +function FLIGHTGROUP:AddWaypoint(Coordinate, Speed, AfterWaypointWithID, Altitude, Updateroute) + + -- Set waypoint index. + local wpnumber=self:GetWaypointIndexAfterID(AfterWaypointWithID) + + if wpnumber>self.currentwp then + self.passedfinalwp=false + end + + -- Speed in knots. + Speed=Speed or 350 + + -- Create air waypoint. + local wp=Coordinate:WaypointAir(COORDINATE.WaypointAltType.BARO, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, UTILS.KnotsToKmph(Speed), true, nil, {}) + + -- Create waypoint data table. + local waypoint=self:_CreateWaypoint(wp) + + -- Set altitude. + if Altitude then + waypoint.alt=UTILS.FeetToMeters(Altitude) + end + + -- Add waypoint to table. + self:_AddWaypoint(waypoint, wpnumber) + + -- Debug info. + self:T(self.lid..string.format("Adding AIR waypoint #%d, speed=%.1f knots. Last waypoint passed was #%s. Total waypoints #%d", wpnumber, Speed, self.currentwp, #self.waypoints)) + + -- Update route. + if Updateroute==nil or Updateroute==true then + self:__UpdateRoute(-1) + end + + return waypoint +end + + + +--- Check if a unit is an element of the flightgroup. +-- @param #FLIGHTGROUP self +-- @param #string unitname Name of unit. +-- @return #boolean If true, unit is element of the flight group or false if otherwise. +function FLIGHTGROUP:_IsElement(unitname) + + for _,_element in pairs(self.elements) do + local element=_element --#FLIGHTGROUP.Element + + if element.name==unitname then + return true + end + + end + + return false +end + + + +--- Set parking spot of element. +-- @param #FLIGHTGROUP self +-- @param #FLIGHTGROUP.Element Element The element. +-- @param Wrapper.Airbase#AIRBASE.ParkingSpot Spot Parking Spot. +function FLIGHTGROUP:_SetElementParkingAt(Element, Spot) + + -- Element is parking here. + Element.parking=Spot + + if Spot then + + -- Debug info. + self:T(self.lid..string.format("Element %s is parking on spot %d", Element.name, Spot.TerminalID)) + + if self.flightcontrol then + + -- Set parking spot to OCCUPIED. + self.flightcontrol:SetParkingOccupied(Element.parking, Element.name) + end + + end + +end + +--- Set parking spot of element to free +-- @param #FLIGHTGROUP self +-- @param #FLIGHTGROUP.Element Element The element. +function FLIGHTGROUP:_SetElementParkingFree(Element) + + if Element.parking then + + -- Set parking to FREE. + if self.flightcontrol then + self.flightcontrol:SetParkingFree(Element.parking) + end + + -- Not parking any more. + Element.parking=nil + + end + +end + +--- Get onboard number. +-- @param #FLIGHTGROUP self +-- @param #string unitname Name of the unit. +-- @return #string Modex. +function FLIGHTGROUP:_GetOnboardNumber(unitname) + + local group=UNIT:FindByName(unitname):GetGroup() + + -- Units of template group. + local units=group:GetTemplate().units + + -- Get numbers. + local numbers={} + for _,unit in pairs(units) do + + if unitname==unit.name then + return tostring(unit.onboard_num) + end + + end + + return nil +end + +--- Checks if a human player sits in the unit. +-- @param #FLIGHTGROUP self +-- @param Wrapper.Unit#UNIT unit Aircraft unit. +-- @return #boolean If true, human player inside the unit. +function FLIGHTGROUP:_IsHumanUnit(unit) + + -- Get player unit or nil if no player unit. + local playerunit=self:_GetPlayerUnitAndName(unit:GetName()) + + if playerunit then + return true + else + return false + end +end + +--- Checks if a group has a human player. +-- @param #FLIGHTGROUP self +-- @param Wrapper.Group#GROUP group Aircraft group. +-- @return #boolean If true, human player inside group. +function FLIGHTGROUP:_IsHuman(group) + + -- Get all units of the group. + local units=group:GetUnits() + + -- Loop over all units. + for _,_unit in pairs(units) do + -- Check if unit is human. + local human=self:_IsHumanUnit(_unit) + if human then + return true + end + end + + return false +end + +--- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned. +-- @param #FLIGHTGROUP self +-- @param #string _unitName Name of the player unit. +-- @return Wrapper.Unit#UNIT Unit of player or nil. +-- @return #string Name of the player or nil. +function FLIGHTGROUP:_GetPlayerUnitAndName(_unitName) + self:F2(_unitName) + + if _unitName ~= nil then + + -- Get DCS unit from its name. + local DCSunit=Unit.getByName(_unitName) + + if DCSunit then + + local playername=DCSunit:getPlayerName() + local unit=UNIT:Find(DCSunit) + + if DCSunit and unit and playername then + return unit, playername + end + + end + + end + + -- Return nil if we could not find a player. + return nil,nil +end + +--- Returns the parking spot of the element. +-- @param #FLIGHTGROUP self +-- @param #FLIGHTGROUP.Element element Element of the flight group. +-- @param #number maxdist Distance threshold in meters. Default 5 m. +-- @param Wrapper.Airbase#AIRBASE airbase (Optional) The airbase to check for parking. Default is closest airbase to the element. +-- @return Wrapper.Airbase#AIRBASE.ParkingSpot Parking spot or nil if no spot is within distance threshold. +function FLIGHTGROUP:GetParkingSpot(element, maxdist, airbase) + + local coord=element.unit:GetCoordinate() + + airbase=airbase or self:GetClosestAirbase() --coord:GetClosestAirbase(nil, self:GetCoalition()) + + -- TODO: replace by airbase.parking if AIRBASE is updated. + local parking=airbase:GetParkingSpotsTable() + + local spot=nil --Wrapper.Airbase#AIRBASE.ParkingSpot + local dist=nil + local distmin=math.huge + for _,_parking in pairs(parking) do + local parking=_parking --Wrapper.Airbase#AIRBASE.ParkingSpot + dist=coord:Get2DDistance(parking.Coordinate) + if dist safedist) + return safe + end + + -- Get client coordinates. + local function _clients() + local clients=_DATABASE.CLIENTS + local coords={} + for clientname, client in pairs(clients) do + local template=_DATABASE:GetGroupTemplateFromUnitName(clientname) + local units=template.units + for i,unit in pairs(units) do + local coord=COORDINATE:New(unit.x, unit.alt, unit.y) + coords[unit.name]=coord + end + end + return coords + end + + -- Get airbase category. + local airbasecategory=airbase:GetAirbaseCategory() + + -- Get parking spot data table. This contains all free and "non-free" spots. + local parkingdata=airbase:GetParkingSpotsTable() + + -- List of obstacles. + local obstacles={} + + -- Loop over all parking spots and get the currently present obstacles. + -- How long does this take on very large airbases, i.e. those with hundereds of parking spots? Seems to be okay! + -- The alternative would be to perform the scan once but with a much larger radius and store all data. + for _,_parkingspot in pairs(parkingdata) do + local parkingspot=_parkingspot --Wrapper.Airbase#AIRBASE.ParkingSpot + + -- Scan a radius of 100 meters around the spot. + local _,_,_,_units,_statics,_sceneries=parkingspot.Coordinate:ScanObjects(scanradius, scanunits, scanstatics, scanscenery) + + -- Check all units. + for _,_unit in pairs(_units) do + local unit=_unit --Wrapper.Unit#UNIT + local _coord=unit:GetCoordinate() + local _size=self:_GetObjectSize(unit:GetDCSObject()) + local _name=unit:GetName() + table.insert(obstacles, {coord=_coord, size=_size, name=_name, type="unit"}) + end + + -- Check all clients. + local clientcoords=_clients() + for clientname,_coord in pairs(clientcoords) do + table.insert(obstacles, {coord=_coord, size=15, name=clientname, type="client"}) + end + + -- Check all statics. + for _,static in pairs(_statics) do + local _vec3=static:getPoint() + local _coord=COORDINATE:NewFromVec3(_vec3) + local _name=static:getName() + local _size=self:_GetObjectSize(static) + table.insert(obstacles, {coord=_coord, size=_size, name=_name, type="static"}) + end + + -- Check all scenery. + for _,scenery in pairs(_sceneries) do + local _vec3=scenery:getPoint() + local _coord=COORDINATE:NewFromVec3(_vec3) + local _name=scenery:getTypeName() + local _size=self:_GetObjectSize(scenery) + table.insert(obstacles,{coord=_coord, size=_size, name=_name, type="scenery"}) + end + + end + + -- Parking data for all assets. + local parking={} + + -- Get terminal type. + local terminaltype=self:_GetTerminal(self.attribute, airbase:GetAirbaseCategory()) + + -- Loop over all units - each one needs a spot. + for i,_element in pairs(self.elements) do + local element=_element --#FLIGHTGROUP.Element + + -- Loop over all parking spots. + local gotit=false + for _,_parkingspot in pairs(parkingdata) do + local parkingspot=_parkingspot --Wrapper.Airbase#AIRBASE.ParkingSpot + + -- Check correct terminal type for asset. We don't want helos in shelters etc. + if AIRBASE._CheckTerminalType(parkingspot.TerminalType, terminaltype) then + + -- Assume free and no problematic obstacle. + local free=true + local problem=nil + + -- Safe parking using TO_AC from DCS result. + if verysafe and parkingspot.TOAC then + free=false + self:T2(self.lid..string.format("Parking spot %d is occupied by other aircraft taking off (TOAC).", parkingspot.TerminalID)) + end + + -- Loop over all obstacles. + for _,obstacle in pairs(obstacles) do + + -- Check if aircraft overlaps with any obstacle. + local dist=parkingspot.Coordinate:Get2DDistance(obstacle.coord) + local safe=_overlap(element.size, obstacle.size, dist) + + -- Spot is blocked. + if not safe then + free=false + problem=obstacle + problem.dist=dist + break + end + + end + + -- Check flightcontrol data. + if self.flightcontrol and self.flightcontrol.airbasename==airbase:GetName() then + local problem=self.flightcontrol:IsParkingReserved(parkingspot) or self.flightcontrol:IsParkingOccupied(parkingspot) + if problem then + free=false + end + end + + -- Check if spot is free + if free then + + -- Add parkingspot for this element. + table.insert(parking, parkingspot) + + self:T2(self.lid..string.format("Parking spot %d is free for element %s!", parkingspot.TerminalID, element.name)) + + -- Add the unit as obstacle so that this spot will not be available for the next unit. + table.insert(obstacles, {coord=parkingspot.Coordinate, size=element.size, name=element.name, type="element"}) + + gotit=true + break + + else + + -- Debug output for occupied spots. + self:T2(self.lid..string.format("Parking spot %d is occupied or not big enough!", parkingspot.TerminalID)) + --if self.Debug then + -- local coord=problem.coord --Core.Point#COORDINATE + -- local text=string.format("Obstacle blocking spot #%d is %s type %s with size=%.1f m and distance=%.1f m.", _termid, problem.name, problem.type, problem.size, problem.dist) + -- coord:MarkToAll(string.format(text)) + --end + + end + + end -- check terminal type + end -- loop over parking spots + + -- No parking spot for at least one asset :( + if not gotit then + self:E(self.lid..string.format("WARNING: No free parking spot for element %s", element.name)) + return nil + end + + end -- loop over asset units + + return parking +end + +--- Size of the bounding box of a DCS object derived from the DCS descriptor table. If boundinb box is nil, a size of zero is returned. +-- @param #FLIGHTGROUP self +-- @param DCS#Object DCSobject The DCS object for which the size is needed. +-- @return #number Max size of object in meters (length (x) or width (z) components not including height (y)). +-- @return #number Length (x component) of size. +-- @return #number Height (y component) of size. +-- @return #number Width (z component) of size. +function FLIGHTGROUP:_GetObjectSize(DCSobject) + local DCSdesc=DCSobject:getDesc() + if DCSdesc.box then + local x=DCSdesc.box.max.x+math.abs(DCSdesc.box.min.x) --length + local y=DCSdesc.box.max.y+math.abs(DCSdesc.box.min.y) --height + local z=DCSdesc.box.max.z+math.abs(DCSdesc.box.min.z) --width + return math.max(x,z), x , y, z + end + return 0,0,0,0 +end + +--- Get the generalized attribute of a group. +-- @param #FLIGHTGROUP self +-- @return #string Generalized attribute of the group. +function FLIGHTGROUP:_GetAttribute() + + -- Default + local attribute=FLIGHTGROUP.Attribute.OTHER + + local group=self.group --Wrapper.Group#GROUP + + if group then + + --- Planes + local transportplane=group:HasAttribute("Transports") and group:HasAttribute("Planes") + local awacs=group:HasAttribute("AWACS") + local fighter=group:HasAttribute("Fighters") or group:HasAttribute("Interceptors") or group:HasAttribute("Multirole fighters") or (group:HasAttribute("Bombers") and not group:HasAttribute("Strategic bombers")) + local bomber=group:HasAttribute("Strategic bombers") + local tanker=group:HasAttribute("Tankers") + local uav=group:HasAttribute("UAVs") + --- Helicopters + local transporthelo=group:HasAttribute("Transport helicopters") + local attackhelicopter=group:HasAttribute("Attack helicopters") + + -- Define attribute. Order is important. + if transportplane then + attribute=FLIGHTGROUP.Attribute.AIR_TRANSPORTPLANE + elseif awacs then + attribute=FLIGHTGROUP.Attribute.AIR_AWACS + elseif fighter then + attribute=FLIGHTGROUP.Attribute.AIR_FIGHTER + elseif bomber then + attribute=FLIGHTGROUP.Attribute.AIR_BOMBER + elseif tanker then + attribute=FLIGHTGROUP.Attribute.AIR_TANKER + elseif transporthelo then + attribute=FLIGHTGROUP.Attribute.AIR_TRANSPORTHELO + elseif attackhelicopter then + attribute=FLIGHTGROUP.Attribute.AIR_ATTACKHELO + elseif uav then + attribute=FLIGHTGROUP.Attribute.AIR_UAV + end + + end + + return attribute +end + +--- Get the proper terminal type based on generalized attribute of the group. +--@param #FLIGHTGROUP self +--@param #FLIGHTGROUP.Attribute _attribute Generlized attibute of unit. +--@param #number _category Airbase category. +--@return Wrapper.Airbase#AIRBASE.TerminalType Terminal type for this group. +function FLIGHTGROUP:_GetTerminal(_attribute, _category) + + -- Default terminal is "large". + local _terminal=AIRBASE.TerminalType.OpenBig + + if _attribute==FLIGHTGROUP.Attribute.AIR_FIGHTER then + -- Fighter ==> small. + _terminal=AIRBASE.TerminalType.FighterAircraft + elseif _attribute==FLIGHTGROUP.Attribute.AIR_BOMBER or _attribute==FLIGHTGROUP.Attribute.AIR_TRANSPORTPLANE or _attribute==FLIGHTGROUP.Attribute.AIR_TANKER or _attribute==FLIGHTGROUP.Attribute.AIR_AWACS then + -- Bigger aircraft. + _terminal=AIRBASE.TerminalType.OpenBig + elseif _attribute==FLIGHTGROUP.Attribute.AIR_TRANSPORTHELO or _attribute==FLIGHTGROUP.Attribute.AIR_ATTACKHELO then + -- Helicopter. + _terminal=AIRBASE.TerminalType.HelicopterUsable + else + --_terminal=AIRBASE.TerminalType.OpenMedOrBig + end + + -- For ships, we allow medium spots for all fixed wing aircraft. There are smaller tankers and AWACS aircraft that can use a carrier. + if _category==Airbase.Category.SHIP then + if not (_attribute==FLIGHTGROUP.Attribute.AIR_TRANSPORTHELO or _attribute==FLIGHTGROUP.Attribute.AIR_ATTACKHELO) then + _terminal=AIRBASE.TerminalType.OpenMedOrBig + end + end + + return _terminal +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- OPTION FUNCTIONS +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- MENU FUNCTIONS +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get the proper terminal type based on generalized attribute of the group. +--@param #FLIGHTGROUP self +--@param #number delay Delay in seconds. +function FLIGHTGROUP:_UpdateMenu(delay) + + if delay and delay>0 then + self:I(self.lid..string.format("FF updating menu in %.1f sec", delay)) + self:ScheduleOnce(delay, FLIGHTGROUP._UpdateMenu, self) + else + + self:I(self.lid.."FF updating menu NOW") + + -- Get current position of group. + local position=self.group:GetCoordinate() + + -- Get all FLIGHTCONTROLS + local fc={} + for airbasename,_flightcontrol in pairs(_DATABASE.FLIGHTCONTROLS) do + + local airbase=AIRBASE:FindByName(airbasename) + + local coord=airbase:GetCoordinate() + + local dist=coord:Get2DDistance(position) + + local fcitem={airbasename=airbasename, dist=dist} + + table.insert(fc, fcitem) + end + + -- Sort table wrt distance to airbases. + local function _sort(a,b) + return a.dist Event --> To State + self:AddTransition("*", "FullStop", "Holding") -- Hold position. + self:AddTransition("*", "Cruise", "Cruising") -- Hold position. + + self:AddTransition("*", "TurnIntoWind", "IntoWind") -- Command the group to turn into the wind. + self:AddTransition("IntoWind", "TurnedIntoWind", "IntoWind") -- Group turned into wind. + self:AddTransition("IntoWind", "TurnIntoWindStop", "IntoWind") -- Stop a turn into wind. + self:AddTransition("IntoWind", "TurnIntoWindOver", "Cruising") -- Turn into wind is over. + + self:AddTransition("*", "TurningStarted", "*") -- Group started turning. + self:AddTransition("*", "TurningStopped", "*") -- Group stopped turning. + + self:AddTransition("*", "Detour", "OnDetour") -- Make a detour to a coordinate and resume route afterwards. + self:AddTransition("OnDetour", "DetourReached", "Cruising") -- Group reached the detour coordinate. + + self:AddTransition("*", "CollisionWarning", "*") -- Collision warning. + self:AddTransition("*", "ClearAhead", "*") -- Clear ahead. + + self:AddTransition("*", "Dive", "Diving") -- Command a submarine to dive. + self:AddTransition("Diving", "Surface", "Cruising") -- Command a submarine to go to the surface. + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Stop". Stops the NAVYGROUP and all its event handlers. + -- @param #NAVYGROUP self + + --- Triggers the FSM event "Stop" after a delay. Stops the NAVYGROUP and all its event handlers. + -- @function [parent=#NAVYGROUP] __Stop + -- @param #NAVYGROUP self + -- @param #number delay Delay in seconds. + + -- TODO: Add pseudo functions. + + + -- Init waypoints. + self:InitWaypoints() + + -- Initialize the group. + self:_InitGroup() + + -- Handle events: + self:HandleEvent(EVENTS.Birth, self.OnEventBirth) + self:HandleEvent(EVENTS.Dead, self.OnEventDead) + self:HandleEvent(EVENTS.RemoveUnit, self.OnEventRemoveUnit) + + -- Start the status monitoring. + self:__Status(-1) + + -- Start queue update timer. + self.timerQueueUpdate=TIMER:New(self._QueueUpdate, self):Start(2, 5) + + -- Start check zone timer. + self.timerCheckZone=TIMER:New(self._CheckInZones, self):Start(2, 60) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Group patrols ad inifintum. If the last waypoint is reached, it will go to waypoint one and repeat its route. +-- @param #NAVYGROUP self +-- @param #boolean switch If true or nil, patrol until the end of time. If false, go along the waypoints once and stop. +-- @return #NAVYGROUP self +function NAVYGROUP:SetPatrolAdInfinitum(switch) + if switch==false then + self.adinfinitum=false + else + self.adinfinitum=true + end + return self +end + +--- Enable/disable pathfinding. +-- @param #NAVYGROUP self +-- @param #boolean Switch If true, enable pathfinding. +-- @param #number CorridorWidth Corridor with in meters. Default 400 m. +-- @return #NAVYGROUP self +function NAVYGROUP:SetPathfinding(Switch, CorridorWidth) + self.pathfindingOn=Switch + self.pathCorridor=CorridorWidth or 400 + return self +end + +--- Enable pathfinding. +-- @param #NAVYGROUP self +-- @param #number CorridorWidth Corridor with in meters. Default 400 m. +-- @return #NAVYGROUP self +function NAVYGROUP:SetPathfindingOn(CorridorWidth) + self:SetPathfinding(true, CorridorWidth) + return self +end + +--- Disable pathfinding. +-- @param #NAVYGROUP self +-- @return #NAVYGROUP self +function NAVYGROUP:SetPathfindingOff() + self:SetPathfinding(true, self.pathCorridor) + return self +end + + +--- Add a *scheduled* task. +-- @param #NAVYGROUP self +-- @param Core.Point#COORDINATE Coordinate Coordinate of the target. +-- @param #string Clock Time when to start the attack. +-- @param #number Radius Radius in meters. Default 100 m. +-- @param #number Nshots Number of shots to fire. Default 3. +-- @param #number WeaponType Type of weapon. Default auto. +-- @param #number Prio Priority of the task. +-- @return Ops.OpsGroup#OPSGROUP.Task The task data. +function NAVYGROUP:AddTaskFireAtPoint(Coordinate, Clock, Radius, Nshots, WeaponType, Prio) + + local DCStask=CONTROLLABLE.TaskFireAtPoint(nil, Coordinate:GetVec2(), Radius, Nshots, WeaponType) + + local task=self:AddTask(DCStask, Clock, nil, Prio) + + return task +end + +--- Add a *waypoint* task. +-- @param #NAVYGROUP self +-- @param Core.Point#COORDINATE Coordinate Coordinate of the target. +-- @param Ops.OpsGroup#OPSGROUP.Waypoint Waypoint Where the task is executed. Default is next waypoint. +-- @param #number Radius Radius in meters. Default 100 m. +-- @param #number Nshots Number of shots to fire. Default 3. +-- @param #number WeaponType Type of weapon. Default auto. +-- @param #number Prio Priority of the task. +-- @param #number Duration Duration in seconds after which the task is cancelled. Default *never*. +-- @return Ops.OpsGroup#OPSGROUP.Task The task table. +function NAVYGROUP:AddTaskWaypointFireAtPoint(Coordinate, Waypoint, Radius, Nshots, WeaponType, Prio, Duration) + + Waypoint=Waypoint or self:GetWaypointNext() + + local DCStask=CONTROLLABLE.TaskFireAtPoint(nil, Coordinate:GetVec2(), Radius, Nshots, WeaponType) + + local task=self:AddTaskWaypoint(DCStask, Waypoint, nil, Prio, Duration) + + return task +end + + +--- Add a *scheduled* task. +-- @param #NAVYGROUP self +-- @param Wrapper.Group#GROUP TargetGroup Target group. +-- @param #number WeaponExpend How much weapons does are used. +-- @param #number WeaponType Type of weapon. Default auto. +-- @param #string Clock Time when to start the attack. +-- @param #number Prio Priority of the task. +-- @return Ops.OpsGroup#OPSGROUP.Task The task data. +function NAVYGROUP:AddTaskAttackGroup(TargetGroup, WeaponExpend, WeaponType, Clock, Prio) + + local DCStask=CONTROLLABLE.TaskAttackGroup(nil, TargetGroup, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit, GroupAttack) + + local task=self:AddTask(DCStask, Clock, nil, Prio) + + return task +end + +--- Create a turn into wind window. Note that this is not executed as it not added to the queue. +-- @param #NAVYGROUP self +-- @param #string starttime Start time, e.g. "8:00" for eight o'clock. Default now. +-- @param #string stoptime Stop time, e.g. "9:00" for nine o'clock. Default 90 minutes after start time. +-- @param #number speed Speed in knots during turn into wind leg. +-- @param #boolean uturn If true (or nil), carrier wil perform a U-turn and go back to where it came from before resuming its route to the next waypoint. If false, it will go directly to the next waypoint. +-- @param #number offset Offset angle in degrees, e.g. to account for an angled runway. Default 0 deg. +-- @return #NAVYGROUP.IntoWind Recovery window. +function NAVYGROUP:_CreateTurnIntoWind(starttime, stoptime, speed, uturn, offset) + + -- Absolute mission time in seconds. + local Tnow=timer.getAbsTime() + + -- Convert number to Clock. + if starttime and type(starttime)=="number" then + starttime=UTILS.SecondsToClock(Tnow+starttime) + end + + -- Input or now. + starttime=starttime or UTILS.SecondsToClock(Tnow) + + -- Set start time. + local Tstart=UTILS.ClockToSeconds(starttime) + + -- Set stop time. + local Tstop=Tstart+90*60 + + if stoptime==nil then + Tstop=Tstart+90*60 + elseif type(stoptime)=="number" then + Tstop=Tstart+stoptime + else + Tstop=UTILS.ClockToSeconds(stoptime) + end + + + -- Consistancy check for timing. + if Tstart>Tstop then + self:E(string.format("ERROR:Into wind stop time %s lies before start time %s. Input rejected!", UTILS.SecondsToClock(Tstart), UTILS.SecondsToClock(Tstop))) + return self + end + if Tstop<=Tnow then + self:E(string.format("WARNING: Into wind stop time %s already over. Tnow=%s! Input rejected.", UTILS.SecondsToClock(Tstop), UTILS.SecondsToClock(Tnow))) + return self + end + + -- Increase counter. + self.intowindcounter=self.intowindcounter+1 + + -- Recovery window. + local recovery={} --#NAVYGROUP.IntoWind + recovery.Tstart=Tstart + recovery.Tstop=Tstop + recovery.Open=false + recovery.Over=false + recovery.Speed=speed or 20 + recovery.Uturn=uturn and uturn or false + recovery.Offset=offset or 0 + recovery.Id=self.intowindcounter + + return recovery +end + +--- Add a time window, where the groups steams into the wind. +-- @param #NAVYGROUP self +-- @param #string starttime Start time, e.g. "8:00" for eight o'clock. Default now. +-- @param #string stoptime Stop time, e.g. "9:00" for nine o'clock. Default 90 minutes after start time. +-- @param #number speed Speed in knots during turn into wind leg. +-- @param #boolean uturn If `true` (or `nil`), carrier wil perform a U-turn and go back to where it came from before resuming its route to the next waypoint. If false, it will go directly to the next waypoint. +-- @param #number offset Offset angle in degrees, e.g. to account for an angled runway. Default 0 deg. +-- @return #NAVYGROUP.IntoWind Turn into window data table. +function NAVYGROUP:AddTurnIntoWind(starttime, stoptime, speed, uturn, offset) + + local recovery=self:_CreateTurnIntoWind(starttime, stoptime, speed, uturn, offset) + + --TODO: check if window is overlapping with an other and if extend the window. + + -- Add to table + table.insert(self.Qintowind, recovery) + + return recovery +end + +--- Remove steam into wind window from queue. If the window is currently active, it is stopped first. +-- @param #NAVYGROUP self +-- @param #NAVYGROUP.IntoWind IntoWindData Turn into window data table. +-- @return #NAVYGROUP self +function NAVYGROUP:RemoveTurnIntoWind(IntoWindData) + + -- Check if this is a window currently open. + if self.intowind and self.intowind.Id==IntoWindData.Id then + --env.info("FF stop in remove") + self:TurnIntoWindStop() + return + end + + for i,_tiw in pairs(self.Qintowind) do + local tiw=_tiw --#NAVYGROUP.IntoWind + if tiw.Id==IntoWindData.Id then + --env.info("FF removing window "..tiw.Id) + table.remove(self.Qintowind, i) + break + end + end + + return self +end + + +--- Check if the group is currently holding its positon. +-- @param #NAVYGROUP self +-- @return #boolean If true, group was ordered to hold. +function NAVYGROUP:IsHolding() + return self:Is("Holding") +end + +--- Check if the group is currently cruising. +-- @param #NAVYGROUP self +-- @return #boolean If true, group cruising. +function NAVYGROUP:IsCruising() + return self:Is("Cruising") +end + +--- Check if the group is currently on a detour. +-- @param #NAVYGROUP self +-- @return #boolean If true, group is on a detour +function NAVYGROUP:IsOnDetour() + return self:Is("OnDetour") +end + + +--- Check if the group is currently diving. +-- @param #NAVYGROUP self +-- @return #boolean If true, group is currently diving. +function NAVYGROUP:IsDiving() + return self:Is("Diving") +end + +--- Check if the group is currently turning. +-- @param #NAVYGROUP self +-- @return #boolean If true, group is currently turning. +function NAVYGROUP:IsTurning() + return self.turning +end + +--- Check if the group is currently steaming into the wind. +-- @param #NAVYGROUP self +-- @return #boolean If true, group is currently steaming into the wind. +function NAVYGROUP:IsSteamingIntoWind() + if self.intowind then + return true + else + return false + end +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Status +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +---- Update status. +-- @param #NAVYGROUP self +function NAVYGROUP:onbeforeStatus(From, Event, To) + + if self:IsDead() then + self:T(self.lid..string.format("Onbefore Status DEAD ==> false")) + return false + elseif self:IsStopped() then + self:T(self.lid..string.format("Onbefore Status STOPPED ==> false")) + return false + end + + return true +end + +--- Update status. +-- @param #NAVYGROUP self +function NAVYGROUP:onafterStatus(From, Event, To) + + -- FSM state. + local fsmstate=self:GetState() + + if self:IsAlive() then + + --- + -- Detection + --- + + -- Check if group has detected any units. + if self.detectionOn then + self:_CheckDetectedUnits() + end + + -- Update last known position, orientation, velocity. + self:_UpdatePosition() + + -- Check if group started or stopped turning. + self:_CheckTurning() + + local freepath=UTILS.NMToMeters(10) + + -- Only check if not currently turning. + if not self:IsTurning() then + + -- Check free path ahead. + freepath=self:_CheckFreePath(freepath, 100) + + if freepath<5000 then + + if not self.collisionwarning then + -- Issue a collision warning event. + self:CollisionWarning(freepath) + end + + if self.pathfindingOn and not self.ispathfinding then + self.ispathfinding=self:_FindPathToNextWaypoint() + end + + end + + end + + -- Check into wind queue. + self:_CheckTurnsIntoWind() + + -- Check if group got stuck. + self:_CheckStuck() + + if self.verbose>=1 then + + -- Get number of tasks and missions. + local nTaskTot, nTaskSched, nTaskWP=self:CountRemainingTasks() + local nMissions=self:CountRemainingMissison() + + local intowind=self:IsSteamingIntoWind() and UTILS.SecondsToClock(self.intowind.Tstop-timer.getAbsTime(), true) or "N/A" + local turning=tostring(self:IsTurning()) + local alt=self.position.y + local speed=UTILS.MpsToKnots(self.velocity) + local speedExpected=UTILS.MpsToKnots(self:GetExpectedSpeed()) --UTILS.MpsToKnots(self.speedWp or 0) + + -- Waypoint stuff. + local wpidxCurr=self.currentwp + local wpuidCurr=self:GetWaypointUIDFromIndex(wpidxCurr) or 0 + local wpidxNext=self:GetWaypointIndexNext() or 0 + local wpuidNext=self:GetWaypointUIDFromIndex(wpidxNext) or 0 + local wpDist=UTILS.MetersToNM(self:GetDistanceToWaypoint() or 0) + local wpETA=UTILS.SecondsToClock(self:GetTimeToWaypoint() or 0, true) + + -- Current ROE and alarm state. + local roe=self:GetROE() or 0 + local als=self:GetAlarmstate() or 0 + + -- Info text. + local text=string.format("%s [ROE=%d,AS=%d, T/M=%d/%d]: Wp=%d[%d]-->%d[%d] (of %d) Dist=%.1f NM ETA=%s - Speed=%.1f (%.1f) kts, Depth=%.1f m, Hdg=%03d, Turn=%s Collision=%d IntoWind=%s", + fsmstate, roe, als, nTaskTot, nMissions, wpidxCurr, wpuidCurr, wpidxNext, wpuidNext, #self.waypoints or 0, wpDist, wpETA, speed, speedExpected, alt, self.heading, turning, freepath, intowind) + self:I(self.lid..text) + + if false then + local text="Waypoints:" + for i,wp in pairs(self.waypoints) do + local waypoint=wp --Ops.OpsGroup#OPSGROUP.Waypoint + text=text..string.format("\n%d. UID=%d", i, waypoint.uid) + if i==self.currentwp then + text=text.." current!" + end + end + env.info(text) + end + + end + + else + + -- Info text. + local text=string.format("State %s: Alive=%s", fsmstate, tostring(self:IsAlive())) + self:T(self.lid..text) + + end + + --- + -- Recovery Windows + --- + + if self.verbose>=2 then + + -- Debug output: + local text=string.format(self.lid.."Turn into wind time windows:") + + -- Handle case with no recoveries. + if #self.Qintowind==0 then + text=text.." none!" + end + + -- Loop over all slots. + for i,_recovery in pairs(self.Qintowind) do + local recovery=_recovery --#NAVYGROUP.IntoWind + + -- Get start/stop clock strings. + local Cstart=UTILS.SecondsToClock(recovery.Tstart) + local Cstop=UTILS.SecondsToClock(recovery.Tstop) + + -- Debug text. + text=text..string.format("\n[%d] ID=%d Start=%s Stop=%s Open=%s Over=%s", i, recovery.Id, Cstart, Cstop, tostring(recovery.Open), tostring(recovery.Over)) + end + + -- Debug output. + self:I(self.lid..text) + + end + + + --- + -- Tasks & Missions + --- + + self:_PrintTaskAndMissionStatus() + + + -- Next status update in 30 seconds. + self:__Status(-30) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Events +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "ElementSpawned" event. +-- @param #NAVYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #NAVYGROUP.Element Element The group element. +function NAVYGROUP:onafterElementSpawned(From, Event, To, Element) + self:T(self.lid..string.format("Element spawned %s", Element.name)) + + -- Set element status. + self:_UpdateStatus(Element, OPSGROUP.ElementStatus.SPAWNED) + +end + +--- On after "Spawned" event. +-- @param #NAVYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function NAVYGROUP:onafterSpawned(From, Event, To) + self:T(self.lid..string.format("Group spawned!")) + + -- Update position. + self:_UpdatePosition() + + if self.isAI then + + -- Set default ROE. + self:SwitchROE(self.option.ROE) + + -- Set default Alarm State. + self:SwitchAlarmstate(self.option.Alarm) + + -- Set TACAN beacon. + self:_SwitchTACAN() + + -- Turn ICLS on. + self:_SwitchICLS() + + -- Set radio. + if self.radioDefault then + self:SwitchRadio() + else + self:SetDefaultRadio(self.radio.Freq, self.radio.Modu, false) + end + + end + + -- Update route. + if #self.waypoints>1 then + self:Cruise() + else + self:FullStop() + end + +end + +--- On after "UpdateRoute" event. +-- @param #NAVYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number n Waypoint number. Default is next waypoint. +-- @param #number Speed Speed in knots to the next waypoint. +-- @param #number Depth Depth in meters to the next waypoint. +function NAVYGROUP:onafterUpdateRoute(From, Event, To, n, Speed, Depth) + + -- Update route from this waypoint number onwards. + n=n or self:GetWaypointIndexNext() + + -- Update waypoint tasks, i.e. inject WP tasks into waypoint table. + self:_UpdateWaypointTasks(n) + + -- Waypoints. + local waypoints={} + + -- Waypoint. + local wp=UTILS.DeepCopy(self.waypoints[n]) --Ops.OpsGroup#OPSGROUP.Waypoint + + -- Speed. + if Speed then + -- Take speed specified. + wp.speed=UTILS.KnotsToMps(Speed) + else + -- Take default waypoint speed. But make sure speed>0 if patrol ad infinitum. + if self.adinfinitum and wp.speed<0.1 then + wp.speed=UTILS.KmphToMps(self.speedCruise) + end + end + + if Depth then + wp.alt=-Depth + elseif self.depth then + wp.alt=-self.depth + else + -- Take default waypoint alt. + end + + -- Current set speed in m/s. + self.speedWp=wp.speed + + -- Add waypoint. + table.insert(waypoints, wp) + + -- Current waypoint. + local current=self:GetCoordinate():WaypointNaval(UTILS.MpsToKmph(self.speedWp), wp.alt) + table.insert(waypoints, 1, current) + + + if not self.passedfinalwp then + + -- Debug info. + self:T(self.lid..string.format("Updateing route: WP %d-->%d (%d/%d), Speed=%.1f knots, Depth=%d m", self.currentwp, n, #waypoints, #self.waypoints, UTILS.MpsToKnots(self.speedWp), wp.alt)) + + -- Route group to all defined waypoints remaining. + self:Route(waypoints) + + else + + --- + -- Passed final WP ==> Full Stop + --- + + self:E(self.lid..string.format("WARNING: Passed final WP ==> Full Stop!")) + self:FullStop() + + end + +end + +--- On after "Detour" event. +-- @param #NAVYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Point#COORDINATE Coordinate Coordinate where to go. +-- @param #number Speed Speed in knots. Default cruise speed. +-- @param #number Depth Depth in meters. Default 0 meters. +-- @param #number ResumeRoute If true, resume route after detour point was reached. If false, the group will stop at the detour point and wait for futher commands. +function NAVYGROUP:onafterDetour(From, Event, To, Coordinate, Speed, Depth, ResumeRoute) + + -- Depth for submarines. + Depth=Depth or 0 + + -- Speed in knots. + Speed=Speed or self:GetSpeedCruise() + + -- ID of current waypoint. + local uid=self:GetWaypointCurrent().uid + + -- Add waypoint after current. + local wp=self:AddWaypoint(Coordinate, Speed, uid, Depth, true) + + -- Set if we want to resume route after reaching the detour waypoint. + if ResumeRoute then + wp.detour=1 + else + wp.detour=0 + end + +end + +--- On after "DetourReached" event. +-- @param #NAVYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function NAVYGROUP:onafterDetourReached(From, Event, To) + self:T(self.lid.."Group reached detour coordinate.") +end + +--- On after "TurnIntoWind" event. +-- @param #NAVYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #NAVYGROUP.IntoWind Into wind parameters. +function NAVYGROUP:onafterTurnIntoWind(From, Event, To, IntoWind) + + IntoWind.Heading=self:GetHeadingIntoWind(IntoWind.Offset) + + IntoWind.Open=true + + IntoWind.Coordinate=self:GetCoordinate() + + self.intowind=IntoWind + + -- Wind speed in m/s. + local _,vwind=self:GetWind() + + -- Convert to knots. + vwind=UTILS.MpsToKnots(vwind) + + -- Speed of carrier relative to wind but at least 2 knots. + local speed=math.max(IntoWind.Speed-vwind, 2) + + -- Debug info. + self:T(self.lid..string.format("Steaming into wind: Heading=%03d Speed=%.1f Vwind=%.1f Vtot=%.1f knots, Tstart=%d Tstop=%d", IntoWind.Heading, speed, vwind, speed+vwind, IntoWind.Tstart, IntoWind.Tstop)) + + local distance=UTILS.NMToMeters(1000) + + local coord=self:GetCoordinate() + local Coord=coord:Translate(distance, IntoWind.Heading) + + -- ID of current waypoint. + local uid=self:GetWaypointCurrent().uid + + local wptiw=self:AddWaypoint(Coord, speed, uid) + wptiw.intowind=true + + IntoWind.waypoint=wptiw + + if IntoWind.Uturn and self.Debug then + IntoWind.Coordinate:MarkToAll("Return coord") + end + +end + +--- On before "TurnIntoWindStop" event. +-- @param #NAVYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function NAVYGROUP:onbeforeTurnIntoWindStop(From, Event, To) + + if self.intowind then + return true + else + return false + end + +end + +--- On after "TurnIntoWindStop" event. +-- @param #NAVYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function NAVYGROUP:onafterTurnIntoWindStop(From, Event, To) + self:TurnIntoWindOver(self.intowind) +end + +--- On after "TurnIntoWindOver" event. +-- @param #NAVYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #NAVYGROUP.IntoWind IntoWindData Data table. +function NAVYGROUP:onafterTurnIntoWindOver(From, Event, To, IntoWindData) + + if IntoWindData and self.intowind and IntoWindData.Id==self.intowind.Id then + + -- Debug message. + self:T2(self.lid.."Turn Into Wind Over!") + + -- Window over and not open anymore. + self.intowind.Over=true + self.intowind.Open=false + + -- Remove additional waypoint. + self:RemoveWaypointByID(self.intowind.waypoint.uid) + + if self.intowind.Uturn then + + --- + -- U-turn ==> Go to coordinate where we left the route. + --- + + -- Detour to where we left the route. + self:T(self.lid.."FF Turn Into Wind Over ==> Uturn!") + self:Detour(self.intowind.Coordinate, self:GetSpeedCruise(), 0, true) + + else + + --- + -- Go directly to next waypoint. + --- + + -- Next waypoint index and speed. + local indx=self:GetWaypointIndexNext() + local speed=self:GetWaypointSpeed(indx) + + -- Update route. + self:T(self.lid..string.format("FF Turn Into Wind Over ==> Next WP Index=%d at %.1f knots via update route!", indx, speed)) + self:__UpdateRoute(-1, indx, speed) + + end + + -- Set current window to nil. + self.intowind=nil + + -- Remove window from queue. + self:RemoveTurnIntoWind(IntoWindData) + + end + +end + +--- On after "FullStop" event. +-- @param #NAVYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function NAVYGROUP:onafterFullStop(From, Event, To) + self:T(self.lid.."Full stop ==> holding") + + -- Get current position. + local pos=self:GetCoordinate() + + -- Create a new waypoint. + local wp=pos:WaypointNaval(0) + + -- Create new route consisting of only this position ==> Stop! + self:Route({wp}) + +end + +--- On after "Cruise" event. +-- @param #NAVYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number Speed Speed in knots until next waypoint is reached. Default is speed set for waypoint. +function NAVYGROUP:onafterCruise(From, Event, To, Speed) + + -- No set depth. + self.depth=nil + + self:__UpdateRoute(-1, nil, Speed) + +end + +--- On after "Dive" event. +-- @param #NAVYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number Depth Dive depth in meters. Default 50 meters. +-- @param #number Speed Speed in knots until next waypoint is reached. +function NAVYGROUP:onafterDive(From, Event, To, Depth, Speed) + + Depth=Depth or 50 + + self:T(self.lid..string.format("Diving to %d meters", Depth)) + + self.depth=Depth + + self:__UpdateRoute(-1, nil, Speed) + +end + +--- On after "Surface" event. +-- @param #NAVYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number Speed Speed in knots until next waypoint is reached. +function NAVYGROUP:onafterSurface(From, Event, To, Speed) + + self.depth=0 + + self:__UpdateRoute(-1, nil, Speed) + +end + +--- On after "TurningStarted" event. +-- @param #NAVYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function NAVYGROUP:onafterTurningStarted(From, Event, To) + self.turning=true +end + +--- On after "TurningStarted" event. +-- @param #NAVYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function NAVYGROUP:onafterTurningStopped(From, Event, To) + self.turning=false + self.collisionwarning=false + + if self:IsSteamingIntoWind() then + self:TurnedIntoWind() + end + +end + +--- On after "CollisionWarning" event. +-- @param #NAVYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number Distance Distance in meters where obstacle was detected. +function NAVYGROUP:onafterCollisionWarning(From, Event, To, Distance) + self:T(self.lid..string.format("Iceberg ahead in %d meters!", Distance or -1)) + self.collisionwarning=true +end + +--- On after Start event. Starts the NAVYGROUP FSM and event handlers. +-- @param #NAVYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function NAVYGROUP:onafterStop(From, Event, To) + + -- Handle events: + self:UnHandleEvent(EVENTS.Birth) + self:UnHandleEvent(EVENTS.Dead) + self:UnHandleEvent(EVENTS.RemoveUnit) + + -- Call OPSGROUP function. + self:GetParent(self).onafterStop(self, From, Event, To) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Events DCS +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Event function handling the birth of a unit. +-- @param #NAVYGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function NAVYGROUP:OnEventBirth(EventData) + + -- Check that this is the right group. + if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then + local unit=EventData.IniUnit + local group=EventData.IniGroup + local unitname=EventData.IniUnitName + + if self.respawning then + + local function reset() + self.respawning=nil + end + + -- Reset switch in 1 sec. This should allow all birth events of n>1 groups to have passed. + -- TODO: Can I do this more rigorously? + self:ScheduleOnce(1, reset) + + else + + -- Get element. + local element=self:GetElementByName(unitname) + + -- Set element to spawned state. + self:T3(self.lid..string.format("EVENT: Element %s born ==> spawned", element.name)) + self:ElementSpawned(element) + + end + + end + +end + +--- Flightgroup event function handling the crash of a unit. +-- @param #NAVYGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function NAVYGROUP:OnEventDead(EventData) + + -- Check that this is the right group. + if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then + self:T(self.lid..string.format("EVENT: Unit %s dead!", EventData.IniUnitName)) + + local unit=EventData.IniUnit + local group=EventData.IniGroup + local unitname=EventData.IniUnitName + + -- Get element. + local element=self:GetElementByName(unitname) + + if element then + self:T(self.lid..string.format("EVENT: Element %s dead ==> destroyed", element.name)) + self:ElementDestroyed(element) + end + + end + +end + +--- Flightgroup event function handling the crash of a unit. +-- @param #NAVYGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function NAVYGROUP:OnEventRemoveUnit(EventData) + + -- Check that this is the right group. + if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then + local unit=EventData.IniUnit + local group=EventData.IniGroup + local unitname=EventData.IniUnitName + + -- Get element. + local element=self:GetElementByName(unitname) + + if element then + self:T(self.lid..string.format("EVENT: Element %s removed ==> dead", element.name)) + self:ElementDead(element) + end + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Routing +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add an a waypoint to the route. +-- @param #NAVYGROUP self +-- @param Core.Point#COORDINATE Coordinate The coordinate of the waypoint. Use COORDINATE:SetAltitude(altitude) to define the altitude. +-- @param #number Speed Speed in knots. Default is default cruise speed or 70% of max speed. +-- @param #number AfterWaypointWithID Insert waypoint after waypoint given ID. Default is to insert as last waypoint. +-- @param #number Depth Depth at waypoint in meters. Only for submarines. +-- @param #boolean Updateroute If true or nil, call UpdateRoute. If false, no call. +-- @return Ops.OpsGroup#OPSGROUP.Waypoint Waypoint table. +function NAVYGROUP:AddWaypoint(Coordinate, Speed, AfterWaypointWithID, Depth, Updateroute) + + -- Check if a coordinate was given or at least a positionable. + if not Coordinate:IsInstanceOf("COORDINATE") then + if Coordinate:IsInstanceOf("POSITIONABLE") or Coordinate:IsInstanceOf("ZONE_BASE") then + self:T(self.lid.."WARNING: Coordinate is not a COORDINATE but a POSITIONABLE or ZONE. Trying to get coordinate") + Coordinate=Coordinate:GetCoordinate() + else + self:E(self.lid.."ERROR: Coordinate is neither a COORDINATE nor any POSITIONABLE or ZONE!") + return nil + end + end + + -- Set waypoint index. + local wpnumber=self:GetWaypointIndexAfterID(AfterWaypointWithID) + + -- Check if final waypoint is still passed. + if wpnumber>self.currentwp then + self.passedfinalwp=false + end + + -- Speed in knots. + Speed=Speed or self:GetSpeedCruise() + + -- Create a Naval waypoint. + local wp=Coordinate:WaypointNaval(UTILS.KnotsToKmph(Speed), Depth) + + -- Create waypoint data table. + local waypoint=self:_CreateWaypoint(wp) + + -- Add waypoint to table. + self:_AddWaypoint(waypoint, wpnumber) + + -- Debug info. + self:T(self.lid..string.format("Adding NAVAL waypoint index=%d uid=%d, speed=%.1f knots. Last waypoint passed was #%d. Total waypoints #%d", wpnumber, waypoint.uid, Speed, self.currentwp, #self.waypoints)) + + -- Update route. + if Updateroute==nil or Updateroute==true then + self:_CheckGroupDone(1) + end + + return waypoint +end + +--- Initialize group parameters. Also initializes waypoints if self.waypoints is nil. +-- @param #NAVYGROUP self +-- @return #NAVYGROUP self +function NAVYGROUP:_InitGroup() + + -- First check if group was already initialized. + if self.groupinitialized then + self:E(self.lid.."WARNING: Group was already initialized!") + return + end + + -- Get template of group. + self.template=self.group:GetTemplate() + + -- Define category. + self.isAircraft=false + self.isNaval=true + self.isGround=false + + --TODO: Submarine check + --self.isSubmarine=self.group:IsSubmarine() + + -- Ships are always AI. + self.isAI=true + + -- Is (template) group late activated. + self.isLateActivated=self.template.lateActivation + + -- Naval groups cannot be uncontrolled. + self.isUncontrolled=false + + -- Max speed in km/h. + self.speedMax=self.group:GetSpeedMax() + + -- Cruise speed: 70% of max speed. + self.speedCruise=self.speedMax*0.7 + + -- Group ammo. + self.ammo=self:GetAmmoTot() + + -- Radio parameters from template. Default is set on spawn if not modified by the user. + self.radio.On=true -- Radio is always on for ships. + self.radio.Freq=tonumber(self.template.units[1].frequency)/1000000 + self.radio.Modu=tonumber(self.template.units[1].modulation) + + -- Set default formation. No really applicable for ships. + self.optionDefault.Formation="Off Road" + self.option.Formation=self.optionDefault.Formation + + -- Default TACAN off. + self:SetDefaultTACAN(nil, nil, nil, nil, true) + self.tacan=UTILS.DeepCopy(self.tacanDefault) + + -- Default ICLS off. + self:SetDefaultICLS(nil, nil, nil, true) + self.icls=UTILS.DeepCopy(self.iclsDefault) + + -- Get all units of the group. + local units=self.group:GetUnits() + + for _,_unit in pairs(units) do + local unit=_unit --Wrapper.Unit#UNIT + + -- Get unit template. + local unittemplate=unit:GetTemplate() + + local element={} --#NAVYGROUP.Element + element.name=unit:GetName() + element.unit=unit + element.status=OPSGROUP.ElementStatus.INUTERO + element.typename=unit:GetTypeName() + element.skill=unittemplate.skill or "Unknown" + element.ai=true + element.category=element.unit:GetUnitCategory() + element.categoryname=element.unit:GetCategoryName() + element.size, element.length, element.height, element.width=unit:GetObjectSize() + element.ammo0=self:GetAmmoUnit(unit, false) + + -- Debug text. + if self.verbose>=2 then + local text=string.format("Adding element %s: status=%s, skill=%s, category=%s (%d), size: %.1f (L=%.1f H=%.1f W=%.1f)", + element.name, element.status, element.skill, element.categoryname, element.category, element.size, element.length, element.height, element.width) + self:I(self.lid..text) + end + + -- Add element to table. + table.insert(self.elements, element) + + -- Get Descriptors. + self.descriptors=self.descriptors or unit:GetDesc() + + -- Set type name. + self.actype=self.actype or unit:GetTypeName() + + if unit:IsAlive() then + -- Trigger spawned event. + self:ElementSpawned(element) + end + + end + + + -- Debug info. + if self.verbose>=1 then + local text=string.format("Initialized Navy Group %s:\n", self.groupname) + text=text..string.format("Unit type = %s\n", self.actype) + text=text..string.format("Speed max = %.1f Knots\n", UTILS.KmphToKnots(self.speedMax)) + text=text..string.format("Speed cruise = %.1f Knots\n", UTILS.KmphToKnots(self.speedCruise)) + text=text..string.format("Elements = %d\n", #self.elements) + text=text..string.format("Waypoints = %d\n", #self.waypoints) + text=text..string.format("Radio = %.1f MHz %s %s\n", self.radio.Freq, UTILS.GetModulationName(self.radio.Modu), tostring(self.radio.On)) + text=text..string.format("Ammo = %d (G=%d/R=%d/M=%d/T=%d)\n", self.ammo.Total, self.ammo.Guns, self.ammo.Rockets, self.ammo.Missiles, self.ammo.Torpedos) + text=text..string.format("FSM state = %s\n", self:GetState()) + text=text..string.format("Is alive = %s\n", tostring(self:IsAlive())) + text=text..string.format("LateActivate = %s\n", tostring(self:IsLateActivated())) + self:I(self.lid..text) + end + + -- Init done. + self.groupinitialized=true + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Option Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check for possible collisions between two coordinates. +-- @param #NAVYGROUP self +-- @param #number DistanceMax Max distance in meters ahead to check. Default 5000. +-- @param #number dx +-- @return #number Free distance in meters. +function NAVYGROUP:_CheckFreePath(DistanceMax, dx) + + local distance=DistanceMax or 5000 + local dx=dx or 100 + + -- If the group is turning, we cannot really tell anything about a possible collision. + if self:IsTurning() then + return distance + end + + -- Offset above sea level. + local offsetY=0.1 + + -- Current bug on Caucasus. LoS returns false. + if UTILS.GetDCSMap()==DCSMAP.Caucasus then + offsetY=5.01 + end + + -- Current coordinate. + --local coordinate=self:GetCoordinate():SetAltitude(offsetY, true) + + local vec3=self:GetVec3() + vec3.y=offsetY + + -- Current heading. + local heading=self:GetHeading() + + -- Check from 500 meters in front. + --coordinate=coordinate:Translate(500, heading, true) + + local function LoS(dist) + --local checkcoord=coordinate:Translate(dist, heading, true) + --return coordinate:IsLOS(checkcoord, offsetY) + local checkvec3=UTILS.VecTranslate(vec3, dist, heading) + local los=land.isVisible(vec3, checkvec3) + return los + end + + -- First check if everything is clear. + if LoS(DistanceMax) then + return DistanceMax + end + + local function check() + + local xmin=0 + local xmax=DistanceMax + + local Nmax=100 + local eps=100 + + local N=1 + while N<=Nmax do + + local d=xmax-xmin + local x=xmin+d/2 + + local los=LoS(x) + + -- Debug message. + self:T2(self.lid..string.format("N=%d: xmin=%.1f xmax=%.1f x=%.1f d=%.3f los=%s", N, xmin, xmax, x, d, tostring(los))) + + if los and d<=eps then + return x + end + + if los then + xmin=x + else + xmax=x + end + + N=N+1 + end + + return 0 + end + + + return check() +end + +--- Check if group is turning. +-- @param #NAVYGROUP self +function NAVYGROUP:_CheckTurning() + + local unit=self.group:GetUnit(1) + + if unit and unit:IsAlive() then + + -- Current orientation of carrier. + local vNew=self.orientX --unit:GetOrientationX() + + -- Last orientation from 30 seconds ago. + local vLast=self.orientXLast + + -- We only need the X-Z plane. + vNew.y=0 ; vLast.y=0 + + -- Angle between current heading and last time we checked ~30 seconds ago. + local deltaLast=math.deg(math.acos(UTILS.VecDot(vNew,vLast)/UTILS.VecNorm(vNew)/UTILS.VecNorm(vLast))) + + -- Carrier is turning when its heading changed by at least two degrees since last check. + local turning=math.abs(deltaLast)>=2 + + -- Check if turning stopped. + if self.turning and not turning then + + -- Carrier was turning but is not any more. + self:TurningStopped() + + elseif turning and not self.turning then + + -- Carrier was not turning but is now. + self:TurningStarted() + + end + + -- Update turning. + self.turning=turning + + end + +end + + +--- Check queued turns into wind. +-- @param #NAVYGROUP self +function NAVYGROUP:_CheckTurnsIntoWind() + + -- Get current abs time. + local time=timer.getAbsTime() + + if self.intowind then + + -- Check if time is over. + if time>=self.intowind.Tstop then + self:TurnIntoWindOver(self.intowind) + end + + else + + -- Get next window. + local IntoWind=self:GetTurnIntoWindNext() + + -- Start turn into wind. + if IntoWind then + self:TurnIntoWind(IntoWind) + end + + end + +end + +--- Get the next turn into wind window, which is not yet running. +-- @param #NAVYGROUP self +-- @return #NAVYGROUP.IntoWind Next into wind data. Could be `nil` if there is not next window. +function NAVYGROUP:GetTurnIntoWindNext() + + if #self.Qintowind>0 then + + -- Get current abs time. + local time=timer.getAbsTime() + + -- Sort windows wrt to start time. + table.sort(self.Qintowind, function(a, b) return a.Tstart=recovery.Tstart and time0 + else + return false + end + + end + + -- Return if path was found. + return findpath() +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- **Ops** - Enhanced Ground Group. +-- +-- ## Main Features: +-- +-- * Patrol waypoints *ad infinitum* +-- * Easy change of ROE and alarm state, formation and other settings +-- * Dynamically add and remove waypoints +-- * Sophisticated task queueing system (know when DCS tasks start and end) +-- * Convenient checks when the group enters or leaves a zone +-- * Detection events for new, known and lost units +-- * Simple LASER and IR-pointer setup +-- * Compatible with AUFTRAG class +-- * Many additional events that the mission designer can hook into +-- +-- === +-- +-- ## Example Missions: +-- +-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/OPS%20-%20Armygroup). +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- == +-- +-- @module Ops.ArmyGroup +-- @image OPS_ArmyGroup.png + + +--- ARMYGROUP class. +-- @type ARMYGROUP +-- @field #boolean adinfinitum Resume route at first waypoint when final waypoint is reached. +-- @field #boolean formationPerma Formation that is used permanently and overrules waypoint formations. +-- @field #boolean isMobile If true, group is mobile. +-- @field #ARMYGROUP.Target engage Engage target. +-- @field #boolean retreatOnOutOfAmmo If true, the group will automatically retreat when out of ammo. Needs a retreat zone! +-- @field Core.Set#SET_ZONE retreatZones Set of retreat zones. +-- @extends Ops.OpsGroup#OPSGROUP + +--- *Your soul may belong to Jesus, but your ass belongs to the marines.* -- Eugene B. Sledge +-- +-- === +-- +-- ![Banner Image](..\Presentations\OPS\ArmyGroup\_Main.png) +-- +-- # The ARMYGROUP Concept +-- +-- This class enhances naval groups. +-- +-- @field #ARMYGROUP +ARMYGROUP = { + ClassName = "ARMYGROUP", + formationPerma = nil, + engage = {}, +} + +--- Army group element. +-- @type ARMYGROUP.Element +-- @field #string name Name of the element, i.e. the unit. +-- @field Wrapper.Unit#UNIT unit The UNIT object. +-- @field #string status The element status. +-- @field #string typename Type name. +-- @field #number length Length of element in meters. +-- @field #number width Width of element in meters. +-- @field #number height Height of element in meters. + +--- Target +-- @type ARMYGROUP.Target +-- @field Ops.Target#TARGET Target The target. +-- @field Core.Point#COORDINATE Coordinate Last known coordinate of the target. + +--- Army Group version. +-- @field #string version +ARMYGROUP.version="0.4.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Retreat. +-- TODO: Suppression of fire. +-- TODO: Check if group is mobile. +-- TODO: F10 menu. +-- DONE: Rearm. Specify a point where to go and wait until ammo is full. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new ARMYGROUP class object. +-- @param #ARMYGROUP self +-- @param Wrapper.Group#GROUP Group The group object. Can also be given by its group name as `#string`. +-- @return #ARMYGROUP self +function ARMYGROUP:New(Group) + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, OPSGROUP:New(Group)) -- #ARMYGROUP + + -- Set some string id for output to DCS.log file. + self.lid=string.format("ARMYGROUP %s | ", self.groupname) + + -- Defaults + self.isArmygroup=true + self:SetDefaultROE() + self:SetDefaultAlarmstate() + self:SetDetection() + self:SetPatrolAdInfinitum(false) + self:SetRetreatZones() + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("*", "FullStop", "Holding") -- Hold position. + self:AddTransition("*", "Cruise", "Cruising") -- Cruise along the given route of waypoints. + + self:AddTransition("*", "Detour", "OnDetour") -- Make a detour to a coordinate and resume route afterwards. + self:AddTransition("OnDetour", "DetourReached", "Cruising") -- Group reached the detour coordinate. + + self:AddTransition("*", "Retreat", "Retreating") -- + self:AddTransition("Retreating", "Retreated", "Retreated") -- + + self:AddTransition("Cruising", "EngageTarget", "Engaging") -- Engage a target + self:AddTransition("Holding", "EngageTarget", "Engaging") -- Engage a target + self:AddTransition("OnDetour", "EngageTarget", "Engaging") -- Engage a target + self:AddTransition("Engaging", "Disengage", "Cruising") -- Engage a target + + self:AddTransition("*", "Rearm", "Rearm") -- Group is send to a coordinate and waits until ammo is refilled. + self:AddTransition("Rearm", "Rearming", "Rearming") -- Group has arrived at the rearming coodinate and is waiting to be fully rearmed. + self:AddTransition("Rearming", "Rearmed", "Cruising") -- Group was rearmed. + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Stop". Stops the ARMYGROUP and all its event handlers. + -- @param #ARMYGROUP self + + --- Triggers the FSM event "Stop" after a delay. Stops the ARMYGROUP and all its event handlers. + -- @function [parent=#ARMYGROUP] __Stop + -- @param #ARMYGROUP self + -- @param #number delay Delay in seconds. + + -- TODO: Add pseudo functions. + + + -- Init waypoints. + self:InitWaypoints() + + -- Initialize the group. + self:_InitGroup() + + -- Handle events: + self:HandleEvent(EVENTS.Birth, self.OnEventBirth) + self:HandleEvent(EVENTS.Dead, self.OnEventDead) + self:HandleEvent(EVENTS.RemoveUnit, self.OnEventRemoveUnit) + + --self:HandleEvent(EVENTS.Hit, self.OnEventHit) + + -- Start the status monitoring. + self:__Status(-1) + + -- Start queue update timer. + self.timerQueueUpdate=TIMER:New(self._QueueUpdate, self):Start(2, 5) + + -- Start check zone timer. + self.timerCheckZone=TIMER:New(self._CheckInZones, self):Start(2, 30) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Group patrols ad inifintum. If the last waypoint is reached, it will go to waypoint one and repeat its route. +-- @param #ARMYGROUP self +-- @param #boolean switch If true or nil, patrol until the end of time. If false, go along the waypoints once and stop. +-- @return #ARMYGROUP self +function ARMYGROUP:SetPatrolAdInfinitum(switch) + if switch==false then + self.adinfinitum=false + else + self.adinfinitum=true + end + return self +end + +--- Get coordinate of the closest road. +-- @param #ARMYGROUP self +-- @return Core.Point#COORDINATE Coordinate of a road closest to the group. +function ARMYGROUP:GetClosestRoad() + return self:GetCoordinate():GetClosestPointToRoad() +end + +--- Get 2D distance to the closest road. +-- @param #ARMYGROUP self +-- @return #number Distance in meters to the closest road. +function ARMYGROUP:GetClosestRoadDist() + local road=self:GetClosestRoad() + if road then + local dist=road:Get2DDistance(self:GetCoordinate()) + return dist + end + return math.huge +end + + +--- Add a *scheduled* task to fire at a given coordinate. +-- @param #ARMYGROUP self +-- @param Core.Point#COORDINATE Coordinate Coordinate of the target. +-- @param #string Clock Time when to start the attack. +-- @param #number Radius Radius in meters. Default 100 m. +-- @param #number Nshots Number of shots to fire. Default 3. +-- @param #number WeaponType Type of weapon. Default auto. +-- @param #number Prio Priority of the task. +-- @return Ops.OpsGroup#OPSGROUP.Task The task table. +function ARMYGROUP:AddTaskFireAtPoint(Coordinate, Clock, Radius, Nshots, WeaponType, Prio) + + Coordinate=self:_CoordinateFromObject(Coordinate) + + local DCStask=CONTROLLABLE.TaskFireAtPoint(nil, Coordinate:GetVec2(), Radius, Nshots, WeaponType) + + local task=self:AddTask(DCStask, Clock, nil, Prio) + + return task +end + +--- Add a *waypoint* task to fire at a given coordinate. +-- @param #ARMYGROUP self +-- @param Core.Point#COORDINATE Coordinate Coordinate of the target. +-- @param Ops.OpsGroup#OPSGROUP.Waypoint Waypoint Where the task is executed. Default is next waypoint. +-- @param #number Radius Radius in meters. Default 100 m. +-- @param #number Nshots Number of shots to fire. Default 3. +-- @param #number WeaponType Type of weapon. Default auto. +-- @param #number Prio Priority of the task. +-- @return Ops.OpsGroup#OPSGROUP.Task The task table. +function ARMYGROUP:AddTaskWaypointFireAtPoint(Coordinate, Waypoint, Radius, Nshots, WeaponType, Prio) + + Coordinate=self:_CoordinateFromObject(Coordinate) + + Waypoint=Waypoint or self:GetWaypointNext() + + local DCStask=CONTROLLABLE.TaskFireAtPoint(nil, Coordinate:GetVec2(), Radius, Nshots, WeaponType) + + local task=self:AddTaskWaypoint(DCStask, Waypoint, nil, Prio) + + return task +end + +--- Add a *scheduled* task. +-- @param #ARMYGROUP self +-- @param Wrapper.Group#GROUP TargetGroup Target group. +-- @param #number WeaponExpend How much weapons does are used. +-- @param #number WeaponType Type of weapon. Default auto. +-- @param #string Clock Time when to start the attack. +-- @param #number Prio Priority of the task. +-- @return Ops.OpsGroup#OPSGROUP.Task The task table. +function ARMYGROUP:AddTaskAttackGroup(TargetGroup, WeaponExpend, WeaponType, Clock, Prio) + + local DCStask=CONTROLLABLE.TaskAttackGroup(nil, TargetGroup, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit, GroupAttack) + + local task=self:AddTask(DCStask, Clock, nil, Prio) + + return task +end + +--- Define a set of possible retreat zones. +-- @param #ARMYGROUP self +-- @param Core.Set#SET_ZONE RetreatZoneSet The retreat zone set. Default is an empty set. +-- @return #ARMYGROUP self +function ARMYGROUP:SetRetreatZones(RetreatZoneSet) + self.retreatZones=RetreatZoneSet or SET_ZONE:New() + return self +end + +--- Add a zone to the retreat zone set. +-- @param #ARMYGROUP self +-- @param Core.Zone#ZONE_BASE RetreatZone The retreat zone. +-- @return #ARMYGROUP self +function ARMYGROUP:AddRetreatZone(RetreatZone) + self.retreatZones:AddZone(RetreatZone) + return self +end + +--- Check if the group is currently holding its positon. +-- @param #ARMYGROUP self +-- @return #boolean If true, group was ordered to hold. +function ARMYGROUP:IsHolding() + return self:Is("Holding") +end + +--- Check if the group is currently cruising. +-- @param #ARMYGROUP self +-- @return #boolean If true, group cruising. +function ARMYGROUP:IsCruising() + return self:Is("Cruising") +end + +--- Check if the group is currently on a detour. +-- @param #ARMYGROUP self +-- @return #boolean If true, group is on a detour. +function ARMYGROUP:IsOnDetour() + return self:Is("OnDetour") +end + +--- Check if the group is ready for combat. I.e. not reaming, retreating, retreated, out of ammo or engaging. +-- @param #ARMYGROUP self +-- @return #boolean If true, group is on a combat ready. +function ARMYGROUP:IsCombatReady() + local combatready=true + + if self:IsRearming() or self:IsRetreating() or self.outofAmmo or self:IsEngaging() or self:is("Retreated") or self:IsDead() or self:IsStopped() or self:IsInUtero() then + combatready=false + end + + return combatready +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Status +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +---- Update status. +-- @param #ARMYGROUP self +function ARMYGROUP:onbeforeStatus(From, Event, To) + + if self:IsDead() then + self:T(self.lid..string.format("Onbefore Status DEAD ==> false")) + return false + elseif self:IsStopped() then + self:T(self.lid..string.format("Onbefore Status STOPPED ==> false")) + return false + end + + return true +end + +--- Update status. +-- @param #ARMYGROUP self +function ARMYGROUP:onafterStatus(From, Event, To) + + -- FSM state. + local fsmstate=self:GetState() + + if self:IsAlive() then + + --- + -- Detection + --- + + -- Check if group has detected any units. + if self.detectionOn then + self:_CheckDetectedUnits() + end + + -- Check ammo status. + self:_CheckAmmoStatus() + + -- Update position etc. + self:_UpdatePosition() + + -- Check if group got stuck. + self:_CheckStuck() + + -- Check damage of elements and group. + self:_CheckDamage() + + -- Update engagement. + if self:IsEngaging() then + self:_UpdateEngageTarget() + end + + if self.verbose>=1 then + + -- Get number of tasks and missions. + local nTaskTot, nTaskSched, nTaskWP=self:CountRemainingTasks() + local nMissions=self:CountRemainingMissison() + + local roe=self:GetROE() + local alarm=self:GetAlarmstate() + local speed=UTILS.MpsToKnots(self.velocity) + local speedEx=UTILS.MpsToKnots(self:GetExpectedSpeed()) + local formation=self.option.Formation or "unknown" + local ammo=self:GetAmmoTot() + + -- Info text. + local text=string.format("%s [ROE-AS=%d-%d T/M=%d/%d]: Wp=%d/%d-->%d (final %s), Life=%.1f, Speed=%.1f (%d), Heading=%03d, Ammo=%d", + fsmstate, roe, alarm, nTaskTot, nMissions, self.currentwp, #self.waypoints, self:GetWaypointIndexNext(), tostring(self.passedfinalwp), self.life or 0, speed, speedEx, self.heading, ammo.Total) + self:I(self.lid..text) + + end + + else + + -- Info text. + local text=string.format("State %s: Alive=%s", fsmstate, tostring(self:IsAlive())) + self:T2(self.lid..text) + + end + + + --- + -- Tasks & Missions + --- + + self:_PrintTaskAndMissionStatus() + + + -- Next status update. + self:__Status(-30) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Events +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "ElementSpawned" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #ARMYGROUP.Element Element The group element. +function ARMYGROUP:onafterElementSpawned(From, Event, To, Element) + self:T(self.lid..string.format("Element spawned %s", Element.name)) + + -- Set element status. + self:_UpdateStatus(Element, OPSGROUP.ElementStatus.SPAWNED) + +end + +--- On after "Spawned" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARMYGROUP:onafterSpawned(From, Event, To) + self:T(self.lid..string.format("Group spawned!")) + + -- Update position. + self:_UpdatePosition() + + if self.isAI then + + -- Set default ROE. + self:SwitchROE(self.option.ROE) + + -- Set default Alarm State. + self:SwitchAlarmstate(self.option.Alarm) + + -- Set TACAN to default. + self:_SwitchTACAN() + + -- Turn on the radio. + if self.radioDefault then + self:SwitchRadio(self.radioDefault.Freq, self.radioDefault.Modu) + else + self:SetDefaultRadio(self.radio.Freq, self.radio.Modu, true) + end + + -- Formation + if not self.option.Formation then + self.option.Formation=self.optionDefault.Formation + end + + end + + -- Update route. + if #self.waypoints>1 then + self:Cruise(nil, self.option.Formation or self.optionDefault.Formation) + else + self:FullStop() + end + +end + +--- On after "UpdateRoute" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number n Waypoint number. Default is next waypoint. +-- @param #number Speed Speed in knots. Default cruise speed. +-- @param #number Formation Formation of the group. +function ARMYGROUP:onafterUpdateRoute(From, Event, To, n, Speed, Formation) + + -- Debug info. + local text=string.format("Update route n=%s, Speed=%s, Formation=%s", tostring(n), tostring(Speed), tostring(Formation)) + self:T(self.lid..text) + + -- Update route from this waypoint number onwards. + n=n or self:GetWaypointIndexNext(self.adinfinitum) + + -- Update waypoint tasks, i.e. inject WP tasks into waypoint table. + self:_UpdateWaypointTasks(n) + + -- Waypoints. + local waypoints={} + + -- Next waypoint. + local wp=UTILS.DeepCopy(self.waypoints[n]) --Ops.OpsGroup#OPSGROUP.Waypoint + + -- Do we want to drive on road to the next wp? + local onroad=wp.action==ENUMS.Formation.Vehicle.OnRoad + + -- Speed. + if Speed then + wp.speed=UTILS.KnotsToMps(Speed) + else + -- Take default waypoint speed. But make sure speed>0 if patrol ad infinitum. + if self.adinfinitum and wp.speed<0.1 then + wp.speed=UTILS.KmphToMps(self.speedCruise) + end + end + + -- Formation. + if self.formationPerma then + wp.action=self.formationPerma + elseif Formation then + wp.action=Formation + end + + -- Current set formation. + self.option.Formation=wp.action + + -- Current set speed in m/s. + self.speedWp=wp.speed + + -- Add waypoint in between because this waypoint is "On Road" but lies "Off Road". + if onroad then + + -- The real waypoint is actually off road. + wp.action=ENUMS.Formation.Vehicle.OffRoad + + -- Add "On Road" waypoint in between. + local wproad=wp.roadcoord:WaypointGround(wp.speed, ENUMS.Formation.Vehicle.OnRoad) --Ops.OpsGroup#OPSGROUP.Waypoint + + -- Insert road waypoint. + table.insert(waypoints, wproad) + end + + -- Add waypoint. + table.insert(waypoints, wp) + + -- Apply formation at the current position or it will only be changed when reaching the next waypoint. + local formation=ENUMS.Formation.Vehicle.OffRoad + if wp.action~=ENUMS.Formation.Vehicle.OnRoad then + formation=wp.action + end + + -- Current point. + local current=self:GetCoordinate():WaypointGround(UTILS.MpsToKmph(self.speedWp), formation) + table.insert(waypoints, 1, current) + + -- Insert a point on road. + if onroad then + local current=self:GetClosestRoad():WaypointGround(UTILS.MpsToKmph(self.speedWp), ENUMS.Formation.Vehicle.OnRoad) + table.insert(waypoints, 2, current) + end + + -- Debug output. + if false then + for i,_wp in pairs(waypoints) do + local wp=_wp + local text=string.format("WP #%d UID=%d type=%s: Speed=%d m/s, alt=%d m, Action=%s", i, wp.uid and wp.uid or 0, wp.type, wp.speed, wp.alt, wp.action) + self:T(text) + end + end + + if self:IsEngaging() or not self.passedfinalwp then + + -- Debug info. + self:T(self.lid..string.format("Updateing route: WP %d-->%d (%d/%d), Speed=%.1f knots, Formation=%s", + self.currentwp, n, #waypoints, #self.waypoints, UTILS.MpsToKnots(self.speedWp), tostring(self.option.Formation))) + + -- Route group to all defined waypoints remaining. + self:Route(waypoints) + + else + + --- + -- Passed final WP ==> Full Stop + --- + + self:E(self.lid..string.format("WARNING: Passed final WP ==> Full Stop!")) + self:FullStop() + + end + +end + +--- On after "GotoWaypoint" event. Group will got to the given waypoint and execute its route from there. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number UID The goto waypoint unique ID. +-- @param #number Speed (Optional) Speed to waypoint in knots. +-- @param #number Formation (Optional) Formation to waypoint. +function ARMYGROUP:onafterGotoWaypoint(From, Event, To, UID, Speed, Formation) + + local n=self:GetWaypointIndex(UID) + + --env.info(string.format("FF AG Goto waypoint UID=%s Index=%s, Speed=%s, Formation=%s", tostring(UID), tostring(n), tostring(Speed), tostring(Formation))) + + if n then + + -- TODO: switch to re-enable waypoint tasks. + if false then + local tasks=self:GetTasksWaypoint(n) + + for _,_task in pairs(tasks) do + local task=_task --Ops.OpsGroup#OPSGROUP.Task + task.status=OPSGROUP.TaskStatus.SCHEDULED + end + + end + + -- Speed to waypoint. + Speed=Speed or self:GetSpeedToWaypoint(n) + + -- Update the route. + self:UpdateRoute(n, Speed, Formation) + + end + +end + +--- On after "Detour" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Point#COORDINATE Coordinate Coordinate where to go. +-- @param #number Speed Speed in knots. Default cruise speed. +-- @param #number Formation Formation of the group. +-- @param #number ResumeRoute If true, resume route after detour point was reached. If false, the group will stop at the detour point and wait for futher commands. +function ARMYGROUP:onafterDetour(From, Event, To, Coordinate, Speed, Formation, ResumeRoute) + + for _,_wp in pairs(self.waypoints) do + local wp=_wp --Ops.OpsGroup#OPSGROUP.Waypoint + if wp.detour then + self:RemoveWaypointByID(wp.uid) + end + end + + -- Speed in knots. + Speed=Speed or self:GetSpeedCruise() + + -- ID of current waypoint. + local uid=self:GetWaypointCurrent().uid + + -- Add waypoint after current. + local wp=self:AddWaypoint(Coordinate, Speed, uid, Formation, true) + + -- Set if we want to resume route after reaching the detour waypoint. + if ResumeRoute then + wp.detour=1 + else + wp.detour=0 + end + +end + +--- On after "Rearm" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Point#COORDINATE Coordinate Coordinate where to rearm. +-- @param #number Formation Formation of the group. +function ARMYGROUP:onafterRearm(From, Event, To, Coordinate, Formation) + + -- ID of current waypoint. + local uid=self:GetWaypointCurrent().uid + + -- Add waypoint after current. + local wp=self:AddWaypoint(Coordinate, nil, uid, Formation, true) + + -- Set if we want to resume route after reaching the detour waypoint. + wp.detour=0 + +end + +--- On after "Rearming" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARMYGROUP:onafterRearming(From, Event, To) + + -- Get current position. + local pos=self:GetCoordinate() + + -- Create a new waypoint. + local wp=pos:WaypointGround(0) + + -- Create new route consisting of only this position ==> Stop! + self:Route({wp}) + +end + +--- On before "Retreat" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Zone#ZONE_BASE Zone (Optional) Zone where to retreat. Default is the closest retreat zone. +-- @param #number Formation (Optional) Formation of the group. +function ARMYGROUP:onbeforeRetreat(From, Event, To, Zone, Formation) + + if not Zone then + + local a=self:GetVec2() + + local distmin=math.huge + local zonemin=nil + for _,_zone in pairs(self.retreatZones:GetSet()) do + local zone=_zone --Core.Zone#ZONE_BASE + + local b=zone:GetVec2() + + local dist=UTILS.VecDist2D(a, b) + + if dist Stop! + self:Route({wp}) + +end + +--- On after "EngageTarget" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Group#GROUP Group the group to be engaged. +function ARMYGROUP:onbeforeEngageTarget(From, Event, To, Target) + + local ammo=self:GetAmmoTot() + + if ammo.Total==0 then + self:E(self.lid.."WARNING: Cannot engage TARGET because no ammo left!") + return false + end + + return true +end + +--- On after "EngageTarget" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Group#GROUP Group the group to be engaged. +function ARMYGROUP:onafterEngageTarget(From, Event, To, Target) + + if Target:IsInstanceOf("TARGET") then + self.engage.Target=Target + else + self.engage.Target=TARGET:New(Target) + end + + -- Target coordinate. + self.engage.Coordinate=UTILS.DeepCopy(self.engage.Target:GetCoordinate()) + + -- TODO: Backup current ROE and alarm state and reset after disengage. + + -- Switch ROE and alarm state. + self:SwitchAlarmstate(ENUMS.AlarmState.Auto) + self:SwitchROE(ENUMS.ROE.WeaponFree) + + -- ID of current waypoint. + local uid=self:GetWaypointCurrent().uid + + -- Add waypoint after current. + self.engage.Waypoint=self:AddWaypoint(self.engage.Coordinate, nil, uid, Formation, true) + + -- Set if we want to resume route after reaching the detour waypoint. + self.engage.Waypoint.detour=1 + +end + +--- Update engage target. +-- @param #ARMYGROUP self +function ARMYGROUP:_UpdateEngageTarget() + + if self.engage.Target and self.engage.Target:IsAlive() then + + --env.info("FF Update Engage Target "..self.engage.Target:GetName()) + + local vec3=self.engage.Target:GetCoordinate():GetVec3() + + local dist=UTILS.VecDist2D(vec3, self.engage.Coordinate:GetVec3()) + + if dist>100 then + + --env.info("FF Update Engage Target Moved "..self.engage.Target:GetName()) + + self.engage.Coordinate:UpdateFromVec3(vec3) + + -- ID of current waypoint. + local uid=self:GetWaypointCurrent().uid + + -- Remove current waypoint + self:RemoveWaypointByID(self.engage.Waypoint.uid) + + -- Add waypoint after current. + self.engage.Waypoint=self:AddWaypoint(self.engage.Coordinate, nil, uid, Formation, true) + + -- Set if we want to resume route after reaching the detour waypoint. + self.engage.Waypoint.detour=0 + + end + + else + self:Disengage() + end + +end + +--- On after "Disengage" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARMYGROUP:onafterDisengage(From, Event, To) + -- TODO: Reset ROE and alarm state. + self:_CheckGroupDone(1) +end + +--- On after "Rearmed" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARMYGROUP:onafterRearmed(From, Event, To) + + self:_CheckGroupDone(1) + +end + +--- On after "DetourReached" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARMYGROUP:onafterDetourReached(From, Event, To) + self:I(self.lid.."Group reached detour coordinate.") +end + + +--- On after "FullStop" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARMYGROUP:onafterFullStop(From, Event, To) + + -- Get current position. + local pos=self:GetCoordinate() + + -- Create a new waypoint. + local wp=pos:WaypointGround(0) + + -- Create new route consisting of only this position ==> Stop! + self:Route({wp}) + +end + +--- On after "Cruise" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number Speed Speed in knots. +-- @param #number Formation Formation. +function ARMYGROUP:onafterCruise(From, Event, To, Speed, Formation) + + self:__UpdateRoute(-1, nil, Speed, Formation) + +end + +--- On after "Stop" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARMYGROUP:onafterStop(From, Event, To) + + -- Handle events: + self:UnHandleEvent(EVENTS.Birth) + self:UnHandleEvent(EVENTS.Dead) + self:UnHandleEvent(EVENTS.RemoveUnit) + + -- Call OPSGROUP function. + self:GetParent(self).onafterStop(self, From, Event, To) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Events DCS +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Event function handling the birth of a unit. +-- @param #ARMYGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function ARMYGROUP:OnEventBirth(EventData) + + -- Check that this is the right group. + if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then + local unit=EventData.IniUnit + local group=EventData.IniGroup + local unitname=EventData.IniUnitName + + if self.respawning then + + local function reset() + self.respawning=nil + end + + -- Reset switch in 1 sec. This should allow all birth events of n>1 groups to have passed. + -- TODO: Can I do this more rigorously? + self:ScheduleOnce(1, reset) + + else + + -- Get element. + local element=self:GetElementByName(unitname) + + -- Set element to spawned state. + self:T3(self.lid..string.format("EVENT: Element %s born ==> spawned", element.name)) + self:ElementSpawned(element) + + end + + end + +end + +--- Event function handling the crash of a unit. +-- @param #ARMYGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function ARMYGROUP:OnEventDead(EventData) + + -- Check that this is the right group. + if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then + self:T(self.lid..string.format("EVENT: Unit %s dead!", EventData.IniUnitName)) + + local unit=EventData.IniUnit + local group=EventData.IniGroup + local unitname=EventData.IniUnitName + + -- Get element. + local element=self:GetElementByName(unitname) + + if element then + self:T(self.lid..string.format("EVENT: Element %s dead ==> destroyed", element.name)) + self:ElementDestroyed(element) + end + + end + +end + +--- Event function handling when a unit is removed from the game. +-- @param #ARMYGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function ARMYGROUP:OnEventRemoveUnit(EventData) + + -- Check that this is the right group. + if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then + local unit=EventData.IniUnit + local group=EventData.IniGroup + local unitname=EventData.IniUnitName + + -- Get element. + local element=self:GetElementByName(unitname) + + if element then + self:T(self.lid..string.format("EVENT: Element %s removed ==> dead", element.name)) + self:ElementDead(element) + end + + end + +end + +--- Event function handling when a unit is hit. +-- @param #ARMYGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function ARMYGROUP:OnEventHit(EventData) + + -- Check that this is the right group. + if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then + local unit=EventData.IniUnit + local group=EventData.IniGroup + local unitname=EventData.IniUnitName + + -- TODO: suppression + + end +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Routing +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add an a waypoint to the route. +-- @param #ARMYGROUP self +-- @param Core.Point#COORDINATE Coordinate The coordinate of the waypoint. +-- @param #number Speed Speed in knots. Default is default cruise speed or 70% of max speed. +-- @param #number AfterWaypointWithID Insert waypoint after waypoint given ID. Default is to insert as last waypoint. +-- @param #number Formation Formation the group will use. +-- @param #boolean Updateroute If true or nil, call UpdateRoute. If false, no call. +-- @return Ops.OpsGroup#OPSGROUP.Waypoint Waypoint table. +function ARMYGROUP:AddWaypoint(Coordinate, Speed, AfterWaypointWithID, Formation, Updateroute) + + local coordinate=self:_CoordinateFromObject(Coordinate) + + -- Set waypoint index. + local wpnumber=self:GetWaypointIndexAfterID(AfterWaypointWithID) + + -- Check if final waypoint is still passed. + if wpnumber>self.currentwp then + self.passedfinalwp=false + end + + -- Speed in knots. + Speed=Speed or self:GetSpeedCruise() + + -- Create a Naval waypoint. + local wp=coordinate:WaypointGround(UTILS.KnotsToKmph(Speed), Formation) + + -- Create waypoint data table. + local waypoint=self:_CreateWaypoint(wp) + + -- Add waypoint to table. + self:_AddWaypoint(waypoint, wpnumber) + + -- Get closest point to road. + waypoint.roadcoord=coordinate:GetClosestPointToRoad(false) + if waypoint.roadcoord then + waypoint.roaddist=coordinate:Get2DDistance(waypoint.roadcoord) + else + waypoint.roaddist=1000*1000 --1000 km. + end + + -- Debug info. + self:T(self.lid..string.format("Adding waypoint UID=%d (index=%d), Speed=%.1f knots, Dist2Road=%d m, Action=%s", waypoint.uid, wpnumber, Speed, waypoint.roaddist, waypoint.action)) + + -- Update route. + if Updateroute==nil or Updateroute==true then + self:_CheckGroupDone(1) + end + + return waypoint +end + +--- Initialize group parameters. Also initializes waypoints if self.waypoints is nil. +-- @param #ARMYGROUP self +-- @return #ARMYGROUP self +function ARMYGROUP:_InitGroup() + + -- First check if group was already initialized. + if self.groupinitialized then + self:E(self.lid.."WARNING: Group was already initialized!") + return + end + + -- Get template of group. + self.template=self.group:GetTemplate() + + -- Define category. + self.isAircraft=false + self.isNaval=false + self.isGround=true + + -- Ground are always AI. + self.isAI=true + + -- Is (template) group late activated. + self.isLateActivated=self.template.lateActivation + + -- Ground groups cannot be uncontrolled. + self.isUncontrolled=false + + -- Max speed in km/h. + self.speedMax=self.group:GetSpeedMax() + + -- Cruise speed in km/h + self.speedCruise=self.speedMax*0.7 + + -- Group ammo. + self.ammo=self:GetAmmoTot() + + -- Radio parameters from template. + self.radio.On=false -- Radio is always OFF for ground. + self.radio.Freq=133 + self.radio.Modu=radio.modulation.AM + + -- Set default radio. + self:SetDefaultRadio(self.radio.Freq, self.radio.Modu, self.radio.On) + + -- Set default formation from first waypoint. + self.optionDefault.Formation=self:GetWaypoint(1).action + + -- Default TACAN off. + self:SetDefaultTACAN(nil, nil, nil, nil, true) + self.tacan=UTILS.DeepCopy(self.tacanDefault) + + -- Units of the group. + local units=self.group:GetUnits() + + for _,_unit in pairs(units) do + local unit=_unit --Wrapper.Unit#UNIT + + -- TODO: this is wrong when grouping is used! + local unittemplate=unit:GetTemplate() + + local element={} --#ARMYGROUP.Element + element.name=unit:GetName() + element.unit=unit + element.status=OPSGROUP.ElementStatus.INUTERO + element.typename=unit:GetTypeName() + element.skill=unittemplate.skill or "Unknown" + element.ai=true + element.category=element.unit:GetUnitCategory() + element.categoryname=element.unit:GetCategoryName() + element.size, element.length, element.height, element.width=unit:GetObjectSize() + element.ammo0=self:GetAmmoUnit(unit, false) + element.life0=unit:GetLife0() + element.life=element.life0 + + -- Debug text. + if self.verbose>=2 then + local text=string.format("Adding element %s: status=%s, skill=%s, life=%.3f category=%s (%d), size: %.1f (L=%.1f H=%.1f W=%.1f)", + element.name, element.status, element.skill, element.life, element.categoryname, element.category, element.size, element.length, element.height, element.width) + self:I(self.lid..text) + end + + -- Add element to table. + table.insert(self.elements, element) + + -- Get Descriptors. + self.descriptors=self.descriptors or unit:GetDesc() + + -- Set type name. + self.actype=self.actype or unit:GetTypeName() + + if unit:IsAlive() then + -- Trigger spawned event. + self:ElementSpawned(element) + end + + end + + -- Debug info. + if self.verbose>=1 then + local text=string.format("Initialized Army Group %s:\n", self.groupname) + text=text..string.format("Unit type = %s\n", self.actype) + text=text..string.format("Speed max = %.1f Knots\n", UTILS.KmphToKnots(self.speedMax)) + text=text..string.format("Speed cruise = %.1f Knots\n", UTILS.KmphToKnots(self.speedCruise)) + text=text..string.format("Elements = %d\n", #self.elements) + text=text..string.format("Waypoints = %d\n", #self.waypoints) + text=text..string.format("Radio = %.1f MHz %s %s\n", self.radio.Freq, UTILS.GetModulationName(self.radio.Modu), tostring(self.radio.On)) + text=text..string.format("Ammo = %d (G=%d/R=%d/M=%d)\n", self.ammo.Total, self.ammo.Guns, self.ammo.Rockets, self.ammo.Missiles) + text=text..string.format("FSM state = %s\n", self:GetState()) + text=text..string.format("Is alive = %s\n", tostring(self:IsAlive())) + text=text..string.format("LateActivate = %s\n", tostring(self:IsLateActivated())) + self:I(self.lid..text) + end + + -- Init done. + self.groupinitialized=true + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Option Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Switch to a specific formation. +-- @param #ARMYGROUP self +-- @param #number Formation New formation the group will fly in. Default is the setting of `SetDefaultFormation()`. +-- @param #boolean Permanently If true, formation always used from now on. +-- @param #boolean NoRouteUpdate If true, route is not updated. +-- @return #ARMYGROUP self +function ARMYGROUP:SwitchFormation(Formation, Permanently, NoRouteUpdate) + + if self:IsAlive() or self:IsInUtero() then + + Formation=Formation or self.optionDefault.Formation + + if Permanently then + self.formationPerma=Formation + else + self.formationPerma=nil + end + + -- Set current formation. + self.option.Formation=Formation + + if self:IsInUtero() then + self:T(self.lid..string.format("Will switch formation to %s (permanently=%s) when group is spawned", self.option.Formation, tostring(Permanently))) + else + + -- Update route with the new formation. + if NoRouteUpdate then + else + self:__UpdateRoute(-1, nil, nil, Formation) + end + + -- Debug info. + self:T(self.lid..string.format("Switching formation to %s (permanently=%s)", self.option.Formation, tostring(Permanently))) + + end + + end + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- **Ops** - Airwing Squadron. +-- +-- **Main Features:** +-- +-- * Set parameters like livery, skill valid for all squadron members. +-- * Define modex and callsigns. +-- * Define mission types, this squadron can perform (see Ops.Auftrag#AUFTRAG). +-- * Pause/unpause squadron operations. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Ops.Squadron +-- @image OPS_Squadron.png + + +--- SQUADRON class. +-- @type SQUADRON +-- @field #string ClassName Name of the class. +-- @field #number verbose Verbosity level. +-- @field #string lid Class id string for output to DCS log file. +-- @field #string name Name of the squadron. +-- @field #string templatename Name of the template group. +-- @field #string aircrafttype Type of the airframe the squadron is using. +-- @field Wrapper.Group#GROUP templategroup Template group. +-- @field #number ngrouping User defined number of units in the asset group. +-- @field #table assets Squadron assets. +-- @field #table missiontypes Capabilities (mission types and performances) of the squadron. +-- @field #number fuellow Low fuel threshold. +-- @field #boolean fuellowRefuel If `true`, flight tries to refuel at the nearest tanker. +-- @field #number maintenancetime Time in seconds needed for maintenance of a returned flight. +-- @field #number repairtime Time in seconds for each +-- @field #string livery Livery of the squadron. +-- @field #number skill Skill of squadron members. +-- @field #number modex Modex. +-- @field #number modexcounter Counter to incease modex number for assets. +-- @field #string callsignName Callsign name. +-- @field #number callsigncounter Counter to increase callsign names for new assets. +-- @field Ops.AirWing#AIRWING airwing The AIRWING object the squadron belongs to. +-- @field #number Ngroups Number of asset flight groups this squadron has. +-- @field #number engageRange Mission range in meters. +-- @field #string attribute Generalized attribute of the squadron template group. +-- @field #number tankerSystem For tanker squads, the refuel system used (boom=0 or probpe=1). Default nil. +-- @field #number refuelSystem For refuelable squads, the refuel system used (boom=0 or probe=1). Default nil. +-- @field #table tacanChannel List of TACAN channels available to the squadron. +-- @field #number radioFreq Radio frequency in MHz the squad uses. +-- @field #number radioModu Radio modulation the squad uses. +-- @field #number takeoffType Take of type. +-- @extends Core.Fsm#FSM + +--- *It is unbelievable what a squadron of twelve aircraft did to tip the balance.* -- Adolf Galland +-- +-- === +-- +-- ![Banner Image](..\Presentations\OPS\Squadron\_Main.png) +-- +-- # The SQUADRON Concept +-- +-- A SQUADRON is essential part of an AIRWING and consists of **one** type of aircraft. +-- +-- +-- +-- @field #SQUADRON +SQUADRON = { + ClassName = "SQUADRON", + verbose = 0, + lid = nil, + name = nil, + templatename = nil, + aircrafttype = nil, + assets = {}, + missiontypes = {}, + repairtime = 0, + maintenancetime= 0, + livery = nil, + skill = nil, + modex = nil, + modexcounter = 0, + callsignName = nil, + callsigncounter= 11, + airwing = nil, + Ngroups = nil, + engageRange = nil, + tankerSystem = nil, + refuelSystem = nil, + tacanChannel = {}, +} + +--- SQUADRON class version. +-- @field #string version +SQUADRON.version="0.5.2" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Parking spots for squadrons? +-- DONE: Engage radius. +-- DONE: Modex. +-- DONE: Call signs. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new SQUADRON object and start the FSM. +-- @param #SQUADRON self +-- @param #string TemplateGroupName Name of the template group. +-- @param #number Ngroups Number of asset groups of this squadron. Default 3. +-- @param #string SquadronName Name of the squadron, e.g. "VFA-37". +-- @return #SQUADRON self +function SQUADRON:New(TemplateGroupName, Ngroups, SquadronName) + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #SQUADRON + + -- Name of the template group. + self.templatename=TemplateGroupName + + -- Squadron name. + self.name=tostring(SquadronName or TemplateGroupName) + + -- Set some string id for output to DCS.log file. + self.lid=string.format("SQUADRON %s | ", self.name) + + -- Template group. + self.templategroup=GROUP:FindByName(self.templatename) + + -- Check if template group exists. + if not self.templategroup then + self:E(self.lid..string.format("ERROR: Template group %s does not exist!", tostring(self.templatename))) + return nil + end + + -- Defaults. + self.Ngroups=Ngroups or 3 + self:SetMissionRange() + self:SetSkill(AI.Skill.GOOD) + --self:SetVerbosity(0) + + -- Everyone can ORBIT. + self:AddMissionCapability(AUFTRAG.Type.ORBIT) + + -- Generalized attribute. + self.attribute=self.templategroup:GetAttribute() + + -- Aircraft type. + self.aircrafttype=self.templategroup:GetTypeName() + + -- Refueling system. + self.refuelSystem=select(2, self.templategroup:GetUnit(1):IsRefuelable()) + self.tankerSystem=select(2, self.templategroup:GetUnit(1):IsTanker()) + + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "OnDuty") -- Start FSM. + self:AddTransition("*", "Status", "*") -- Status update. + + self:AddTransition("OnDuty", "Pause", "Paused") -- Pause squadron. + self:AddTransition("Paused", "Unpause", "OnDuty") -- Unpause squadron. + + self:AddTransition("*", "Stop", "Stopped") -- Stop squadron. + + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the SQUADRON. Initializes parameters and starts event handlers. + -- @function [parent=#SQUADRON] Start + -- @param #SQUADRON self + + --- Triggers the FSM event "Start" after a delay. Starts the SQUADRON. Initializes parameters and starts event handlers. + -- @function [parent=#SQUADRON] __Start + -- @param #SQUADRON self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the SQUADRON and all its event handlers. + -- @param #SQUADRON self + + --- Triggers the FSM event "Stop" after a delay. Stops the SQUADRON and all its event handlers. + -- @function [parent=#SQUADRON] __Stop + -- @param #SQUADRON self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Status". + -- @function [parent=#SQUADRON] Status + -- @param #SQUADRON self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#SQUADRON] __Status + -- @param #SQUADRON self + -- @param #number delay Delay in seconds. + + + -- Debug trace. + if false then + BASE:TraceOnOff(true) + BASE:TraceClass(self.ClassName) + BASE:TraceLevel(1) + end + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set livery painted on all squadron aircraft. +-- Note that the livery name in general is different from the name shown in the mission editor. +-- +-- Valid names are the names of the **livery directories**. Check out the folder in your DCS installation for: +-- +-- * Full modules: `DCS World OpenBeta\CoreMods\aircraft\\Liveries\\` +-- * AI units: `DCS World OpenBeta\Bazar\Liveries\\` +-- +-- The folder name `` is the string you want. +-- +-- Or personal liveries you have installed somewhere in your saved games folder. +-- +-- @param #SQUADRON self +-- @param #string LiveryName Name of the livery. +-- @return #SQUADRON self +function SQUADRON:SetLivery(LiveryName) + self.livery=LiveryName + return self +end + +--- Set skill level of all squadron team members. +-- @param #SQUADRON self +-- @param #string Skill Skill of all flights. +-- @usage mysquadron:SetSkill(AI.Skill.EXCELLENT) +-- @return #SQUADRON self +function SQUADRON:SetSkill(Skill) + self.skill=Skill + return self +end + +--- Set verbosity level. +-- @param #SQUADRON self +-- @param #number VerbosityLevel Level of output (higher=more). Default 0. +-- @return #SQUADRON self +function SQUADRON:SetVerbosity(VerbosityLevel) + self.verbose=VerbosityLevel or 0 + return self +end + +--- Set turnover and repair time. If an asset returns from a mission to the airwing, it will need some time until the asset is available for further missions. +-- @param #SQUADRON self +-- @param #number MaintenanceTime Time in minutes it takes until a flight is combat ready again. Default is 0 min. +-- @param #number RepairTime Time in minutes it takes to repair a flight for each life point taken. Default is 0 min. +-- @return #SQUADRON self +function SQUADRON:SetTurnoverTime(MaintenanceTime, RepairTime) + self.maintenancetime=MaintenanceTime and MaintenanceTime*60 or 0 + self.repairtime=RepairTime and RepairTime*60 or 0 + return self +end + +--- Set radio frequency and modulation the squad uses. +-- @param #SQUADRON self +-- @param #number Frequency Radio frequency in MHz. Default 251 MHz. +-- @param #number Modulation Radio modulation. Default 0=AM. +-- @usage mysquadron:SetSkill(AI.Skill.EXCELLENT) +-- @return #SQUADRON self +function SQUADRON:SetRadio(Frequency, Modulation) + self.radioFreq=Frequency or 251 + self.radioModu=Modulation or radio.modulation.AM + return self +end + +--- Set number of units in groups. +-- @param #SQUADRON self +-- @param #number nunits Number of units. Must be >=1 and <=4. Default 2. +-- @return #SQUADRON self +function SQUADRON:SetGrouping(nunits) + self.ngrouping=nunits or 2 + if self.ngrouping<1 then self.ngrouping=1 end + if self.ngrouping>4 then self.ngrouping=4 end + return self +end + + +--- Set takeoff type. All assets of this squadron will be spawned with cold (default) or hot engines. +-- Spawning on runways is not supported. +-- @param #SQUADRON self +-- @param #string TakeoffType Take off type: "Cold" (default) or "Hot" with engines on. +-- @return #SQUADRON self +function SQUADRON:SetTakeoffType(TakeoffType) + TakeoffType=TakeoffType or "Cold" + if TakeoffType:lower()=="hot" then + self.takeoffType=COORDINATE.WaypointType.TakeOffParkingHot + elseif TakeoffType:lower()=="cold" then + self.takeoffType=COORDINATE.WaypointType.TakeOffParking + else + self.takeoffType=COORDINATE.WaypointType.TakeOffParking + end + return self +end + +--- Set takeoff type cold (default). +-- @param #SQUADRON self +-- @return #SQUADRON self +function SQUADRON:SetTakeoffCold() + self:SetTakeoffType("Cold") + return self +end + +--- Set takeoff type hot. +-- @param #SQUADRON self +-- @return #SQUADRON self +function SQUADRON:SetTakeoffHot() + self:SetTakeoffType("Hot") + return self +end + + +--- Set mission types this squadron is able to perform. +-- @param #SQUADRON self +-- @param #table MissionTypes Table of mission types. Can also be passed as a #string if only one type. +-- @param #number Performance Performance describing how good this mission can be performed. Higher is better. Default 50. Max 100. +-- @return #SQUADRON self +function SQUADRON:AddMissionCapability(MissionTypes, Performance) + + -- Ensure Missiontypes is a table. + if MissionTypes and type(MissionTypes)~="table" then + MissionTypes={MissionTypes} + end + + -- Set table. + self.missiontypes=self.missiontypes or {} + + for _,missiontype in pairs(MissionTypes) do + + -- Check not to add the same twice. + if self:CheckMissionCapability(missiontype, self.missiontypes) then + self:E(self.lid.."WARNING: Mission capability already present! No need to add it twice.") + -- TODO: update performance. + else + + local capability={} --Ops.Auftrag#AUFTRAG.Capability + capability.MissionType=missiontype + capability.Performance=Performance or 50 + table.insert(self.missiontypes, capability) + + end + end + + -- Debug info. + self:T2(self.missiontypes) + + return self +end + +--- Get mission types this squadron is able to perform. +-- @param #SQUADRON self +-- @return #table Table of mission types. Could be empty {}. +function SQUADRON:GetMissionTypes() + + local missiontypes={} + + for _,Capability in pairs(self.missiontypes) do + local capability=Capability --Ops.Auftrag#AUFTRAG.Capability + table.insert(missiontypes, capability.MissionType) + end + + return missiontypes +end + +--- Get mission capabilities of this squadron. +-- @param #SQUADRON self +-- @return #table Table of mission capabilities. +function SQUADRON:GetMissionCapabilities() + return self.missiontypes +end + +--- Get mission performance for a given type of misson. +-- @param #SQUADRON self +-- @param #string MissionType Type of mission. +-- @return #number Performance or -1. +function SQUADRON:GetMissionPeformance(MissionType) + + for _,Capability in pairs(self.missiontypes) do + local capability=Capability --Ops.Auftrag#AUFTRAG.Capability + if capability.MissionType==MissionType then + return capability.Performance + end + end + + return -1 +end + +--- Set max mission range. Only missions in a circle of this radius around the squadron airbase are executed. +-- @param #SQUADRON self +-- @param #number Range Range in NM. Default 100 NM. +-- @return #SQUADRON self +function SQUADRON:SetMissionRange(Range) + self.engageRange=UTILS.NMToMeters(Range or 100) + return self +end + +--- Set call sign. +-- @param #SQUADRON self +-- @param #number Callsign Callsign from CALLSIGN.Aircraft, e.g. "Chevy" for CALLSIGN.Aircraft.CHEVY. +-- @param #number Index Callsign index, Chevy-**1**. +-- @return #SQUADRON self +function SQUADRON:SetCallsign(Callsign, Index) + self.callsignName=Callsign + self.callsignIndex=Index + return self +end + +--- Set modex. +-- @param #SQUADRON self +-- @param #number Modex A number like 100. +-- @param #string Prefix A prefix string, which is put before the `Modex` number. +-- @param #string Suffix A suffix string, which is put after the `Modex` number. +-- @return #SQUADRON self +function SQUADRON:SetModex(Modex, Prefix, Suffix) + self.modex=Modex + self.modexPrefix=Prefix + self.modexSuffix=Suffix + return self +end + +--- Set low fuel threshold. +-- @param #SQUADRON self +-- @param #number LowFuel Low fuel threshold in percent. Default 25. +-- @return #SQUADRON self +function SQUADRON:SetFuelLowThreshold(LowFuel) + self.fuellow=LowFuel or 25 + return self +end + +--- Set if low fuel threshold is reached, flight tries to refuel at the neares tanker. +-- @param #SQUADRON self +-- @param #boolean switch If true or nil, flight goes for refuelling. If false, turn this off. +-- @return #SQUADRON self +function SQUADRON:SetFuelLowRefuel(switch) + if switch==false then + self.fuellowRefuel=false + else + self.fuellowRefuel=true + end + return self +end + +--- Set airwing. +-- @param #SQUADRON self +-- @param Ops.AirWing#AIRWING Airwing The airwing. +-- @return #SQUADRON self +function SQUADRON:SetAirwing(Airwing) + self.airwing=Airwing + return self +end + +--- Add airwing asset to squadron. +-- @param #SQUADRON self +-- @param Ops.AirWing#AIRWING.SquadronAsset Asset The airwing asset. +-- @return #SQUADRON self +function SQUADRON:AddAsset(Asset) + self:T(self.lid..string.format("Adding asset %s of type %s", Asset.spawngroupname, Asset.unittype)) + Asset.squadname=self.name + table.insert(self.assets, Asset) + return self +end + +--- Remove airwing asset from squadron. +-- @param #SQUADRON self +-- @param Ops.AirWing#AIRWING.SquadronAsset Asset The airwing asset. +-- @return #SQUADRON self +function SQUADRON:DelAsset(Asset) + for i,_asset in pairs(self.assets) do + local asset=_asset --Ops.AirWing#AIRWING.SquadronAsset + if Asset.uid==asset.uid then + self:T2(self.lid..string.format("Removing asset %s", asset.spawngroupname)) + table.remove(self.assets, i) + break + end + end + return self +end + +--- Remove airwing asset group from squadron. +-- @param #SQUADRON self +-- @param #string GroupName Name of the asset group. +-- @return #SQUADRON self +function SQUADRON:DelGroup(GroupName) + for i,_asset in pairs(self.assets) do + local asset=_asset --Ops.AirWing#AIRWING.SquadronAsset + if GroupName==asset.spawngroupname then + self:T2(self.lid..string.format("Removing asset %s", asset.spawngroupname)) + table.remove(self.assets, i) + break + end + end + return self +end + +--- Get name of the squadron +-- @param #SQUADRON self +-- @return #string Name of the squadron. +function SQUADRON:GetName() + return self.name +end + +--- Get radio frequency and modulation. +-- @param #SQUADRON self +-- @return #number Radio frequency in MHz. +-- @return #number Radio Modulation (0=AM, 1=FM). +function SQUADRON:GetRadio() + return self.radioFreq, self.radioModu +end + +--- Create a callsign for the asset. +-- @param #SQUADRON self +-- @param Ops.AirWing#AIRWING.SquadronAsset Asset The airwing asset. +-- @return #SQUADRON self +function SQUADRON:GetCallsign(Asset) + + if self.callsignName then + + Asset.callsign={} + + for i=1,Asset.nunits do + + local callsign={} + + callsign[1]=self.callsignName + callsign[2]=math.floor(self.callsigncounter / 10) + callsign[3]=self.callsigncounter % 10 + if callsign[3]==0 then + callsign[3]=1 + self.callsigncounter=self.callsigncounter+2 + else + self.callsigncounter=self.callsigncounter+1 + end + + Asset.callsign[i]=callsign + + self:T3({callsign=callsign}) + + --TODO: there is also a table entry .name, which is a string. + end + + + end + +end + +--- Create a modex for the asset. +-- @param #SQUADRON self +-- @param Ops.AirWing#AIRWING.SquadronAsset Asset The airwing asset. +-- @return #SQUADRON self +function SQUADRON:GetModex(Asset) + + if self.modex then + + Asset.modex={} + + for i=1,Asset.nunits do + + Asset.modex[i]=string.format("%03d", self.modex+self.modexcounter) + + self.modexcounter=self.modexcounter+1 + + self:T3({modex=Asset.modex[i]}) + + end + + end + +end + + +--- Add TACAN channels to the squadron. Note that channels can only range from 1 to 126. +-- @param #SQUADRON self +-- @param #number ChannelMin Channel. +-- @param #number ChannelMax Channel. +-- @return #SQUADRON self +-- @usage mysquad:AddTacanChannel(64,69) -- adds channels 64, 65, 66, 67, 68, 69 +function SQUADRON:AddTacanChannel(ChannelMin, ChannelMax) + + ChannelMax=ChannelMax or ChannelMin + + if ChannelMin>126 then + self:E(self.lid.."ERROR: TACAN Channel must be <= 126! Will not add to available channels") + return self + end + if ChannelMax>126 then + self:E(self.lid.."WARNING: TACAN Channel must be <= 126! Adjusting ChannelMax to 126") + ChannelMax=126 + end + + for i=ChannelMin,ChannelMax do + self.tacanChannel[i]=true + end + + return self +end + +--- Get an unused TACAN channel. +-- @param #SQUADRON self +-- @return #number TACAN channel or *nil* if no channel is free. +function SQUADRON:FetchTacan() + + for channel,free in pairs(self.tacanChannel) do + if free then + self:T(self.lid..string.format("Checking out Tacan channel %d", channel)) + self.tacanChannel[channel]=false + return channel + end + end + + return nil +end + +--- "Return" a used TACAN channel. +-- @param #SQUADRON self +-- @param #number channel The channel that is available again. +function SQUADRON:ReturnTacan(channel) + self:T(self.lid..string.format("Returning Tacan channel %d", channel)) + self.tacanChannel[channel]=true +end + +--- Check if squadron is "OnDuty". +-- @param #SQUADRON self +-- @return #boolean If true, squdron is in state "OnDuty". +function SQUADRON:IsOnDuty() + return self:Is("OnDuty") +end + +--- Check if squadron is "Stopped". +-- @param #SQUADRON self +-- @return #boolean If true, squdron is in state "Stopped". +function SQUADRON:IsStopped() + return self:Is("Stopped") +end + +--- Check if squadron is "Paused". +-- @param #SQUADRON self +-- @return #boolean If true, squdron is in state "Paused". +function SQUADRON:IsPaused() + return self:Is("Paused") +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Start & Status +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after Start event. Starts the FLIGHTGROUP FSM and event handlers. +-- @param #SQUADRON self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function SQUADRON:onafterStart(From, Event, To) + + -- Short info. + local text=string.format("Starting SQUADRON", self.name) + self:T(self.lid..text) + + -- Start the status monitoring. + self:__Status(-1) +end + +--- On after "Status" event. +-- @param #SQUADRON self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function SQUADRON:onafterStatus(From, Event, To) + + if self.verbose>=1 then + + -- FSM state. + local fsmstate=self:GetState() + + local callsign=self.callsignName and UTILS.GetCallsignName(self.callsignName) or "N/A" + local modex=self.modex and self.modex or -1 + local skill=self.skill and tostring(self.skill) or "N/A" + + local NassetsTot=#self.assets + local NassetsInS=self:CountAssetsInStock() + local NassetsQP=0 ; local NassetsP=0 ; local NassetsQ=0 + if self.airwing then + NassetsQP, NassetsP, NassetsQ=self.airwing:CountAssetsOnMission(nil, self) + end + + -- Short info. + local text=string.format("%s [Type=%s, Call=%s, Modex=%d, Skill=%s]: Assets Total=%d, Stock=%d, Mission=%d [Active=%d, Queue=%d]", + fsmstate, self.aircrafttype, callsign, modex, skill, NassetsTot, NassetsInS, NassetsQP, NassetsP, NassetsQ) + self:I(self.lid..text) + + -- Check if group has detected any units. + self:_CheckAssetStatus() + + end + + if not self:IsStopped() then + self:__Status(-60) + end +end + + +--- Check asset status. +-- @param #SQUADRON self +function SQUADRON:_CheckAssetStatus() + + if self.verbose>=2 and #self.assets>0 then + + local text="" + for j,_asset in pairs(self.assets) do + local asset=_asset --Ops.AirWing#AIRWING.SquadronAsset + + -- Text. + text=text..string.format("\n[%d] %s (%s*%d): ", j, asset.spawngroupname, asset.unittype, asset.nunits) + + if asset.spawned then + + --- + -- Spawned + --- + + -- Mission info. + local mission=self.airwing and self.airwing:GetAssetCurrentMission(asset) or false + if mission then + local distance=asset.flightgroup and UTILS.MetersToNM(mission:GetTargetDistance(asset.flightgroup.group:GetCoordinate())) or 0 + text=text..string.format("Mission %s - %s: Status=%s, Dist=%.1f NM", mission.name, mission.type, mission.status, distance) + else + text=text.."Mission None" + end + + -- Flight status. + text=text..", Flight: " + if asset.flightgroup and asset.flightgroup:IsAlive() then + local status=asset.flightgroup:GetState() + local fuelmin=asset.flightgroup:GetFuelMin() + local fuellow=asset.flightgroup:IsFuelLow() + local fuelcri=asset.flightgroup:IsFuelCritical() + + text=text..string.format("%s Fuel=%d", status, fuelmin) + if fuelcri then + text=text.." (Critical!)" + elseif fuellow then + text=text.." (Low)" + end + + local lifept, lifept0=asset.flightgroup:GetLifePoints() + text=text..string.format(", Life=%d/%d", lifept, lifept0) + + local ammo=asset.flightgroup:GetAmmoTot() + text=text..string.format(", Ammo=%d [G=%d, R=%d, B=%d, M=%d]", ammo.Total,ammo.Guns, ammo.Rockets, ammo.Bombs, ammo.Missiles) + else + text=text.."N/A" + end + + -- Payload info. + local payload=asset.payload and table.concat(self.airwing:GetPayloadMissionTypes(asset.payload), ", ") or "None" + text=text..", Payload={"..payload.."}" + + else + + --- + -- In Stock + --- + + text=text..string.format("In Stock") + + if self:IsRepaired(asset) then + text=text..", Combat Ready" + else + + text=text..string.format(", Repaired in %d sec", self:GetRepairTime(asset)) + + if asset.damage then + text=text..string.format(" (Damage=%.1f)", asset.damage) + end + end + + if asset.Treturned then + local T=timer.getAbsTime()-asset.Treturned + text=text..string.format(", Returned for %d sec", T) + end + + end + end + self:I(self.lid..text) + end + +end + +--- On after "Stop" event. +-- @param #SQUADRON self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function SQUADRON:onafterStop(From, Event, To) + + self:I(self.lid.."STOPPING Squadron!") + + -- Remove all assets. + for i=#self.assets,1,-1 do + local asset=self.assets[i] + self:DelAsset(asset) + end + + self.CallScheduler:Clear() + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check if there is a squadron that can execute a given mission. +-- We check the mission type, the refuelling system, engagement range +-- @param #SQUADRON self +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +-- @return #boolean If true, Squadron can do that type of mission. +function SQUADRON:CanMission(Mission) + + local cando=true + + -- On duty?= + if not self:IsOnDuty() then + self:T(self.lid..string.format("Squad in not OnDuty but in state %s. Cannot do mission %s with target %s", self:GetState(), Mission.name, Mission:GetTargetName())) + return false + end + + -- Check mission type. WARNING: This assumes that all assets of the squad can do the same mission types! + if not self:CheckMissionType(Mission.type, self:GetMissionTypes()) then + self:T(self.lid..string.format("INFO: Squad cannot do mission type %s (%s, %s)", Mission.type, Mission.name, Mission:GetTargetName())) + return false + end + + -- Check that tanker mission + if Mission.type==AUFTRAG.Type.TANKER then + + if Mission.refuelSystem and Mission.refuelSystem==self.tankerSystem then + -- Correct refueling system. + else + self:T(self.lid..string.format("INFO: Wrong refueling system requested=%s != %s=available", tostring(Mission.refuelSystem), tostring(self.tankerSystem))) + return false + end + + end + + -- Distance to target. + local TargetDistance=Mission:GetTargetDistance(self.airwing:GetCoordinate()) + + -- Max engage range. + local engagerange=Mission.engageRange and math.max(self.engageRange, Mission.engageRange) or self.engageRange + + -- Set range is valid. Mission engage distance can overrule the squad engage range. + if TargetDistance>engagerange then + self:I(self.lid..string.format("INFO: Squad is not in range. Target dist=%d > %d NM max mission Range", UTILS.MetersToNM(TargetDistance), UTILS.MetersToNM(engagerange))) + return false + end + + return true +end + +--- Count assets in airwing (warehous) stock. +-- @param #SQUADRON self +-- @return #number Assets not spawned. +function SQUADRON:CountAssetsInStock() + + local N=0 + for _,_asset in pairs(self.assets) do + local asset=_asset --Ops.AirWing#AIRWING.SquadronAsset + if asset.spawned then + + else + N=N+1 + end + end + + return N +end + +--- Get assets for a mission. +-- @param #SQUADRON self +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +-- @param #number Nplayloads Number of payloads available. +-- @return #table Assets that can do the required mission. +function SQUADRON:RecruitAssets(Mission, Npayloads) + + -- Number of payloads available. + Npayloads=Npayloads or self.airwing:CountPayloadsInStock(Mission.type, self.aircrafttype, Mission.payloads) + + local assets={} + + -- Loop over assets. + for _,_asset in pairs(self.assets) do + local asset=_asset --Ops.AirWing#AIRWING.SquadronAsset + + + -- Check if asset is currently on a mission (STARTED or QUEUED). + if self.airwing:IsAssetOnMission(asset) then + + --- + -- Asset is already on a mission. + --- + + -- Check if this asset is currently on a GCICAP mission (STARTED or EXECUTING). + if self.airwing:IsAssetOnMission(asset, AUFTRAG.Type.GCICAP) and Mission.type==AUFTRAG.Type.INTERCEPT then + + -- Check if the payload of this asset is compatible with the mission. + -- Note: we do not check the payload as an asset that is on a GCICAP mission should be able to do an INTERCEPT as well! + self:I(self.lid.."Adding asset on GCICAP mission for an INTERCEPT mission") + table.insert(assets, asset) + + end + + else + + --- + -- Asset as NO current mission + --- + + if asset.spawned then + + --- + -- Asset is already SPAWNED (could be uncontrolled on the airfield or inbound after another mission) + --- + + local flightgroup=asset.flightgroup + + -- Firstly, check if it has the right payload. + if self:CheckMissionCapability(Mission.type, asset.payload.capabilities) and flightgroup and flightgroup:IsAlive() then + + -- Assume we are ready and check if any condition tells us we are not. + local combatready=true + + if Mission.type==AUFTRAG.Type.INTERCEPT then + combatready=flightgroup:CanAirToAir() + else + local excludeguns=Mission.type==AUFTRAG.Type.BOMBING or Mission.type==AUFTRAG.Type.BOMBRUNWAY or Mission.type==AUFTRAG.Type.BOMBCARPET or Mission.type==AUFTRAG.Type.SEAD or Mission.type==AUFTRAG.Type.ANTISHIP + combatready=flightgroup:CanAirToGround(excludeguns) + end + + -- No more attacks if fuel is already low. Safety first! + if flightgroup:IsFuelLow() then + combatready=false + end + + -- Check if in a state where we really do not want to fight any more. + if flightgroup:IsHolding() or flightgroup:IsLanding() or flightgroup:IsLanded() or flightgroup:IsArrived() or flightgroup:IsDead() or flightgroup:IsStopped() then + combatready=false + end + + -- This asset is "combatready". + if combatready then + self:I(self.lid.."Adding SPAWNED asset to ANOTHER mission as it is COMBATREADY") + table.insert(assets, asset) + end + + end + + else + + --- + -- Asset is still in STOCK + --- + + -- Check that asset is not already requested for another mission. + if Npayloads>0 and self:IsRepaired(asset) and (not asset.requested) then + + -- Add this asset to the selection. + table.insert(assets, asset) + + -- Reduce number of payloads so we only return the number of assets that could do the job. + Npayloads=Npayloads-1 + + end + + end + end + end -- loop over assets + + return assets +end + + +--- Get the time an asset needs to be repaired. +-- @param #SQUADRON self +-- @param Ops.AirWing#AIRWING.SquadronAsset Asset The asset. +-- @return #number Time in seconds until asset is repaired. +function SQUADRON:GetRepairTime(Asset) + + if Asset.Treturned then + + local t=self.maintenancetime + t=t+Asset.damage*self.repairtime + + -- Seconds after returned. + local dt=timer.getAbsTime()-Asset.Treturned + + local T=t-dt + + return T + else + return 0 + end + +end + +--- Checks if a mission type is contained in a table of possible types. +-- @param #SQUADRON self +-- @param Ops.AirWing#AIRWING.SquadronAsset Asset The asset. +-- @return #boolean If true, the requested mission type is part of the possible mission types. +function SQUADRON:IsRepaired(Asset) + + if Asset.Treturned then + local Tnow=timer.getAbsTime() + local Trepaired=Asset.Treturned+self.maintenancetime + if Tnow>=Trepaired then + return true + else + return false + end + + else + return true + end + +end + + +--- Checks if a mission type is contained in a table of possible types. +-- @param #SQUADRON self +-- @param #string MissionType The requested mission type. +-- @param #table PossibleTypes A table with possible mission types. +-- @return #boolean If true, the requested mission type is part of the possible mission types. +function SQUADRON:CheckMissionType(MissionType, PossibleTypes) + + if type(PossibleTypes)=="string" then + PossibleTypes={PossibleTypes} + end + + for _,canmission in pairs(PossibleTypes) do + if canmission==MissionType then + return true + end + end + + return false +end + +--- Check if a mission type is contained in a list of possible capabilities. +-- @param #SQUADRON self +-- @param #string MissionType The requested mission type. +-- @param #table Capabilities A table with possible capabilities. +-- @return #boolean If true, the requested mission type is part of the possible mission types. +function SQUADRON:CheckMissionCapability(MissionType, Capabilities) + + for _,cap in pairs(Capabilities) do + local capability=cap --Ops.Auftrag#AUFTRAG.Capability + if capability.MissionType==MissionType then + return true + end + end + + return false +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- **Ops** - Airwing Warehouse. +-- +-- **Main Features:** +-- +-- * Manage squadrons. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Ops.Airwing +-- @image OPS_AirWing.png + + +--- AIRWING class. +-- @type AIRWING +-- @field #string ClassName Name of the class. +-- @field #number verbose Verbosity of output. +-- @field #string lid Class id string for output to DCS log file. +-- @field #table menu Table of menu items. +-- @field #table squadrons Table of squadrons. +-- @field #table missionqueue Mission queue table. +-- @field #table payloads Playloads for specific aircraft and mission types. +-- @field #number payloadcounter Running index of payloads. +-- @field Core.Set#SET_ZONE zonesetCAP Set of CAP zones. +-- @field Core.Set#SET_ZONE zonesetTANKER Set of TANKER zones. +-- @field Core.Set#SET_ZONE zonesetAWACS Set of AWACS zones. +-- @field #number nflightsCAP Number of CAP flights constantly in the air. +-- @field #number nflightsAWACS Number of AWACS flights constantly in the air. +-- @field #number nflightsTANKERboom Number of TANKER flights with BOOM constantly in the air. +-- @field #number nflightsTANKERprobe Number of TANKER flights with PROBE constantly in the air. +-- @field #number nflightsRescueHelo Number of Rescue helo flights constantly in the air. +-- @field #table pointsCAP Table of CAP points. +-- @field #table pointsTANKER Table of Tanker points. +-- @field #table pointsAWACS Table of AWACS points. +-- @field #boolean markpoints Display markers on the F10 map. +-- @field Ops.WingCommander#WINGCOMMANDER wingcommander The wing commander responsible for this airwing. +-- +-- @field Ops.RescueHelo#RESCUEHELO rescuehelo The rescue helo. +-- @field Ops.RecoveryTanker#RECOVERYTANKER recoverytanker The recoverytanker. +-- +-- @extends Functional.Warehouse#WAREHOUSE + +--- Be surprised! +-- +-- === +-- +-- ![Banner Image](..\Presentations\OPS\AirWing\_Main.png) +-- +-- # The AIRWING Concept +-- +-- An AIRWING consists of multiple SQUADRONS. These squadrons "live" in a WAREHOUSE, i.e. a physical structure that is connected to an airbase (airdrome, FRAP or ship). +-- For an airwing to be operational, it needs airframes, weapons/fuel and an airbase. +-- +-- # Create an Airwing +-- +-- ## Constructing the Airwing +-- +-- airwing=AIRWING:New("Warehouse Batumi", "8th Fighter Wing") +-- airwing:Start() +-- +-- The first parameter specified the warehouse, i.e. the static building housing the airwing (or the name of the aircraft carrier). The second parameter is optional +-- and sets an alias. +-- +-- ## Adding Squadrons +-- +-- At this point the airwing does not have any assets (aircraft). In order to add these, one needs to first define SQUADRONS. +-- +-- VFA151=SQUADRON:New("F-14 Group", 8, "VFA-151 (Vigilantes)") +-- VFA151:AddMissionCapability({AUFTRAG.Type.GCICAP, AUFTRAG.Type.INTERCEPT}) +-- +-- airwing:AddSquadron(VFA151) +-- +-- This adds eight Tomcat groups beloning to VFA-151 to the airwing. This squadron has the ability to perform combat air patrols and intercepts. +-- +-- ## Adding Payloads +-- +-- Adding pure airframes is not enough. The aircraft also need weapons (and fuel) for certain missions. These must be given to the airwing from template groups +-- defined in the Mission Editor. +-- +-- -- F-14 payloads for CAP and INTERCEPT. Phoenix are first, sparrows are second choice. +-- airwing:NewPayload(GROUP:FindByName("F-14 Payload AIM-54C"), 2, {AUFTRAG.Type.INTERCEPT, AUFTRAG.Type.GCICAP}, 80) +-- airwing:NewPayload(GROUP:FindByName("F-14 Payload AIM-7M"), 20, {AUFTRAG.Type.INTERCEPT, AUFTRAG.Type.GCICAP}) +-- +-- This will add two AIM-54C and 20 AIM-7M payloads. +-- +-- If the airwing gets an intercept or patrol mission assigned, it will first use the AIM-54s. Once these are consumed, the AIM-7s are attached to the aircraft. +-- +-- When an airwing does not have a payload for a certain mission type, the mission cannot be carried out. +-- +-- You can set the number of payloads to "unlimited" by setting its quantity to -1. +-- +-- # Adding Missions +-- +-- Various mission types can be added easily via the AUFTRAG class. +-- +-- Once you created an AUFTRAG you can add it to the AIRWING with the :AddMission(mission) function. +-- +-- This mission will be put into the AIRWING queue. Once the mission start time is reached and all resources (airframes and pylons) are available, the mission is started. +-- If the mission stop time is over (and the mission is not finished), it will be cancelled and removed from the queue. This applies also to mission that were not even +-- started. +-- +-- # Command an Airwing +-- +-- An airwing can receive missions from a WINGCOMMANDER. See docs of that class for details. +-- +-- However, you are still free to add missions at anytime. +-- +-- +-- @field #AIRWING +AIRWING = { + ClassName = "AIRWING", + verbose = 0, + lid = nil, + menu = nil, + squadrons = {}, + missionqueue = {}, + payloads = {}, + payloadcounter = 0, + pointsCAP = {}, + pointsTANKER = {}, + pointsAWACS = {}, + wingcommander = nil, + markpoints = false, +} + +--- Squadron asset. +-- @type AIRWING.SquadronAsset +-- @field #AIRWING.Payload payload The payload of the asset. +-- @field Ops.FlightGroup#FLIGHTGROUP flightgroup The flightgroup object. +-- @field #string squadname Name of the squadron this asset belongs to. +-- @field #number Treturned Time stamp when asset returned to the airwing. +-- @extends Functional.Warehouse#WAREHOUSE.Assetitem + +--- Payload data. +-- @type AIRWING.Payload +-- @field #number uid Unique payload ID. +-- @field #string unitname Name of the unit this pylon was extracted from. +-- @field #string aircrafttype Type of aircraft, which can use this payload. +-- @field #table capabilities Mission types and performances for which this payload can be used. +-- @field #table pylons Pylon data extracted for the unit template. +-- @field #number navail Number of available payloads of this type. +-- @field #boolean unlimited If true, this payload is unlimited and does not get consumed. + +--- Patrol data. +-- @type AIRWING.PatrolData +-- @field #string type Type name. +-- @field Core.Point#COORDINATE coord Patrol coordinate. +-- @field #number altitude Altitude in feet. +-- @field #number heading Heading in degrees. +-- @field #number leg Leg length in NM. +-- @field #number speed Speed in knots. +-- @field #number noccupied Number of flights on this patrol point. +-- @field Wrapper.Marker#MARKER marker F10 marker. + +--- AIRWING class version. +-- @field #string version +AIRWING.version="0.5.2" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ToDo list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Spawn in air or hot ==> Needs WAREHOUSE update. +-- TODO: Make special request to transfer squadrons to anther airwing (or warehouse). +-- TODO: Check that airbase has enough parking spots if a request is BIG. Alternatively, split requests. +-- DONE: Add squadrons to warehouse. +-- DONE: Build mission queue. +-- DONE: Find way to start missions. +-- DONE: Check if missions are done/cancelled. +-- DONE: Payloads as resources. +-- DONE: Define CAP zones. +-- DONE: Define TANKER zones for refuelling. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new AIRWING class object for a specific aircraft carrier unit. +-- @param #AIRWING self +-- @param #string warehousename Name of the warehouse static or unit object representing the warehouse. +-- @param #string airwingname Name of the air wing, e.g. "AIRWING-8". +-- @return #AIRWING self +function AIRWING:New(warehousename, airwingname) + + -- Inherit everything from WAREHOUSE class. + local self=BASE:Inherit(self, WAREHOUSE:New(warehousename, airwingname)) -- #AIRWING + + -- Nil check. + if not self then + BASE:E(string.format("ERROR: Could not find warehouse %s!", warehousename)) + return nil + end + + -- Set some string id for output to DCS.log file. + self.lid=string.format("AIRWING %s | ", self.alias) + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("*", "MissionRequest", "*") -- Add a (mission) request to the warehouse. + self:AddTransition("*", "MissionCancel", "*") -- Cancel mission. + + self:AddTransition("*", "SquadAssetReturned", "*") -- Flight was spawned with a mission. + + self:AddTransition("*", "FlightOnMission", "*") -- Flight was spawned with a mission. + + -- Defaults: + --self:SetVerbosity(0) + self.nflightsCAP=0 + self.nflightsAWACS=0 + self.nflightsTANKERboom=0 + self.nflightsTANKERprobe=0 + self.nflightsRecoveryTanker=0 + self.nflightsRescueHelo=0 + self.markpoints=false + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the AIRWING. Initializes parameters and starts event handlers. + -- @function [parent=#AIRWING] Start + -- @param #AIRWING self + + --- Triggers the FSM event "Start" after a delay. Starts the AIRWING. Initializes parameters and starts event handlers. + -- @function [parent=#AIRWING] __Start + -- @param #AIRWING self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the AIRWING and all its event handlers. + -- @param #AIRWING self + + --- Triggers the FSM event "Stop" after a delay. Stops the AIRWING and all its event handlers. + -- @function [parent=#AIRWING] __Stop + -- @param #AIRWING self + -- @param #number delay Delay in seconds. + + --- On after "FlightOnMission" event. Triggered when an asset group starts a mission. + -- @function [parent=#AIRWING] OnAfterFlightOnMission + -- @param #AIRWING self + -- @param #string From The From state + -- @param #string Event The Event called + -- @param #string To The To state + -- @param Ops.FlightGroup#FLIGHTGROUP Flightgroup The Flightgroup on mission + -- @param Ops.Auftrag#AUFTRAG Mission The Auftrag of the Flightgroup + + --- On after "AssetReturned" event. Triggered when an asset group returned to its airwing. + -- @function [parent=#AIRWING] OnAfterAssetReturned + -- @param #AIRWING self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Squadron#SQUADRON Squadron The asset squadron. + -- @param #AIRWING.SquadronAsset Asset The asset that returned. + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add a squadron to the air wing. +-- @param #AIRWING self +-- @param Ops.Squadron#SQUADRON Squadron The squadron object. +-- @return #AIRWING self +function AIRWING:AddSquadron(Squadron) + + -- Add squadron to airwing. + table.insert(self.squadrons, Squadron) + + -- Add assets to squadron. + self:AddAssetToSquadron(Squadron, Squadron.Ngroups) + + -- Tanker and AWACS get unlimited payloads. + if Squadron.attribute==GROUP.Attribute.AIR_AWACS then + self:NewPayload(Squadron.templategroup, -1, AUFTRAG.Type.AWACS) + elseif Squadron.attribute==GROUP.Attribute.AIR_TANKER then + self:NewPayload(Squadron.templategroup, -1, AUFTRAG.Type.TANKER) + end + + -- Set airwing to squadron. + Squadron:SetAirwing(self) + + -- Start squadron. + if Squadron:IsStopped() then + Squadron:Start() + end + + return self +end + +--- Add a **new** payload to the airwing resources. +-- @param #AIRWING self +-- @param Wrapper.Unit#UNIT Unit The unit, the payload is extracted from. Can also be given as *#string* name of the unit. +-- @param #number Npayloads Number of payloads to add to the airwing resources. Default 99 (which should be enough for most scenarios). Set to -1 for unlimited. +-- @param #table MissionTypes Mission types this payload can be used for. +-- @param #number Performance A number between 0 (worst) and 100 (best) to describe the performance of the loadout for the given mission types. Default is 50. +-- @return #AIRWING.Payload The payload table or nil if the unit does not exist. +function AIRWING:NewPayload(Unit, Npayloads, MissionTypes, Performance) + + -- Default performance. + Performance=Performance or 50 + + if type(Unit)=="string" then + local name=Unit + Unit=UNIT:FindByName(name) + if not Unit then + Unit=GROUP:FindByName(name) + end + end + + if Unit then + + -- If a GROUP object was given, get the first unit. + if Unit:IsInstanceOf("GROUP") then + Unit=Unit:GetUnit(1) + end + + -- Ensure Missiontypes is a table. + if MissionTypes and type(MissionTypes)~="table" then + MissionTypes={MissionTypes} + end + + -- Create payload. + local payload={} --#AIRWING.Payload + payload.uid=self.payloadcounter + payload.unitname=Unit:GetName() + payload.aircrafttype=Unit:GetTypeName() + payload.pylons=Unit:GetTemplatePayload() + payload.unlimited=Npayloads<0 + if payload.unlimited then + payload.navail=1 + else + payload.navail=Npayloads or 99 + end + + payload.capabilities={} + for _,missiontype in pairs(MissionTypes) do + local capability={} --Ops.Auftrag#AUFTRAG.Capability + capability.MissionType=missiontype + capability.Performance=Performance + table.insert(payload.capabilities, capability) + end + + -- Add ORBIT for all. + if not self:CheckMissionType(AUFTRAG.Type.ORBIT, MissionTypes) then + local capability={} --Ops.Auftrag#AUFTRAG.Capability + capability.MissionType=AUFTRAG.Type.ORBIT + capability.Performance=50 + table.insert(payload.capabilities, capability) + end + + -- Info + self:T(self.lid..string.format("Adding new payload from unit %s for aircraft type %s: ID=%d, N=%d (unlimited=%s), performance=%d, missions: %s", + payload.unitname, payload.aircrafttype, payload.uid, payload.navail, tostring(payload.unlimited), Performance, table.concat(MissionTypes, ", "))) + + -- Add payload + table.insert(self.payloads, payload) + + -- Increase counter + self.payloadcounter=self.payloadcounter+1 + + return payload + + end + + self:E(self.lid.."ERROR: No UNIT found to create PAYLOAD!") + return nil +end + +--- Add a mission capability to an existing payload. +-- @param #AIRWING self +-- @param #AIRWING.Payload Payload The payload table to which the capability should be added. +-- @param #table MissionTypes Mission types to be added. +-- @param #number Performance A number between 0 (worst) and 100 (best) to describe the performance of the loadout for the given mission types. Default is 50. +-- @return #AIRWING self +function AIRWING:AddPayloadCapability(Payload, MissionTypes, Performance) + + -- Ensure Missiontypes is a table. + if MissionTypes and type(MissionTypes)~="table" then + MissionTypes={MissionTypes} + end + + Payload.capabilities=Payload.capabilities or {} + + for _,missiontype in pairs(MissionTypes) do + + local capability={} --Ops.Auftrag#AUFTRAG.Capability + capability.MissionType=missiontype + capability.Performance=Performance + + --TODO: check that capability does not already exist! + + table.insert(Payload.capabilities, capability) + end + + return self +end + +--- Fetch a payload from the airwing resources for a given unit and mission type. +-- The payload with the highest priority is preferred. +-- @param #AIRWING self +-- @param #string UnitType The type of the unit. +-- @param #string MissionType The mission type. +-- @param #table Payloads Specific payloads only to be considered. +-- @return #AIRWING.Payload Payload table or *nil*. +function AIRWING:FetchPayloadFromStock(UnitType, MissionType, Payloads) + + -- Quick check if we have any payloads. + if not self.payloads or #self.payloads==0 then + self:T(self.lid.."WARNING: No payloads in stock!") + return nil + end + + -- Debug. + if self.verbose>=4 then + self:I(self.lid..string.format("Looking for payload for unit type=%s and mission type=%s", UnitType, MissionType)) + for i,_payload in pairs(self.payloads) do + local payload=_payload --#AIRWING.Payload + local performance=self:GetPayloadPeformance(payload, MissionType) + self:I(self.lid..string.format("[%d] Payload type=%s navail=%d unlimited=%s", i, payload.aircrafttype, payload.navail, tostring(payload.unlimited))) + end + end + + --- Sort payload wrt the following criteria: + -- 1) Highest performance is the main selection criterion. + -- 2) If payloads have the same performance, unlimited payloads are preferred over limited ones. + -- 3) If payloads have the same performance _and_ are limited, the more abundant one is preferred. + local function sortpayloads(a,b) + local pA=a --#AIRWING.Payload + local pB=b --#AIRWING.Payload + if a and b then -- I had the case that a or b were nil even though the self.payloads table was looking okay. Very strange! Seems to be solved by pre-selecting valid payloads. + local performanceA=self:GetPayloadPeformance(a, MissionType) + local performanceB=self:GetPayloadPeformance(b, MissionType) + return (performanceA>performanceB) or (performanceA==performanceB and a.unlimited==true) or (performanceA==performanceB and a.unlimited==true and b.unlimited==true and a.navail>b.navail) + elseif not a then + self:I(self.lid..string.format("FF ERROR in sortpayloads: a is nil")) + return false + elseif not b then + self:I(self.lid..string.format("FF ERROR in sortpayloads: b is nil")) + return true + else + self:I(self.lid..string.format("FF ERROR in sortpayloads: a and b are nil")) + return false + end + end + + local function _checkPayloads(payload) + if Payloads then + for _,Payload in pairs(Payloads) do + if Payload.uid==payload.uid then + return true + end + end + else + -- Payload was not specified. + return nil + end + return false + end + + -- Pre-selection: filter out only those payloads that are valid for the airframe and mission type and are available. + local payloads={} + for _,_payload in pairs(self.payloads) do + local payload=_payload --#AIRWING.Payload + + local specialpayload=_checkPayloads(payload) + local compatible=self:CheckMissionCapability(MissionType, payload.capabilities) + + local goforit = specialpayload or (specialpayload==nil and compatible) + + if payload.aircrafttype==UnitType and payload.navail>0 and goforit then + table.insert(payloads, payload) + end + end + + -- Debug. + if self.verbose>=4 then + self:I(self.lid..string.format("Sorted payloads for mission type X and aircraft type=Y:")) + for _,_payload in ipairs(self.payloads) do + local payload=_payload --#AIRWING.Payload + if payload.aircrafttype==UnitType and self:CheckMissionCapability(MissionType, payload.capabilities) then + local performace=self:GetPayloadPeformance(payload, MissionType) + self:I(self.lid..string.format("FF %s payload for %s: avail=%d performace=%d", MissionType, payload.aircrafttype, payload.navail, performace)) + end + end + end + + -- Cases: + if #payloads==0 then + -- No payload available. + self:T(self.lid.."Warning could not find a payload for airframe X mission type Y!") + return nil + elseif #payloads==1 then + -- Only one payload anyway. + local payload=payloads[1] --#AIRWING.Payload + if not payload.unlimited then + payload.navail=payload.navail-1 + end + return payload + else + -- Sort payloads. + table.sort(payloads, sortpayloads) + local payload=payloads[1] --#AIRWING.Payload + if not payload.unlimited then + payload.navail=payload.navail-1 + end + return payload + end + +end + +--- Return payload from asset back to stock. +-- @param #AIRWING self +-- @param #AIRWING.SquadronAsset asset The squadron asset. +function AIRWING:ReturnPayloadFromAsset(asset) + + local payload=asset.payload + + if payload then + + -- Increase count if not unlimited. + if not payload.unlimited then + payload.navail=payload.navail+1 + end + + -- Remove asset payload. + asset.payload=nil + + else + self:E(self.lid.."ERROR: asset had no payload attached!") + end + +end + + +--- Add asset group(s) to squadron. +-- @param #AIRWING self +-- @param Ops.Squadron#SQUADRON Squadron The squadron object. +-- @param #number Nassets Number of asset groups to add. +-- @return #AIRWING self +function AIRWING:AddAssetToSquadron(Squadron, Nassets) + + if Squadron then + + -- Get the template group of the squadron. + local Group=GROUP:FindByName(Squadron.templatename) + + if Group then + + -- Debug text. + local text=string.format("Adding asset %s to squadron %s", Group:GetName(), Squadron.name) + self:T(self.lid..text) + + -- Add assets to airwing warehouse. + self:AddAsset(Group, Nassets, nil, nil, nil, nil, Squadron.skill, Squadron.livery, Squadron.name) + + else + self:E(self.lid.."ERROR: Group does not exist!") + end + + else + self:E(self.lid.."ERROR: Squadron does not exit!") + end + + return self +end + +--- Get squadron by name. +-- @param #AIRWING self +-- @param #string SquadronName Name of the squadron, e.g. "VFA-37". +-- @return Ops.Squadron#SQUADRON The squadron object. +function AIRWING:GetSquadron(SquadronName) + + for _,_squadron in pairs(self.squadrons) do + local squadron=_squadron --Ops.Squadron#SQUADRON + + if squadron.name==SquadronName then + return squadron + end + + end + + return nil +end + +--- Set verbosity level. +-- @param #AIRWING self +-- @param #number VerbosityLevel Level of output (higher=more). Default 0. +-- @return #AIRWING self +function AIRWING:SetVerbosity(VerbosityLevel) + self.verbose=VerbosityLevel or 0 + return self +end + +--- Get squadron of an asset. +-- @param #AIRWING self +-- @param #AIRWING.SquadronAsset Asset The squadron asset. +-- @return Ops.Squadron#SQUADRON The squadron object. +function AIRWING:GetSquadronOfAsset(Asset) + return self:GetSquadron(Asset.squadname) +end + +--- Remove asset from squadron. +-- @param #AIRWING self +-- @param #AIRWING.SquadronAsset Asset The squad asset. +function AIRWING:RemoveAssetFromSquadron(Asset) + local squad=self:GetSquadronOfAsset(Asset) + if squad then + squad:DelAsset(Asset) + end +end + +--- Add mission to queue. +-- @param #AIRWING self +-- @param Ops.Auftrag#AUFTRAG Mission for this group. +-- @return #AIRWING self +function AIRWING:AddMission(Mission) + + -- Set status to QUEUED. This also attaches the airwing to this mission. + Mission:Queued(self) + + -- Add mission to queue. + table.insert(self.missionqueue, Mission) + + -- Info text. + local text=string.format("Added mission %s (type=%s). Starting at %s. Stopping at %s", + tostring(Mission.name), tostring(Mission.type), UTILS.SecondsToClock(Mission.Tstart, true), Mission.Tstop and UTILS.SecondsToClock(Mission.Tstop, true) or "INF") + self:T(self.lid..text) + + return self +end + +--- Remove mission from queue. +-- @param #AIRWING self +-- @param Ops.Auftrag#AUFTRAG Mission Mission to be removed. +-- @return #AIRWING self +function AIRWING:RemoveMission(Mission) + + for i,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + if mission.auftragsnummer==Mission.auftragsnummer then + table.remove(self.missionqueue, i) + break + end + + end + + return self +end + +--- Set number of CAP flights constantly carried out. +-- @param #AIRWING self +-- @param #number n Number of flights. Default 1. +-- @return #AIRWING self +function AIRWING:SetNumberCAP(n) + self.nflightsCAP=n or 1 + return self +end + +--- Set number of TANKER flights with Boom constantly in the air. +-- @param #AIRWING self +-- @param #number Nboom Number of flights. Default 1. +-- @return #AIRWING self +function AIRWING:SetNumberTankerBoom(Nboom) + self.nflightsTANKERboom=Nboom or 1 + return self +end + +--- Set markers on the map for Patrol Points. +-- @param #AIRWING self +-- @param #boolean onoff Set to true to switch markers on. +-- @return #AIRWING self +function AIRWING:ShowPatrolPointMarkers(onoff) + if onoff then + self.markpoints = true + else + self.markpoints = false + end + return self +end + +--- Set number of TANKER flights with Probe constantly in the air. +-- @param #AIRWING self +-- @param #number Nprobe Number of flights. Default 1. +-- @return #AIRWING self +function AIRWING:SetNumberTankerProbe(Nprobe) + self.nflightsTANKERprobe=Nprobe or 1 + return self +end + +--- Set number of AWACS flights constantly in the air. +-- @param #AIRWING self +-- @param #number n Number of flights. Default 1. +-- @return #AIRWING self +function AIRWING:SetNumberAWACS(n) + self.nflightsAWACS=n or 1 + return self +end + +--- Set number of Rescue helo flights constantly in the air. +-- @param #AIRWING self +-- @param #number n Number of flights. Default 1. +-- @return #AIRWING self +function AIRWING:SetNumberRescuehelo(n) + self.nflightsRescueHelo=n or 1 + return self +end + +--- +-- @param #AIRWING self +-- @param #AIRWING.PatrolData point Patrol point table. +-- @return #string Marker text. +function AIRWING:_PatrolPointMarkerText(point) + + local text=string.format("%s Occupied=%d, \nheading=%03d, leg=%d NM, alt=%d ft, speed=%d kts", + point.type, point.noccupied, point.heading, point.leg, point.altitude, point.speed) + + return text +end + +--- Update marker of the patrol point. +-- @param #AIRWING.PatrolData point Patrol point table. +function AIRWING:UpdatePatrolPointMarker(point) + if self.markpoints then -- sometimes there's a direct call from #OPSGROUP + local text=string.format("%s Occupied=%d\nheading=%03d, leg=%d NM, alt=%d ft, speed=%d kts", + point.type, point.noccupied, point.heading, point.leg, point.altitude, point.speed) + + point.marker:UpdateText(text, 1) + end +end + + +--- Create a new generic patrol point. +-- @param #AIRWING self +-- @param #string Type Patrol point type, e.g. "CAP" or "AWACS". Default "Unknown". +-- @param Core.Point#COORDINATE Coordinate Coordinate of the patrol point. Default 10-15 NM away from the location of the airwing. +-- @param #number Altitude Orbit altitude in feet. Default random between Angels 10 and 20. +-- @param #number Heading Heading in degrees. Default random (0, 360] degrees. +-- @param #number LegLength Length of race-track orbit in NM. Default 15 NM. +-- @param #number Speed Orbit speed in knots. Default 350 knots. +-- @return #AIRWING.PatrolData Patrol point table. +function AIRWING:NewPatrolPoint(Type, Coordinate, Altitude, Speed, Heading, LegLength) + + local patrolpoint={} --#AIRWING.PatrolData + patrolpoint.type=Type or "Unknown" + patrolpoint.coord=Coordinate or self:GetCoordinate():Translate(UTILS.NMToMeters(math.random(10, 15)), math.random(360)) + patrolpoint.heading=Heading or math.random(360) + patrolpoint.leg=LegLength or 15 + patrolpoint.altitude=Altitude or math.random(10,20)*1000 + patrolpoint.speed=Speed or 350 + patrolpoint.noccupied=0 + + if self.markpoints then + patrolpoint.marker=MARKER:New(Coordinate, "New Patrol Point"):ToAll() + AIRWING.UpdatePatrolPointMarker(patrolpoint) + end + + return patrolpoint +end + +--- Add a patrol Point for CAP missions. +-- @param #AIRWING self +-- @param Core.Point#COORDINATE Coordinate Coordinate of the patrol point. +-- @param #number Altitude Orbit altitude in feet. +-- @param #number Speed Orbit speed in knots. +-- @param #number Heading Heading in degrees. +-- @param #number LegLength Length of race-track orbit in NM. +-- @return #AIRWING self +function AIRWING:AddPatrolPointCAP(Coordinate, Altitude, Speed, Heading, LegLength) + + local patrolpoint=self:NewPatrolPoint("CAP", Coordinate, Altitude, Speed, Heading, LegLength) + + table.insert(self.pointsCAP, patrolpoint) + + return self +end + +--- Add a patrol Point for TANKER missions. +-- @param #AIRWING self +-- @param Core.Point#COORDINATE Coordinate Coordinate of the patrol point. +-- @param #number Altitude Orbit altitude in feet. +-- @param #number Speed Orbit speed in knots. +-- @param #number Heading Heading in degrees. +-- @param #number LegLength Length of race-track orbit in NM. +-- @return #AIRWING self +function AIRWING:AddPatrolPointTANKER(Coordinate, Altitude, Speed, Heading, LegLength) + + local patrolpoint=self:NewPatrolPoint("Tanker", Coordinate, Altitude, Speed, Heading, LegLength) + + table.insert(self.pointsTANKER, patrolpoint) + + return self +end + +--- Add a patrol Point for AWACS missions. +-- @param #AIRWING self +-- @param Core.Point#COORDINATE Coordinate Coordinate of the patrol point. +-- @param #number Altitude Orbit altitude in feet. +-- @param #number Speed Orbit speed in knots. +-- @param #number Heading Heading in degrees. +-- @param #number LegLength Length of race-track orbit in NM. +-- @return #AIRWING self +function AIRWING:AddPatrolPointAWACS(Coordinate, Altitude, Speed, Heading, LegLength) + + local patrolpoint=self:NewPatrolPoint("AWACS", Coordinate, Altitude, Speed, Heading, LegLength) + + table.insert(self.pointsAWACS, patrolpoint) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Start & Status +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Start AIRWING FSM. +-- @param #AIRWING self +function AIRWING:onafterStart(From, Event, To) + + -- Start parent Warehouse. + self:GetParent(self).onafterStart(self, From, Event, To) + + -- Info. + self:I(self.lid..string.format("Starting AIRWING v%s", AIRWING.version)) + +end + +--- Update status. +-- @param #AIRWING self +function AIRWING:onafterStatus(From, Event, To) + + -- Status of parent Warehouse. + self:GetParent(self).onafterStatus(self, From, Event, To) + + local fsmstate=self:GetState() + + -- Check CAP missions. + self:CheckCAP() + + -- Check TANKER missions. + self:CheckTANKER() + + -- Check AWACS missions. + self:CheckAWACS() + + -- Check Rescue Helo missions. + self:CheckRescuhelo() + + + -- General info: + if self.verbose>=1 then + + -- Count missions not over yet. + local Nmissions=self:CountMissionsInQueue() + + -- Count ALL payloads in stock. If any payload is unlimited, this gives 999. + local Npayloads=self:CountPayloadsInStock(AUFTRAG.Type) + + -- Assets tot + local Npq, Np, Nq=self:CountAssetsOnMission() + + local assets=string.format("%d (OnMission: Total=%d, Active=%d, Queued=%d)", self:CountAssets(), Npq, Np, Nq) + + -- Output. + local text=string.format("%s: Missions=%d, Payloads=%d (%d), Squads=%d, Assets=%s", fsmstate, Nmissions, Npayloads, #self.payloads, #self.squadrons, assets) + self:I(self.lid..text) + end + + ------------------ + -- Mission Info -- + ------------------ + if self.verbose>=2 then + local text=string.format("Missions Total=%d:", #self.missionqueue) + for i,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + local prio=string.format("%d/%s", mission.prio, tostring(mission.importance)) ; if mission.urgent then prio=prio.." (!)" end + local assets=string.format("%d/%d", mission:CountOpsGroups(), mission.nassets) + local target=string.format("%d/%d Damage=%.1f", mission:CountMissionTargets(), mission:GetTargetInitialNumber(), mission:GetTargetDamage()) + + text=text..string.format("\n[%d] %s %s: Status=%s, Prio=%s, Assets=%s, Targets=%s", i, mission.name, mission.type, mission.status, prio, assets, target) + end + self:I(self.lid..text) + end + + ------------------- + -- Squadron Info -- + ------------------- + if self.verbose>=3 then + local text="Squadrons:" + for i,_squadron in pairs(self.squadrons) do + local squadron=_squadron --Ops.Squadron#SQUADRON + + local callsign=squadron.callsignName and UTILS.GetCallsignName(squadron.callsignName) or "N/A" + local modex=squadron.modex and squadron.modex or -1 + local skill=squadron.skill and tostring(squadron.skill) or "N/A" + + -- Squadron text + text=text..string.format("\n* %s %s: %s*%d/%d, Callsign=%s, Modex=%d, Skill=%s", squadron.name, squadron:GetState(), squadron.aircrafttype, squadron:CountAssetsInStock(), #squadron.assets, callsign, modex, skill) + end + self:I(self.lid..text) + end + + -------------- + -- Mission --- + -------------- + + -- Check if any missions should be cancelled. + self:_CheckMissions() + + -- Get next mission. + local mission=self:_GetNextMission() + + -- Request mission execution. + if mission then + self:MissionRequest(mission) + end + +end + +--- Get patrol data +-- @param #AIRWING self +-- @param #table PatrolPoints Patrol data points. +-- @return #AIRWING.PatrolData +function AIRWING:_GetPatrolData(PatrolPoints) + + -- Sort wrt lowest number of flights on this point. + local function sort(a,b) + return a.noccupied0 then + + -- Sort data wrt number of flights at that point. + table.sort(PatrolPoints, sort) + return PatrolPoints[1] + + else + + return self:NewPatrolPoint() + + end + +end + +--- Check how many CAP missions are assigned and add number of missing missions. +-- @param #AIRWING self +-- @return #AIRWING self +function AIRWING:CheckCAP() + + local Ncap=self:CountMissionsInQueue({AUFTRAG.Type.GCICAP, AUFTRAG.Type.INTERCEPT}) + + for i=1,self.nflightsCAP-Ncap do + + local patrol=self:_GetPatrolData(self.pointsCAP) + + local altitude=patrol.altitude+1000*patrol.noccupied + + local missionCAP=AUFTRAG:NewGCICAP(patrol.coord, altitude, patrol.speed, patrol.heading, patrol.leg) + + missionCAP.patroldata=patrol + + patrol.noccupied=patrol.noccupied+1 + + if self.markpoints then AIRWING.UpdatePatrolPointMarker(patrol) end + + self:AddMission(missionCAP) + + end + + return self +end + +--- Check how many TANKER missions are assigned and add number of missing missions. +-- @param #AIRWING self +-- @return #AIRWING self +function AIRWING:CheckTANKER() + + local Nboom=0 + local Nprob=0 + + -- Count tanker mission. + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + if mission:IsNotOver() and mission.type==AUFTRAG.Type.TANKER then + if mission.refuelSystem==0 then + Nboom=Nboom+1 + elseif mission.refuelSystem==1 then + Nprob=Nprob+1 + end + + end + + end + + for i=1,self.nflightsTANKERboom-Nboom do + + local patrol=self:_GetPatrolData(self.pointsTANKER) + + local altitude=patrol.altitude+1000*patrol.noccupied + + local mission=AUFTRAG:NewTANKER(patrol.coord, altitude, patrol.speed, patrol.heading, patrol.leg, 1) + + mission.patroldata=patrol + + patrol.noccupied=patrol.noccupied+1 + + if self.markpoints then AIRWING.UpdatePatrolPointMarker(patrol) end + + self:AddMission(mission) + + end + + for i=1,self.nflightsTANKERprobe-Nprob do + + local patrol=self:_GetPatrolData(self.pointsTANKER) + + local altitude=patrol.altitude+1000*patrol.noccupied + + local mission=AUFTRAG:NewTANKER(patrol.coord, altitude, patrol.speed, patrol.heading, patrol.leg, 0) + + mission.patroldata=patrol + + patrol.noccupied=patrol.noccupied+1 + + if self.markpoints then AIRWING.UpdatePatrolPointMarker(patrol) end + + self:AddMission(mission) + + end + + return self +end + +--- Check how many AWACS missions are assigned and add number of missing missions. +-- @param #AIRWING self +-- @return #AIRWING self +function AIRWING:CheckAWACS() + + local N=self:CountMissionsInQueue({AUFTRAG.Type.AWACS}) + + for i=1,self.nflightsAWACS-N do + + local patrol=self:_GetPatrolData(self.pointsAWACS) + + local altitude=patrol.altitude+1000*patrol.noccupied + + local mission=AUFTRAG:NewAWACS(patrol.coord, altitude, patrol.speed, patrol.heading, patrol.leg) + + mission.patroldata=patrol + + patrol.noccupied=patrol.noccupied+1 + + if self.markpoints then AIRWING.UpdatePatrolPointMarker(patrol) end + + self:AddMission(mission) + + end + + return self +end + +--- Check how many Rescue helos are currently in the air. +-- @param #AIRWING self +-- @return #AIRWING self +function AIRWING:CheckRescuhelo() + + local N=self:CountMissionsInQueue({AUFTRAG.Type.RESCUEHELO}) + + local name=self.airbase:GetName() + + local carrier=UNIT:FindByName(name) + + for i=1,self.nflightsRescueHelo-N do + + local mission=AUFTRAG:NewRESCUEHELO(carrier) + + self:AddMission(mission) + + end + + return self +end + +--- Check how many AWACS missions are assigned and add number of missing missions. +-- @param #AIRWING self +-- @param Ops.FlightGroup#FLIGHTGROUP flightgroup The flightgroup. +-- @return #AIRWING.SquadronAsset The tanker asset. +function AIRWING:GetTankerForFlight(flightgroup) + + local tankers=self:GetAssetsOnMission(AUFTRAG.Type.TANKER) + + if #tankers>0 then + + local tankeropt={} + for _,_tanker in pairs(tankers) do + local tanker=_tanker --#AIRWING.SquadronAsset + + -- Check that donor and acceptor use the same refuelling system. + if flightgroup.refueltype and flightgroup.refueltype==tanker.flightgroup.tankertype then + + local tankercoord=tanker.flightgroup.group:GetCoordinate() + local assetcoord=flightgroup.group:GetCoordinate() + + local dist=assetcoord:Get2DDistance(tankercoord) + + -- Ensure that the flight does not find itself. Asset could be a tanker! + if dist>5 then + table.insert(tankeropt, {tanker=tanker, dist=dist}) + end + + end + end + + -- Sort tankers wrt to distance. + table.sort(tankeropt, function(a,b) return a.dist0 then + return tankeropt[1].tanker + else + return nil + end + end + + return nil +end + + +--- Check if mission is not over and ready to cancel. +-- @param #AIRWING self +function AIRWING:_CheckMissions() + + -- Loop over missions in queue. + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + if mission:IsNotOver() and mission:IsReadyToCancel() then + mission:Cancel() + end + end + +end +--- Get next mission. +-- @param #AIRWING self +-- @return Ops.Auftrag#AUFTRAG Next mission or *nil*. +function AIRWING:_GetNextMission() + + -- Number of missions. + local Nmissions=#self.missionqueue + + -- Treat special cases. + if Nmissions==0 then + return nil + end + + -- Sort results table wrt prio and start time. + local function _sort(a, b) + local taskA=a --Ops.Auftrag#AUFTRAG + local taskB=b --Ops.Auftrag#AUFTRAG + return (taskA.prio0 then + self:E(self.lid..string.format("ERROR: mission %s of type %s has already assets attached!", mission.name, mission.type)) + end + mission.assets={} + + -- Assign assets to mission. + for i=1,mission.nassets do + local asset=assets[i] --#AIRWING.SquadronAsset + + -- Should not happen as we just checked! + if not asset.payload then + self:E(self.lid.."ERROR: No payload for asset! This should not happen!") + end + + -- Add asset to mission. + mission:AddAsset(asset) + end + + -- Now return the remaining payloads. + for i=mission.nassets+1,#assets do + local asset=assets[i] --#AIRWING.SquadronAsset + for _,uid in pairs(gotpayload) do + if uid==asset.uid then + self:ReturnPayloadFromAsset(asset) + break + end + end + end + + return mission + end + + end -- mission due? + end -- mission loop + + return nil +end + +--- Calculate the mission score of an asset. +-- @param #AIRWING self +-- @param #AIRWING.SquadronAsset asset Asset +-- @param Ops.Auftrag#AUFTRAG Mission Mission for which the best assets are desired. +-- @param #boolean includePayload If true, include the payload in the calulation if the asset has one attached. +-- @return #number Mission score. +function AIRWING:CalculateAssetMissionScore(asset, Mission, includePayload) + + local score=0 + + -- Prefer highly skilled assets. + if asset.skill==AI.Skill.AVERAGE then + score=score+0 + elseif asset.skill==AI.Skill.GOOD then + score=score+10 + elseif asset.skill==AI.Skill.HIGH then + score=score+20 + elseif asset.skill==AI.Skill.EXCELLENT then + score=score+30 + end + + -- Add mission performance to score. + local squad=self:GetSquadronOfAsset(asset) + local missionperformance=squad:GetMissionPeformance(Mission.type) + score=score+missionperformance + + -- Add payload performance to score. + if includePayload and asset.payload then + score=score+self:GetPayloadPeformance(asset.payload, Mission.type) + end + + -- Intercepts need to be carried out quickly. We prefer spawned assets. + if Mission.type==AUFTRAG.Type.INTERCEPT then + if asset.spawned then + self:T(self.lid.."Adding 25 to asset because it is spawned") + score=score+25 + end + end + + -- TODO: This could be vastly improved. Need to gather ideas during testing. + -- Calculate ETA? Assets on orbit missions should arrive faster even if they are further away. + -- Max speed of assets. + -- Fuel amount? + -- Range of assets? + + return score +end + +--- Optimize chosen assets for the mission at hand. +-- @param #AIRWING self +-- @param #table assets Table of (unoptimized) assets. +-- @param Ops.Auftrag#AUFTRAG Mission Mission for which the best assets are desired. +-- @param #boolean includePayload If true, include the payload in the calulation if the asset has one attached. +function AIRWING:_OptimizeAssetSelection(assets, Mission, includePayload) + + local TargetVec2=Mission:GetTargetVec2() + + --local dStock=self:GetCoordinate():Get2DDistance(TargetCoordinate) + + local dStock=UTILS.VecDist2D(TargetVec2, self:GetVec2()) + + -- Calculate distance to mission target. + local distmin=math.huge + local distmax=0 + for _,_asset in pairs(assets) do + local asset=_asset --#AIRWING.SquadronAsset + + if asset.spawned then + local group=GROUP:FindByName(asset.spawngroupname) + --asset.dist=group:GetCoordinate():Get2DDistance(TargetCoordinate) + asset.dist=UTILS.VecDist2D(group:GetVec2(), TargetVec2) + else + asset.dist=dStock + end + + if asset.distdistmax then + distmax=asset.dist + end + + end + + -- Calculate the mission score of all assets. + for _,_asset in pairs(assets) do + local asset=_asset --#AIRWING.SquadronAsset + --self:I(string.format("FF asset %s has payload %s", asset.spawngroupname, asset.payload and "yes" or "no!")) + asset.score=self:CalculateAssetMissionScore(asset, Mission, includePayload) + end + + --- Sort assets wrt to their mission score. Higher is better. + local function optimize(a, b) + local assetA=a --#AIRWING.SquadronAsset + local assetB=b --#AIRWING.SquadronAsset + + -- Higher score wins. If equal score ==> closer wins. + -- TODO: Need to include the distance in a smarter way! + return (assetA.score>assetB.score) or (assetA.score==assetB.score and assetA.dist0 then + + --local text=string.format("Requesting assets for mission %s:", Mission.name) + for i,_asset in pairs(Assetlist) do + local asset=_asset --#AIRWING.SquadronAsset + + -- Set asset to requested! Important so that new requests do not use this asset! + asset.requested=true + + if Mission.missionTask then + asset.missionTask=Mission.missionTask + end + + end + + -- Add request to airwing warehouse. + -- TODO: better Assignment string. + self:AddRequest(self, WAREHOUSE.Descriptor.ASSETLIST, Assetlist, #Assetlist, nil, nil, Mission.prio, tostring(Mission.auftragsnummer)) + + -- The queueid has been increased in the onafterAddRequest function. So we can simply use it here. + Mission.requestID=self.queueid + end + +end + +--- On after "MissionCancel" event. Cancels the missions of all flightgroups. Deletes request from warehouse queue. +-- @param #AIRWING self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.Auftrag#AUFTRAG Mission The mission to be cancelled. +function AIRWING:onafterMissionCancel(From, Event, To, Mission) + + -- Info message. + self:I(self.lid..string.format("Cancel mission %s", Mission.name)) + + local Ngroups = Mission:CountOpsGroups() + + if Mission:IsPlanned() or Mission:IsQueued() or Mission:IsRequested() or Ngroups == 0 then + + Mission:Done() + + else + + for _,_asset in pairs(Mission.assets) do + local asset=_asset --#AIRWING.SquadronAsset + + local flightgroup=asset.flightgroup + + if flightgroup then + flightgroup:MissionCancel(Mission) + end + + -- Not requested any more (if it was). + asset.requested=nil + end + + end + + -- Remove queued request (if any). + if Mission.requestID then + self:_DeleteQueueItemByID(Mission.requestID, self.queue) + end + +end + +--- On after "NewAsset" event. Asset is added to the given squadron (asset assignment). +-- @param #AIRWING self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #AIRWING.SquadronAsset asset The asset that has just been added. +-- @param #string assignment The (optional) assignment for the asset. +function AIRWING:onafterNewAsset(From, Event, To, asset, assignment) + + -- Call parent warehouse function first. + self:GetParent(self).onafterNewAsset(self, From, Event, To, asset, assignment) + + -- Debug text. + local text=string.format("New asset %s with assignment %s and request assignment %s", asset.spawngroupname, tostring(asset.assignment), tostring(assignment)) + self:T3(self.lid..text) + + -- Get squadron. + local squad=self:GetSquadron(asset.assignment) + + -- Check if asset is already part of the squadron. If an asset returns, it will be added again! We check that asset.assignment is also assignment. + if squad then + + if asset.assignment==assignment then + + local nunits=#asset.template.units + + -- Debug text. + local text=string.format("Adding asset to squadron %s: assignment=%s, type=%s, attribute=%s, nunits=%d %s", squad.name, assignment, asset.unittype, asset.attribute, nunits, tostring(squad.ngrouping)) + self:T(self.lid..text) + + -- Adjust number of elements in the group. + if squad.ngrouping then + local template=asset.template + + local N=math.max(#template.units, squad.ngrouping) + + -- Handle units. + for i=1,N do + + -- Unit template. + local unit = template.units[i] + + -- If grouping is larger than units present, copy first unit. + if i>nunits then + table.insert(template.units, UTILS.DeepCopy(template.units[1])) + end + + -- Remove units if original template contains more than in grouping. + if squad.ngroupingnunits then + unit=nil + end + end + + asset.nunits=squad.ngrouping + end + + -- Set takeoff type. + asset.takeoffType=squad.takeoffType + + -- Create callsign and modex (needs to be after grouping). + squad:GetCallsign(asset) + squad:GetModex(asset) + + -- Set spawn group name. This has to include "AID-" for warehouse. + asset.spawngroupname=string.format("%s_AID-%d", squad.name, asset.uid) + + -- Add asset to squadron. + squad:AddAsset(asset) + + -- TODO + --asset.terminalType=AIRBASE.TerminalType.OpenBig + else + + --env.info("FF squad asset returned") + self:SquadAssetReturned(squad, asset) + + end + + end +end + +--- On after "AssetReturned" event. Triggered when an asset group returned to its airwing. +-- @param #AIRWING self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.Squadron#SQUADRON Squadron The asset squadron. +-- @param #AIRWING.SquadronAsset Asset The asset that returned. +function AIRWING:onafterSquadAssetReturned(From, Event, To, Squadron, Asset) + -- Debug message. + self:T(self.lid..string.format("Asset %s from squadron %s returned! asset.assignment=\"%s\"", Asset.spawngroupname, Squadron.name, tostring(Asset.assignment))) + + -- Stop flightgroup. + if Asset.flightgroup and not Asset.flightgroup:IsStopped() then + Asset.flightgroup:Stop() + end + + -- Return payload. + self:ReturnPayloadFromAsset(Asset) + + -- Return tacan channel. + if Asset.tacan then + Squadron:ReturnTacan(Asset.tacan) + end + + -- Set timestamp. + Asset.Treturned=timer.getAbsTime() +end + + +--- On after "AssetSpawned" event triggered when an asset group is spawned into the cruel world. +-- Creates a new flightgroup element and adds the mission to the flightgroup queue. +-- @param #AIRWING self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Group#GROUP group The group spawned. +-- @param #AIRWING.SquadronAsset asset The asset that was spawned. +-- @param Functional.Warehouse#WAREHOUSE.Pendingitem request The request of the dead asset. +function AIRWING:onafterAssetSpawned(From, Event, To, group, asset, request) + + -- Call parent warehouse function first. + self:GetParent(self).onafterAssetSpawned(self, From, Event, To, group, asset, request) + + -- Get the SQUADRON of the asset. + local squadron=self:GetSquadronOfAsset(asset) + + -- Check if we have a squadron or if this was some other request. + if squadron then + + -- Create a flight group. + local flightgroup=self:_CreateFlightGroup(asset) + + --- + -- Asset + --- + + -- Set asset flightgroup. + asset.flightgroup=flightgroup + + -- Not requested any more. + asset.requested=nil + + -- Did not return yet. + asset.Treturned=nil + + --- + -- Squadron + --- + + -- Get TACAN channel. + local Tacan=squadron:FetchTacan() + if Tacan then + asset.tacan=Tacan + end + + -- Set radio frequency and modulation + local radioFreq, radioModu=squadron:GetRadio() + if radioFreq then + flightgroup:SwitchRadio(radioFreq, radioModu) + end + + if squadron.fuellow then + flightgroup:SetFuelLowThreshold(squadron.fuellow) + end + + if squadron.fuellowRefuel then + flightgroup:SetFuelLowRefuel(squadron.fuellowRefuel) + end + + --- + -- Mission + --- + + -- Get Mission (if any). + local mission=self:GetMissionByID(request.assignment) + + -- Add mission to flightgroup queue. + if mission then + + if Tacan then + mission:SetTACAN(Tacan, Morse, UnitName, Band) + end + + -- Add mission to flightgroup queue. + asset.flightgroup:AddMission(mission) + + -- Trigger event. + self:FlightOnMission(flightgroup, mission) + + else + + if Tacan then + flightgroup:SwitchTACAN(Tacan, Morse, UnitName, Band) + end + + end + + -- Add group to the detection set of the WINGCOMMANDER. + if self.wingcommander and self.wingcommander.chief then + self.wingcommander.chief.detectionset:AddGroup(asset.flightgroup.group) + end + + end + +end + +--- On after "AssetDead" event triggered when an asset group died. +-- @param #AIRWING self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #AIRWING.SquadronAsset asset The asset that is dead. +-- @param Functional.Warehouse#WAREHOUSE.Pendingitem request The request of the dead asset. +function AIRWING:onafterAssetDead(From, Event, To, asset, request) + + -- Call parent warehouse function first. + self:GetParent(self).onafterAssetDead(self, From, Event, To, asset, request) + + -- Add group to the detection set of the WINGCOMMANDER. + if self.wingcommander and self.wingcommander.chief then + self.wingcommander.chief.detectionset:RemoveGroupsByName({asset.spawngroupname}) + end + + -- Remove asset from mission is done via Mission:AssetDead() call from flightgroup onafterFlightDead function + -- Remove asset from squadron same +end + +--- On after "Destroyed" event. Remove assets from squadrons. Stop squadrons. Remove airwing from wingcommander. +-- @param #AIRWING self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AIRWING:onafterDestroyed(From, Event, To) + + self:I(self.lid.."Airwing warehouse destroyed!") + + -- Cancel all missions. + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + mission:Cancel() + end + + -- Remove all squadron assets. + for _,_squadron in pairs(self.squadrons) do + local squadron=_squadron --Ops.Squadron#SQUADRON + -- Stop Squadron. This also removes all assets. + squadron:Stop() + end + + -- Call parent warehouse function first. + self:GetParent(self).onafterDestroyed(self, From, Event, To) + +end + + +--- On after "Request" event. +-- @param #AIRWING self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Functional.Warehouse#WAREHOUSE.Queueitem Request Information table of the request. +function AIRWING:onafterRequest(From, Event, To, Request) + + -- Assets + local assets=Request.cargoassets + + -- Get Mission + local Mission=self:GetMissionByID(Request.assignment) + + if Mission and assets then + + for _,_asset in pairs(assets) do + local asset=_asset --#AIRWING.SquadronAsset + -- This would be the place to modify the asset table before the asset is spawned. + end + + end + + -- Call parent warehouse function after assets have been adjusted. + self:GetParent(self).onafterRequest(self, From, Event, To, Request) + +end + +--- On after "SelfRequest" event. +-- @param #AIRWING self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Set#SET_GROUP groupset The set of asset groups that was delivered to the warehouse itself. +-- @param Functional.Warehouse#WAREHOUSE.Pendingitem request Pending self request. +function AIRWING:onafterSelfRequest(From, Event, To, groupset, request) + + -- Call parent warehouse function first. + self:GetParent(self).onafterSelfRequest(self, From, Event, To, groupset, request) + + -- Get Mission + local mission=self:GetMissionByID(request.assignment) + + for _,_asset in pairs(request.assets) do + local asset=_asset --#AIRWING.SquadronAsset + end + + for _,_group in pairs(groupset:GetSet()) do + local group=_group --Wrapper.Group#GROUP + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new flight group after an asset was spawned. +-- @param #AIRWING self +-- @param #AIRWING.SquadronAsset asset The asset. +-- @return Ops.FlightGroup#FLIGHTGROUP The created flightgroup object. +function AIRWING:_CreateFlightGroup(asset) + + -- Create flightgroup. + local flightgroup=FLIGHTGROUP:New(asset.spawngroupname) + + -- Set airwing. + flightgroup:SetAirwing(self) + + -- Set squadron. + flightgroup.squadron=self:GetSquadronOfAsset(asset) + + -- Set home base. + flightgroup.homebase=self.airbase + + return flightgroup +end + + +--- Check if an asset is currently on a mission (STARTED or EXECUTING). +-- @param #AIRWING self +-- @param #AIRWING.SquadronAsset asset The asset. +-- @param #table MissionTypes Types on mission to be checked. Default all. +-- @return #boolean If true, asset has at least one mission of that type in the queue. +function AIRWING:IsAssetOnMission(asset, MissionTypes) + + if MissionTypes then + if type(MissionTypes)~="table" then + MissionTypes={MissionTypes} + end + else + -- Check all possible types. + MissionTypes=AUFTRAG.Type + end + + if asset.flightgroup and asset.flightgroup:IsAlive() then + + -- Loop over mission queue. + for _,_mission in pairs(asset.flightgroup.missionqueue or {}) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + if mission:IsNotOver() then + + -- Get flight status. + local status=mission:GetGroupStatus(asset.flightgroup) + + -- Only if mission is started or executing. + if (status==AUFTRAG.GroupStatus.STARTED or status==AUFTRAG.GroupStatus.EXECUTING) and self:CheckMissionType(mission.type, MissionTypes) then + return true + end + + end + + end + + end + + -- Alternative: run over all missions and compare to mission assets. + --[[ + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + if mission:IsNotOver() then + for _,_asset in pairs(mission.assets) do + local sqasset=_asset --#AIRWING.SquadronAsset + + if sqasset.uid==asset.uid then + return true + end + + end + end + + end + ]] + + return false +end + +--- Get the current mission of the asset. +-- @param #AIRWING self +-- @param #AIRWING.SquadronAsset asset The asset. +-- @return Ops.Auftrag#AUFTRAG Current mission or *nil*. +function AIRWING:GetAssetCurrentMission(asset) + + if asset.flightgroup then + return asset.flightgroup:GetMissionCurrent() + end + + return nil +end + +--- Count payloads in stock. +-- @param #AIRWING self +-- @param #table MissionTypes Types on mission to be checked. Default *all* possible types `AUFTRAG.Type`. +-- @param #table UnitTypes Types of units. +-- @param #table Payloads Specific payloads to be counted only. +-- @return #number Count of available payloads in stock. +function AIRWING:CountPayloadsInStock(MissionTypes, UnitTypes, Payloads) + + if MissionTypes then + if type(MissionTypes)=="string" then + MissionTypes={MissionTypes} + end + end + + if UnitTypes then + if type(UnitTypes)=="string" then + UnitTypes={UnitTypes} + end + end + + local function _checkUnitTypes(payload) + if UnitTypes then + for _,unittype in pairs(UnitTypes) do + if unittype==payload.aircrafttype then + return true + end + end + else + -- Unit type was not specified. + return true + end + return false + end + + local function _checkPayloads(payload) + if Payloads then + for _,Payload in pairs(Payloads) do + if Payload.uid==payload.uid then + return true + end + end + else + -- Payload was not specified. + return nil + end + return false + end + + local n=0 + for _,_payload in pairs(self.payloads) do + local payload=_payload --#AIRWING.Payload + + for _,MissionType in pairs(MissionTypes) do + + local specialpayload=_checkPayloads(payload) + local compatible=self:CheckMissionCapability(MissionType, payload.capabilities) + + local goforit = specialpayload or (specialpayload==nil and compatible) + + if goforit and _checkUnitTypes(payload) then + + if payload.unlimited then + -- Payload is unlimited. Return a BIG number. + return 999 + else + n=n+payload.navail + end + + end + + end + end + + return n +end + +--- Count missions in mission queue. +-- @param #AIRWING self +-- @param #table MissionTypes Types on mission to be checked. Default *all* possible types `AUFTRAG.Type`. +-- @return #number Number of missions that are not over yet. +function AIRWING:CountMissionsInQueue(MissionTypes) + + MissionTypes=MissionTypes or AUFTRAG.Type + + local N=0 + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + -- Check if this mission type is requested. + if mission:IsNotOver() and self:CheckMissionType(mission.type, MissionTypes) then + N=N+1 + end + + end + + return N +end + +--- Count total number of assets. This is the sum of all squadron assets. +-- @param #AIRWING self +-- @return #number Amount of asset groups. +function AIRWING:CountAssets() + + local N=0 + + for _,_squad in pairs(self.squadrons) do + local squad=_squad --Ops.Squadron#SQUADRON + N=N+#squad.assets + end + + return N +end + +--- Count assets on mission. +-- @param #AIRWING self +-- @param #table MissionTypes Types on mission to be checked. Default all. +-- @param Ops.Squadron#SQUADRON Squadron Only count assets of this squadron. Default count assets of all squadrons. +-- @return #number Number of pending and queued assets. +-- @return #number Number of pending assets. +-- @return #number Number of queued assets. +function AIRWING:CountAssetsOnMission(MissionTypes, Squadron) + + local Nq=0 + local Np=0 + + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + -- Check if this mission type is requested. + if self:CheckMissionType(mission.type, MissionTypes or AUFTRAG.Type) then + + for _,_asset in pairs(mission.assets or {}) do + local asset=_asset --#AIRWING.SquadronAsset + + if Squadron==nil or Squadron.name==asset.squadname then + + local request, isqueued=self:GetRequestByID(mission.requestID) + + if isqueued then + Nq=Nq+1 + else + Np=Np+1 + end + + end + + end + end + end + + --env.info(string.format("FF N=%d Np=%d, Nq=%d", Np+Nq, Np, Nq)) + return Np+Nq, Np, Nq +end + +--- Count assets on mission. +-- @param #AIRWING self +-- @param #table MissionTypes Types on mission to be checked. Default all. +-- @return #table Assets on pending requests. +function AIRWING:GetAssetsOnMission(MissionTypes) + + local assets={} + local Np=0 + + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + -- Check if this mission type is requested. + if self:CheckMissionType(mission.type, MissionTypes) then + + for _,_asset in pairs(mission.assets or {}) do + local asset=_asset --#AIRWING.SquadronAsset + + table.insert(assets, asset) + + end + end + end + + return assets +end + +--- Get the aircraft types of this airwing. +-- @param #AIRWING self +-- @param #boolean onlyactive Count only the active ones. +-- @param #table squadrons Table of squadrons. Default all. +-- @return #table Table of unit types. +function AIRWING:GetAircraftTypes(onlyactive, squadrons) + + -- Get all unit types that can do the job. + local unittypes={} + + -- Loop over all squadrons. + for _,_squadron in pairs(squadrons or self.squadrons) do + local squadron=_squadron --Ops.Squadron#SQUADRON + + if (not onlyactive) or squadron:IsOnDuty() then + + local gotit=false + for _,unittype in pairs(unittypes) do + if squadron.aircrafttype==unittype then + gotit=true + break + end + end + if not gotit then + table.insert(unittypes, squadron.aircrafttype) + end + + end + end + + return unittypes +end + +--- Check if assets for a given mission type are available. +-- @param #AIRWING self +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +-- @return #boolean If true, enough assets are available. +-- @return #table Assets that can do the required mission. +function AIRWING:CanMission(Mission) + + -- Assume we CAN and NO assets are available. + local Can=true + local Assets={} + + -- Squadrons for the job. If user assigned to mission or simply all. + local squadrons=Mission.squadrons or self.squadrons + + -- Get aircraft unit types for the job. + local unittypes=self:GetAircraftTypes(true, squadrons) + + -- Count all payloads in stock. + local Npayloads=self:CountPayloadsInStock(Mission.type, unittypes, Mission.payloads) + + if Npayloads #Assets then + self:T(self.lid..string.format("INFO: Not enough assets available! Got %d but need at least %d", #Assets, Mission.nassets)) + Can=false + end + + return Can, Assets +end + +--- Check if assets for a given mission type are available. +-- @param #AIRWING self +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +-- @return #table Assets that can do the required mission. +function AIRWING:RecruitAssets(Mission) + +end + + +--- Check if a mission type is contained in a list of possible types. +-- @param #AIRWING self +-- @param #string MissionType The requested mission type. +-- @param #table PossibleTypes A table with possible mission types. +-- @return #boolean If true, the requested mission type is part of the possible mission types. +function AIRWING:CheckMissionType(MissionType, PossibleTypes) + + if type(PossibleTypes)=="string" then + PossibleTypes={PossibleTypes} + end + + for _,canmission in pairs(PossibleTypes) do + if canmission==MissionType then + return true + end + end + + return false +end + +--- Check if a mission type is contained in a list of possible capabilities. +-- @param #AIRWING self +-- @param #string MissionType The requested mission type. +-- @param #table Capabilities A table with possible capabilities. +-- @return #boolean If true, the requested mission type is part of the possible mission types. +function AIRWING:CheckMissionCapability(MissionType, Capabilities) + + for _,cap in pairs(Capabilities) do + local capability=cap --Ops.Auftrag#AUFTRAG.Capability + if capability.MissionType==MissionType then + return true + end + end + + return false +end + +--- Get payload performance for a given type of misson type. +-- @param #AIRWING self +-- @param #AIRWING.Payload Payload The payload table. +-- @param #string MissionType Type of mission. +-- @return #number Performance or -1. +function AIRWING:GetPayloadPeformance(Payload, MissionType) + + if Payload then + + for _,Capability in pairs(Payload.capabilities) do + local capability=Capability --Ops.Auftrag#AUFTRAG.Capability + if capability.MissionType==MissionType then + return capability.Performance + end + end + + else + self:E(self.lid.."ERROR: Payload is nil!") + end + + return -1 +end + +--- Get mission types a payload can perform. +-- @param #AIRWING self +-- @param #AIRWING.Payload Payload The payload table. +-- @return #table Mission types. +function AIRWING:GetPayloadMissionTypes(Payload) + + local missiontypes={} + + for _,Capability in pairs(Payload.capabilities) do + local capability=Capability --Ops.Auftrag#AUFTRAG.Capability + table.insert(missiontypes, capability.MissionType) + end + + return missiontypes +end + +--- Returns the mission for a given mission ID (Autragsnummer). +-- @param #AIRWING self +-- @param #number mid Mission ID (Auftragsnummer). +-- @return Ops.Auftrag#AUFTRAG Mission table. +function AIRWING:GetMissionByID(mid) + + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + if mission.auftragsnummer==tonumber(mid) then + return mission + end + + end + + return nil +end + +--- Returns the mission for a given request ID. +-- @param #AIRWING self +-- @param #number RequestID Unique ID of the request. +-- @return Ops.Auftrag#AUFTRAG Mission table or *nil*. +function AIRWING:GetMissionFromRequestID(RequestID) + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + if mission.requestID and mission.requestID==RequestID then + return mission + end + end + return nil +end + +--- Returns the mission for a given request. +-- @param #AIRWING self +-- @param Functional.Warehouse#WAREHOUSE.Queueitem Request The warehouse request. +-- @return Ops.Auftrag#AUFTRAG Mission table or *nil*. +function AIRWING:GetMissionFromRequest(Request) + return self:GetMissionFromRequestID(Request.uid) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- **Ops** - Office of Military Intelligence. +-- +-- **Main Features:** +-- +-- * Detect and track contacts consistently +-- * Detect and track clusters of contacts consistently +-- * Use FSM events to link functionality into your scripts +-- * Easy setup +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Ops.Intel +-- @image OPS_Intel.png + + +--- INTEL class. +-- @type INTEL +-- @field #string ClassName Name of the class. +-- @field #number verbose Verbosity level. +-- @field #string lid Class id string for output to DCS log file. +-- @field #number coalition Coalition side number, e.g. `coalition.side.RED`. +-- @field #string alias Name of the agency. +-- @field Core.Set#SET_GROUP detectionset Set of detection groups, aka agents. +-- @field #table filterCategory Filter for unit categories. +-- @field #table filterCategoryGroup Filter for group categories. +-- @field Core.Set#SET_ZONE acceptzoneset Set of accept zones. If defined, only contacts in these zones are considered. +-- @field Core.Set#SET_ZONE rejectzoneset Set of reject zones. Contacts in these zones are not considered, even if they are in accept zones. +-- @field #table Contacts Table of detected items. +-- @field #table ContactsLost Table of lost detected items. +-- @field #table ContactsUnknown Table of new detected items. +-- @field #table Clusters Clusters of detected groups. +-- @field #boolean clusteranalysis If true, create clusters of detected targets. +-- @field #boolean clustermarkers If true, create cluster markers on F10 map. +-- @field #number clustercounter Running number of clusters. +-- @field #number dTforget Time interval in seconds before a known contact which is not detected any more is forgotten. +-- @field #number clusterradius Radius im kilometers in which groups/units are considered to belong to a cluster +-- @field #number prediction Seconds default to be used with CalcClusterFuturePosition. +-- @extends Core.Fsm#FSM + +--- Top Secret! +-- +-- === +-- +-- ![Banner Image](..\Presentations\CarrierAirWing\INTEL_Main.jpg) +-- +-- # The INTEL Concept +-- +-- * Lightweight replacement for @{Functional.Detection#DETECTION} +-- * Detect and track contacts consistently +-- * Detect and track clusters of contacts consistently +-- * Once detected and still alive, planes will be tracked 10 minutes, helicopters 20 minutes, ships and trains 1 hour, ground units 2 hours +-- * Use FSM events to link functionality into your scripts +-- +-- # Basic Usage +-- +-- ## set up a detection SET_GROUP +-- +-- `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(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` +-- +-- ## 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` +-- +-- +-- @field #INTEL +INTEL = { + ClassName = "INTEL", + verbose = 0, + lid = nil, + alias = nil, + filterCategory = {}, + detectionset = nil, + Contacts = {}, + ContactsLost = {}, + ContactsUnknown = {}, + Clusters = {}, + clustercounter = 1, + clusterradius = 15, + clusteranalysis = true, + clustermarkers = false, + prediction = 300, +} + +--- Detected item info. +-- @type INTEL.Contact +-- @field #string groupname Name of the group. +-- @field Wrapper.Group#GROUP group The contact group. +-- @field #string typename Type name of detected item. +-- @field #number category Category number. +-- @field #string categoryname Category name. +-- @field #string attribute Generalized attribute. +-- @field #number threatlevel Threat level of this item. +-- @field #number Tdetected Time stamp in abs. mission time seconds when this item was last detected. +-- @field Core.Point#COORDINATE position Last known position of the item. +-- @field DCS#Vec3 velocity 3D velocity vector. Components x,y and z in m/s. +-- @field #number speed Last known speed in m/s. +-- @field #boolean isship +-- @field #boolean ishelo +-- @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 + +--- Cluster info. +-- @type INTEL.Cluster +-- @field #number index Cluster index. +-- @field #number size Number of groups in the cluster. +-- @field #table Contacts Table of contacts in the cluster. +-- @field #number threatlevelMax Max threat level of cluster. +-- @field #number threatlevelSum Sum of threat levels. +-- @field #number threatlevelAve Average of threat levels. +-- @field Core.Point#COORDINATE coordinate Coordinate of the cluster. +-- @field Wrapper.Marker#MARKER marker F10 marker. +-- @field Ops.Auftrag#AUFTRAG mission The current Auftrag attached to this cluster + + +--- INTEL class version. +-- @field #string version +INTEL.version="0.2.7" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ToDo list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- DONE: Filter detection methods. +-- TODO: process detected set asynchroniously for better performance. +-- DONE: Accept zones. +-- DONE: Reject zones. +-- NOGO: SetAttributeZone --> return groups of generalized attributes in a zone. +-- DONE: Loose units only if they remain undetected for a given time interval. We want to avoid fast oscillation between detected/lost states. Maybe 1-5 min would be a good time interval?! +-- DONE: Combine units to groups for all, new and lost. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new INTEL object and start the FSM. +-- @param #INTEL self +-- @param Core.Set#SET_GROUP DetectionSet Set of detection groups. +-- @param #number Coalition Coalition side. Can also be passed as a string "red", "blue" or "neutral". +-- @param #string Alias An *optional* alias how this object is called in the logs etc. +-- @return #INTEL self +function INTEL:New(DetectionSet, Coalition, Alias) + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #INTEL + + -- Detection set. + self.detectionset=DetectionSet or SET_GROUP:New() + + if Coalition and type(Coalition)=="string" then + if Coalition=="blue" then + Coalition=coalition.side.BLUE + elseif Coalition=="red" then + Coalition=coalition.side.RED + elseif Coalition=="neutral" then + Coalition=coalition.side.NEUTRAL + else + self:E("ERROR: Unknown coalition in INTEL!") + end + end + + -- Determine coalition from first group in set. + self.coalition=Coalition or DetectionSet:CountAlive()>0 and DetectionSet:GetFirst():GetCoalition() or nil + + -- Filter coalition. + if self.coalition then + local coalitionname=UTILS.GetCoalitionName(self.coalition):lower() + self.detectionset:FilterCoalitions(coalitionname) + end + + -- Filter once. + self.detectionset:FilterOnce() + + -- Set alias. + if Alias then + self.alias=tostring(Alias) + else + self.alias="SPECTRE" + if self.coalition then + if self.coalition==coalition.side.RED then + self.alias="KGB" + elseif self.coalition==coalition.side.BLUE then + self.alias="CIA" + end + end + end + + self.DetectVisual = true + self.DetectOptical = true + self.DetectRadar = true + self.DetectIRST = true + self.DetectRWR = true + self.DetectDLINK = true + + self.statusupdate = -60 + + -- 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") + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start FSM. + self:AddTransition("*", "Status", "*") -- INTEL status update + + self:AddTransition("*", "Detect", "*") -- Start detection run. Not implemented yet! + + self:AddTransition("*", "NewContact", "*") -- New contact has been detected. + 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("*", "Stop", "Stopped") + + -- Defaults + self:SetForgetTime() + self:SetAcceptZones() + self:SetRejectZones() + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the INTEL. Initializes parameters and starts event handlers. + -- @function [parent=#INTEL] Start + -- @param #INTEL self + + --- Triggers the FSM event "Start" after a delay. Starts the INTEL. Initializes parameters and starts event handlers. + -- @function [parent=#INTEL] __Start + -- @param #INTEL self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the INTEL and all its event handlers. + -- @param #INTEL self + + --- Triggers the FSM event "Stop" after a delay. Stops the INTEL and all its event handlers. + -- @function [parent=#INTEL] __Stop + -- @param #INTEL self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Status". + -- @function [parent=#INTEL] Status + -- @param #INTEL self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#INTEL] __Status + -- @param #INTEL self + -- @param #number delay Delay in seconds. + + --- On After "NewContact" event. + -- @function [parent=#INTEL] OnAfterNewContact + -- @param #INTEL self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #INTEL.Contact Contact Detected contact. + + --- On After "LostContact" event. + -- @function [parent=#INTEL] OnAfterLostContact + -- @param #INTEL self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #INTEL.Contact Contact Lost contact. + + --- On After "NewCluster" event. + -- @function [parent=#INTEL] OnAfterNewCluster + -- @param #INTEL self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #INTEL.Contact Contact Detected contact. + -- @param #INTEL.Cluster Cluster Detected cluster + + --- On After "LostCluster" event. + -- @function [parent=#INTEL] OnAfterLostCluster + -- @param #INTEL self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #INTEL.Cluster Cluster Lost cluster + -- @param Ops.Auftrag#AUFTRAG Mission The Auftrag connected with this cluster or nil + + return self +end +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set accept zones. Only contacts detected in this/these zone(s) are considered. +-- @param #INTEL self +-- @param Core.Set#SET_ZONE AcceptZoneSet Set of accept zones. +-- @return #INTEL self +function INTEL:SetAcceptZones(AcceptZoneSet) + self.acceptzoneset=AcceptZoneSet or SET_ZONE:New() + return self +end + +--- Add an accept zone. Only contacts detected in this zone are considered. +-- @param #INTEL self +-- @param Core.Zone#ZONE AcceptZone Add a zone to the accept zone set. +-- @return #INTEL self +function INTEL:AddAcceptZone(AcceptZone) + self.acceptzoneset:AddZone(AcceptZone) + return self +end + +--- Remove an accept zone from the accept zone set. +-- @param #INTEL self +-- @param Core.Zone#ZONE AcceptZone Remove a zone from the accept zone set. +-- @return #INTEL self +function INTEL:RemoveAcceptZone(AcceptZone) + self.acceptzoneset:Remove(AcceptZone:GetName(), true) + return self +end + +--- Set reject zones. Contacts detected in this/these zone(s) are rejected and not reported by the detection. +-- Note that reject zones overrule accept zones, i.e. if a unit is inside and accept zone and inside a reject zone, it is rejected. +-- @param #INTEL self +-- @param Core.Set#SET_ZONE RejectZoneSet Set of reject zone(s). +-- @return #INTEL self +function INTEL:SetRejectZones(RejectZoneSet) + self.rejectzoneset=RejectZoneSet or SET_ZONE:New() + return self +end + +--- Add a reject zone. Contacts detected in this zone are rejected and not reported by the detection. +-- Note that reject zones overrule accept zones, i.e. if a unit is inside and accept zone and inside a reject zone, it is rejected. +-- @param #INTEL self +-- @param Core.Zone#ZONE RejectZone Add a zone to the reject zone set. +-- @return #INTEL self +function INTEL:AddRejectZone(RejectZone) + self.rejectzoneset:AddZone(RejectZone) + return self +end + +--- Remove a reject zone from the reject zone set. +-- @param #INTEL self +-- @param Core.Zone#ZONE RejectZone Remove a zone from the reject zone set. +-- @return #INTEL self +function INTEL:RemoveRejectZone(RejectZone) + self.rejectzoneset:Remove(RejectZone:GetName(), true) + return self +end + +--- Set forget contacts time interval. +-- Previously known contacts that are not detected any more, are "lost" after this time. +-- This avoids fast oscillations between a contact being detected and undetected. +-- @param #INTEL self +-- @param #number TimeInterval Time interval in seconds. Default is 120 sec. +-- @return #INTEL self +function INTEL:SetForgetTime(TimeInterval) + self.dTforget=TimeInterval or 120 + return self +end + +--- Filter unit categories. Valid categories are: +-- +-- * Unit.Category.AIRPLANE +-- * Unit.Category.HELICOPTER +-- * Unit.Category.GROUND_UNIT +-- * Unit.Category.SHIP +-- * Unit.Category.STRUCTURE +-- +-- @param #INTEL self +-- @param #table Categories Filter categories, e.g. {Unit.Category.AIRPLANE, Unit.Category.HELICOPTER}. +-- @return #INTEL self +function INTEL:SetFilterCategory(Categories) + if type(Categories)~="table" then + Categories={Categories} + end + + self.filterCategory=Categories + + local text="Filter categories: " + for _,category in pairs(self.filterCategory) do + text=text..string.format("%d,", category) + end + self:T(self.lid..text) + + return self +end + +--- Filter group categories. Valid categories are: +-- +-- * Group.Category.AIRPLANE +-- * Group.Category.HELICOPTER +-- * Group.Category.GROUND +-- * Group.Category.SHIP +-- * Group.Category.TRAIN +-- +-- @param #INTEL self +-- @param #table GroupCategories Filter categories, e.g. `{Group.Category.AIRPLANE, Group.Category.HELICOPTER}`. +-- @return #INTEL self +function INTEL:FilterCategoryGroup(GroupCategories) + if type(GroupCategories)~="table" then + GroupCategories={GroupCategories} + end + + self.filterCategoryGroup=GroupCategories + + local text="Filter group categories: " + for _,category in pairs(self.filterCategoryGroup) do + text=text..string.format("%d,", category) + end + self:T(self.lid..text) + + return self +end + +--- Enable or disable cluster analysis of detected targets. +-- Targets will be grouped in coupled clusters. +-- @param #INTEL self +-- @param #boolean Switch If true, enable cluster analysis. +-- @param #boolean Markers If true, place markers on F10 map. +-- @return #INTEL self +function INTEL:SetClusterAnalysis(Switch, Markers) + self.clusteranalysis=Switch + self.clustermarkers=Markers + return self +end + +--- Set verbosity level for debugging. +-- @param #INTEL self +-- @param #number Verbosity The higher, the noisier, e.g. 0=off, 2=debug +-- @return #INTEL self +function INTEL:SetVerbosity(Verbosity) + self.verbose=Verbosity or 2 + return self +end + +--- Add a Mission (Auftrag) to a contact for tracking. +-- @param #INTEL self +-- @param #INTEL.Contact Contact The contact +-- @param Ops.Auftrag#AUFTRAG Mission The mission connected with this contact +-- @return #INTEL self +function INTEL:AddMissionToContact(Contact, Mission) + if Mission and Contact then + Contact.mission = Mission + end + return self +end + +--- Add a Mission (Auftrag) to a cluster for tracking. +-- @param #INTEL self +-- @param #INTEL.Cluster Cluster The cluster +-- @param Ops.Auftrag#AUFTRAG Mission The mission connected with this cluster +-- @return #INTEL self +function INTEL:AddMissionToCluster(Cluster, Mission) + if Mission and Cluster then + Cluster.mission = Mission + end + return self +end + +--- Change radius of the Clusters +-- @param #INTEL self +-- @param #number radius The radius of the clusters +-- @return #INTEL self +function INTEL:SetClusterRadius(radius) + local radius = radius or 15 + self.clusterradius = 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 +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after Start event. Starts the FLIGHTGROUP FSM and event handlers. +-- @param #INTEL self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function INTEL:onafterStart(From, Event, To) + + -- Short info. + local text=string.format("Starting INTEL v%s", self.version) + self:I(self.lid..text) + + -- Start the status monitoring. + self:__Status(-math.random(10)) +end + +--- On after "Status" event. +-- @param #INTEL self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function INTEL:onafterStatus(From, Event, To) + + -- FSM state. + local fsmstate=self:GetState() + + -- Fresh arrays. + self.ContactsLost={} + self.ContactsUnknown={} + + -- Check if group has detected any units. + self:UpdateIntel() + + -- Number of total contacts. + local Ncontacts=#self.Contacts + local Nclusters=#self.Clusters + + -- Short info. + if self.verbose>=1 then + local text=string.format("Status %s [Agents=%s]: Contacts=%d, Clusters=%d, New=%d, Lost=%d", fsmstate, self.detectionset:CountAlive(), Ncontacts, Nclusters, #self.ContactsUnknown, #self.ContactsLost) + self:I(self.lid..text) + end + + -- Detailed info. + if self.verbose>=2 and Ncontacts>0 then + local text="Detected Contacts:" + for _,_contact in pairs(self.Contacts) do + local contact=_contact --#INTEL.Contact + local dT=timer.getAbsTime()-contact.Tdetected + 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 "unknown") + end + end + self:I(self.lid..text) + end + + self:__Status(self.statusupdate) +end + + +--- Update detected items. +-- @param #INTEL self +function INTEL:UpdateIntel() + + -- Set of all detected units. + local DetectedUnits={} + -- Set of which units was detected by which recce + local RecceDetecting = {} + -- Loop over all units providing intel. + for _,_group in pairs(self.detectionset.Set or {}) do + local group=_group --Wrapper.Group#GROUP + + if group and group:IsAlive() then + + for _,_recce in pairs(group:GetUnits()) do + local recce=_recce --Wrapper.Unit#UNIT + + -- Get detected units. + self:GetDetectedUnits(recce, DetectedUnits, RecceDetecting, self.DetectVisual, self.DetectOptical, self.DetectRadar, self.DetectIRST, self.DetectRWR, self.DetectDLINK) + + end + + end + end + + local remove={} + for unitname,_unit in pairs(DetectedUnits) do + local unit=_unit --Wrapper.Unit#UNIT + + -- Check if unit is in any of the accept zones. + if self.acceptzoneset:Count()>0 then + local inzone=false + for _,_zone in pairs(self.acceptzoneset.Set) do + local zone=_zone --Core.Zone#ZONE + if unit:IsInZone(zone) then + inzone=true + break + end + end + + -- Unit is not in accept zone ==> remove! + if not inzone then + table.insert(remove, unitname) + end + end + + -- Check if unit is in any of the reject zones. + if self.rejectzoneset:Count()>0 then + local inzone=false + for _,_zone in pairs(self.rejectzoneset.Set) do + local zone=_zone --Core.Zone#ZONE + if unit:IsInZone(zone) then + inzone=true + break + end + end + + -- Unit is inside a reject zone ==> remove! + if inzone then + table.insert(remove, unitname) + end + end + + -- Filter unit categories. + if #self.filterCategory>0 then + local unitcategory=unit:GetUnitCategory() + local keepit=false + for _,filtercategory in pairs(self.filterCategory) do + if unitcategory==filtercategory then + keepit=true + break + end + end + if not keepit then + self:T(self.lid..string.format("Removing unit %s category=%d", unitname, unit:GetCategory())) + table.insert(remove, unitname) + end + end + + end + + -- Remove filtered units. + for _,unitname in pairs(remove) do + DetectedUnits[unitname]=nil + end + + -- Create detected groups. + local DetectedGroups={} + local RecceGroups={} + for unitname,_unit in pairs(DetectedUnits) do + local unit=_unit --Wrapper.Unit#UNIT + local group=unit:GetGroup() + if group then + local groupname = group:GetName() + DetectedGroups[groupname]=group + RecceGroups[groupname]=RecceDetecting[unitname] + end + end + + -- Create detected contacts. + self:CreateDetectedItems(DetectedGroups, RecceGroups) + + -- Paint a picture of the battlefield. + if self.clusteranalysis then + self:PaintPicture() + end + +end + + + + + +--- Create detected items. +-- @param #INTEL self +-- @param #table DetectedGroups Table of detected Groups +-- @param #table RecceDetecting Table of detecting recce names +function INTEL:CreateDetectedItems(DetectedGroups, RecceDetecting) + self:F({RecceDetecting=RecceDetecting}) + -- Current time. + local Tnow=timer.getAbsTime() + + for groupname,_group in pairs(DetectedGroups) do + local group=_group --Wrapper.Group#GROUP + + + -- Get contact if already known. + local detecteditem=self:GetContactByName(groupname) + + if detecteditem then + --- + -- Detected item already exists ==> Update data. + --- + + detecteditem.Tdetected=Tnow + detecteditem.position=group:GetCoordinate() + detecteditem.velocity=group:GetVelocityVec3() + detecteditem.speed=group:GetVelocityMPS() + + else + --- + -- Detected item does not exist in our list yet. + --- + + -- Create new contact. + local item={} --#INTEL.Contact + + item.groupname=groupname + item.group=group + item.Tdetected=Tnow + item.typename=group:GetTypeName() + item.attribute=group:GetAttribute() + item.category=group:GetCategory() + item.categoryname=group:GetCategoryName() + item.threatlevel=group:GetThreatLevel() + item.position=group:GetCoordinate() + item.velocity=group:GetVelocityVec3() + item.speed=group:GetVelocityMPS() + item.recce=RecceDetecting[groupname] + item.isground = group:IsGround() or false + item.isship = group:IsShip() or false + 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) + + -- Trigger new contact event. + self:NewContact(item) + end + + end + + -- Now check if there some groups could not be detected any more. + for i=#self.Contacts,1,-1 do + local item=self.Contacts[i] --#INTEL.Contact + + -- Check if deltaT>Tforget. We dont want quick oscillations between detected and undetected states. + if self:_CheckContactLost(item) then + + -- Trigger LostContact event. This also adds the contact to the self.ContactsLost table. + self:LostContact(item) + + -- Remove contact from table. + self:RemoveContact(item) + + end + end + +end + +--- (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 +-- @param Wrapper.Unit#UNIT Unit The unit detecting. +-- @param #table DetectedUnits Table of detected units to be filled +-- @param #table RecceDetecting Table of recce per unit to be filled +-- @param #boolean DetectVisual (Optional) If *false*, do not include visually detected targets. +-- @param #boolean DetectOptical (Optional) If *false*, do not include optically detected targets. +-- @param #boolean DetectRadar (Optional) If *false*, do not include targets detected by radar. +-- @param #boolean DetectIRST (Optional) If *false*, do not include targets detected by IRST. +-- @param #boolean DetectRWR (Optional) If *false*, do not include targets detected by RWR. +-- @param #boolean DetectDLINK (Optional) If *false*, do not include targets detected by data link. +function INTEL:GetDetectedUnits(Unit, DetectedUnits, RecceDetecting, DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) + + -- Get detected DCS units. + local reccename = Unit:GetName() + local detectedtargets=Unit:GetDetectedTargets(DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) + + for DetectionObjectID, Detection in pairs(detectedtargets or {}) do + local DetectedObject=Detection.object -- DCS#Object + + if DetectedObject and DetectedObject:isExist() and DetectedObject.id_<50000000 then + + local unit=UNIT:Find(DetectedObject) + + if unit and unit:IsAlive() then + + local unitname=unit:GetName() + + DetectedUnits[unitname]=unit + RecceDetecting[unitname]=reccename + self:T(string.format("Unit %s detect by %s", unitname, reccename)) + end + end + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Events +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "NewContact" event. +-- @param #INTEL self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #INTEL.Contact Contact Detected contact. +function INTEL:onafterNewContact(From, Event, To, Contact) + self:F(self.lid..string.format("NEW contact %s", Contact.groupname)) + table.insert(self.ContactsUnknown, Contact) +end + +--- On after "LostContact" event. +-- @param #INTEL self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #INTEL.Contact Contact Detected contact. +function INTEL:onafterLostContact(From, Event, To, Contact) + self:F(self.lid..string.format("LOST contact %s", Contact.groupname)) + table.insert(self.ContactsLost, Contact) +end + +--- On after "NewCluster" event. +-- @param #INTEL self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #INTEL.Contact Contact Detected contact. +-- @param #INTEL.Cluster Cluster Detected cluster +function INTEL:onafterNewCluster(From, Event, To, Contact, Cluster) + self:F(self.lid..string.format("NEW cluster %d size %d with contact %s", Cluster.index, Cluster.size, Contact.groupname)) +end + +--- On after "LostCluster" event. +-- @param #INTEL self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #INTEL.Cluster Cluster Lost cluster +-- @param Ops.Auftrag#AUFTRAG Mission The Auftrag connected with this cluster or nil +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 "unknown") + end + self:T(text) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get a contact by name. +-- @param #INTEL self +-- @param #string groupname Name of the contact group. +-- @return #INTEL.Contact The contact. +function INTEL:GetContactByName(groupname) + + for i,_contact in pairs(self.Contacts) do + local contact=_contact --#INTEL.Contact + if contact.groupname==groupname then + return contact + end + end + + return nil +end + +--- Add a contact to our list. +-- @param #INTEL self +-- @param #INTEL.Contact Contact The contact to be added. +function INTEL:AddContact(Contact) + table.insert(self.Contacts, Contact) +end + +--- Remove a contact from our list. +-- @param #INTEL self +-- @param #INTEL.Contact Contact The contact to be removed. +function INTEL:RemoveContact(Contact) + + for i,_contact in pairs(self.Contacts) do + local contact=_contact --#INTEL.Contact + + if contact.groupname==Contact.groupname then + table.remove(self.Contacts, i) + end + + end + +end + +--- Check if a contact was lost. +-- @param #INTEL self +-- @param #INTEL.Contact Contact The contact to be removed. +-- @return #boolean If true, contact was not detected for at least *dTforget* seconds. +function INTEL:_CheckContactLost(Contact) + + -- Group dead? + if Contact.group==nil or not Contact.group:IsAlive() then + return true + end + + -- Time since last detected. + local dT=timer.getAbsTime()-Contact.Tdetected + + local dTforget=self.dTforget + if Contact.category==Group.Category.GROUND then + dTforget=60*60*2 -- 2 hours + elseif Contact.category==Group.Category.AIRPLANE then + dTforget=60*10 -- 10 min + elseif Contact.category==Group.Category.HELICOPTER then + dTforget=60*20 -- 20 min + elseif Contact.category==Group.Category.SHIP then + dTforget=60*60 -- 1 hour + elseif Contact.category==Group.Category.TRAIN then + dTforget=60*60 -- 1 hour + end + + if dT>dTforget then + return true + else + return false + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Cluster Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- [Internal] Paint picture of the battle field. Does Cluster analysis and updates clusters. Sets markers if markers are enabled. +-- @param #INTEL self +function INTEL:PaintPicture() + + -- First remove all lost contacts from clusters. + for _,_contact in pairs(self.ContactsLost) do + local contact=_contact --#INTEL.Contact + local cluster=self:GetClusterOfContact(contact) + if cluster then + self:RemoveContactFromCluster(contact, cluster) + end + end + -- clean up cluster table + local ClusterSet = {} + for _i,_cluster in pairs(self.Clusters) do + if (_cluster.size > 0) and (self:ClusterCountUnits(_cluster) > 0) then + table.insert(ClusterSet,_cluster) + else + local mission = _cluster.mission or nil + local marker = _cluster.marker + local markerID = _cluster.markerID + if marker then + marker:Remove() + end + if markerID then + COORDINATE:RemoveMark(markerID) + end + self:LostCluster(_cluster, mission) + end + end + self.Clusters = ClusterSet + -- update positions + self:_UpdateClusterPositions() + + for _,_contact in pairs(self.Contacts) do + local contact=_contact --#INTEL.Contact + self:T(string.format("Paint Picture: checking for %s",contact.groupname)) + -- Check if this contact is in any cluster. + local isincluster=self:CheckContactInClusters(contact) + + -- Get the current cluster (if any) this contact belongs to. + local currentcluster=self:GetClusterOfContact(contact) + + if currentcluster then + --self:I(string.format("Paint Picture: %s has current cluster",contact.groupname)) + --- + -- Contact is currently part of a cluster. + --- + + -- Check if the contact is still connected to the cluster. + local isconnected=self:IsContactConnectedToCluster(contact, currentcluster) + + if (not isconnected) and (currentcluster.size > 1) then + --self:I(string.format("Paint Picture: %s has LOST current cluster",contact.groupname)) + local cluster=self:IsContactPartOfAnyClusters(contact) + + if cluster then + self:AddContactToCluster(contact, cluster) + else + + local newcluster=self:CreateCluster(contact.position) + self:AddContactToCluster(contact, newcluster) + self:NewCluster(contact, newcluster) + end + + end + + + else + + --- + -- Contact is not in any cluster yet. + --- + --self:I(string.format("Paint Picture: %s has NO current cluster",contact.groupname)) + local cluster=self:IsContactPartOfAnyClusters(contact) + + if cluster then + self:AddContactToCluster(contact, cluster) + else + + local newcluster=self:CreateCluster(contact.position) + self:AddContactToCluster(contact, newcluster) + self:NewCluster(contact, newcluster) + end + + end + + end + + + + -- Update F10 marker text if cluster has changed. + if self.clustermarkers then + for _,_cluster in pairs(self.Clusters) do + local cluster=_cluster --#INTEL.Cluster + --local coordinate=self:GetClusterCoordinate(cluster) + -- Update F10 marker. + self:UpdateClusterMarker(cluster) + self:CalcClusterFuturePosition(cluster,self.prediction) + end + end +end + +--- Create a new cluster. +-- @param #INTEL self +-- @param Core.Point#COORDINATE coordinate The coordinate of the cluster. +-- @return #INTEL.Cluster cluster The cluster. +function INTEL:CreateCluster(coordinate) + + -- Create new cluster + local cluster={} --#INTEL.Cluster + + cluster.index=self.clustercounter + cluster.coordinate=coordinate + cluster.threatlevelSum=0 + cluster.threatlevelMax=0 + cluster.size=0 + cluster.Contacts={} + + -- Add cluster. + table.insert(self.Clusters, cluster) + + -- Increase counter. + self.clustercounter=self.clustercounter+1 + + return cluster +end + +--- Add a contact to the cluster. +-- @param #INTEL self +-- @param #INTEL.Contact contact The contact. +-- @param #INTEL.Cluster cluster The cluster. +function INTEL:AddContactToCluster(contact, cluster) + + if contact and cluster then + + -- Add neighbour to cluster contacts. + table.insert(cluster.Contacts, contact) + + cluster.threatlevelSum=cluster.threatlevelSum+contact.threatlevel + + cluster.size=cluster.size+1 + end + +end + +--- Remove a contact from a cluster. +-- @param #INTEL self +-- @param #INTEL.Contact contact The contact. +-- @param #INTEL.Cluster cluster The cluster. +function INTEL:RemoveContactFromCluster(contact, cluster) + + if contact and cluster then + + for i,_contact in pairs(cluster.Contacts) do + local Contact=_contact --#INTEL.Contact + + if Contact.groupname==contact.groupname then + + cluster.threatlevelSum=cluster.threatlevelSum-contact.threatlevel + cluster.size=cluster.size-1 + + table.remove(cluster.Contacts, i) + + return + end + + end + + end + +end + +--- Calculate cluster threat level sum. +-- @param #INTEL self +-- @param #INTEL.Cluster cluster The cluster of contacts. +-- @return #number Sum of all threat levels of all groups in the cluster. +function INTEL:CalcClusterThreatlevelSum(cluster) + + local threatlevel=0 + + for _,_contact in pairs(cluster.Contacts) do + local contact=_contact --#INTEL.Contact + + threatlevel=threatlevel+contact.threatlevel + + end + cluster.threatlevelSum = threatlevel + return threatlevel +end + +--- Calculate cluster threat level average. +-- @param #INTEL self +-- @param #INTEL.Cluster cluster The cluster of contacts. +-- @return #number Average of all threat levels of all groups in the cluster. +function INTEL:CalcClusterThreatlevelAverage(cluster) + + local threatlevel=self:CalcClusterThreatlevelSum(cluster) + threatlevel=threatlevel/cluster.size + cluster.threatlevelAve = threatlevel + return threatlevel +end + +--- Calculate max cluster threat level. +-- @param #INTEL self +-- @param #INTEL.Cluster cluster The cluster of contacts. +-- @return #number Max threat levels of all groups in the cluster. +function INTEL:CalcClusterThreatlevelMax(cluster) + + local threatlevel=0 + + for _,_contact in pairs(cluster.Contacts) do + + local contact=_contact --#INTEL.Contact + + if contact.threatlevel>threatlevel then + threatlevel=contact.threatlevel + end + + end + cluster.threatlevelMax = threatlevel + return threatlevel +end + +--- Calculate cluster heading. +-- @param #INTEL self +-- @param #INTEL.Cluster cluster The cluster of contacts. +-- @return #number Heading average of all groups in the cluster. +function INTEL:CalcClusterDirection(cluster) + + local direction = 0 + local n=0 + for _,_contact in pairs(cluster.Contacts) do + local group = _contact.group -- Wrapper.Group#GROUP + if group:IsAlive() then + direction = direction + group:GetHeading() + n=n+1 + end + end + return math.floor(direction / n) + +end + +--- Calculate cluster speed. +-- @param #INTEL self +-- @param #INTEL.Cluster cluster The cluster of contacts. +-- @return #number Speed average of all groups in the cluster in MPS. +function INTEL:CalcClusterSpeed(cluster) + + local velocity = 0 + local n=0 + for _,_contact in pairs(cluster.Contacts) do + local group = _contact.group -- Wrapper.Group#GROUP + if group:IsAlive() then + velocity = velocity + group:GetVelocityMPS() + n=n+1 + end + end + return math.floor(velocity / n) + +end + +--- Calculate cluster future position after given seconds. +-- @param #INTEL self +-- @param #INTEL.Cluster cluster The cluster of contacts. +-- @param #number seconds Timeframe in seconds. +-- @return Core.Point#COORDINATE Calculated future position of the cluster. +function INTEL:CalcClusterFuturePosition(cluster,seconds) + local speed = self:CalcClusterSpeed(cluster) -- #number MPS + local direction = self:CalcClusterDirection(cluster) -- #number heading + -- local currposition = cluster.coordinate -- Core.Point#COORDINATE + local currposition = self:GetClusterCoordinate(cluster) -- Core.Point#COORDINATE + local distance = speed * seconds -- #number in meters the cluster will travel + local futureposition = currposition:Translate(distance,direction,true,false) + if self.clustermarkers and (self.verbose > 1) then + if cluster.markerID then + COORDINATE:RemoveMark(cluster.markerID) + end + cluster.markerID = currposition:ArrowToAll(futureposition,self.coalition,{1,0,0},1,{1,1,0},0.5,2,true,"Postion Calc") + end + return futureposition +end + + +--- Check if contact is in any known cluster. +-- @param #INTEL self +-- @param #INTEL.Contact contact The contact. +-- @return #boolean If true, contact is in clusters +function INTEL:CheckContactInClusters(contact) + + for _,_cluster in pairs(self.Clusters) do + local cluster=_cluster --#INTEL.Cluster + + for _,_contact in pairs(cluster.Contacts) do + local Contact=_contact --#INTEL.Contact + + if Contact.groupname==contact.groupname then + return true + end + end + end + + return false +end + +--- Check if contact is close to any other contact this cluster. +-- @param #INTEL self +-- @param #INTEL.Contact contact The contact. +-- @param #INTEL.Cluster cluster The cluster the check. +-- @return #boolean If true, contact is connected to this cluster. +function INTEL:IsContactConnectedToCluster(contact, cluster) + + for _,_contact in pairs(cluster.Contacts) do + local Contact=_contact --#INTEL.Contact + + if Contact.groupname~=contact.groupname then + + --local dist=Contact.position:Get2DDistance(contact.position) + local dist=Contact.position:DistanceFromPointVec2(contact.position) + + local radius = self.clusterradius or 15 + if dist1000 then + return true + else + return false + end + +end + +--- Update coordinates of the known clusters. +-- @param #INTEL self +function INTEL:_UpdateClusterPositions() + for _,_cluster in pairs (self.Clusters) do + local coord = self:GetClusterCoordinate(_cluster) + _cluster.coordinate = coord + self:T(self.lid..string.format("Cluster size: %s", _cluster.size)) + end +end + +--- Count number of units in cluster +-- @param #INTEL self +-- @param #INTEL.Cluster Cluster The cluster +-- @return #number unitcount +function INTEL:ClusterCountUnits(Cluster) + local unitcount = 0 + for _,_group in pairs (Cluster.Contacts) do -- get Wrapper.GROUP#GROUP _group + unitcount = unitcount + _group.group:CountAliveUnits() + end + return unitcount +end + +--- Update cluster F10 marker. +-- @param #INTEL self +-- @param #INTEL.Cluster cluster The cluster. +-- @return #INTEL self +function INTEL:UpdateClusterMarker(cluster) + + -- Create a marker. + local unitcount = self:ClusterCountUnits(cluster) + local text=string.format("Cluster #%d. Size %d, Units %d, TLsum=%d", cluster.index, cluster.size, unitcount, cluster.threatlevelSum) + + if not cluster.marker then + if self.coalition == coalition.side.RED then + cluster.marker=MARKER:New(cluster.coordinate, text):ToRed() + elseif self.coalition == coalition.side.BLUE then + cluster.marker=MARKER:New(cluster.coordinate, text):ToBlue() + else + cluster.marker=MARKER:New(cluster.coordinate, text):ToNeutral() + end + else + + local refresh=false + + if cluster.marker.text~=text then + cluster.marker.text=text + refresh=true + end + + if cluster.marker.coordinate~=cluster.coordinate then + cluster.marker.coordinate=cluster.coordinate + refresh=true + end + + if refresh then + cluster.marker:Refresh() + end + + end + + return self +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 +---------------------------------------------------------------------------------------------- +--- **Ops** -- Combat Search and Rescue. +-- +-- === +-- +-- **CSAR** - MOOSE based Helicopter CSAR Operations. +-- +-- === +-- +-- ## Missions: +-- +-- ### [CSAR - Combat Search & Rescue](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/OPS%20-%20CSAR) +-- +-- === +-- +-- **Main Features:** +-- +-- * MOOSE-based Helicopter CSAR Operations for Players. +-- +-- === +-- +-- ### Author: **Applevangelist** (Moose Version), ***Ciribob*** (original), Thanks to: Shadowze, Cammel (testing) +-- @module Ops.CSAR +-- @image OPS_CSAR.jpg + +-- Date: Oct 2021 + +------------------------------------------------------------------------- +--- **CSAR** class, extends Core.Base#BASE, Core.Fsm#FSM +-- @type CSAR +-- @field #string ClassName Name of the class. +-- @field #number verbose Verbosity level. +-- @field #string lid Class id string for output to DCS log file. +-- @field #number coalition Coalition side number, e.g. `coalition.side.RED`. +-- @extends Core.Fsm#FSM + +--- *Combat search and rescue (CSAR) are search and rescue operations that are carried out during war that are within or near combat zones.* (Wikipedia) +-- +-- === +-- +-- ![Banner Image](OPS_CSAR.jpg) +-- +-- # CSAR Concept +-- +-- * MOOSE-based Helicopter CSAR Operations for Players. +-- * Object oriented refactoring of Ciribob\'s fantastic CSAR script. +-- * No need for extra MIST loading. +-- * Additional events to tailor your mission. +-- +-- ## 0. Prerequisites +-- +-- You need to load an .ogg soundfile for the pilot\'s beacons into the mission, e.g. "beacon.ogg", use a once trigger, "sound to country" for that. +-- Create a late-activated single infantry unit as template in the mission editor and name it e.g. "Downed Pilot". +-- +-- ## 1. Basic Setup +-- +-- A basic setup example is the following: +-- +-- -- Instantiate and start a CSAR for the blue side, with template "Downed Pilot" and alias "Luftrettung" +-- local my_csar = CSAR:New(coalition.side.BLUE,"Downed Pilot","Luftrettung") +-- -- options +-- my_csar.immortalcrew = true -- downed pilot spawn is immortal +-- my_csar.invisiblecrew = false -- downed pilot spawn is visible +-- -- start the FSM +-- my_csar:__Start(5) +-- +-- ## 2. Options +-- +-- The following options are available (with their defaults). Only set the ones you want changed: +-- +-- self.allowDownedPilotCAcontrol = false -- Set to false if you don\'t want to allow control by Combined Arms. +-- self.allowFARPRescue = true -- allows pilots to be rescued by landing at a FARP or Airbase. Else MASH only! +-- self.FARPRescueDistance = 1000 -- you need to be this close to a FARP or Airport for the pilot to be rescued. +-- self.autosmoke = false -- automatically smoke a downed pilot\'s location when a heli is near. +-- self.autosmokedistance = 1000 -- distance for autosmoke +-- self.coordtype = 1 -- Use Lat/Long DDM (0), Lat/Long DMS (1), MGRS (2), Bullseye imperial (3) or Bullseye metric (4) for coordinates. +-- self.csarOncrash = false -- (WIP) If set to true, will generate a downed pilot when a plane crashes as well. +-- self.enableForAI = false -- set to false to disable AI pilots from being rescued. +-- self.pilotRuntoExtractPoint = true -- Downed pilot will run to the rescue helicopter up to self.extractDistance in meters. +-- self.extractDistance = 500 -- Distance the downed pilot will start to run to the rescue helicopter. +-- self.immortalcrew = true -- Set to true to make wounded crew immortal. +-- self.invisiblecrew = false -- Set to true to make wounded crew insvisible. +-- self.loadDistance = 75 -- configure distance for pilots to get into helicopter in meters. +-- self.mashprefix = {"MASH"} -- prefixes of #GROUP objects used as MASHes. +-- self.max_units = 6 -- max number of pilots that can be carried if #CSAR.AircraftType is undefined. +-- self.messageTime = 15 -- Time to show messages for in seconds. Doubled for long messages. +-- self.radioSound = "beacon.ogg" -- the name of the sound file to use for the pilots\' radio beacons. +-- self.smokecolor = 4 -- Color of smokemarker, 0 is green, 1 is red, 2 is white, 3 is orange and 4 is blue. +-- self.useprefix = true -- Requires CSAR helicopter #GROUP names to have the prefix(es) defined below. +-- self.csarPrefix = { "helicargo", "MEDEVAC"} -- #GROUP name prefixes used for useprefix=true - DO NOT use # in helicopter names in the Mission Editor! +-- self.verbose = 0 -- set to > 1 for stats output for debugging. +-- -- (added 0.1.4) limit amount of downed pilots spawned by **ejection** events +-- self.limitmaxdownedpilots = true +-- self.maxdownedpilots = 10 +-- -- (added 0.1.8) - allow to set far/near distance for approach and optionally pilot must open doors +-- self.approachdist_far = 5000 -- switch do 10 sec interval approach mode, meters +-- self.approachdist_near = 3000 -- switch to 5 sec interval approach mode, meters +-- self.pilotmustopendoors = false -- switch to true to enable check of open doors +-- -- (added 0.1.9) +-- self.suppressmessages = false -- switch off all messaging if you want to do your own +-- -- (added 0.1.11) +-- self.rescuehoverheight = 20 -- max height for a hovering rescue in meters +-- self.rescuehoverdistance = 10 -- max distance for a hovering rescue in meters +-- -- (added 0.1.12) +-- -- Country codes for spawned pilots +-- self.countryblue= country.id.USA +-- self.countryred = country.id.RUSSIA +-- self.countryneutral = country.id.UN_PEACEKEEPERS +-- +-- ## 2.1 Experimental Features +-- +-- WARNING - Here\'ll be dragons! +-- DANGER - For this to work you need to de-sanitize your mission environment (all three entries) in \Scripts\MissionScripting.lua +-- Needs SRS => 1.9.6 to work (works on the **server** side of SRS) +-- self.useSRS = false -- Set true to use FF\'s SRS integration +-- self.SRSPath = "E:\\Progra~1\\DCS-SimpleRadio-Standalone\\" -- adjust your own path in your SRS installation -- server(!) +-- self.SRSchannel = 300 -- radio channel +-- self.SRSModulation = radio.modulation.AM -- modulation +-- +-- ## 3. Results +-- +-- Number of successful landings with save pilots and aggregated number of saved pilots is stored in these variables in the object: +-- +-- self.rescues -- number of successful landings *with* saved pilots +-- self.rescuedpilots -- aggregated number of pilots rescued from the field (of *all* players) +-- +-- ## 4. Events +-- +-- The class comes with a number of FSM-based events that missions designers can use to shape their mission. +-- These are: +-- +-- ### 4.1. PilotDown. +-- +-- The event is triggered when a new downed pilot is detected. Use e.g. `function my_csar:OnAfterPilotDown(...)` to link into this event: +-- +-- function my_csar:OnAfterPilotDown(from, event, to, spawnedgroup, frequency, groupname, coordinates_text) +-- ... your code here ... +-- end +-- +-- ### 4.2. Approach. +-- +-- A CSAR helicpoter is closing in on a downed pilot. Use e.g. `function my_csar:OnAfterApproach(...)` to link into this event: +-- +-- function my_csar:OnAfterApproach(from, event, to, heliname, groupname) +-- ... your code here ... +-- end +-- +-- ### 4.3. Boarded. +-- +-- The pilot has been boarded to the helicopter. Use e.g. `function my_csar:OnAfterBoarded(...)` to link into this event: +-- +-- function my_csar:OnAfterBoarded(from, event, to, heliname, groupname) +-- ... your code here ... +-- end +-- +-- ### 4.4. Returning. +-- +-- The CSAR helicopter is ready to return to an Airbase, FARP or MASH. Use e.g. `function my_csar:OnAfterReturning(...)` to link into this event: +-- +-- function my_csar:OnAfterReturning(from, event, to, heliname, groupname) +-- ... your code here ... +-- end +-- +-- ### 4.5. Rescued. +-- +-- The CSAR helicopter has landed close to an Airbase/MASH/FARP and the pilots are safe. Use e.g. `function my_csar:OnAfterRescued(...)` to link into this event: +-- +-- function my_csar:OnAfterRescued(from, event, to, heliunit, heliname, pilotssaved) +-- ... your code here ... +-- end +-- +-- ## 5. Spawn downed pilots at location to be picked up. +-- +-- If missions designers want to spawn downed pilots into the field, e.g. at mission begin to give the helicopter guys works, they can do this like so: +-- +-- -- Create downed "Pilot Wagner" in #ZONE "CSAR_Start_1" at a random point for the blue coalition +-- my_csar:SpawnCSARAtZone( "CSAR_Start_1", coalition.side.BLUE, "Pilot Wagner", true ) +-- +-- +-- @field #CSAR +CSAR = { + ClassName = "CSAR", + verbose = 0, + lid = "", + coalition = 1, + coalitiontxt = "blue", + FreeVHFFrequencies = {}, + UsedVHFFrequencies = {}, + takenOff = {}, + csarUnits = {}, -- table of unit names + downedPilots = {}, + woundedGroups = {}, + landedStatus = {}, + addedTo = {}, + woundedGroups = {}, -- contains the new group of units + inTransitGroups = {}, -- contain a table for each SAR with all units he has with the original names + smokeMarkers = {}, -- tracks smoke markers for groups + heliVisibleMessage = {}, -- tracks if the first message has been sent of the heli being visible + heliCloseMessage = {}, -- tracks heli close message ie heli < 500m distance + max_units = 6, --number of pilots that can be carried + hoverStatus = {}, -- tracks status of a helis hover above a downed pilot + pilotDisabled = {}, -- tracks what aircraft a pilot is disabled for + pilotLives = {}, -- tracks how many lives a pilot has + useprefix = true, -- Use the Prefixed defined below, Requires Unit have the Prefix defined below + csarPrefix = {}, + template = nil, + mash = {}, + smokecolor = 4, + rescues = 0, + rescuedpilots = 0, + limitmaxdownedpilots = true, + maxdownedpilots = 10, +} + +--- Downed pilots info. +-- @type CSAR.DownedPilot +-- @field #number index Pilot index. +-- @field #string name Name of the spawned group. +-- @field #number side Coalition. +-- @field #string originalUnit Name of the original unit. +-- @field #string desc Description. +-- @field #string typename Typename of Unit. +-- @field #number frequency Frequency of the NDB. +-- @field #string player Player name if applicable. +-- @field Wrapper.Group#GROUP group Spawned group object. +-- @field #number timestamp Timestamp for approach process +-- @field #boolean alive Group is alive or dead/rescued + +--- All slot / Limit settings +-- @type CSAR.AircraftType +-- @field #string typename Unit type name. +CSAR.AircraftType = {} -- Type and limit +CSAR.AircraftType["SA342Mistral"] = 2 +CSAR.AircraftType["SA342Minigun"] = 2 +CSAR.AircraftType["SA342L"] = 4 +CSAR.AircraftType["SA342M"] = 4 +CSAR.AircraftType["UH-1H"] = 8 +CSAR.AircraftType["Mi-8MTV2"] = 12 +CSAR.AircraftType["Mi-8MT"] = 12 +CSAR.AircraftType["Mi-24P"] = 8 +CSAR.AircraftType["Mi-24V"] = 8 +CSAR.AircraftType["Bell-47"] = 2 + +--- CSAR class version. +-- @field #string version +CSAR.version="0.1.11r2" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ToDo list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- DONE: SRS Integration (to be tested) +-- TODO: Maybe - add option to smoke/flare closest MASH + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new CSAR object and start the FSM. +-- @param #CSAR self +-- @param #number Coalition Coalition side. Can also be passed as a string "red", "blue" or "neutral". +-- @param #string Template Name of the late activated infantry unit standing in for the downed pilot. +-- @param #string Alias An *optional* alias how this object is called in the logs etc. +-- @return #CSAR self +function CSAR:New(Coalition, Template, Alias) + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #CSAR + + --set Coalition + if Coalition and type(Coalition)=="string" then + if Coalition=="blue" then + self.coalition=coalition.side.BLUE + self.coalitiontxt = Coalition + elseif Coalition=="red" then + self.coalition=coalition.side.RED + self.coalitiontxt = Coalition + elseif Coalition=="neutral" then + self.coalition=coalition.side.NEUTRAL + self.coalitiontxt = Coalition + else + self:E("ERROR: Unknown coalition in CSAR!") + end + else + self.coalition = Coalition + self.coalitiontxt = string.lower(UTILS.GetCoalitionName(self.coalition)) + end + + -- Set alias. + if Alias then + self.alias=tostring(Alias) + else + self.alias="Red Cross" + if self.coalition then + if self.coalition==coalition.side.RED then + self.alias="IFRC" + elseif self.coalition==coalition.side.BLUE then + self.alias="CSAR" + end + end + end + + -- Set some string id for output to DCS.log file. + self.lid=string.format("%s (%s) | ", self.alias, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown") + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start FSM. + self:AddTransition("*", "Status", "*") -- CSAR status update. + self:AddTransition("*", "PilotDown", "*") -- Downed Pilot added + self:AddTransition("*", "Approach", "*") -- CSAR heli closing in. + self:AddTransition("*", "Boarded", "*") -- Pilot boarded. + self:AddTransition("*", "Returning", "*") -- CSAR able to return to base. + self:AddTransition("*", "Rescued", "*") -- Pilot at MASH. + self:AddTransition("*", "KIA", "*") -- Pilot killed in action. + self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. + + -- tables, mainly for tracking actions + self.addedTo = {} + self.allheligroupset = {} -- GROUP_SET of all helis + self.csarUnits = {} -- table of CSAR unit names + self.FreeVHFFrequencies = {} + self.heliVisibleMessage = {} -- tracks if the first message has been sent of the heli being visible + self.heliCloseMessage = {} -- tracks heli close message ie heli < 500m distance + self.hoverStatus = {} -- tracks status of a helis hover above a downed pilot + self.inTransitGroups = {} -- contain a table for each SAR with all units he has with the original names + self.landedStatus = {} + self.lastCrash = {} + self.takenOff = {} + self.smokeMarkers = {} -- tracks smoke markers for groups + self.UsedVHFFrequencies = {} + self.woundedGroups = {} -- contains the new group of units + self.downedPilots = {} -- Replacement woundedGroups + self.downedpilotcounter = 1 + + -- settings, counters etc + self.rescues = 0 -- counter for successful rescue landings at FARP/AFB/MASH + self.rescuedpilots = 0 -- counter for saved pilots + self.csarOncrash = false -- If set to true, will generate a csar when a plane crashes as well. + self.allowDownedPilotCAcontrol = false -- Set to false if you don\'t want to allow control by Combined arms. + self.enableForAI = false -- set to false to disable AI units from being rescued. + self.smokecolor = 4 -- Color of smokemarker for blue side, 0 is green, 1 is red, 2 is white, 3 is orange and 4 is blue + self.coordtype = 2 -- Use Lat/Long DDM (0), Lat/Long DMS (1), MGRS (2), Bullseye imperial (3) or Bullseye metric (4) for coordinates. + self.immortalcrew = true -- Set to true to make wounded crew immortal + self.invisiblecrew = false -- Set to true to make wounded crew insvisible + self.messageTime = 15 -- Time to show longer messages for in seconds + self.pilotRuntoExtractPoint = true -- Downed Pilot will run to the rescue helicopter up to self.extractDistance METERS + self.loadDistance = 75 -- configure distance for pilot to get in helicopter in meters. + self.extractDistance = 500 -- Distance the Downed pilot will run to the rescue helicopter + self.loadtimemax = 135 -- seconds + self.radioSound = "beacon.ogg" -- the name of the sound file to use for the Pilot radio beacons. If this isnt added to the mission BEACONS WONT WORK! + self.allowFARPRescue = true --allows pilot to be rescued by landing at a FARP or Airbase + self.FARPRescueDistance = 1000 -- you need to be this close to a FARP or Airport for the pilot to be rescued. + self.max_units = 6 --max number of pilots that can be carried + self.useprefix = true -- Use the Prefixed defined below, Requires Unit have the Prefix defined below + self.csarPrefix = { "helicargo", "MEDEVAC"} -- prefixes used for useprefix=true - DON\'T use # in names! + self.template = Template or "generic" -- template for downed pilot + self.mashprefix = {"MASH"} -- prefixes used to find MASHes + + self.autosmoke = false -- automatically smoke location when heli is near + self.autosmokedistance = 2000 -- distance for autosmoke + -- added 0.1.4 + self.limitmaxdownedpilots = true + self.maxdownedpilots = 25 + -- generate Frequencies + self:_GenerateVHFrequencies() + -- added 0.1.8 + self.approachdist_far = 5000 -- switch do 10 sec interval approach mode, meters + self.approachdist_near = 3000 -- switch to 5 sec interval approach mode, meters + self.pilotmustopendoors = false -- switch to true to enable check on open doors + self.suppressmessages = false + + -- added 0.1.11r1 + self.rescuehoverheight = 20 + self.rescuehoverdistance = 10 + + -- added 0.1.12 + self.countryblue= country.id.USA + self.countryred = country.id.RUSSIA + self.countryneutral = country.id.UN_PEACEKEEPERS + + -- WARNING - here\'ll be dragons + -- for this to work you need to de-sanitize your mission environment in \Scripts\MissionScripting.lua + -- needs SRS => 1.9.6 to work (works on the *server* side) + self.useSRS = false -- Use FF\'s SRS integration + self.SRSPath = "E:\\Progra~1\\DCS-SimpleRadio-Standalone\\" -- adjust your own path in your server(!) + self.SRSchannel = 300 -- radio channel + self.SRSModulation = radio.modulation.AM -- modulation + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the CSAR. Initializes parameters and starts event handlers. + -- @function [parent=#CSAR] Start + -- @param #CSAR self + + --- Triggers the FSM event "Start" after a delay. Starts the CSAR. Initializes parameters and starts event handlers. + -- @function [parent=#CSAR] __Start + -- @param #CSAR self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the CSAR and all its event handlers. + -- @param #CSAR self + + --- Triggers the FSM event "Stop" after a delay. Stops the CSAR and all its event handlers. + -- @function [parent=#CSAR] __Stop + -- @param #CSAR self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Status". + -- @function [parent=#CSAR] Status + -- @param #CSAR self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#CSAR] __Status + -- @param #CSAR self + -- @param #number delay Delay in seconds. + + --- On After "PilotDown" event. Downed Pilot detected. + -- @function [parent=#CSAR] OnAfterPilotDown + -- @param #CSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Group#GROUP Group Group object of the downed pilot. + -- @param #number Frequency Beacon frequency in kHz. + -- @param #string Leadername Name of the #UNIT of the downed pilot. + -- @param #string CoordinatesText String of the position of the pilot. Format determined by self.coordtype. + + --- On After "Aproach" event. Heli close to downed Pilot. + -- @function [parent=#CSAR] OnAfterApproach + -- @param #CSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string Heliname Name of the helicopter group. + -- @param #string Woundedgroupname Name of the downed pilot\'s group. + + --- On After "Boarded" event. Downed pilot boarded heli. + -- @function [parent=#CSAR] OnAfterBoarded + -- @param #CSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string Heliname Name of the helicopter group. + -- @param #string Woundedgroupname Name of the downed pilot\'s group. + + --- On After "Returning" event. Heli can return home with downed pilot(s). + -- @function [parent=#CSAR] OnAfterReturning + -- @param #CSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string Heliname Name of the helicopter group. + -- @param #string Woundedgroupname Name of the downed pilot\'s group. + + --- On After "Rescued" event. Pilot(s) have been brought to the MASH/FARP/AFB. + -- @function [parent=#CSAR] OnAfterRescued + -- @param #CSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Unit#UNIT HeliUnit Unit of the helicopter. + -- @param #string HeliName Name of the helicopter group. + -- @param #number PilotsSaved Number of the saved pilots on board when landing. + + --- On After "KIA" event. Pilot is dead. + -- @function [parent=#CSAR] OnAfterKIA + -- @param #CSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string Pilotname Name of the pilot KIA. + + return self +end + +------------------------ +--- Helper Functions --- +------------------------ + +--- (Internal) Function to insert downed pilot tracker object. +-- @param #CSAR self +-- @param Wrapper.Group#GROUP Group The #GROUP object +-- @param #string Groupname Name of the spawned group. +-- @param #number Side Coalition. +-- @param #string OriginalUnit Name of original Unit. +-- @param #string Description Descriptive text. +-- @param #string Typename Typename of unit. +-- @param #number Frequency Frequency of the NDB in Hz +-- @param #string Playername Name of Player (if applicable) +-- @return #CSAR self. +function CSAR:_CreateDownedPilotTrack(Group,Groupname,Side,OriginalUnit,Description,Typename,Frequency,Playername) + self:T({"_CreateDownedPilotTrack",Groupname,Side,OriginalUnit,Description,Typename,Frequency,Playername}) + + -- create new entry + local DownedPilot = {} -- #CSAR.DownedPilot + DownedPilot.desc = Description or "" + DownedPilot.frequency = Frequency or 0 + DownedPilot.index = self.downedpilotcounter + DownedPilot.name = Groupname or "" + DownedPilot.originalUnit = OriginalUnit or "" + DownedPilot.player = Playername or "" + DownedPilot.side = Side or 0 + DownedPilot.typename = Typename or "" + DownedPilot.group = Group + DownedPilot.timestamp = 0 + DownedPilot.alive = true + + -- Add Pilot + local PilotTable = self.downedPilots + local counter = self.downedpilotcounter + PilotTable[counter] = {} + PilotTable[counter] = DownedPilot + self:T({Table=PilotTable}) + self.downedPilots = PilotTable + -- Increase counter + self.downedpilotcounter = self.downedpilotcounter+1 + return self +end + +--- (Internal) Count pilots on board. +-- @param #CSAR self +-- @param #string _heliName +-- @return #number count +function CSAR:_PilotsOnboard(_heliName) + self:T(self.lid .. " _PilotsOnboard") + local count = 0 + if self.inTransitGroups[_heliName] then + for _, _group in pairs(self.inTransitGroups[_heliName]) do + count = count + 1 + end + end + return count +end + +--- (Internal) Function to check for dupe eject events. +-- @param #CSAR self +-- @param #string _unitname Name of unit. +-- @return #boolean Outcome +function CSAR:_DoubleEjection(_unitname) + if self.lastCrash[_unitname] then + local _time = self.lastCrash[_unitname] + if timer.getTime() - _time < 10 then + self:E(self.lid.."Caught double ejection!") + return true + end + end + self.lastCrash[_unitname] = timer.getTime() + return false +end + +--- (Internal) Spawn a downed pilot +-- @param #CSAR self +-- @param #number country Country for template. +-- @param Core.Point#COORDINATE point Coordinate to spawn at. +-- @param #number frequency Frequency of the pilot's beacon +-- @return Wrapper.Group#GROUP group The #GROUP object. +-- @return #string alias The alias name. +function CSAR:_SpawnPilotInField(country,point,frequency) + self:T({country,point,frequency}) + local freq = frequency or 1000 + local freq = freq / 1000 -- kHz + for i=1,10 do + math.random(i,10000) + end + if point:IsSurfaceTypeWater() then point.y = 0 end + local template = self.template + local alias = string.format("Pilot %.2fkHz-%d", freq, math.random(1,99)) + local coalition = self.coalition + local pilotcacontrol = self.allowDownedPilotCAcontrol -- Switch AI on/oof - is this really correct for CA? + local _spawnedGroup = SPAWN + :NewWithAlias(template,alias) + :InitCoalition(coalition) + :InitCountry(country) + :InitAIOnOff(pilotcacontrol) + :InitDelayOff() + :SpawnFromCoordinate(point) + + return _spawnedGroup, alias -- Wrapper.Group#GROUP object +end + +--- (Internal) Add options to a downed pilot +-- @param #CSAR self +-- @param Wrapper.Group#GROUP group Group to use. +function CSAR:_AddSpecialOptions(group) + self:T(self.lid.." _AddSpecialOptions") + self:T({group}) + + local immortalcrew = self.immortalcrew + local invisiblecrew = self.invisiblecrew + if immortalcrew then + local _setImmortal = { + id = 'SetImmortal', + params = { + value = true + } + } + group:SetCommand(_setImmortal) + end + + if invisiblecrew then + local _setInvisible = { + id = 'SetInvisible', + params = { + value = true + } + } + group:SetCommand(_setInvisible) + end + + group:OptionAlarmStateGreen() + group:OptionROEHoldFire() + return self +end + +--- (Internal) Function to spawn a CSAR object into the scene. +-- @param #CSAR self +-- @param #number _coalition Coalition +-- @param DCS#country.id _country Country ID +-- @param Core.Point#COORDINATE _point Coordinate +-- @param #string _typeName Typename +-- @param #string _unitName Unitname +-- @param #string _playerName Playername +-- @param #number _freq Frequency +-- @param #boolean noMessage +-- @param #string _description Description +-- @param #boolean forcedesc Use the description only for the pilot track entry +function CSAR:_AddCsar(_coalition , _country, _point, _typeName, _unitName, _playerName, _freq, noMessage, _description, forcedesc ) + self:T(self.lid .. " _AddCsar") + self:T({_coalition , _country, _point, _typeName, _unitName, _playerName, _freq, noMessage, _description}) + + local template = self.template + + if not _freq then + _freq = self:_GenerateADFFrequency() + if not _freq then _freq = 333000 end --noob catch + end + + local _spawnedGroup, _alias = self:_SpawnPilotInField(_country,_point,_freq) + + local _typeName = _typeName or "Pilot" + + if not noMessage then + self:_DisplayToAllSAR("MAYDAY MAYDAY! " .. _typeName .. " is down. ", self.coalition, self.messageTime) + end + + if _freq then + self:_AddBeaconToGroup(_spawnedGroup, _freq) + end + + self:_AddSpecialOptions(_spawnedGroup) + + 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 + + self:_CreateDownedPilotTrack(_spawnedGroup,_GroupName,_coalition,_unitName,_text,_typeName,_freq,_playerName) + + self:_InitSARForPilot(_spawnedGroup, _GroupName, _freq, noMessage) + + return self +end + +--- (Internal) Function to add a CSAR object into the scene at a zone coordinate. For mission designers wanting to add e.g. PoWs to the scene. +-- @param #CSAR self +-- @param #string _zone Name of the zone. +-- @param #number _coalition Coalition. +-- @param #string _description (optional) Description. +-- @param #boolean _randomPoint (optional) Random yes or no. +-- @param #boolean _nomessage (optional) If true, don\'t send a message to SAR. +-- @param #string unitname (optional) Name of the lost unit. +-- @param #string typename (optional) Type of plane. +-- @param #boolean forcedesc (optional) Force to use the description passed only for the pilot track entry. Use to have fully custom names. +function CSAR:_SpawnCsarAtZone( _zone, _coalition, _description, _randomPoint, _nomessage, unitname, typename, forcedesc) + self:T(self.lid .. " _SpawnCsarAtZone") + local freq = self:_GenerateADFFrequency() + local _triggerZone = ZONE:New(_zone) -- trigger to use as reference position + if _triggerZone == nil then + self:E(self.lid.."ERROR: Can\'t find zone called " .. _zone, 10) + return + end + + local _description = _description or "PoW" + local unitname = unitname or "Old Rusty" + local typename = typename or "Phantom II" + + local pos = {} + if _randomPoint then + local _pos = _triggerZone:GetRandomPointVec3() + pos = COORDINATE:NewFromVec3(_pos) + else + pos = _triggerZone:GetCoordinate() + end + + local _country = 0 + if _coalition == coalition.side.BLUE then + _country = self.countryblue + elseif _coalition == coalition.side.RED then + _country = self.countryred + else + _country = self.countryneutral + end + + self:_AddCsar(_coalition, _country, pos, typename, unitname, _description, freq, _nomessage, _description, forcedesc) + + return self +end + +--- Function to add a CSAR object into the scene at a zone coordinate. For mission designers wanting to add e.g. PoWs to the scene. +-- @param #CSAR self +-- @param #string Zone Name of the zone. +-- @param #number Coalition Coalition. +-- @param #string Description (optional) Description. +-- @param #boolean RandomPoint (optional) Random yes or no. +-- @param #boolean Nomessage (optional) If true, don\'t send a message to SAR. +-- @param #string Unitname (optional) Name of the lost unit. +-- @param #string Typename (optional) Type of plane. +-- @param #boolean Forcedesc (optional) Force to use the **description passed only** for the pilot track entry. Use to have fully custom names. +-- @usage If missions designers want to spawn downed pilots into the field, e.g. at mission begin, to give the helicopter guys work, they can do this like so: +-- +-- -- Create downed "Pilot Wagner" in #ZONE "CSAR_Start_1" at a random point for the blue coalition +-- my_csar:SpawnCSARAtZone( "CSAR_Start_1", coalition.side.BLUE, "Wagner", true, false, "Charly-1-1", "F5E" ) +function CSAR:SpawnCSARAtZone(Zone, Coalition, Description, RandomPoint, Nomessage, Unitname, Typename, Forcedesc) + self:_SpawnCsarAtZone(Zone, Coalition, Description, RandomPoint, Nomessage, Unitname, Typename, Forcedesc) + return self +end + +--- (Internal) Event handler. +-- @param #CSAR self +function CSAR:_EventHandler(EventData) + self:T(self.lid .. " _EventHandler") + self:T({Event = EventData.id}) + + local _event = EventData -- Core.Event#EVENTDATA + + -- no event + if _event == nil or _event.initiator == nil then + return false + + -- take off + elseif _event.id == EVENTS.Takeoff then -- taken off + self:T(self.lid .. " Event unit - Takeoff") + + local _coalition = _event.IniCoalition + if _coalition ~= self.coalition then + return --ignore! + end + + if _event.IniGroupName then + self.takenOff[_event.IniUnitName] = true + end + + return true + + -- player enter unit + elseif _event.id == EVENTS.PlayerEnterAircraft or _event.id == EVENTS.PlayerEnterUnit then --player entered unit + self:T(self.lid .. " Event unit - Player Enter") + + local _coalition = _event.IniCoalition + if _coalition ~= self.coalition then + return --ignore! + end + + if _event.IniPlayerName then + self.takenOff[_event.IniPlayerName] = nil + end + + local _unit = _event.IniUnit + local _group = _event.IniGroup + if _unit:IsHelicopter() or _group:IsHelicopter() then + self:_AddMedevacMenuItem() + end + + return true + + elseif (_event.id == EVENTS.PilotDead and self.csarOncrash == false) then + -- Pilot dead + + self:T(self.lid .. " Event unit - Pilot Dead") + + local _unit = _event.IniUnit + local _unitname = _event.IniUnitName + local _group = _event.IniGroup + + if _unit == nil then + return -- error! + end + + local _coalition = _event.IniCoalition + if _coalition ~= self.coalition then + return --ignore! + end + + -- Catch multiple events here? + if self.takenOff[_event.IniUnitName] == true or _group:IsAirborne() then + if self:_DoubleEjection(_unitname) then + return + end + + else + self:T(self.lid .. " Pilot has not taken off, ignore") + end + + return + + elseif _event.id == EVENTS.PilotDead or _event.id == EVENTS.Ejection then + if _event.id == EVENTS.PilotDead and self.csarOncrash == false then + return + end + self:T(self.lid .. " Event unit - Pilot Ejected") + + local _unit = _event.IniUnit + local _unitname = _event.IniUnitName + local _group = _event.IniGroup + + if _unit == nil then + return -- error! + end + + local _coalition = _unit:GetCoalition() + if _coalition ~= self.coalition then + return --ignore! + end + + if self.enableForAI == false and _event.IniPlayerName == nil then + return + end + + if not self.takenOff[_event.IniUnitName] and not _group:IsAirborne() then + self:T(self.lid .. " Pilot has not taken off, ignore") + return -- give up, pilot hasnt taken off + end + + if self:_DoubleEjection(_unitname) then + return + end + + -- limit no of pilots in the field. + if self.limitmaxdownedpilots and self:_ReachedPilotLimit() then + return + end + + -- all checks passed, get going. + local _freq = self:_GenerateADFFrequency() + self:_AddCsar(_coalition, _unit:GetCountry(), _unit:GetCoordinate() , _unit:GetTypeName(), _unit:GetName(), _event.IniPlayerName, _freq, false, "none") + + return true + + elseif _event.id == EVENTS.Land then + self:T(self.lid .. " Landing") + + if _event.IniUnitName then + self.takenOff[_event.IniUnitName] = nil + end + + if self.allowFARPRescue then + + local _unit = _event.IniUnit -- Wrapper.Unit#UNIT + + if _unit == nil then + self:T(self.lid .. " Unit nil on landing") + return -- error! + end + + local _coalition = _event.IniCoalition + if _coalition ~= self.coalition then + return --ignore! + end + + self.takenOff[_event.IniUnitName] = nil + + local _place = _event.Place -- Wrapper.Airbase#AIRBASE + + if _place == nil then + self:T(self.lid .. " Landing Place Nil") + return -- error! + end + + -- anyone on board? + if self.inTransitGroups[_event.IniUnitName] == nil then + -- ignore + return + end + + if _place:GetCoalition() == self.coalition or _place:GetCoalition() == coalition.side.NEUTRAL then + self:_ScheduledSARFlight(_event.IniUnitName,_event.IniGroupName,true) + else + self:T(string.format("Airfield %d, Unit %d", _place:GetCoalition(), _unit:GetCoalition())) + end + end + + return true + end + return self +end + +--- (Internal) Initialize the action for a pilot. +-- @param #CSAR self +-- @param Wrapper.Group#GROUP _downedGroup The group to rescue. +-- @param #string _GroupName Name of the Group +-- @param #number _freq Beacon frequency. +-- @param #boolean _nomessage Send message true or false. +function CSAR:_InitSARForPilot(_downedGroup, _GroupName, _freq, _nomessage) + self:T(self.lid .. " _InitSARForPilot") + local _leader = _downedGroup:GetUnit(1) + local _groupName = _GroupName + local _freqk = _freq / 1000 + local _coordinatesText = self:_GetPositionOfWounded(_downedGroup) + local _leadername = _leader:GetName() + + if not _nomessage then + local _text = string.format("%s requests SAR at %s, beacon at %.2f KHz", _leadername, _coordinatesText, _freqk) + self:_DisplayToAllSAR(_text,self.coalition,self.messageTime) + end + + for _,_heliName in pairs(self.csarUnits) do + self:_CheckWoundedGroupStatus(_heliName, _groupName) + end + + -- trigger FSM event + self:__PilotDown(2,_downedGroup, _freqk, _groupName, _coordinatesText) + + return self +end + +--- (Internal) Check if a name is in downed pilot table +-- @param #CSAR self +-- @param #string name Name to search for. +-- @return #boolean Outcome. +-- @return #CSAR.DownedPilot Table if found else nil. +function CSAR:_CheckNameInDownedPilots(name) + local PilotTable = self.downedPilots --#CSAR.DownedPilot + local found = false + local table = nil + for _,_pilot in pairs(PilotTable) do + if _pilot.name == name and _pilot.alive == true then + found = true + table = _pilot + break + end + end + return found, table +end + +--- (Internal) Check if a name is in downed pilot table and remove it. +-- @param #CSAR self +-- @param #string name Name to search for. +-- @param #boolean force Force removal. +-- @return #boolean Outcome. +function CSAR:_RemoveNameFromDownedPilots(name,force) + local PilotTable = self.downedPilots --#CSAR.DownedPilot + local found = false + for _index,_pilot in pairs(PilotTable) do + if _pilot.name == name then + self.downedPilots[_index].alive = false + end + end + return found +end + +--- (Internal) Check state of wounded group. +-- @param #CSAR self +-- @param #string heliname heliname +-- @param #string woundedgroupname woundedgroupname +function CSAR:_CheckWoundedGroupStatus(heliname,woundedgroupname) + self:T(self.lid .. " _CheckWoundedGroupStatus") + local _heliName = heliname + local _woundedGroupName = woundedgroupname + self:T({Heli = _heliName, Downed = _woundedGroupName}) + -- if wounded group is not here then message already been sent to SARs + -- stop processing any further + local _found, _downedpilot = self:_CheckNameInDownedPilots(_woundedGroupName) + if not _found then + self:T("...not found in list!") + return + end + + local _woundedGroup = _downedpilot.group + if _woundedGroup ~= nil and _woundedGroup:IsAlive() then + local _heliUnit = self:_GetSARHeli(_heliName) -- Wrapper.Unit#UNIT + + local _lookupKeyHeli = _heliName .. "_" .. _woundedGroupName --lookup key for message state tracking + + if _heliUnit == nil then + self.heliVisibleMessage[_lookupKeyHeli] = nil + self.heliCloseMessage[_lookupKeyHeli] = nil + self.landedStatus[_lookupKeyHeli] = nil + self:T("...helinunit nil!") + return + end + + local _heliCoord = _heliUnit:GetCoordinate() + local _leaderCoord = _woundedGroup:GetCoordinate() + local _distance = self:_GetDistance(_heliCoord,_leaderCoord) + -- autosmoke + if (self.autosmoke == true) and (_distance < self.autosmokedistance) and (_distance ~= -1) then + self:_PopSmokeForGroup(_woundedGroupName, _woundedGroup) + end + + if _distance < self.approachdist_near and _distance > 0 then + if self:_CheckCloseWoundedGroup(_distance, _heliUnit, _heliName, _woundedGroup, _woundedGroupName) == true then + -- we\'re close, reschedule + _downedpilot.timestamp = timer.getAbsTime() + self:__Approach(-5,heliname,woundedgroupname) + end + elseif _distance >= self.approachdist_near and _distance < self.approachdist_far then + -- message once + if self.heliVisibleMessage[_lookupKeyHeli] == nil then + local _pilotName = _downedpilot.desc + if self.autosmoke == true then + local dist = self.autosmokedistance / 1000 + local disttext = string.format("%.0fkm",dist) + if _SETTINGS:IsImperial() then + local dist = UTILS.MetersToNM(self.autosmokedistance) + disttext = string.format("%.0fnm",dist) + end + self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. I hear you! 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?!") + if not _downedpilot.alive then + --self:__KIA(1,_downedpilot.name) + self:_RemoveNameFromDownedPilots(_downedpilot.name, true) + end + end + return self +end + +--- (Internal) Function to pop a smoke at a wounded pilot\'s positions. +-- @param #CSAR self +-- @param #string _woundedGroupName Name of the group. +-- @param Wrapper.Group#GROUP _woundedLeader Object of the group. +function CSAR:_PopSmokeForGroup(_woundedGroupName, _woundedLeader) + self:T(self.lid .. " _PopSmokeForGroup") + -- have we popped smoke already in the last 5 mins + local _lastSmoke = self.smokeMarkers[_woundedGroupName] + if _lastSmoke == nil or timer.getTime() > _lastSmoke then + + local _smokecolor = self.smokecolor + local _smokecoord = _woundedLeader:GetCoordinate() + _smokecoord:Smoke(_smokecolor) + self.smokeMarkers[_woundedGroupName] = timer.getTime() + 300 -- next smoke time + end + return self +end + +--- (Internal) Function to pickup the wounded pilot from the ground. +-- @param #CSAR self +-- @param Wrapper.Unit#UNIT _heliUnit Object of the group. +-- @param #string _pilotName Name of the pilot. +-- @param Wrapper.Group#GROUP _woundedGroup Object of the group. +-- @param #string _woundedGroupName Name of the group. +function CSAR:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName) + self:T(self.lid .. " _PickupUnit") + -- board + local _heliName = _heliUnit:GetName() + local _groups = self.inTransitGroups[_heliName] + local _unitsInHelicopter = self:_PilotsOnboard(_heliName) + + -- init table if there is none for this helicopter + if not _groups then + self.inTransitGroups[_heliName] = {} + _groups = self.inTransitGroups[_heliName] + end + + -- if the heli can\'t pick them up, show a message and return + local _maxUnits = self.AircraftType[_heliUnit:GetTypeName()] + if _maxUnits == nil then + _maxUnits = self.max_units + end + if _unitsInHelicopter + 1 > _maxUnits then + self:_DisplayMessageToSAR(_heliUnit, string.format("%s, %s. We\'re already crammed with %d guys! Sorry!", _pilotName, _heliName, _unitsInHelicopter, _unitsInHelicopter), self.messageTime) + return true + end + + local found,downedgrouptable = self:_CheckNameInDownedPilots(_woundedGroupName) + local grouptable = downedgrouptable --#CSAR.DownedPilot + self.inTransitGroups[_heliName][_woundedGroupName] = + { + originalUnit = grouptable.originalUnit, + woundedGroup = _woundedGroupName, + side = self.coalition, + desc = grouptable.desc, + player = grouptable.player, + } + + _woundedGroup:Destroy(false) + self:_RemoveNameFromDownedPilots(_woundedGroupName,true) + + self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s I\'m in! Get to the MASH ASAP! ", _heliName, _pilotName), self.messageTime,true,true) + + self:__Boarded(5,_heliName,_woundedGroupName) + + return true +end + +--- (Internal) Move group to destination. +-- @param #CSAR self +-- @param Wrapper.Group#GROUP _leader +-- @param Core.Point#COORDINATE _destination +function CSAR:_OrderGroupToMoveToPoint(_leader, _destination) + self:T(self.lid .. " _OrderGroupToMoveToPoint") + local group = _leader + local coordinate = _destination:GetVec2() + + group:SetAIOn() + group:RouteToVec2(coordinate,5) + return self +end + + +--- (internal) Function to check if the heli door(s) are open. Thanks to Shadowze. +-- @param #CSAR self +-- @param #string unit_name Name of unit. +-- @return #boolean outcome The outcome. +function CSAR:_IsLoadingDoorOpen( unit_name ) + self:T(self.lid .. " _IsLoadingDoorOpen") + return UTILS.IsLoadingDoorOpen(unit_name) +end + +--- (Internal) Function to check if heli is close to group. +-- @param #CSAR self +-- @param #number _distance +-- @param Wrapper.Unit#UNIT _heliUnit +-- @param #string _heliName +-- @param Wrapper.Group#GROUP _woundedGroup +-- @param #string _woundedGroupName +-- @return #boolean Outcome +function CSAR:_CheckCloseWoundedGroup(_distance, _heliUnit, _heliName, _woundedGroup, _woundedGroupName) + self:T(self.lid .. " _CheckCloseWoundedGroup") + + local _woundedLeader = _woundedGroup + local _lookupKeyHeli = _heliUnit:GetName() .. "_" .. _woundedGroupName --lookup key for message state tracking + + local _found, _pilotable = self:_CheckNameInDownedPilots(_woundedGroupName) -- #boolean, #CSAR.DownedPilot + local _pilotName = _pilotable.desc + + + local _reset = true + + if (_distance < 500) then + + if self.heliCloseMessage[_lookupKeyHeli] == nil then + if self.autosmoke == true then + self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. You\'re close now! Land or hover at the smoke.", _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,false,true) + end + self.heliCloseMessage[_lookupKeyHeli] = true + end + + -- have we landed close enough? + if not _heliUnit:InAir() then + + if self.pilotRuntoExtractPoint == true then + if (_distance < self.extractDistance) then + local _time = self.landedStatus[_lookupKeyHeli] + if _time == nil then + self.landedStatus[_lookupKeyHeli] = math.floor( (_distance - self.loadDistance) / 3.6 ) + _time = self.landedStatus[_lookupKeyHeli] + self:_OrderGroupToMoveToPoint(_woundedGroup, _heliUnit:GetCoordinate()) + self:_DisplayMessageToSAR(_heliUnit, "Wait till " .. _pilotName .. " gets in. \nETA " .. _time .. " more seconds.", self.messageTime, false) + else + _time = self.landedStatus[_lookupKeyHeli] - 10 + self.landedStatus[_lookupKeyHeli] = _time + end + if _time <= 0 or _distance < self.loadDistance then + if self.pilotmustopendoors and not self:_IsLoadingDoorOpen(_heliName) then + self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true) + return true + else + self.landedStatus[_lookupKeyHeli] = nil + self:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName) + return false + end + end + end + 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!", self.messageTime, true) + return true + else + self:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName) + return false + end + end + end + else + + local _unitsInHelicopter = self:_PilotsOnboard(_heliName) + local _maxUnits = self.AircraftType[_heliUnit:GetTypeName()] + if _maxUnits == nil then + _maxUnits = self.max_units + end + + if _heliUnit:InAir() and _unitsInHelicopter + 1 <= _maxUnits then + -- TODO - make variable + if _distance < self.rescuehoverdistance then + + --check height! + local leaderheight = _woundedLeader:GetHeight() + if leaderheight < 0 then leaderheight = 0 end + local _height = _heliUnit:GetHeight() - leaderheight + + -- TODO - make variable + if _height <= self.rescuehoverheight then + + local _time = self.hoverStatus[_lookupKeyHeli] + + if _time == nil then + self.hoverStatus[_lookupKeyHeli] = 10 + _time = 10 + else + _time = self.hoverStatus[_lookupKeyHeli] - 10 + self.hoverStatus[_lookupKeyHeli] = _time + end + + if _time > 0 then + self:_DisplayMessageToSAR(_heliUnit, "Hovering above " .. _pilotName .. ". \n\nHold hover for " .. _time .. " seconds to winch them up. \n\nIf the countdown stops you\'re too far away!", self.messageTime, true) + else + if self.pilotmustopendoors and not self:_IsLoadingDoorOpen(_heliName) then + self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true) + return true + else + self.hoverStatus[_lookupKeyHeli] = nil + self:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName) + return false + end + end + _reset = false + else + self:_DisplayMessageToSAR(_heliUnit, "Too high to winch " .. _pilotName .. " \nReduce height and hover for 10 seconds!", self.messageTime, true,true) + end + end + + end + end + end + + if _reset then + self.hoverStatus[_lookupKeyHeli] = nil + end + + if _distance < 500 then + return true + else + return false + end +end + +--- (Internal) Monitor in-flight returning groups. +-- @param #CSAR self +-- @param #string heliname Heli name +-- @param #string groupname Group name +-- @param #boolean isairport If true, EVENT.Landing took place at an airport or FARP +function CSAR:_ScheduledSARFlight(heliname,groupname, isairport) + self:T(self.lid .. " _ScheduledSARFlight") + self:T({heliname,groupname}) + local _heliUnit = self:_GetSARHeli(heliname) + local _woundedGroupName = groupname + + if (_heliUnit == nil) then + --helicopter crashed? + self.inTransitGroups[heliname] = nil + return + end + + if self.inTransitGroups[heliname] == nil or self.inTransitGroups[heliname][_woundedGroupName] == nil then + -- Groups already rescued + return + end + + local _dist = self:_GetClosestMASH(_heliUnit) + + if _dist == -1 then + return + end + + if ( _dist < self.FARPRescueDistance or isairport ) and _heliUnit:InAir() == false then + if self.pilotmustopendoors and self:_IsLoadingDoorOpen(heliname) == false then + self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me out!", self.messageTime, true) + else + self:_RescuePilots(_heliUnit) + return + end + end + + --queue up + self:__Returning(-5,heliname,_woundedGroupName, isairport) + return self +end + +--- (Internal) Mark pilot as rescued and remove from tables. +-- @param #CSAR self +-- @param Wrapper.Unit#UNIT _heliUnit +function CSAR:_RescuePilots(_heliUnit) + self:T(self.lid .. " _RescuePilots") + local _heliName = _heliUnit:GetName() + local _rescuedGroups = self.inTransitGroups[_heliName] + + if _rescuedGroups == nil then + -- Groups already rescued + return + end + + local PilotsSaved = self:_PilotsOnboard(_heliName) + + self.inTransitGroups[_heliName] = nil + + local _txt = string.format("%s: The %d pilot(s) have been taken to the\nmedical clinic. Good job!", _heliName, PilotsSaved) + + self:_DisplayMessageToSAR(_heliUnit, _txt, self.messageTime) + -- trigger event + self:__Rescued(-1,_heliUnit,_heliName, PilotsSaved) + return self +end + +--- (Internal) Check and return Wrappe.Unit#UNIT based on the name if alive. +-- @param #CSAR self +-- @param #string _unitname Name of Unit +-- @return Wrapper.Unit#UNIT The unit or nil +function CSAR:_GetSARHeli(_unitName) + self:T(self.lid .. " _GetSARHeli") + local unit = UNIT:FindByName(_unitName) + if unit and unit:IsAlive() then + return unit + else + return nil + end +end + +--- (Internal) Display message to single Unit. +-- @param #CSAR self +-- @param Wrapper.Unit#UNIT _unit Unit #UNIT to display to. +-- @param #string _text Text of message. +-- @param #number _time Message show duration. +-- @param #boolean _clear (optional) Clear screen. +-- @param #boolean _speak (optional) Speak message via SRS. +function CSAR:_DisplayMessageToSAR(_unit, _text, _time, _clear, _speak) + self:T(self.lid .. " _DisplayMessageToSAR") + local group = _unit:GetGroup() + local _clear = _clear or nil + local _time = _time or self.messageTime + if not self.suppressmessages then + local m = MESSAGE:New(_text,_time,"Info",_clear):ToGroup(group) + end + -- integrate SRS + if _speak and self.useSRS then + local srstext = SOUNDTEXT:New(_text) + local path = self.SRSPath + local modulation = self.SRSModulation + local channel = self.SRSchannel + local msrs = MSRS:New(path,channel,modulation) + msrs:PlaySoundText(srstext, 2) + end + return self +end + +--- (Internal) Function to get string of a group\'s position. +-- @param #CSAR self +-- @param Wrapper.Controllable#CONTROLLABLE _woundedGroup Group or Unit object. +-- @return #string Coordinates as Text +function CSAR:_GetPositionOfWounded(_woundedGroup) + self:T(self.lid .. " _GetPositionOfWounded") + local _coordinate = _woundedGroup:GetCoordinate() + local _coordinatesText = "None" + if _coordinate then + if self.coordtype == 0 then -- Lat/Long DMTM + _coordinatesText = _coordinate:ToStringLLDDM() + elseif self.coordtype == 1 then -- Lat/Long DMS + _coordinatesText = _coordinate:ToStringLLDMS() + elseif self.coordtype == 2 then -- MGRS + _coordinatesText = _coordinate:ToStringMGRS() + else -- Bullseye Metric --(medevac.coordtype == 4 or 3) + _coordinatesText = _coordinate:ToStringBULLS(self.coalition) + end + end + return _coordinatesText +end + +--- (Internal) Display active SAR tasks to player. +-- @param #CSAR self +-- @param #string _unitName Unit to display to +function CSAR:_DisplayActiveSAR(_unitName) + self:T(self.lid .. " _DisplayActiveSAR") + local _msg = "Active MEDEVAC/SAR:" + local _heli = self:_GetSARHeli(_unitName) -- Wrapper.Unit#UNIT + if _heli == nil then + return + end + + local _heliSide = self.coalition + local _csarList = {} + + local _DownedPilotTable = self.downedPilots + self:T({Table=_DownedPilotTable}) + for _, _value in pairs(_DownedPilotTable) do + local _groupName = _value.name + self:T(string.format("Display Active Pilot: %s", tostring(_groupName))) + self:T({Table=_value}) + local _woundedGroup = _value.group + if _woundedGroup and _value.alive then + local _coordinatesText = self:_GetPositionOfWounded(_woundedGroup) + local _helicoord = _heli:GetCoordinate() + local _woundcoord = _woundedGroup:GetCoordinate() + local _distance = self:_GetDistance(_helicoord, _woundcoord) + self:T({_distance = _distance}) + local distancetext = "" + if _SETTINGS:IsImperial() then + distancetext = string.format("%.1fnm",UTILS.MetersToNM(_distance)) + else + distancetext = string.format("%.1fkm", _distance/1000.0) + end + table.insert(_csarList, { dist = _distance, msg = string.format("%s at %s - %.2f KHz ADF - %s ", _value.desc, _coordinatesText, _value.frequency / 1000, distancetext) }) + end + end + + local function sortDistance(a, b) + return a.dist < b.dist + end + + table.sort(_csarList, sortDistance) + + for _, _line in pairs(_csarList) do + _msg = _msg .. "\n" .. _line.msg + end + + self:_DisplayMessageToSAR(_heli, _msg, self.messageTime*2) + return self +end + +--- (Internal) Find the closest downed pilot to a heli. +-- @param #CSAR self +-- @param Wrapper.Unit#UNIT _heli Helicopter #UNIT +-- @return #table Table of results +function CSAR:_GetClosestDownedPilot(_heli) + self:T(self.lid .. " _GetClosestDownedPilot") + local _side = self.coalition + local _closestGroup = nil + local _shortestDistance = -1 + local _distance = 0 + local _closestGroupInfo = nil + local _heliCoord = _heli:GetCoordinate() or _heli:GetCoordinate() + + if _heliCoord == nil then + self:E("****Error obtaining coordinate!") + return nil + end + + local DownedPilotsTable = self.downedPilots + + for _, _groupInfo in UTILS.spairs(DownedPilotsTable) do + --for _, _groupInfo in pairs(DownedPilotsTable) do + local _woundedName = _groupInfo.name + local _tempWounded = _groupInfo.group + + -- check group exists and not moving to someone else + if _tempWounded then + local _tempCoord = _tempWounded:GetCoordinate() + _distance = self:_GetDistance(_heliCoord, _tempCoord) + + if _distance ~= nil and (_shortestDistance == -1 or _distance < _shortestDistance) then + _shortestDistance = _distance + _closestGroup = _tempWounded + _closestGroupInfo = _groupInfo + end + end + end + + return { pilot = _closestGroup, distance = _shortestDistance, groupInfo = _closestGroupInfo } +end + +--- (Internal) Fire a flare at the point of a downed pilot. +-- @param #CSAR self +-- @param #string _unitName Name of the unit. +function CSAR:_SignalFlare(_unitName) + self:T(self.lid .. " _SignalFlare") + local _heli = self:_GetSARHeli(_unitName) + if _heli == nil then + return + end + + local _closest = self:_GetClosestDownedPilot(_heli) + local smokedist = 8000 + if self.approachdist_far > smokedist then smokedist = self.approachdist_far end + if _closest ~= nil and _closest.pilot ~= nil and _closest.distance < smokedist then + + local _clockDir = self:_GetClockDirection(_heli, _closest.pilot) + local _distance = 0 + if _SETTINGS:IsImperial() then + _distance = string.format("%.1fnm",UTILS.MetersToNM(_closest.distance)) + else + _distance = string.format("%.1fkm",_closest.distance) + end + local _msg = string.format("%s - Popping signal flare at your %s o\'clock. Distance %s", _unitName, _clockDir, _distance) + self:_DisplayMessageToSAR(_heli, _msg, self.messageTime, false, true) + + local _coord = _closest.pilot:GetCoordinate() + _coord:FlareRed(_clockDir) + else + local _distance = smokedist + if _SETTINGS:IsImperial() then + _distance = string.format("%.1fnm",UTILS.MetersToNM(smokedist)) + else + _distance = string.format("%.1fkm",smokedist/1000) + end + self:_DisplayMessageToSAR(_heli, string.format("No Pilots within %s",_distance), self.messageTime) + end + return self +end + +--- (Internal) Display info to all SAR groups. +-- @param #CSAR self +-- @param #string _message Message to display. +-- @param #number _side Coalition of message. +-- @param #number _messagetime How long to show. +function CSAR:_DisplayToAllSAR(_message, _side, _messagetime) + self:T(self.lid .. " _DisplayToAllSAR") + local messagetime = _messagetime or self.messageTime + for _, _unitName in pairs(self.csarUnits) do + local _unit = self:_GetSARHeli(_unitName) + if _unit and not self.suppressmessages then + self:_DisplayMessageToSAR(_unit, _message, _messagetime) + end + end + return self +end + +---(Internal) Request smoke at closest downed pilot. +--@param #CSAR self +--@param #string _unitName Name of the helicopter +function CSAR:_Reqsmoke( _unitName ) + self:T(self.lid .. " _Reqsmoke") + local _heli = self:_GetSARHeli(_unitName) + if _heli == nil then + return + end + local smokedist = 8000 + if smokedist < self.approachdist_far then smokedist = self.approachdist_far end + local _closest = self:_GetClosestDownedPilot(_heli) + if _closest ~= nil and _closest.pilot ~= nil and _closest.distance < smokedist then + local _clockDir = self:_GetClockDirection(_heli, _closest.pilot) + local _distance = 0 + if _SETTINGS:IsImperial() then + _distance = string.format("%.1fnm",UTILS.MetersToNM(_closest.distance)) + else + _distance = string.format("%.1fkm",_closest.distance) + end + local _msg = string.format("%s - Popping signal smoke at your %s o\'clock. Distance %s", _unitName, _clockDir, _distance) + self:_DisplayMessageToSAR(_heli, _msg, self.messageTime, false, true) + local _coord = _closest.pilot:GetCoordinate() + local color = self.smokecolor + _coord:Smoke(color) + else + local _distance = 0 + if _SETTINGS:IsImperial() then + _distance = string.format("%.1fnm",UTILS.MetersToNM(smokedist)) + else + _distance = string.format("%.1fkm",smokedist/1000) + end + self:_DisplayMessageToSAR(_heli, string.format("No Pilots within %s",_distance), self.messageTime) + end + return self +end + +--- (Internal) Determine distance to closest MASH. +-- @param #CSAR self +-- @param Wrapper.Unit#UNIT _heli Helicopter #UNIT +-- @retunr +function CSAR:_GetClosestMASH(_heli) + self:T(self.lid .. " _GetClosestMASH") + local _mashset = self.mash -- Core.Set#SET_GROUP + local _mashes = _mashset:GetSetObjects() -- #table + local _shortestDistance = -1 + local _distance = 0 + local _helicoord = _heli:GetCoordinate() + + local function GetCloseAirbase(coordinate,Coalition,Category) + + local a=coordinate:GetVec3() + local distmin=math.huge + local airbase=nil + for DCSairbaseID, DCSairbase in pairs(world.getAirbases(Coalition)) do + local b=DCSairbase:getPoint() + + local c=UTILS.VecSubstract(a,b) + local dist=UTILS.VecNorm(c) + + if dist 0 then + local PilotTable = self.downedPilots + for _,_pilot in pairs (PilotTable) do + self:T({_pilot}) + local pilot = _pilot -- #CSAR.DownedPilot + local group = pilot.group + local frequency = pilot.frequency or 0 -- thanks to @Thrud + if group and group:IsAlive() and frequency > 0 then + self:_AddBeaconToGroup(group,frequency) + end + end + end + return self +end + +--- (Internal) Helper function to count active downed pilots. +-- @param #CSAR self +-- @return #number Number of pilots in the field. +function CSAR:_CountActiveDownedPilots() + self:T(self.lid .. " _CountActiveDownedPilots") + local PilotsInFieldN = 0 + for _, _unitName in pairs(self.downedPilots) do + self:T({_unitName.desc}) + if _unitName.alive == true then + PilotsInFieldN = PilotsInFieldN + 1 + end + end + return PilotsInFieldN +end + +--- (Internal) Helper to decide if we're over max limit. +-- @param #CSAR self +-- @return #boolean True or false. +function CSAR:_ReachedPilotLimit() + self:T(self.lid .. " _ReachedPilotLimit") + local limit = self.maxdownedpilots + local islimited = self.limitmaxdownedpilots + local count = self:_CountActiveDownedPilots() + if islimited and (count >= limit) then + return true + else + return false + end +end + + ------------------------------ + --- FSM internal Functions --- + ------------------------------ + +--- (Internal) Function called after Start() event. +-- @param #CSAR self. +-- @param #string From From state. +-- @param #string Event Event triggered. +-- @param #string To To state. +function CSAR:onafterStart(From, Event, To) + self:T({From, Event, To}) + self:I(self.lid .. "Started.") + -- event handler + self:HandleEvent(EVENTS.Takeoff, self._EventHandler) + self:HandleEvent(EVENTS.Land, self._EventHandler) + self:HandleEvent(EVENTS.Ejection, self._EventHandler) + self:HandleEvent(EVENTS.PlayerEnterAircraft, self._EventHandler) + self:HandleEvent(EVENTS.PlayerEnterUnit, self._EventHandler) + self:HandleEvent(EVENTS.PilotDead, self._EventHandler) + if self.useprefix then + local prefixes = self.csarPrefix or {} + self.allheligroupset = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(prefixes):FilterCategoryHelicopter():FilterStart() + else + self.allheligroupset = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterCategoryHelicopter():FilterStart() + end + self.mash = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(self.mashprefix):FilterStart() -- currently only GROUP objects, maybe support STATICs also? + self:__Status(-10) + return self +end + +--- (Internal) Function called before Status() event. +-- @param #CSAR self +function CSAR:_CheckDownedPilotTable() + local pilots = self.downedPilots + local npilots = {} + + for _ind,_entry in pairs(pilots) do + local _group = _entry.group + if _group:IsAlive() then + npilots[_ind] = _entry + else + if _entry.alive then + self:__KIA(1,_entry.desc) + end + end + end + self.downedPilots = npilots + return self +end + +--- (Internal) Function called before Status() event. +-- @param #CSAR self. +-- @param #string From From state. +-- @param #string Event Event triggered. +-- @param #string To To state. +function CSAR:onbeforeStatus(From, Event, To) + self:T({From, Event, To}) + -- housekeeping + self:_AddMedevacMenuItem() + self:_RefreshRadioBeacons() + self:_CheckDownedPilotTable() + for _,_sar in pairs (self.csarUnits) do + local PilotTable = self.downedPilots + for _,_entry in pairs (PilotTable) do + if _entry.alive then + local entry = _entry -- #CSAR.DownedPilot + local name = entry.name + local timestamp = entry.timestamp or 0 + local now = timer.getAbsTime() + if now - timestamp > 17 then -- only check if we\'re not in approach mode, which is iterations of 5 and 10. + self:_CheckWoundedGroupStatus(_sar,name) + end + end + end + end + return self +end + +--- (Internal) Function called after Status() event. +-- @param #CSAR self. +-- @param #string From From state. +-- @param #string Event Event triggered. +-- @param #string To To state. +function CSAR:onafterStatus(From, Event, To) + self:T({From, Event, To}) + -- collect some stats + local NumberOfSARPilots = 0 + for _, _unitName in pairs(self.csarUnits) do + NumberOfSARPilots = NumberOfSARPilots + 1 + end + + local PilotsInFieldN = self:_CountActiveDownedPilots() + + local PilotsBoarded = 0 + for _, _unitName in pairs(self.inTransitGroups) do + for _,_units in pairs(_unitName) do + PilotsBoarded = PilotsBoarded + 1 + end + end + + if self.verbose > 0 then + local text = string.format("%s Active SAR: %d | Downed Pilots in field: %d (max %d) | Pilots boarded: %d | Landings: %d | Pilots rescued: %d", + self.lid,NumberOfSARPilots,PilotsInFieldN,self.maxdownedpilots,PilotsBoarded,self.rescues,self.rescuedpilots) + self:T(text) + if self.verbose < 2 then + self:I(text) + elseif self.verbose > 1 then + self:I(text) + local m = MESSAGE:New(text,"10","Status",true):ToCoalition(self.coalition) + end + end + self:__Status(-20) + return self +end + +--- (Internal) Function called after Stop() event. +-- @param #CSAR self. +-- @param #string From From state. +-- @param #string Event Event triggered. +-- @param #string To To state. +function CSAR:onafterStop(From, Event, To) + self:T({From, Event, To}) + -- event handler + self:UnHandleEvent(EVENTS.Takeoff) + self:UnHandleEvent(EVENTS.Land) + self:UnHandleEvent(EVENTS.Ejection) + self:UnHandleEvent(EVENTS.PlayerEnterUnit) + self:UnHandleEvent(EVENTS.PlayerEnterAircraft) + self:UnHandleEvent(EVENTS.PilotDead) + self:T(self.lid .. "Stopped.") + return self +end + +--- (Internal) Function called before Approach() event. +-- @param #CSAR self. +-- @param #string From From state. +-- @param #string Event Event triggered. +-- @param #string To To state. +-- @param #string Heliname Name of the helicopter group. +-- @param #string Woundedgroupname Name of the downed pilot\'s group. +function CSAR:onbeforeApproach(From, Event, To, Heliname, Woundedgroupname) + self:T({From, Event, To, Heliname, Woundedgroupname}) + self:_CheckWoundedGroupStatus(Heliname,Woundedgroupname) + return self +end + +--- (Internal) Function called before Boarded() event. +-- @param #CSAR self. +-- @param #string From From state. +-- @param #string Event Event triggered. +-- @param #string To To state. +-- @param #string Heliname Name of the helicopter group. +-- @param #string Woundedgroupname Name of the downed pilot\'s group. +function CSAR:onbeforeBoarded(From, Event, To, Heliname, Woundedgroupname) + self:T({From, Event, To, Heliname, Woundedgroupname}) + self:_ScheduledSARFlight(Heliname,Woundedgroupname) + return self +end + +--- (Internal) Function called before Returning() event. +-- @param #CSAR self. +-- @param #string From From state. +-- @param #string Event Event triggered. +-- @param #string To To state. +-- @param #string Heliname Name of the helicopter group. +-- @param #string Woundedgroupname Name of the downed pilot\'s group. +-- @param #boolean IsAirport True if heli has landed on an AFB (from event land). +function CSAR:onbeforeReturning(From, Event, To, Heliname, Woundedgroupname, IsAirPort) + self:T({From, Event, To, Heliname, Woundedgroupname}) + self:_ScheduledSARFlight(Heliname,Woundedgroupname, IsAirPort) + return self +end + +--- (Internal) Function called before Rescued() event. +-- @param #CSAR self. +-- @param #string From From state. +-- @param #string Event Event triggered. +-- @param #string To To state. +-- @param Wrapper.Unit#UNIT HeliUnit Unit of the helicopter. +-- @param #string HeliName Name of the helicopter group. +-- @param #number PilotsSaved Number of the saved pilots on board when landing. +function CSAR:onbeforeRescued(From, Event, To, HeliUnit, HeliName, PilotsSaved) + self:T({From, Event, To, HeliName, HeliUnit}) + self.rescues = self.rescues + 1 + self.rescuedpilots = self.rescuedpilots + PilotsSaved + return self +end + +--- (Internal) Function called before PilotDown() event. +-- @param #CSAR self. +-- @param #string From From state. +-- @param #string Event Event triggered. +-- @param #string To To state. +-- @param Wrapper.Group#GROUP Group Group object of the downed pilot. +-- @param #number Frequency Beacon frequency in kHz. +-- @param #string Leadername Name of the #UNIT of the downed pilot. +-- @param #string CoordinatesText String of the position of the pilot. Format determined by self.coordtype. +function CSAR:onbeforePilotDown(From, Event, To, Group, Frequency, Leadername, CoordinatesText) + self:T({From, Event, To, Group, Frequency, Leadername, CoordinatesText}) + return self +end +-------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- End Ops.CSAR +-------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- **Ops** -- Combat Troops & Logistics Department. +-- +-- === +-- +-- **CTLD** - MOOSE based Helicopter CTLD Operations. +-- +-- === +-- +-- ## Missions: +-- +-- ### [CTLD - Combat Troop & Logistics Deployment](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/OPS%20-%20CTLD) +-- +-- === +-- +-- **Main Features:** +-- +-- * MOOSE-based Helicopter CTLD Operations for Players. +-- +-- === +-- +-- ### Author: **Applevangelist** (Moose Version), ***Ciribob*** (original), Thanks to: Shadowze, Cammel (testing), bbirchnz (additional code!!) +-- @module Ops.CTLD +-- @image OPS_CTLD.jpg + +-- Date: Oct 2021 + +do +------------------------------------------------------ +--- **CTLD_ENGINEERING** class, extends Core.Base#BASE +-- @type CTLD_ENGINEERING +-- @field #string ClassName +-- @field #string lid +-- @field #string Name +-- @field Wrapper.Group#GROUP Group +-- @field Wrapper.Unit#UNIT Unit +-- @field Wrapper.Group#GROUP HeliGroup +-- @field Wrapper.Unit#UNIT HeliUnit +-- @field #string State +-- @extends Core.Base#BASE +CTLD_ENGINEERING = { + ClassName = "CTLD_ENGINEERING", + lid = "", + Name = "none", + Group = nil, + Unit = nil, + --C_Ops = nil, + HeliGroup = nil, + HeliUnit = nil, + State = "", + } + + --- CTLD_ENGINEERING class version. + -- @field #string version + CTLD_ENGINEERING.Version = "0.0.3" + + --- Create a new instance. + -- @param #CTLD_ENGINEERING self + -- @param #string Name + -- @param #string GroupName Name of Engineering #GROUP object + -- @param Wrapper.Group#GROUP HeliGroup HeliGroup + -- @param Wrapper.Unit#UNIT HeliUnit HeliUnit + -- @return #CTLD_ENGINEERING self + function CTLD_ENGINEERING:New(Name, GroupName, HeliGroup, HeliUnit) + + -- Inherit everything from BASE class. + local self=BASE:Inherit(self, BASE:New()) -- #CTLD_ENGINEERING + + --BASE:I({Name, GroupName}) + + self.Name = Name or "Engineer Squad" -- #string + self.Group = GROUP:FindByName(GroupName) -- Wrapper.Group#GROUP + self.Unit = self.Group:GetUnit(1) -- Wrapper.Unit#UNIT + --self.C_Ops = C_Ops -- Ops.CTLD#CTLD + self.HeliGroup = HeliGroup -- Wrapper.Group#GROUP + self.HeliUnit = HeliUnit -- Wrapper.Unit#UNIT + --self.distance = Distance or UTILS.NMToMeters(1) + self.currwpt = nil -- Core.Point#COORDINATE + self.lid = string.format("%s (%s) | ",self.Name, self.Version) + -- Start State. + self.State = "Stopped" + self.marktimer = 300 -- wait this many secs before trying a crate again + + --[[ Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start FSM. + self:AddTransition("*", "Status", "*") + self:AddTransition("*", "Search", "Searching") + self:AddTransition("*", "Move", "Moving") + self:AddTransition("*", "Arrive", "Arrived") + self:AddTransition("*", "Build", "Building") + self:AddTransition("*", "Done", "Running") + self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. + + self:__Start(5) + --]] + self:Start() + local parent = self:GetParent(self) + return self + end + + --- (Internal) Set the status + -- @param #CTLD_ENGINEERING self + -- @param #string State + -- @return #CTLD_ENGINEERING self + function CTLD_ENGINEERING:SetStatus(State) + self.State = State + return self + end + + --- (Internal) Get the status + -- @param #CTLD_ENGINEERING self + -- @return #string State + function CTLD_ENGINEERING:GetStatus() + return self.State + end + + --- (Internal) Check the status + -- @param #CTLD_ENGINEERING self + -- @param #string State + -- @return #boolean Outcome + function CTLD_ENGINEERING:IsStatus(State) + return self.State == State + end + + --- (Internal) Check the negative status + -- @param #CTLD_ENGINEERING self + -- @param #string State + -- @return #boolean Outcome + function CTLD_ENGINEERING:IsNotStatus(State) + return self.State ~= State + end + + --- (Internal) Set start status. + -- @param #CTLD_ENGINEERING self + -- @return #CTLD_ENGINEERING self + function CTLD_ENGINEERING:Start() + self:T(self.lid.."Start") + self:SetStatus("Running") + return self + end + + --- (Internal) Set stop status. + -- @param #CTLD_ENGINEERING self + -- @return #CTLD_ENGINEERING self + function CTLD_ENGINEERING:Stop() + self:T(self.lid.."Stop") + self:SetStatus("Stopped") + return self + end + + --- (Internal) Set build status. + -- @param #CTLD_ENGINEERING self + -- @return #CTLD_ENGINEERING self + function CTLD_ENGINEERING:Build() + self:T(self.lid.."Build") + self:SetStatus("Building") + return self + end + + --- (Internal) Set done status. + -- @param #CTLD_ENGINEERING self + -- @return #CTLD_ENGINEERING self + function CTLD_ENGINEERING:Done() + self:T(self.lid.."Done") + local grp = self.Group -- Wrapper.Group#GROUP + grp:RelocateGroundRandomInRadius(7,100,false,false,"Diamond") + self:SetStatus("Running") + return self + end + + --- (Internal) Search for crates in reach. + -- @param #CTLD_ENGINEERING self + -- @param #table crates Table of found crate Ops.CTLD#CTLD_CARGO objects. + -- @param #number number Number of crates found. + -- @return #CTLD_ENGINEERING self + function CTLD_ENGINEERING:Search(crates,number) + self:T(self.lid.."Search") + self:SetStatus("Searching") + -- find crates close by + --local COps = self.C_Ops -- Ops.CTLD#CTLD + local dist = self.distance -- #number + local group = self.Group -- Wrapper.Group#GROUP + --local crates,number = COps:_FindCratesNearby(group,nil, dist) -- #table + local ctable = {} + local ind = 0 + if number > 0 then + -- get set of dropped only + for _,_cargo in pairs (crates) do + local cgotype = _cargo:GetType() + if _cargo:WasDropped() and cgotype ~= CTLD_CARGO.Enum.STATIC then + local ok = false + local chalk = _cargo:GetMark() + if chalk == nil then + ok = true + else + -- have we tried this cargo recently? + local tag = chalk.tag or "none" + local timestamp = chalk.timestamp or 0 + --self:I({chalk}) + -- enough time gone? + local gone = timer.getAbsTime() - timestamp + --self:I({time=gone}) + if gone >= self.marktimer then + ok = true + _cargo:WipeMark() + end -- end time check + end -- end chalk + if ok then + local chalk = {} + chalk.tag = "Engineers" + chalk.timestamp = timer.getAbsTime() + _cargo:AddMark(chalk) + ind = ind + 1 + table.insert(ctable,ind,_cargo) + end + end -- end dropped + end -- end for + end -- end number + + if ind > 0 then + local crate = ctable[1] -- Ops.CTLD#CTLD_CARGO + local static = crate:GetPositionable() -- Wrapper.Static#STATIC + local crate_pos = static:GetCoordinate() -- Core.Point#COORDINATE + local gpos = group:GetCoordinate() -- Core.Point#COORDINATE + -- see how far we are from the crate + local distance = self:_GetDistance(gpos,crate_pos) + self:T(string.format("%s Distance to crate: %d", self.lid, distance)) + -- move there + if distance > 30 and distance ~= -1 and self:IsStatus("Searching") then + group:RouteGroundTo(crate_pos,15,"Line abreast",1) + self.currwpt = crate_pos -- Core.Point#COORDINATE + self:Move() + elseif distance <= 30 and distance ~= -1 then + -- arrived + self:Arrive() + end + else + self:T(self.lid.."No crates in reach!") + end + return self + end + + --- (Internal) Move towards crates in reach. + -- @param #CTLD_ENGINEERING self + -- @return #CTLD_ENGINEERING self + function CTLD_ENGINEERING:Move() + self:T(self.lid.."Move") + self:SetStatus("Moving") + -- check if we arrived on target + --local COps = self.C_Ops -- Ops.CTLD#CTLD + local group = self.Group -- Wrapper.Group#GROUP + local tgtpos = self.currwpt -- Core.Point#COORDINATE + local gpos = group:GetCoordinate() -- Core.Point#COORDINATE + -- see how far we are from the crate + local distance = self:_GetDistance(gpos,tgtpos) + self:T(string.format("%s Distance remaining: %d", self.lid, distance)) + if distance <= 30 and distance ~= -1 then + -- arrived + self:Arrive() + end + return self + end + + --- (Internal) Arrived at crates in reach. Stop group. + -- @param #CTLD_ENGINEERING self + -- @return #CTLD_ENGINEERING self + function CTLD_ENGINEERING:Arrive() + self:T(self.lid.."Arrive") + self:SetStatus("Arrived") + self.currwpt = nil + local Grp = self.Group -- Wrapper.Group#GROUP + Grp:RouteStop() + return self + end + + --- (Internal) Return distance in meters between two coordinates. + -- @param #CTLD_ENGINEERING self + -- @param Core.Point#COORDINATE _point1 Coordinate one + -- @param Core.Point#COORDINATE _point2 Coordinate two + -- @return #number Distance in meters or -1 + function CTLD_ENGINEERING:_GetDistance(_point1, _point2) + self:T(self.lid .. " _GetDistance") + if _point1 and _point2 then + local distance1 = _point1:Get2DDistance(_point2) + local distance2 = _point1:DistanceFromPointVec2(_point2) + --self:I({dist1=distance1, dist2=distance2}) + if distance1 and type(distance1) == "number" then + return distance1 + elseif distance2 and type(distance2) == "number" then + return distance2 + else + self:E("*****Cannot calculate distance!") + self:E({_point1,_point2}) + return -1 + end + else + self:E("******Cannot calculate distance!") + self:E({_point1,_point2}) + return -1 + end + end +------------------------------------------------------ +--- **CTLD_CARGO** class, extends Core.Base#BASE +-- @type CTLD_CARGO +-- @field #number ID ID of this cargo. +-- @field #string Name Name for menu. +-- @field #table Templates Table of #POSITIONABLE objects. +-- @field #CTLD_CARGO.Enum CargoType Enumerator of Type. +-- @field #boolean HasBeenMoved Flag for moving. +-- @field #boolean LoadDirectly Flag for direct loading. +-- @field #number CratesNeeded Crates needed to build. +-- @field Wrapper.Positionable#POSITIONABLE Positionable Representation of cargo in the mission. +-- @field #boolean HasBeenDropped True if dropped from heli. +-- @field #number PerCrateMass Mass in kg +-- @field #number Stock Number of builds available, -1 for unlimited +-- @extends Core.Base#BASE +CTLD_CARGO = { + ClassName = "CTLD_CARGO", + ID = 0, + Name = "none", + Templates = {}, + CargoType = "none", + HasBeenMoved = false, + LoadDirectly = false, + CratesNeeded = 0, + Positionable = nil, + HasBeenDropped = false, + PerCrateMass = 0, + Stock = nil, + Mark = nil, + } + + --- Define cargo types. + -- @type CTLD_CARGO.Enum + -- @field #string Type Type of Cargo. + CTLD_CARGO.Enum = { + ["VEHICLE"] = "Vehicle", -- #string vehicles + ["TROOPS"] = "Troops", -- #string troops + ["FOB"] = "FOB", -- #string FOB + ["CRATE"] = "Crate", -- #string crate + ["REPAIR"] = "Repair", -- #string repair + ["ENGINEERS"] = "Engineers", -- #string engineers + ["STATIC"] = "Static", -- #string engineers + } + + --- Function to create new CTLD_CARGO object. + -- @param #CTLD_CARGO self + -- @param #number ID ID of this #CTLD_CARGO + -- @param #string Name Name for menu. + -- @param #table Templates Table of #POSITIONABLE objects. + -- @param #CTLD_CARGO.Enum Sorte Enumerator of Type. + -- @param #boolean HasBeenMoved Flag for moving. + -- @param #boolean LoadDirectly Flag for direct loading. + -- @param #number CratesNeeded Crates needed to build. + -- @param Wrapper.Positionable#POSITIONABLE Positionable Representation of cargo in the mission. + -- @param #boolean Dropped Cargo/Troops have been unloaded from a chopper. + -- @param #number PerCrateMass Mass in kg + -- @param #number Stock Number of builds available, nil for unlimited + -- @return #CTLD_CARGO self + function CTLD_CARGO:New(ID, Name, Templates, Sorte, HasBeenMoved, LoadDirectly, CratesNeeded, Positionable, Dropped, PerCrateMass, Stock) + -- Inherit everything from BASE class. + local self=BASE:Inherit(self, BASE:New()) -- #CTLD + self:T({ID, Name, Templates, Sorte, HasBeenMoved, LoadDirectly, CratesNeeded, Positionable, Dropped}) + self.ID = ID or math.random(100000,1000000) + self.Name = Name or "none" -- #string + self.Templates = Templates or {} -- #table + self.CargoType = Sorte or "type" -- #CTLD_CARGO.Enum + 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 + self.PerCrateMass = PerCrateMass or 0 -- #number + self.Stock = Stock or nil --#number + self.Mark = nil + return self + end + + --- Query ID. + -- @param #CTLD_CARGO self + -- @return #number ID + function CTLD_CARGO:GetID() + return self.ID + end + + --- Query Mass. + -- @param #CTLD_CARGO self + -- @return #number Mass in kg + function CTLD_CARGO:GetMass() + return self.PerCrateMass + end + --- Query Name. + -- @param #CTLD_CARGO self + -- @return #string Name + function CTLD_CARGO:GetName() + return self.Name + end + + --- Query Templates. + -- @param #CTLD_CARGO self + -- @return #table Templates + function CTLD_CARGO:GetTemplates() + return self.Templates + end + + --- Query has moved. + -- @param #CTLD_CARGO self + -- @return #boolean Has moved + function CTLD_CARGO:HasMoved() + return self.HasBeenMoved + end + + --- Query was dropped. + -- @param #CTLD_CARGO self + -- @return #boolean Has been dropped. + function CTLD_CARGO:WasDropped() + return self.HasBeenDropped + end + + --- Query directly loadable. + -- @param #CTLD_CARGO self + -- @return #boolean loadable + function CTLD_CARGO:CanLoadDirectly() + return self.LoadDirectly + end + + --- Query number of crates or troopsize. + -- @param #CTLD_CARGO self + -- @return #number Crates or size of troops. + function CTLD_CARGO:GetCratesNeeded() + return self.CratesNeeded + end + + --- Query type. + -- @param #CTLD_CARGO self + -- @return #CTLD_CARGO.Enum Type + function CTLD_CARGO:GetType() + return self.CargoType + end + + --- Query type. + -- @param #CTLD_CARGO self + -- @return Wrapper.Positionable#POSITIONABLE Positionable + function CTLD_CARGO:GetPositionable() + return self.Positionable + end + + --- Set HasMoved. + -- @param #CTLD_CARGO self + -- @param #boolean moved + function CTLD_CARGO:SetHasMoved(moved) + self.HasBeenMoved = moved or false + end + + --- Query if cargo has been loaded. + -- @param #CTLD_CARGO self + -- @param #boolean loaded + function CTLD_CARGO:Isloaded() + if self.HasBeenMoved and not self.WasDropped() then + return true + else + return false + end + end + + --- Set WasDropped. + -- @param #CTLD_CARGO self + -- @param #boolean dropped + function CTLD_CARGO:SetWasDropped(dropped) + self.HasBeenDropped = dropped or false + end + + --- Get Stock. + -- @param #CTLD_CARGO self + -- @return #number Stock + function CTLD_CARGO:GetStock() + if self.Stock then + return self.Stock + else + return -1 + end + end + + --- Add Stock. + -- @param #CTLD_CARGO self + -- @param #number Number to add, one if nil. + -- @return #CTLD_CARGO self + function CTLD_CARGO:AddStock(Number) + if self.Stock then -- Stock nil? + local number = Number or 1 + self.Stock = self.Stock + number + end + return self + end + + --- Remove Stock. + -- @param #CTLD_CARGO self + -- @param #number Number to reduce, one if nil. + -- @return #CTLD_CARGO self + function CTLD_CARGO:RemoveStock(Number) + if self.Stock then -- Stock nil? + local number = Number or 1 + self.Stock = self.Stock - number + if self.Stock < 0 then self.Stock = 0 end + end + return self + 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 + + --- Query crate type for STATIC + -- @param #CTLD_CARGO self + -- @param #boolean + function CTLD_CARGO:IsStatic() + if self.CargoType == "Static" then + return true + else + return false + end + end + + function CTLD_CARGO:AddMark(Mark) + self.Mark = Mark + return self + end + + function CTLD_CARGO:GetMark(Mark) + return self.Mark + end + + function CTLD_CARGO:WipeMark() + self.Mark = nil + return self + end + +end + +do +------------------------------------------------------------------------- +--- **CTLD** class, extends Core.Base#BASE, Core.Fsm#FSM +-- @type CTLD +-- @field #string ClassName Name of the class. +-- @field #number verbose Verbosity level. +-- @field #string lid Class id string for output to DCS log file. +-- @field #number coalition Coalition side number, e.g. `coalition.side.RED`. +-- @extends Core.Fsm#FSM + +--- *Combat Troop & Logistics Deployment (CTLD): Everyone wants to be a POG, until there\'s POG stuff to be done.* (Mil Saying) +-- +-- === +-- +-- ![Banner Image](OPS_CTLD.jpg) +-- +-- # CTLD Concept +-- +-- * MOOSE-based CTLD for Players. +-- * Object oriented refactoring of Ciribob\'s fantastic CTLD script. +-- * No need for extra MIST loading. +-- * Additional events to tailor your mission. +-- * ANY late activated group can serve as cargo, either as troops, crates, which have to be build on-location, or static like ammo chests. +-- * Option to persist (save&load) your dropped troops, crates and vehicles. +-- +-- ## 0. Prerequisites +-- +-- You need to load an .ogg soundfile for the pilot\'s beacons into the mission, e.g. "beacon.ogg", use a once trigger, "sound to country" for that. +-- Create the late-activated troops, vehicles (no statics at this point!) that will make up your deployable forces. +-- +-- ## 1. Basic Setup +-- +-- ## 1.1 Create and start a CTLD instance +-- +-- A basic setup example is the following: +-- +-- -- Instantiate and start a CTLD for the blue side, using helicopter groups named "Helicargo" and alias "Lufttransportbrigade I" +-- local my_ctld = CTLD:New(coalition.side.BLUE,{"Helicargo"},"Lufttransportbrigade I") +-- my_ctld:__Start(5) +-- +-- ## 1.2 Add cargo types available +-- +-- Add *generic* cargo types that you need for your missions, here infantry units, vehicles and a FOB. These need to be late-activated Wrapper.Group#GROUP objects: +-- +-- -- add infantry unit called "Anti-Tank Small" using template "ATS", of type TROOP with size 3 +-- -- infantry units will be loaded directly from LOAD zones into the heli (matching number of free seats needed) +-- my_ctld:AddTroopsCargo("Anti-Tank Small",{"ATS"},CTLD_CARGO.Enum.TROOPS,3) +-- -- if you want to add weight to your Heli, troops can have a weight in kg **per person**. Currently no max weight checked. Fly carefully. +-- my_ctld:AddTroopsCargo("Anti-Tank Small",{"ATS"},CTLD_CARGO.Enum.TROOPS,3,80) +-- +-- -- add infantry unit called "Anti-Tank" using templates "AA" and "AA"", of type TROOP with size 4. No weight. We only have 2 in stock: +-- my_ctld:AddTroopsCargo("Anti-Air",{"AA","AA2"},CTLD_CARGO.Enum.TROOPS,4,nil,2) +-- +-- -- add an engineers unit called "Wrenches" using template "Engineers", of type ENGINEERS with size 2. Engineers can be loaded, dropped, +-- -- and extracted like troops. However, they will seek to build and/or repair crates found in a given radius. Handy if you can\'t stay +-- -- to build or repair or under fire. +-- my_ctld:AddTroopsCargo("Wrenches",{"Engineers"},CTLD_CARGO.Enum.ENGINEERS,4) +-- myctld.EngineerSearch = 2000 -- teams will search for crates in this radius. +-- +-- -- add vehicle called "Humvee" using template "Humvee", of type VEHICLE, size 2, i.e. needs two crates to be build +-- -- vehicles and FOB will be spawned as crates in a LOAD zone first. Once transported to DROP zones, they can be build into the objects +-- my_ctld:AddCratesCargo("Humvee",{"Humvee"},CTLD_CARGO.Enum.VEHICLE,2) +-- -- if you want to add weight to your Heli, crates can have a weight in kg **per crate**. Currently no max weight checked. Fly carefully. +-- my_ctld:AddCratesCargo("Humvee",{"Humvee"},CTLD_CARGO.Enum.VEHICLE,2,2775) +-- -- if you want to limit your stock, add a number (here: 10) as parameter after weight. No parameter / nil means unlimited stock. +-- my_ctld:AddCratesCargo("Humvee",{"Humvee"},CTLD_CARGO.Enum.VEHICLE,2,2775,10) +-- +-- -- 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) +-- my_ctld.repairtime = 300 -- takes 300 seconds to repair something +-- +-- -- add static cargo objects, e.g ammo chests - the name needs to refer to a STATIC object in the mission editor, +-- -- here: it\'s the UNIT name (not the GROUP name!), the second parameter is the weight in kg. +-- my_ctld:AddStaticsCargo("Ammunition",500) +-- +-- ## 1.3 Add logistics zones +-- +-- Add zones for loading troops and crates and dropping, building crates +-- +-- -- Add a zone of type LOAD to our setup. Players can load troops and crates. +-- -- "Loadzone" is the name of the zone from the ME. Players can load, if they are inside the zone. +-- -- Smoke and Flare color for this zone is blue, it is active (can be used) and has a radio beacon. +-- my_ctld:AddCTLDZone("Loadzone",CTLD.CargoZoneType.LOAD,SMOKECOLOR.Blue,true,true) +-- +-- -- Add a zone of type DROP. Players can drop crates here. +-- -- Smoke and Flare color for this zone is blue, it is active (can be used) and has a radio beacon. +-- -- NOTE: Troops can be unloaded anywhere, also when hovering in parameters. +-- my_ctld:AddCTLDZone("Dropzone",CTLD.CargoZoneType.DROP,SMOKECOLOR.Red,true,true) +-- +-- -- Add two zones of type MOVE. Dropped troops and vehicles will move to the nearest one. See options. +-- -- Smoke and Flare color for this zone is blue, it is active (can be used) and has a radio beacon. +-- my_ctld:AddCTLDZone("Movezone",CTLD.CargoZoneType.MOVE,SMOKECOLOR.Orange,false,false) +-- +-- my_ctld:AddCTLDZone("Movezone2",CTLD.CargoZoneType.MOVE,SMOKECOLOR.White,true,true) +-- +-- -- Add a zone of type SHIP to our setup. Players can load troops and crates from this ship +-- -- "Tarawa" is the unitname (callsign) of the ship from the ME. Players can load, if they are inside the zone. +-- -- The ship is 240 meters long and 20 meters wide. +-- -- Note that you need to adjust the max hover height to deck height plus 5 meters or so for loading to work. +-- -- When the ship is moving, forcing hoverload might not be a good idea. +-- my_ctld:AddCTLDZone("Tarawa",CTLD.CargoZoneType.SHIP,SMOKECOLOR.Blue,true,true,240,20) +-- +-- ## 2. Options +-- +-- The following options are available (with their defaults). Only set the ones you want changed: +-- +-- 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 = 35 -- 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. +-- my_ctld.hoverautoloading = true -- Crates in CrateDistance in a LOAD zone will be loaded automatically if space allows. +-- my_ctld.smokedistance = 2000 -- Smoke or flares can be request for zones this far away (in meters). +-- my_ctld.movetroopstowpzone = true -- Troops and vehicles will move to the nearest MOVE zone... +-- 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. +-- my_ctld.cratecountry = country.id.GERMANY -- ID of crates. Will default to country.id.RUSSIA for RED coalition setups. +-- my_ctld.allowcratepickupagain = true -- allow re-pickup crates that were dropped. +-- my_ctld.enableslingload = false -- allow cargos to be slingloaded - might not work for all cargo types +-- my_ctld.pilotmustopendoors = false -- -- force opening of doors +-- +-- ## 2.1 User functions +-- +-- ### 2.1.1 Adjust or add chopper unit-type capabilities +-- +-- Use this function to adjust what a heli type can or cannot do: +-- +-- -- E.g. update unit capabilities for testing. Please stay realistic in your mission design. +-- -- Make a Gazelle into a heavy truck, this type can load both crates and troops and eight of each type: +-- my_ctld:UnitCapabilities("SA342L", true, true, 8, 8, 12) +-- +-- -- Default unit type capabilities are: +-- +-- ["SA342Mistral"] = {type="SA342Mistral", crates=false, troops=true, cratelimit = 0, trooplimit = 4, length = 12}, +-- ["SA342L"] = {type="SA342L", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 12}, +-- ["SA342M"] = {type="SA342M", crates=false, troops=true, cratelimit = 0, trooplimit = 4, length = 12}, +-- ["SA342Minigun"] = {type="SA342Minigun", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 12}, +-- ["UH-1H"] = {type="UH-1H", crates=true, troops=true, cratelimit = 1, trooplimit = 8, length = 15}, +-- ["Mi-8MT"] = {type="Mi-8MTV2", crates=true, troops=true, cratelimit = 2, trooplimit = 12, length = 15}, +-- ["Ka-50"] = {type="Ka-50", crates=false, troops=false, cratelimit = 0, trooplimit = 0, length = 15}, +-- ["Mi-24P"] = {type="Mi-24P", crates=true, troops=true, cratelimit = 2, trooplimit = 8, length = 18}, +-- ["Mi-24V"] = {type="Mi-24V", crates=true, troops=true, cratelimit = 2, trooplimit = 8, length = 18}, +-- ["Hercules"] = {type="Hercules", crates=true, troops=true, cratelimit = 7, trooplimit = 64, length = 25}, +-- +-- +-- ### 2.1.2 Activate and deactivate zones +-- +-- Activate a zone: +-- +-- -- Activate zone called Name of type #CTLD.CargoZoneType ZoneType: +-- my_ctld:ActivateZone(Name,CTLD.CargoZoneType.MOVE) +-- +-- Deactivate a zone: +-- +-- -- Deactivate zone called Name of type #CTLD.CargoZoneType ZoneType: +-- my_ctld:DeactivateZone(Name,CTLD.CargoZoneType.DROP) +-- +-- ## 2.1.3 Limit and manage available resources +-- +-- When adding generic cargo types, you can effectively limit how many units can be dropped/build by the players, e.g. +-- +-- -- if you want to limit your stock, add a number (here: 10) as parameter after weight. No parameter / nil means unlimited stock. +-- my_ctld:AddCratesCargo("Humvee",{"Humvee"},CTLD_CARGO.Enum.VEHICLE,2,2775,10) +-- +-- You can manually add or remove the available stock like so: +-- +-- -- Crates +-- my_ctld:AddStockCrates("Humvee", 2) +-- my_ctld:RemoveStockCrates("Humvee", 2) +-- +-- -- Troops +-- my_ctld:AddStockTroops("Anti-Air", 2) +-- my_ctld:RemoveStockTroops("Anti-Air", 2) +-- +-- Notes: +-- Troops dropped back into a LOAD zone will effectively be added to the stock. Crates lost in e.g. a heli crash are just that - lost. +-- +-- ## 3. Events +-- +-- The class comes with a number of FSM-based events that missions designers can use to shape their mission. +-- These are: +-- +-- ## 3.1 OnAfterTroopsPickedUp +-- +-- This function is called when a player has loaded Troops: +-- +-- function my_ctld:OnAfterTroopsPickedUp(From, Event, To, Group, Unit, Cargo) +-- ... your code here ... +-- end +-- +-- ## 3.2 OnAfterCratesPickedUp +-- +-- This function is called when a player has picked up crates: +-- +-- function my_ctld:OnAfterCratesPickedUp(From, Event, To, Group, Unit, Cargo) +-- ... your code here ... +-- end +-- +-- ## 3.3 OnAfterTroopsDeployed +-- +-- This function is called when a player has deployed troops into the field: +-- +-- function my_ctld:OnAfterTroopsDeployed(From, Event, To, Group, Unit, Troops) +-- ... your code here ... +-- end +-- +-- ## 3.4 OnAfterTroopsExtracted +-- +-- 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: +-- +-- function my_ctld:OnAfterCratesDropped(From, Event, To, Group, Unit, Cargotable) +-- ... your code here ... +-- end +-- +-- ## 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.7 A simple SCORING example: +-- +-- To award player with points, using the SCORING Class (SCORING: my_Scoring, CTLD: CTLD_Cargotransport) +-- +-- function CTLD_Cargotransport:OnAfterCratesDropped(From, Event, To, Group, Unit, Cargotable) +-- local points = 10 +-- if Unit then +-- local PlayerName = Unit:GetPlayerName() +-- my_scoring:_AddPlayerFromUnit( Unit ) +-- my_scoring:AddGoalScore(Unit, "CTLD", string.format("Pilot %s has been awarded %d points for transporting cargo crates!", PlayerName, points), points) +-- end +-- end +-- +-- function CTLD_Cargotransport:OnAfterCratesBuild(From, Event, To, Group, Unit, Vehicle) +-- local points = 5 +-- if Unit then + -- local PlayerName = Unit:GetPlayerName() + -- my_scoring:_AddPlayerFromUnit( Unit ) + -- my_scoring:AddGoalScore(Unit, "CTLD", string.format("Pilot %s has been awarded %d points for the construction of Units!", PlayerName, points), points) +-- end +-- end +-- +-- ## 4. F10 Menu structure +-- +-- CTLD management menu is under the F10 top menu and called "CTLD" +-- +-- ## 4.1 Manage Crates +-- +-- 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, 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 +-- +-- Lists what you have loaded. Shows load capabilities for number of crates and number of seats for troops. +-- +-- ## 4.4 Smoke & Flare zones nearby +-- +-- Does what it says. +-- +-- ## 4.5 List active zone beacons +-- +-- Lists active radio beacons for all zones, where zones are both active and have a beacon. @see `CTLD:AddCTLDZone()` +-- +-- ## 4.6 Show hover parameters +-- +-- Lists hover parameters and indicates if these are curently fulfilled. Also @see options on hover heights. +-- +-- ## 4.7 List Inventory +-- +-- Lists invetory of available units to drop or build. +-- +-- ## 5. Support for Hercules mod by Anubis +-- +-- Basic support for the Hercules mod By Anubis has been build into CTLD. Currently this does **not** cover objects and troops which can +-- be loaded from the Rearm/Refuel menu, i.e. you can drop them into the field, but you cannot use them in functions scripted with this class. +-- +-- local my_ctld = CTLD:New(coalition.side.BLUE,{"Helicargo", "Hercules"},"Lufttransportbrigade I") +-- +-- Enable these options for Hercules support: +-- +-- 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 270kph or 150kn +-- +-- Also, the following options need to be set to `true`: +-- +-- my_ctld.useprefix = true -- this is true by default and MUST BE ON. +-- +-- Standard transport capabilities as per the real Hercules are: +-- +-- ["Hercules"] = {type="Hercules", crates=true, troops=true, cratelimit = 7, trooplimit = 64}, -- 19t cargo, 64 paratroopers +-- +-- ## 6. Save and load back units - persistance +-- +-- You can save and later load back units dropped or build to make your mission persistent. +-- For this to work, you need to de-sanitize **io** and **lfs** in your MissionScripting.lua, which is located in your DCS installtion folder under Scripts. +-- There is a risk involved in doing that; if you do not know what that means, this is possibly not for you. +-- +-- Use the following options to manage your saves: +-- +-- my_ctld.enableLoadSave = true -- allow auto-saving and loading of files +-- my_ctld.saveinterval = 600 -- save every 10 minutes +-- my_ctld.filename = "missionsave.csv" -- example filename +-- my_ctld.filepath = "C:\\Users\\myname\\Saved Games\\DCS\Missions\\MyMission" -- example path +-- my_ctld.eventoninject = true -- fire OnAfterCratesBuild and OnAfterTroopsDeployed events when loading (uses Inject functions) +-- +-- Then use an initial load at the beginning of your mission: +-- +-- my_ctld:__Load(10) +-- +-- **Caveat:** +-- If you use units build by multiple templates, they will effectively double on loading. Dropped crates are not saved. Current stock is not saved. +-- +-- @field #CTLD +CTLD = { + ClassName = "CTLD", + verbose = 0, + lid = "", + coalition = 1, + coalitiontxt = "blue", + PilotGroups = {}, -- #GROUP_SET of heli pilots + CtldUnits = {}, -- Table of helicopter #GROUPs + FreeVHFFrequencies = {}, -- Table of VHF + FreeUHFFrequencies = {}, -- Table of UHF + FreeFMFrequencies = {}, -- Table of FM + CargoCounter = 0, + wpZones = {}, + Cargo_Troops = {}, -- generic troops objects + Cargo_Crates = {}, -- generic crate objects + Loaded_Cargo = {}, -- cargo aboard units + Spawned_Crates = {}, -- Holds objects for crates spawned generally + Spawned_Cargo = {}, -- Binds together spawned_crates and their CTLD_CARGO objects + CrateDistance = 35, -- list crates in this radius + debug = false, + wpZones = {}, + dropOffZones = {}, + pickupZones = {}, +} + +------------------------------ +-- DONE: Zone Checks +-- DONE: TEST Hover load and unload +-- DONE: Crate unload +-- DONE: Hover (auto-)load +-- DONE: (More) Housekeeping +-- DONE: Troops running to WP Zone +-- DONE: Zone Radio Beacons +-- DONE: Stats Running +-- DONE: Added support for Hercules +-- TODO: Possibly - either/or loading crates and troops +-- DONE: Make inject respect existing cargo types +-- TODO: Drop beacons or flares/smoke +-- DONE: Add statics as cargo +-- DONE: List cargo in stock +-- DONE: Limit of troops, crates buildable? +-- DONE: Allow saving of Troops & Vehicles +------------------------------ + +--- Radio Beacons +-- @type CTLD.ZoneBeacon +-- @field #string name -- Name of zone for the coordinate +-- @field #number frequency -- in mHz +-- @field #number modulation -- i.e.radio.modulation.FM or radio.modulation.AM + +--- Zone Info. +-- @type CTLD.CargoZone +-- @field #string name Name of Zone. +-- @field #string color Smoke color for zone, e.g. SMOKECOLOR.Red. +-- @field #boolean active Active or not. +-- @field #string type Type of zone, i.e. load,drop,move,ship +-- @field #boolean hasbeacon Create and run radio beacons if active. +-- @field #table fmbeacon Beacon info as #CTLD.ZoneBeacon +-- @field #table uhfbeacon Beacon info as #CTLD.ZoneBeacon +-- @field #table vhfbeacon Beacon info as #CTLD.ZoneBeacon +-- @field #number shiplength For ships - length of ship +-- @field #number shipwidth For ships - width of ship + +--- Zone Type Info. +-- @type CTLD.CargoZoneType +CTLD.CargoZoneType = { + LOAD = "load", + DROP = "drop", + MOVE = "move", + SHIP = "ship", +} + +--- Buildable table info. +-- @type CTLD.Buildable +-- @field #string Name Name of the object. +-- @field #number Required Required crates. +-- @field #number Found Found crates. +-- @field #table Template Template names for this build. +-- @field #boolean CanBuild Is buildable or not. +-- @field #CTLD_CARGO.Enum Type Type enumerator (for moves). + +--- Unit capabilities. +-- @type CTLD.UnitCapabilities +-- @field #string type Unit type. +-- @field #boolean crates Can transport crate. +-- @field #boolean troops Can transport troops. +-- @field #number cratelimit Number of crates transportable. +-- @field #number trooplimit Number of troop units transportable. +CTLD.UnitTypes = { + ["SA342Mistral"] = {type="SA342Mistral", crates=false, troops=true, cratelimit = 0, trooplimit = 4, length = 12}, + ["SA342L"] = {type="SA342L", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 12}, + ["SA342M"] = {type="SA342M", crates=false, troops=true, cratelimit = 0, trooplimit = 4, length = 12}, + ["SA342Minigun"] = {type="SA342Minigun", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 12}, + ["UH-1H"] = {type="UH-1H", crates=true, troops=true, cratelimit = 1, trooplimit = 8, length = 15}, + ["Mi-8MTV2"] = {type="Mi-8MTV2", crates=true, troops=true, cratelimit = 2, trooplimit = 12, length = 15}, + ["Mi-8MT"] = {type="Mi-8MTV2", crates=true, troops=true, cratelimit = 2, trooplimit = 12, length = 15}, + ["Ka-50"] = {type="Ka-50", crates=false, troops=false, cratelimit = 0, trooplimit = 0, length = 15}, + ["Mi-24P"] = {type="Mi-24P", crates=true, troops=true, cratelimit = 2, trooplimit = 8, length = 18}, + ["Mi-24V"] = {type="Mi-24V", crates=true, troops=true, cratelimit = 2, trooplimit = 8, length = 18}, + ["Hercules"] = {type="Hercules", crates=true, troops=true, cratelimit = 7, trooplimit = 64, length = 25}, -- 19t cargo, 64 paratroopers. + --Actually it's longer, but the center coord is off-center of the model. +} + +--- CTLD class version. +-- @field #string version +CTLD.version="0.2.4" + +--- Instantiate a new CTLD. +-- @param #CTLD self +-- @param #string Coalition Coalition of this CTLD. I.e. coalition.side.BLUE or coalition.side.RED or coalition.side.NEUTRAL +-- @param #table Prefixes Table of pilot prefixes. +-- @param #string Alias Alias of this CTLD for logging. +-- @return #CTLD self +function CTLD:New(Coalition, Prefixes, Alias) + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #CTLD + + BASE:T({Coalition, Prefixes, Alias}) + + --set Coalition + if Coalition and type(Coalition)=="string" then + if Coalition=="blue" then + self.coalition=coalition.side.BLUE + self.coalitiontxt = Coalition + elseif Coalition=="red" then + self.coalition=coalition.side.RED + self.coalitiontxt = Coalition + elseif Coalition=="neutral" then + self.coalition=coalition.side.NEUTRAL + self.coalitiontxt = Coalition + else + self:E("ERROR: Unknown coalition in CTLD!") + end + else + self.coalition = Coalition + self.coalitiontxt = string.lower(UTILS.GetCoalitionName(self.coalition)) + end + + -- Set alias. + if Alias then + self.alias=tostring(Alias) + else + self.alias="UNHCR" + if self.coalition then + if self.coalition==coalition.side.RED then + self.alias="Red CTLD" + elseif self.coalition==coalition.side.BLUE then + self.alias="Blue CTLD" + end + end + end + + -- Set some string id for output to DCS.log file. + self.lid=string.format("%s (%s) | ", self.alias, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown") + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start FSM. + self:AddTransition("*", "Status", "*") -- 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("*", "CratesRepaired", "*") -- CTLD repair event. + self:AddTransition("*", "Load", "*") -- CTLD load event. + self:AddTransition("*", "Save", "*") -- CTLD save event. + self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. + + -- tables + self.PilotGroups ={} + self.CtldUnits = {} + + -- Beacons + self.FreeVHFFrequencies = {} + self.FreeUHFFrequencies = {} + self.FreeFMFrequencies = {} + self.UsedVHFFrequencies = {} + self.UsedUHFFrequencies = {} + self.UsedFMFrequencies = {} + + -- radio beacons + self.RadioSound = "beacon.ogg" + + -- zones stuff + self.pickupZones = {} + self.dropOffZones = {} + self.wpZones = {} + self.shipZones = {} + + -- Cargo + self.Cargo_Crates = {} + self.Cargo_Troops = {} + self.Cargo_Statics = {} + self.Loaded_Cargo = {} + self.Spawned_Crates = {} + self.Spawned_Cargo = {} + self.MenusDone = {} + self.DroppedTroops = {} + self.DroppedCrates = {} + self.CargoCounter = 0 + self.CrateCounter = 0 + self.TroopCounter = 0 + + -- added engineering + self.Engineers = 0 -- #number use as counter + self.EngineersInField = {} -- #table holds #CTLD_ENGINEERING objects + self.EngineerSearch = 2000 -- #number search distance for crates to build or repair + + -- setup + self.CrateDistance = 35 -- list/load crates in this radius + self.ExtractFactor = 3.33 -- factor for troops extraction, i.e. CrateDistance * Extractfactor + self.prefixes = Prefixes or {"Cargoheli"} + --self.I({prefixes = self.prefixes}) + self.useprefix = true + + self.maximumHoverHeight = 15 + self.minimumHoverHeight = 4 + self.forcehoverload = true + self.hoverautoloading = true + self.dropcratesanywhere = false -- #1570 + + self.smokedistance = 2000 + self.movetroopstowpzone = true + self.movetroopsdistance = 5000 + + -- added support Hercules Mod + 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 + + -- place spawned crates in front of aircraft + self.placeCratesAhead = false + + -- country of crates spawned + self.cratecountry = country.id.GERMANY + + -- for opening doors + self.pilotmustopendoors = false + + if self.coalition == coalition.side.RED then + self.cratecountry = country.id.RUSSIA + end + + -- load and save dropped TROOPS + self.enableLoadSave = false + self.filepath = nil + self.saveinterval = 600 + self.eventoninject = true + + local AliaS = string.gsub(self.alias," ","_") + self.filename = string.format("CTLD_%s_Persist.csv",AliaS) + + -- allow re-pickup crates + self.allowcratepickupagain = true + + -- slingload + self.enableslingload = false + + for i=1,100 do + math.random() + end + + self:_GenerateVHFrequencies() + self:_GenerateUHFrequencies() + self:_GenerateFMFrequencies() + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the CTLD. Initializes parameters and starts event handlers. + -- @function [parent=#CTLD] Start + -- @param #CTLD self + + --- Triggers the FSM event "Start" after a delay. Starts the CTLD. Initializes parameters and starts event handlers. + -- @function [parent=#CTLD] __Start + -- @param #CTLD self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the CTLD and all its event handlers. + -- @param #CTLD self + + --- Triggers the FSM event "Stop" after a delay. Stops the CTLD and all its event handlers. + -- @function [parent=#CTLD] __Stop + -- @param #CTLD self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Status". + -- @function [parent=#CTLD] Status + -- @param #CTLD self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#CTLD] __Status + -- @param #CTLD self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Load". + -- @function [parent=#CTLD] Load + -- @param #CTLD self + + --- Triggers the FSM event "Load" after a delay. + -- @function [parent=#CTLD] __Load + -- @param #CTLD self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Save". + -- @function [parent=#CTLD] Load + -- @param #CTLD self + + --- Triggers the FSM event "Save" after a delay. + -- @function [parent=#CTLD] __Save + -- @param #CTLD self + -- @param #number delay Delay in seconds. + + --- FSM Function OnAfterTroopsPickedUp. + -- @function [parent=#CTLD] OnAfterTroopsPickedUp + -- @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 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 + -- @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 crate. + -- @return #CTLD self + + --- FSM Function OnAfterTroopsDeployed. + -- @function [parent=#CTLD] OnAfterTroopsDeployed + -- @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 + + --- FSM Function OnAfterCratesDropped. + -- @function [parent=#CTLD] OnAfterCratesDropped + -- @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 #table Cargotable Table of #CTLD_CARGO objects dropped. + -- @return #CTLD self + + --- FSM Function OnAfterCratesBuild. + -- @function [parent=#CTLD] OnAfterCratesBuild + -- @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 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 + -- @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. + + --- FSM Function OnAfterLoad. + -- @function [parent=#CTLD] OnAfterLoad + -- @param #CTLD self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string path (Optional) Path where the file is located. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if the lfs module is desanitized. + -- @param #string filename (Optional) File name for loading. Default is "CTLD__Persist.csv". + + --- FSM Function OnAfterSave. + -- @function [parent=#CTLD] OnAfterSave + -- @param #CTLD self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string path (Optional) Path where the file is saved. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if the lfs module is desanitized. + -- @param #string filename (Optional) File name for saving. Default is "CTLD__Persist.csv". + + return self +end + +------------------------------------------------------------------- +-- Helper and User Functions +------------------------------------------------------------------- + +--- (Internal) Function to get capabilities of a chopper +-- @param #CTLD self +-- @param Wrapper.Unit#UNIT Unit The unit +-- @return #table Capabilities Table of caps +function CTLD:_GetUnitCapabilities(Unit) + self:T(self.lid .. " _GetUnitCapabilities") + local _unit = Unit -- Wrapper.Unit#UNIT + local unittype = _unit:GetTypeName() + local capabilities = self.UnitTypes[unittype] -- #CTLD.UnitCapabilities + if not capabilities or capabilities == {} then + -- e.g. ["Ka-50"] = {type="Ka-50", crates=false, troops=false, cratelimit = 0, trooplimit = 0}, + capabilities = {} + capabilities.troops = false + capabilities.crates = false + capabilities.cratelimit = 0 + capabilities.trooplimit = 0 + capabilities.type = "generic" + capabilities.length = 20 + end + return capabilities +end + + +--- (Internal) Function to generate valid UHF Frequencies +-- @param #CTLD self +function CTLD:_GenerateUHFrequencies() + self:T(self.lid .. " _GenerateUHFrequencies") + self.FreeUHFFrequencies = {} + self.FreeUHFFrequencies = UTILS.GenerateUHFrequencies() + return self +end + +--- (Internal) Function to generate valid FM Frequencies +-- @param #CTLD self +function CTLD:_GenerateFMFrequencies() + self:T(self.lid .. " _GenerateFMrequencies") + self.FreeFMFrequencies = {} + self.FreeFMFrequencies = UTILS.GenerateFMFrequencies() + return self +end + +--- (Internal) Populate table with available VHF beacon frequencies. +-- @param #CTLD self +function CTLD:_GenerateVHFrequencies() + self:T(self.lid .. " _GenerateVHFrequencies") + self.FreeVHFFrequencies = {} + self.UsedVHFFrequencies = {} + self.FreeVHFFrequencies = UTILS.GenerateVHFrequencies() + return self +end + +--- (Internal) Event handler function +-- @param #CTLD self +-- @param Core.Event#EVENTDATA EventData +function CTLD:_EventHandler(EventData) + 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 + local _coalition = event.IniCoalition + if _coalition ~= self.coalition then + return --ignore! + end + -- check is Helicopter + local _unit = event.IniUnit + local _group = event.IniGroup + if _unit:IsHelicopter() or _group:IsHelicopter() then + local unitname = event.IniUnitName or "none" + self.Loaded_Cargo[unitname] = nil + self:_RefreshF10Menus() + end + -- Herc support + --self:T_unit:GetTypeName()) + if _unit:GetTypeName() == "Hercules" and self.enableHercules then + self.Loaded_Cargo[unitname] = nil + self:_RefreshF10Menus() + end + return + elseif event.id == EVENTS.PlayerLeaveUnit then + -- remove from pilot table + local unitname = event.IniUnitName or "none" + self.CtldUnits[unitname] = nil + self.Loaded_Cargo[unitname] = nil + end + return self +end + +--- (Internal) Function to message a group. +-- @param #CTLD self +-- @param #string Text The text to display. +-- @param #number Time Number of seconds to display the message. +-- @param #boolean Clearscreen Clear screen or not. +-- @param Wrapper.Group#GROUP Group The group receiving the message. +function CTLD:_SendMessage(Text, Time, Clearscreen, Group) + self:T(self.lid .. " _SendMessage") + if not self.suppressmessages then + local m = MESSAGE:New(Text,Time,"CTLD",Clearscreen):ToGroup(Group) + end + return self +end + +--- (Internal) Function to load troops into a heli. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Unit#UNIT Unit +-- @param #CTLD_CARGO Cargotype +function CTLD:_LoadTroops(Group, Unit, Cargotype) + self:T(self.lid .. " _LoadTroops") + -- check if we have stock + local instock = Cargotype:GetStock() + local cgoname = Cargotype:GetName() + local cgotype = Cargotype:GetType() + if type(instock) == "number" and tonumber(instock) <= 0 and tonumber(instock) ~= -1 then + -- nothing left over + self:_SendMessage(string.format("Sorry, all %s are gone!", cgoname), 10, false, Group) + return self + end + -- landed or hovering over load zone? + local grounded = not self:IsUnitInAir(Unit) + local hoverload = self:CanHoverLoad(Unit) + --local dooropen = UTILS.IsLoadingDoorOpen(Unit:GetName()) and self.pilotmustopendoors + -- check if we are in LOAD zone + local inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) + if not inzone then + inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.SHIP) + end + if not inzone then + self:_SendMessage("You are not close enough to a logistics zone!", 10, false, Group) + 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) + if not self.debug then return self end + elseif self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then + self:_SendMessage("You need to open the door(s) to load troops!", 10, false, Group) + if not self.debug then return self end + end + -- load troops into heli + local group = Group -- Wrapper.Group#GROUP + local unit = Unit -- Wrapper.Unit#UNIT + local unitname = unit:GetName() + local cargotype = Cargotype -- #CTLD_CARGO + local cratename = cargotype:GetName() -- #string + -- 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 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, cgotype, true, true, Cargotype.CratesNeeded,nil,nil,Cargotype.PerCrateMass) + 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:__TroopsPickedUp(1,Group, Unit, Cargotype) + self:_UpdateUnitCargoMass(Unit) + Cargotype:RemoveStock() + 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 = 10000 + for k,v in pairs(self.DroppedTroops) do + local distance = self:_GetDistance(v:GetCoordinate(),unitcoord) + local unit = v:GetUnit(1) -- Wrapper.Unit#UNIT + local desc = unit:GetDesc() or nil + --self:I({desc = desc.attributes}) + if distance < nearestDistance and distance ~= -1 and not desc.attributes.Infantry then + nearestGroup = v + nearestGroupIndex = k + nearestDistance = distance + end + end + + -- found one and matching distance? + if nearestGroup == nil or nearestDistance > self.EngineerSearch then + self:_SendMessage("No unit close enough to repair!", 10, false, Group) + return nil, nil + end + + local groupname = nearestGroup:GetName() + + -- 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. +-- @param #boolean Engineering If true it is an Engineering repair. +function CTLD:_RepairObjectFromCrates(Group,Unit,Crates,Build,Number,Engineering) + 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 + if not Engineering then + self:_SendMessage(string.format("Repair started using %s taking %d secs", build.Name, self.repairtime), 10, false, Group) + end + -- 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,NearestGroup:GetCoordinate()) + buildtimer:Start(self.repairtime) + --self:_BuildObjectFromCrates(Group,Unit,object) + else + if not Engineering then + self:_SendMessage("Can't repair this unit with " .. build.Name, 10, false, Group) + else + self:T("Can't repair this unit with " .. build.Name) + end + 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 + 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 + if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then + self:_SendMessage("You need to open the door(s) to extract troops!", 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 + local nearestList = {} + local distancekeys = {} + local extractdistance = self.CrateDistance * self.ExtractFactor + for k,v in pairs(self.DroppedTroops) do + local distance = self:_GetDistance(v:GetCoordinate(),unitcoord) + if distance <= extractdistance and distance ~= -1 then + nearestGroup = v + nearestGroupIndex = k + nearestDistance = distance + table.insert(nearestList, math.floor(distance), v) + distancekeys[#distancekeys+1] = math.floor(distance) + end + end + + if nearestGroup == nil or nearestDistance > extractdistance then + self:_SendMessage("No units close enough to extract!", 10, false, Group) + return self + end + + -- sort reference keys + table.sort(distancekeys) + + local secondarygroups = {} + + for i=1,#distancekeys do + local nearestGroup = nearestList[distancekeys[i]] + -- find matching cargo type + local groupType = string.match(nearestGroup:GetName(), "(.+)-(.+)$") + local Cargotype = nil + for k,v in pairs(self.Cargo_Troops) do + local comparison = "" + if type(v.Templates) == "string" then comparison = v.Templates else comparison = v.Templates[1] end + if comparison == groupType then + Cargotype = v + break + end + end + if Cargotype == nil then + self:_SendMessage("Can't onboard " .. groupType, 10, false, Group) + else + + 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 self + else + self.CargoCounter = self.CargoCounter + 1 + local loadcargotype = CTLD_CARGO:New(self.CargoCounter, Cargotype.Name, Cargotype.Templates, Cargotype.CargoType, true, true, Cargotype.CratesNeeded,nil,nil,Cargotype.PerCrateMass) + 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:_UpdateUnitCargoMass(Unit) + self:__TroopsExtracted(1,Group, Unit, nearestGroup) + + -- clean up: + --table.remove(self.DroppedTroops, nearestGroupIndex) + if type(Cargotype.Templates) == "table" and Cargotype.Templates[2] then + --self:I("*****This CargoType has multiple templates: "..Cargotype.Name) + for _,_key in pairs (Cargotype.Templates) do + table.insert(secondarygroups,_key) + end + end + nearestGroup:Destroy(false) + end + end + end + -- clean up secondary groups + for _,_name in pairs(secondarygroups) do + for _,_group in pairs(nearestList) do + if _group and _group:IsAlive() then + local groupname = string.match(_group:GetName(), "(.+)-(.+)$") + if _name == groupname then + _group:Destroy(false) + end + end + end + end + self:CleanDroppedTroops() + return self + end + +--- (Internal) Function to spawn crates in front of the heli. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Unit#UNIT Unit +-- @param #CTLD_CARGO Cargo +-- @param #number number Number of crates to generate (for dropping) +-- @param #boolean drop If true we\'re dropping from heli rather than loading. +function CTLD:_GetCrates(Group, Unit, Cargo, number, drop) + self:T(self.lid .. " _GetCrates") + if not drop then + local cgoname = Cargo:GetName() + -- check if we have stock + local instock = Cargo:GetStock() + if type(instock) == "number" and tonumber(instock) <= 0 and tonumber(instock) ~= -1 then + -- nothing left over + self:_SendMessage(string.format("Sorry, we ran out of %s", cgoname), 10, false, Group) + return self + end + end + -- check if we are in LOAD zone + local inzone = false + local drop = drop or false + local ship = nil + local width = 20 + if not drop then + inzone = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) + if not inzone then + inzone, ship, zone, distance, width = self:IsUnitInZone(Unit,CTLD.CargoZoneType.SHIP) + end + else + 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) + if not self.debug then return self end + end + + -- avoid crate spam + local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitCapabilities + local canloadcratesno = capabilities.cratelimit + local loaddist = self.CrateDistance or 35 + 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) + return self + end + -- spawn crates in front of helicopter + local IsHerc = self:IsHercules(Unit) -- Herc + local cargotype = Cargo -- Ops.CTLD#CTLD_CARGO + local number = number or cargotype:GetCratesNeeded() --#number + local cratesneeded = cargotype:GetCratesNeeded() --#number + local cratename = cargotype:GetName() + local cratetemplate = "Container"-- #string + local cgotype = cargotype:GetType() + local cgomass = cargotype:GetMass() + local isstatic = false + if cgotype == CTLD_CARGO.Enum.STATIC then + cratetemplate = cargotype:GetTemplates() + isstatic = true + end + -- get position and heading of heli + local position = Unit:GetCoordinate() + local heading = Unit:GetHeading() + 1 + local height = Unit:GetHeight() + local droppedcargo = {} + local cratedistance = 0 + local rheading = 0 + local angleOffNose = 0 + local addon = 0 + if IsHerc then + -- spawn behind the Herc + addon = 180 + end + -- loop crates needed + for i=1,number do + local cratealias = string.format("%s-%d", cratetemplate, math.random(1,100000)) + if not self.placeCratesAhead then + cratedistance = (i-1)*2.5 + capabilities.length + if cratedistance > self.CrateDistance then cratedistance = self.CrateDistance end + -- altered heading logic + -- DONE: right standard deviation? + rheading = UTILS.RandomGaussian(0,30,-90,90,100) + rheading = math.fmod((heading + rheading + addon), 360) + else + local initialSpacing = IsHerc and 16 or 12 -- initial spacing of the first crates + local crateSpacing = 4 -- further spacing of remaining crates + local lateralSpacing = 4 -- lateral spacing of crates + local nrSideBySideCrates = 3 -- number of crates that are placed side-by-side + + if cratesneeded == 1 then + -- single crate needed spawns straight ahead + cratedistance = initialSpacing + rheading = heading + else + if (i - 1) % nrSideBySideCrates == 0 then + cratedistance = i == 1 and initialSpacing or cratedistance + crateSpacing + angleOffNose = math.ceil(math.deg(math.atan(lateralSpacing / cratedistance))) + rheading = heading - angleOffNose + else + rheading = rheading + angleOffNose + end + end + end + local cratecoord = position:Translate(cratedistance,rheading) + local cratevec2 = cratecoord:GetVec2() + self.CrateCounter = self.CrateCounter + 1 + local basetype = "container_cargo" + if isstatic then + basetype = cratetemplate + end + if type(ship) == "string" then + self:T("Spawning on ship "..ship) + local Ship = UNIT:FindByName(ship) + local shipcoord = Ship:GetCoordinate() + local unitcoord = Unit:GetCoordinate() + local dist = shipcoord:Get2DDistance(unitcoord) + dist = dist - (20 + math.random(1,10)) + local width = width / 2 + local Offy = math.random(-width,width) + self.Spawned_Crates[self.CrateCounter] = SPAWNSTATIC:NewFromType(basetype,"Cargos",self.cratecountry) + --:InitCoordinate(cratecoord) + :InitCargoMass(cgomass) + :InitCargo(self.enableslingload) + :InitLinkToUnit(Ship,dist,Offy,0) + :Spawn(270,cratealias) + else + self.Spawned_Crates[self.CrateCounter] = SPAWNSTATIC:NewFromType(basetype,"Cargos",self.cratecountry) + :InitCoordinate(cratecoord) + :InitCargoMass(cgomass) + :InitCargo(self.enableslingload) + --:InitLinkToUnit(Unit,OffsetX,OffsetY,OffsetAngle) + :Spawn(270,cratealias) + end + local templ = cargotype:GetTemplates() + local sorte = cargotype:GetType() + self.CargoCounter = self.CargoCounter + 1 + local realcargo = nil + if drop then + realcargo = CTLD_CARGO:New(self.CargoCounter,cratename,templ,sorte,true,false,cratesneeded,self.Spawned_Crates[self.CrateCounter],true,cargotype.PerCrateMass) + table.insert(droppedcargo,realcargo) + else + realcargo = CTLD_CARGO:New(self.CargoCounter,cratename,templ,sorte,false,false,cratesneeded,self.Spawned_Crates[self.CrateCounter],true,cargotype.PerCrateMass) + Cargo:RemoveStock() + end + table.insert(self.Spawned_Cargo, realcargo) + end + local text = string.format("Crates for %s have been positioned near you!",cratename) + if drop then + text = string.format("Crates for %s have been dropped!",cratename) + self:__CratesDropped(1, Group, Unit, droppedcargo) + end + self:_SendMessage(text, 10, false, Group) + return self +end + +--- (Internal) Inject crates and static cargo objects. +-- @param #CTLD self +-- @param Core.Zone#ZONE Zone Zone to spawn in. +-- @param #CTLD_CARGO Cargo The cargo type to spawn. +-- @param #boolean RandomCoord Randomize coordinate. +-- @return #CTLD self +function CTLD:InjectStatics(Zone, Cargo, RandomCoord) + self:T(self.lid .. " InjectStatics") + local cratecoord = Zone:GetCoordinate() + if RandomCoord then + cratecoord = Zone:GetRandomCoordinate(5,20) + end + local surface = cratecoord:GetSurfaceType() + if surface == land.SurfaceType.WATER then + return self + end + local cargotype = Cargo -- #CTLD_CARGO + --local number = 1 + local cratesneeded = cargotype:GetCratesNeeded() --#number + local cratetemplate = "Container"-- #string + local cratealias = string.format("%s-%d", cratetemplate, math.random(1,100000)) + local cratename = cargotype:GetName() + local cgotype = cargotype:GetType() + local cgomass = cargotype:GetMass() + local isstatic = false + if cgotype == CTLD_CARGO.Enum.STATIC then + cratetemplate = cargotype:GetTemplates() + isstatic = true + end + local basetype = "container_cargo" + if isstatic then + basetype = cratetemplate + end + self.CrateCounter = self.CrateCounter + 1 + self.Spawned_Crates[self.CrateCounter] = SPAWNSTATIC:NewFromType(basetype,"Cargos",self.cratecountry) + :InitCargoMass(cgomass) + :InitCargo(self.enableslingload) + :InitCoordinate(cratecoord) + :Spawn(270,cratealias) + local templ = cargotype:GetTemplates() + local sorte = cargotype:GetType() + self.CargoCounter = self.CargoCounter + 1 + cargotype.Positionable = self.Spawned_Crates[self.CrateCounter] + table.insert(self.Spawned_Cargo, cargotype) + return self +end + +--- (User) Inject static cargo objects. +-- @param #CTLD self +-- @param Core.Zone#ZONE Zone Zone to spawn in. Will be a somewhat random coordinate. +-- @param #string Template Unit(!) name of the static cargo object to be used as template. +-- @param #number Mass Mass of the static in kg. +-- @return #CTLD self +function CTLD:InjectStaticFromTemplate(Zone, Template, Mass) + self:T(self.lid .. " InjectStaticFromTemplate") + local cargotype = self:GetStaticsCargoFromTemplate(Template,Mass) -- #CTLD_CARGO + self:InjectStatics(Zone,cargotype,true) + return self +end + +--- (Internal) Function to find and list nearby crates. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Unit#UNIT Unit +-- @return #CTLD self +function CTLD:_ListCratesNearby( _group, _unit) + self:T(self.lid .. " _ListCratesNearby") + local finddist = self.CrateDistance or 35 + local crates,number = self:_FindCratesNearby(_group,_unit, finddist) -- #table + if number > 0 then + local text = REPORT:New("Crates Found Nearby:") + text:Add("------------------------------------------------------------") + for _,_entry in pairs (crates) do + local entry = _entry -- #CTLD_CARGO + local name = entry:GetName() --#string + local dropped = entry:WasDropped() + if dropped then + text:Add(string.format("Dropped crate for %s, %dkg",name, entry.PerCrateMass)) + else + text:Add(string.format("Crate for %s, %dkg",name, entry.PerCrateMass)) + end + end + if text:GetCount() == 1 then + text:Add(" N O N E") + end + text:Add("------------------------------------------------------------") + self:_SendMessage(text:Text(), 30, true, _group) + else + self:_SendMessage(string.format("No (loadable) crates within %d meters!",finddist), 10, false, _group) + end + return self +end + +--- (Internal) Return distance in meters between two coordinates. +-- @param #CTLD self +-- @param Core.Point#COORDINATE _point1 Coordinate one +-- @param Core.Point#COORDINATE _point2 Coordinate two +-- @return #number Distance in meters +function CTLD:_GetDistance(_point1, _point2) + self:T(self.lid .. " _GetDistance") + if _point1 and _point2 then + local distance1 = _point1:Get2DDistance(_point2) + local distance2 = _point1:DistanceFromPointVec2(_point2) + --self:I({dist1=distance1, dist2=distance2}) + if distance1 and type(distance1) == "number" then + return distance1 + elseif distance2 and type(distance2) == "number" then + return distance2 + else + self:E("*****Cannot calculate distance!") + self:E({_point1,_point2}) + return -1 + end + else + self:E("******Cannot calculate distance!") + self:E({_point1,_point2}) + return -1 + end +end + +--- (Internal) Function to find and return nearby crates. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP _group Group +-- @param Wrapper.Unit#UNIT _unit Unit +-- @param #number _dist Distance +-- @return #table Table of crates +-- @return #number Number Number of crates found +function CTLD:_FindCratesNearby( _group, _unit, _dist) + self:T(self.lid .. " _FindCratesNearby") + local finddist = _dist + local location = _group:GetCoordinate() + local existingcrates = self.Spawned_Cargo -- #table + -- cycle + local index = 0 + local found = {} + for _,_cargoobject in pairs (existingcrates) do + local cargo = _cargoobject -- #CTLD_CARGO + local static = cargo:GetPositionable() -- Wrapper.Static#STATIC -- crates + local staticid = cargo:GetID() + if static and static:IsAlive() then + local staticpos = static:GetCoordinate() + local distance = self:_GetDistance(location,staticpos) + if distance <= finddist and static then + index = index + 1 + table.insert(found, staticid, cargo) + end + end + end + return found, index +end + +--- (Internal) Function to get and load nearby crates. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Unit#UNIT Unit +-- @return #CTLD self +function CTLD:_LoadCratesNearby(Group, Unit) + self:T(self.lid .. " _LoadCratesNearby") + -- load crates into heli + local group = Group -- Wrapper.Group#GROUP + local unit = Unit -- Wrapper.Unit#UNIT + local unitname = unit:GetName() + -- see if this heli can load crates + local unittype = unit:GetTypeName() + local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitCapabilities + --local capabilities = self.UnitTypes[unittype] -- #CTLD.UnitCapabilities + local cancrates = capabilities.crates -- #boolean + local cratelimit = capabilities.cratelimit -- #number + local grounded = not self:IsUnitInAir(Unit) + local canhoverload = self:CanHoverLoad(Unit) + --- cases ------------------------------- + -- Chopper can\'t do crates - bark & return + -- Chopper can do crates - + -- --> hover if forcedhover or bark and return + -- --> hover or land if not forcedhover + ----------------------------------------- + if not cancrates then + self:_SendMessage("Sorry this chopper cannot carry crates!", 10, false, Group) + elseif self.forcehoverload and not canhoverload then + self:_SendMessage("Hover over the crates to pick them up!", 10, false, Group) + elseif not grounded and not canhoverload then + self:_SendMessage("Land or hover over the crates to pick them up!", 10, false, Group) + else + -- have we loaded stuff already? + local numberonboard = 0 + local massonboard = 0 + local loaded = {} + if self.Loaded_Cargo[unitname] then + loaded = self.Loaded_Cargo[unitname] -- #CTLD.LoadedCargo + numberonboard = loaded.Cratesloaded or 0 + else + loaded = {} -- #CTLD.LoadedCargo + loaded.Troopsloaded = 0 + loaded.Cratesloaded = 0 + loaded.Cargo = {} + end + -- get nearby crates + local finddist = self.CrateDistance or 35 + local nearcrates,number = self:_FindCratesNearby(Group,Unit,finddist) -- #table + if number == 0 and self.hoverautoloading then + return self -- exit + elseif number == 0 then + self:_SendMessage("Sorry no loadable crates nearby!", 10, false, Group) + return self -- exit + elseif numberonboard == cratelimit then + self:_SendMessage("Sorry no fully loaded!", 10, false, Group) + return self -- exit + else + -- go through crates and load + local capacity = cratelimit - numberonboard + local crateidsloaded = {} + local loops = 0 + while loaded.Cratesloaded < cratelimit and loops < number do + loops = loops + 1 + local crateind = 0 + -- get crate with largest index + for _ind,_crate in pairs (nearcrates) do + if self.allowcratepickupagain then + if _crate:GetID() > crateind and _crate.Positionable ~= nil then + crateind = _crate:GetID() + end + else + if not _crate:HasMoved() and _crate:WasDropped() and _crate:GetID() > crateind then + crateind = _crate:GetID() + end + end + end + -- load one if we found one + if crateind > 0 then + local crate = nearcrates[crateind] -- #CTLD_CARGO + loaded.Cratesloaded = loaded.Cratesloaded + 1 + crate:SetHasMoved(true) + crate:SetWasDropped(false) + table.insert(loaded.Cargo, crate) + table.insert(crateidsloaded,crate:GetID()) + -- destroy crate + crate:GetPositionable():Destroy(false) + crate.Positionable = nil + self:_SendMessage(string.format("Crate ID %d for %s loaded!",crate:GetID(),crate:GetName()), 10, false, Group) + table.remove(nearcrates,crate:GetID()) + self:__CratesPickedUp(1, Group, Unit, crate) + end + end + self.Loaded_Cargo[unitname] = loaded + self:_UpdateUnitCargoMass(Unit) + -- clean up real world crates + local existingcrates = self.Spawned_Cargo -- #table + local newexcrates = {} + for _,_crate in pairs(existingcrates) do + local excrate = _crate -- #CTLD_CARGO + local ID = excrate:GetID() + for _,_ID in pairs(crateidsloaded) do + if ID ~= _ID then + table.insert(newexcrates,_crate) + end + end + end + self.Spawned_Cargo = nil + self.Spawned_Cargo = newexcrates + end + end + return self +end + +--- (Internal) Function to get current loaded mass +-- @param #CTLD self +-- @param Wrapper.Unit#UNIT Unit +-- @return #number mass in kgs +function CTLD:_GetUnitCargoMass(Unit) + self:T(self.lid .. " _GetUnitCargoMass") + local unitname = Unit:GetName() + local loadedcargo = self.Loaded_Cargo[unitname] or {} -- #CTLD.LoadedCargo + local loadedmass = 0 -- #number + if self.Loaded_Cargo[unitname] then + local cargotable = loadedcargo.Cargo or {} -- #table + for _,_cargo in pairs(cargotable) do + local cargo = _cargo -- #CTLD_CARGO + local type = cargo:GetType() -- #CTLD_CARGO.Enum + if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS) and not cargo:WasDropped() then + loadedmass = loadedmass + (cargo.PerCrateMass * cargo:GetCratesNeeded()) + end + if type ~= CTLD_CARGO.Enum.TROOPS and type ~= CTLD_CARGO.Enum.ENGINEERS and not cargo:WasDropped() then + loadedmass = loadedmass + cargo.PerCrateMass + end + end + end + return loadedmass +end + +--- (Internal) Function to calculate and set Unit internal cargo mass +-- @param #CTLD self +-- @param Wrapper.Unit#UNIT Unit +function CTLD:_UpdateUnitCargoMass(Unit) + self:T(self.lid .. " _UpdateUnitCargoMass") + local calculatedMass = self:_GetUnitCargoMass(Unit) + Unit:SetUnitInternalCargo(calculatedMass) + --local report = REPORT:New("Loadmaster report") + --report:Add("Carrying " .. calculatedMass .. "Kg") + --self:_SendMessage(report:Text(),10,false,Unit:GetGroup()) + return self +end + +--- (Internal) Function to list loaded cargo. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Unit#UNIT Unit +-- @return #CTLD self +function CTLD:_ListCargo(Group, Unit) + self:T(self.lid .. " _ListCargo") + local unitname = Unit:GetName() + local unittype = Unit:GetTypeName() + local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitCapabilities + local trooplimit = capabilities.trooplimit -- #boolean + local cratelimit = capabilities.cratelimit -- #number + local loadedcargo = self.Loaded_Cargo[unitname] or {} -- #CTLD.LoadedCargo + local loadedmass = self:_GetUnitCargoMass(Unit) -- #number + if self.Loaded_Cargo[unitname] then + local no_troops = loadedcargo.Troopsloaded or 0 + local no_crates = loadedcargo.Cratesloaded or 0 + local cargotable = loadedcargo.Cargo or {} -- #table + local report = REPORT:New("Transport Checkout Sheet") + report:Add("------------------------------------------------------------") + report:Add(string.format("Troops: %d(%d), Crates: %d(%d)",no_troops,trooplimit,no_crates,cratelimit)) + report:Add("------------------------------------------------------------") + report:Add(" -- TROOPS --") + for _,_cargo in pairs(cargotable) do + local cargo = _cargo -- #CTLD_CARGO + local type = cargo:GetType() -- #CTLD_CARGO.Enum + if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS) and (not cargo:WasDropped() or self.allowcratepickupagain) then + report:Add(string.format("Troop: %s size %d",cargo:GetName(),cargo:GetCratesNeeded())) + end + end + if report:GetCount() == 4 then + report:Add(" N O N E") + end + report:Add("------------------------------------------------------------") + report:Add(" -- CRATES --") + local cratecount = 0 + for _,_cargo in pairs(cargotable) do + local cargo = _cargo -- #CTLD_CARGO + local type = cargo:GetType() -- #CTLD_CARGO.Enum + if (type ~= CTLD_CARGO.Enum.TROOPS and type ~= CTLD_CARGO.Enum.ENGINEERS) and (not cargo:WasDropped() or self.allowcratepickupagain) then + report:Add(string.format("Crate: %s size 1",cargo:GetName())) + cratecount = cratecount + 1 + end + end + if cratecount == 0 then + report:Add(" N O N E") + end + report:Add("------------------------------------------------------------") + report:Add("Total Mass: ".. loadedmass .. " kg") + local text = report:Text() + self:_SendMessage(text, 30, true, Group) + else + self:_SendMessage(string.format("Nothing loaded!\nTroop limit: %d | Crate limit %d",trooplimit,cratelimit), 10, false, Group) + end + return self +end + +--- (Internal) Function to list loaded cargo. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Unit#UNIT Unit +-- @return #CTLD self +function CTLD:_ListInventory(Group, Unit) + self:T(self.lid .. " _ListInventory") + local unitname = Unit:GetName() + local unittype = Unit:GetTypeName() + local cgotypes = self.Cargo_Crates + local trptypes = self.Cargo_Troops + local stctypes = self.Cargo_Statics + + local function countcargo(cgotable) + local counter = 0 + for _,_cgo in pairs(cgotable) do + counter = counter + 1 + end + return counter + end + + local crateno = countcargo(cgotypes) + local troopno = countcargo(trptypes) + local staticno = countcargo(stctypes) + + if (crateno > 0 or troopno > 0 or staticno > 0) then + + local report = REPORT:New("Inventory Sheet") + report:Add("------------------------------------------------------------") + report:Add(string.format("Troops: %d, Cratetypes: %d",troopno,crateno+staticno)) + report:Add("------------------------------------------------------------") + report:Add(" -- TROOPS --") + for _,_cargo in pairs(trptypes) do + local cargo = _cargo -- #CTLD_CARGO + local type = cargo:GetType() -- #CTLD_CARGO.Enum + if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS) and not cargo:WasDropped() then + local stockn = cargo:GetStock() + local stock = "none" + if stockn == -1 then + stock = "unlimited" + elseif stockn > 0 then + stock = tostring(stockn) + end + report:Add(string.format("Unit: %s | Soldiers: %d | Stock: %s",cargo:GetName(),cargo:GetCratesNeeded(),stock)) + end + end + if report:GetCount() == 4 then + report:Add(" N O N E") + end + report:Add("------------------------------------------------------------") + report:Add(" -- CRATES --") + local cratecount = 0 + for _,_cargo in pairs(cgotypes) do + local cargo = _cargo -- #CTLD_CARGO + local type = cargo:GetType() -- #CTLD_CARGO.Enum + if (type ~= CTLD_CARGO.Enum.TROOPS and type ~= CTLD_CARGO.Enum.ENGINEERS) and not cargo:WasDropped() then + local stockn = cargo:GetStock() + local stock = "none" + if stockn == -1 then + stock = "unlimited" + elseif stockn > 0 then + stock = tostring(stockn) + end + report:Add(string.format("Type: %s | Crates per Set: %d | Stock: %s",cargo:GetName(),cargo:GetCratesNeeded(),stock)) + cratecount = cratecount + 1 + end + end + -- Statics + for _,_cargo in pairs(stctypes) do + local cargo = _cargo -- #CTLD_CARGO + local type = cargo:GetType() -- #CTLD_CARGO.Enum + if (type == CTLD_CARGO.Enum.STATIC) and not cargo:WasDropped() then + local stockn = cargo:GetStock() + local stock = "none" + if stockn == -1 then + stock = "unlimited" + elseif stockn > 0 then + stock = tostring(stockn) + end + report:Add(string.format("Type: %s | Stock: %s",cargo:GetName(),stock)) + cratecount = cratecount + 1 + end + end + if cratecount == 0 then + report:Add(" N O N E") + end + local text = report:Text() + self:_SendMessage(text, 30, true, Group) + else + self:_SendMessage(string.format("Nothing in stock!"), 10, false, Group) + end + return self +end + +--- (Internal) Function to check if a unit is a Hercules C-130. +-- @param #CTLD self +-- @param Wrapper.Unit#UNIT Unit +-- @return #boolean Outcome +function CTLD:IsHercules(Unit) + if Unit:GetTypeName() == "Hercules" then + return true + else + return false + end +end + +--- (Internal) Function to unload troops from heli. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Unit#UNIT Unit +function CTLD:_UnloadTroops(Group, Unit) + self:T(self.lid .. " _UnloadTroops") + -- check if we are in LOAD zone + local droppingatbase = false + local canunload = true + if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then + self:_SendMessage("You need to open the door(s) to unload troops!", 10, false, Group) + if not self.debug then return self end + end + local inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) + if not inzone then + inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.SHIP) + end + if inzone then + droppingatbase = true + end + -- check for hover unload + local hoverunload = self:IsCorrectHover(Unit) --if true we\'re hovering in parameters + local IsHerc = self:IsHercules(Unit) + if IsHerc then + -- no hover but airdrop here + hoverunload = self:IsCorrectFlightParameters(Unit) + end + -- check if we\'re landed + local grounded = not self:IsUnitInAir(Unit) + -- Get what we have loaded + local unitname = Unit:GetName() + if self.Loaded_Cargo[unitname] and (grounded or hoverunload) then + if not droppingatbase or self.debug then + local loadedcargo = self.Loaded_Cargo[unitname] or {} -- #CTLD.LoadedCargo + -- looking for troops + local cargotable = loadedcargo.Cargo + for _,_cargo in pairs (cargotable) do + local cargo = _cargo -- #CTLD_CARGO + local type = cargo:GetType() -- #CTLD_CARGO.Enum + if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS) and not cargo:WasDropped() then + -- unload troops + local name = cargo:GetName() or "none" + local temptable = cargo:GetTemplates() or {} + local position = Group:GetCoordinate() + local zoneradius = 100 -- drop zone radius + local factor = 1 + if IsHerc then + factor = cargo:GetCratesNeeded() or 1 -- spread a bit more if airdropping + zoneradius = Unit:GetVelocityMPS() or 100 + end + local zone = ZONE_GROUP:New(string.format("Unload zone-%s",unitname),Group,zoneradius*factor) + local randomcoord = zone:GetRandomCoordinate(10,30*factor):GetVec2() + for _,_template in pairs(temptable) do + self.TroopCounter = self.TroopCounter + 1 + 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) + if self.movetroopstowpzone and type ~= CTLD_CARGO.Enum.ENGINEERS then + self:_MoveGroupToZone(self.DroppedTroops[self.TroopCounter]) + end + end -- template loop + cargo:SetWasDropped(true) + -- engineering group? + --self:I("Dropped Troop Type: "..type) + if type == CTLD_CARGO.Enum.ENGINEERS then + self.Engineers = self.Engineers + 1 + local grpname = self.DroppedTroops[self.TroopCounter]:GetName() + self.EngineersInField[self.Engineers] = CTLD_ENGINEERING:New(name, grpname) + self:_SendMessage(string.format("Dropped Engineers %s into action!",name), 10, false, Group) + else + self:_SendMessage(string.format("Dropped Troops %s into action!",name), 10, false, Group) + end + 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) + self:__TroopsRTB(1, Group, Unit) + end + -- cleanup load list + local loaded = {} -- #CTLD.LoadedCargo + loaded.Troopsloaded = 0 + loaded.Cratesloaded = 0 + loaded.Cargo = {} + local loadedcargo = self.Loaded_Cargo[unitname] or {} -- #CTLD.LoadedCargo + local cargotable = loadedcargo.Cargo or {} + for _,_cargo in pairs (cargotable) do + local cargo = _cargo -- #CTLD_CARGO + local type = cargo:GetType() -- #CTLD_CARGO.Enum + local dropped = cargo:WasDropped() + if type ~= CTLD_CARGO.Enum.TROOPS and type ~= CTLD_CARGO.Enum.ENGINEERS and not dropped then + table.insert(loaded.Cargo,_cargo) + loaded.Cratesloaded = loaded.Cratesloaded + 1 + else + -- add troops back to stock + if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS) and droppingatbase then + -- find right generic type + local name = cargo:GetName() + local gentroops = self.Cargo_Troops + for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO + if _troop.Name == name then + local stock = _troop:GetStock() + -- avoid making unlimited stock limited + if stock and tonumber(stock) >= 0 then _troop:AddStock() end + end + end + end + end + end + self.Loaded_Cargo[unitname] = nil + self.Loaded_Cargo[unitname] = loaded + self:_UpdateUnitCargoMass(Unit) + else + if IsHerc then + self:_SendMessage("Nothing loaded or not within airdrop parameters!", 10, false, Group) + else + self:_SendMessage("Nothing loaded or not hovering within parameters!", 10, false, Group) + end + end + return self +end + +--- (Internal) Function to unload crates from heli. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Unit#UNIT Unit +function CTLD:_UnloadCrates(Group, Unit) + self:T(self.lid .. " _UnloadCrates") + + 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 + local hoverunload = self:IsCorrectHover(Unit) --if true we\'re hovering in parameters + local IsHerc = self:IsHercules(Unit) + if IsHerc then + -- no hover but airdrop here + hoverunload = self:IsCorrectFlightParameters(Unit) + end + -- check if we\'re landed + local grounded = not self:IsUnitInAir(Unit) + -- Get what we have loaded + local unitname = Unit:GetName() + if self.Loaded_Cargo[unitname] and (grounded or hoverunload) then + local loadedcargo = self.Loaded_Cargo[unitname] or {} -- #CTLD.LoadedCargo + -- looking for crate + local cargotable = loadedcargo.Cargo + for _,_cargo in pairs (cargotable) do + local cargo = _cargo -- #CTLD_CARGO + local type = cargo:GetType() -- #CTLD_CARGO.Enum + if type ~= CTLD_CARGO.Enum.TROOPS and type ~= CTLD_CARGO.Enum.ENGINEERS and (not cargo:WasDropped() or self.allowcratepickupagain) then + -- unload crates + self:_GetCrates(Group, Unit, cargo, 1, true) + cargo:SetWasDropped(true) + cargo:SetHasMoved(true) + end + end + -- cleanup load list + local loaded = {} -- #CTLD.LoadedCargo + loaded.Troopsloaded = 0 + loaded.Cratesloaded = 0 + loaded.Cargo = {} + + for _,_cargo in pairs (cargotable) do + local cargo = _cargo -- #CTLD_CARGO + local type = cargo:GetType() -- #CTLD_CARGO.Enum + local size = cargo:GetCratesNeeded() + if type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS then + table.insert(loaded.Cargo,_cargo) + loaded.Troopsloaded = loaded.Troopsloaded + size + end + end + self.Loaded_Cargo[unitname] = nil + self.Loaded_Cargo[unitname] = loaded + + self:_UpdateUnitCargoMass(Unit) + else + if IsHerc then + self:_SendMessage("Nothing loaded or not within airdrop parameters!", 10, false, Group) + else + self:_SendMessage("Nothing loaded or not hovering within parameters!", 10, false, Group) + end + end + return self +end + +--- (Internal) Function to build nearby crates. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Unit#UNIT Unit +-- @param #boolean Engineering If true build is by an engineering team. +function CTLD:_BuildCrates(Group, Unit,Engineering) + self:T(self.lid .. " _BuildCrates") + -- avoid users trying to build from flying Hercs + local type = Unit:GetTypeName() + if type == "Hercules" and self.enableHercules and not Engineering then + local speed = Unit:GetVelocityKMH() + if speed > 1 then + self:_SendMessage("You need to land / stop to build something, Pilot!", 10, false, Group) + return self + end + end + -- get nearby crates + local finddist = self.CrateDistance or 35 + 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 not Crate:IsRepair() and not Crate:IsStatic() 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({buildables = buildables}) + end -- end dropped + end -- end crate loop + -- ok let\'s list what we have + local report = REPORT:New("Checklist Buildable Crates") + 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 Build %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() + if not Engineering then + self:_SendMessage(text, 30, true, Group) + else + self:T(text) + end + -- 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:_CleanUpCrates(crates,build,number) + self:_BuildObjectFromCrates(Group,Unit,build) + end + end + end + else + if not Engineering then self:_SendMessage(string.format("No crates within %d meters!",finddist), 10, false, Group) end + end -- number > 0 + return self +end + +--- (Internal) Function to repair nearby vehicles / FOBs +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Unit#UNIT Unit +-- @param #boolean Engineering If true, this is an engineering role +function CTLD:_RepairCrates(Group, Unit, Engineering) + self:T(self.lid .. " _RepairCrates") + -- get nearby crates + local finddist = self.CrateDistance or 35 + 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() and not Crate:IsStatic() 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() + if not Engineering then + self:_SendMessage(text, 30, true, Group) + else + self:T(text) + end + -- 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,Engineering) + end + end + end + else + if not Engineering then self:_SendMessage(string.format("No crates within %d meters!",finddist), 10, false, Group) end + end -- number > 0 + return self +end + +--- (Internal) Function to actually SPAWN buildables in the mission. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Group#UNIT Unit +-- @param #CTLD.Buildable Build +-- @param #boolean Repair If true this is a repair and not a new build +-- @param Core.Point#COORDINATE Coordinate Location for repair (e.g. where the destroyed unit was) +function CTLD:_BuildObjectFromCrates(Group,Unit,Build,Repair,RepairLocation) + self:T(self.lid .. " _BuildObjectFromCrates") + -- Spawn-a-crate-content + if Group and Group:IsAlive() then + local position = Unit:GetCoordinate() or Group:GetCoordinate() + local unitname = Unit:GetName() or Group:GetName() + local name = Build.Name + local ctype = Build.Type -- #CTLD_CARGO.Enum + local canmove = false + if ctype == CTLD_CARGO.Enum.VEHICLE then canmove = true end + if ctype == CTLD_CARGO.Enum.STATIC then + return self + end + local temptable = Build.Template or {} + if type(temptable) == "string" then + temptable = {temptable} + end + local zone = ZONE_GROUP:New(string.format("Unload zone-%s",unitname),Group,100) + local randomcoord = zone:GetRandomCoordinate(35):GetVec2() + if Repair then + randomcoord = RepairLocation:GetVec2() + end + for _,_template in pairs(temptable) do + self.TroopCounter = self.TroopCounter + 1 + local alias = string.format("%s-%d", _template, math.random(1,100000)) + if canmove then + 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) + :InitDelayOff() + :SpawnFromVec2(randomcoord) + end + if self.movetroopstowpzone and canmove then + self:_MoveGroupToZone(self.DroppedTroops[self.TroopCounter]) + end + 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 + else + self:T(self.lid.."Group KIA while building!") + end + return self +end + +--- (Internal) Function to move group to WP zone. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group The Group to move. +function CTLD:_MoveGroupToZone(Group) + self:T(self.lid .. " _MoveGroupToZone") + local groupname = Group:GetName() or "none" + local groupcoord = Group:GetCoordinate() + -- 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() + local zonecoord = zone:GetRandomCoordinate(20,125) -- Core.Point#COORDINATE + local coordinate = zonecoord:GetVec2() + Group:SetAIOn() + Group:OptionAlarmStateAuto() + Group:OptionDisperseOnAttack(30) + Group:OptionROEOpenFirePossible() + Group:RouteToVec2(coordinate,5) + end + return self +end + +--- (Internal) Housekeeping - Cleanup crates when build +-- @param #CTLD self +-- @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:_CleanUpCrates(Crates,Build,Number) + self:T(self.lid .. " _CleanUpCrates") + -- clean up real world crates + local build = Build -- #CTLD.Buildable + local existingcrates = self.Spawned_Cargo -- #table of exising crates + local newexcrates = {} + -- get right number of crates to destroy + local numberdest = Build.Required + local nametype = Build.Name + local found = 0 + local rounds = Number + local destIDs = {} + + -- loop and find matching IDs in the set + for _,_crate in pairs(Crates) do + local nowcrate = _crate -- #CTLD_CARGO + local name = nowcrate:GetName() + local thisID = nowcrate:GetID() + if name == nametype then -- matching crate type + table.insert(destIDs,thisID) + found = found + 1 + nowcrate:GetPositionable():Destroy(false) + nowcrate.Positionable = nil + nowcrate.HasBeenDropped = false + end + if found == numberdest then break end -- got enough + end + -- loop and remove from real world representation + for _,_crate in pairs(existingcrates) do + local excrate = _crate -- #CTLD_CARGO + local ID = excrate:GetID() + for _,_ID in pairs(destIDs) do + if ID ~= _ID then + table.insert(newexcrates,_crate) + end + end + end + + -- reset Spawned_Cargo + self.Spawned_Cargo = nil + self.Spawned_Cargo = newexcrates + return self +end + +--- (Internal) Housekeeping - Function to refresh F10 menus. +-- @param #CTLD self +-- @return #CTLD self +function CTLD:_RefreshF10Menus() + self:T(self.lid .. " _RefreshF10Menus") + local PlayerSet = self.PilotGroups -- Core.Set#SET_GROUP + local PlayerTable = PlayerSet:GetSetObjects() -- #table of #GROUP objects + -- rebuild units table + local _UnitList = {} + for _key, _group in pairs (PlayerTable) do + local _unit = _group:GetUnit(1) -- Wrapper.Unit#UNIT Asume that there is only one unit in the flight for players + if _unit then + if _unit:IsAlive() and _unit:IsPlayer() then + if _unit:IsHelicopter() or (_unit:GetTypeName() == "Hercules" and self.enableHercules) then --ensure no stupid unit entries here + local unitName = _unit:GetName() + _UnitList[unitName] = unitName + end + end -- end isAlive + end -- end if _unit + end -- end for + self.CtldUnits = _UnitList + + -- build unit menus + local menucount = 0 + local menus = {} + for _, _unitName in pairs(self.CtldUnits) do + if not self.MenusDone[_unitName] then + local _unit = UNIT:FindByName(_unitName) -- Wrapper.Unit#UNIT + if _unit then + local _group = _unit:GetGroup() -- Wrapper.Group#GROUP + if _group then + -- get chopper capabilities + local unittype = _unit:GetTypeName() + local capabilities = self:_GetUnitCapabilities(_unit) -- #CTLD.UnitCapabilities + local cantroops = capabilities.troops + local cancrates = capabilities.crates + -- top menu + local topmenu = MENU_GROUP:New(_group,"CTLD",nil) + local toptroops = MENU_GROUP:New(_group,"Manage Troops",topmenu) + local topcrates = MENU_GROUP:New(_group,"Manage Crates",topmenu) + local listmenu = MENU_GROUP_COMMAND:New(_group,"List boarded cargo",topmenu, self._ListCargo, self, _group, _unit) + local invtry = MENU_GROUP_COMMAND:New(_group,"Inventory",topmenu, self._ListInventory, self, _group, _unit) + local rbcns = MENU_GROUP_COMMAND:New(_group,"List active zone beacons",topmenu, self._ListRadioBeacons, self, _group, _unit) + local smokemenu = MENU_GROUP_COMMAND:New(_group,"Smoke zones nearby",topmenu, self.SmokeZoneNearBy, self, _unit, false) + local smokemenu = MENU_GROUP_COMMAND:New(_group,"Flare zones nearby",topmenu, self.SmokeZoneNearBy, self, _unit, true):Refresh() + -- sub menus + -- sub menu troops management + if cantroops then + local troopsmenu = MENU_GROUP:New(_group,"Load troops",toptroops) + for _,_entry in pairs(self.Cargo_Troops) do + local entry = _entry -- #CTLD_CARGO + menucount = menucount + 1 + 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 + -- sub menu crates management + if cancrates then + local loadmenu = MENU_GROUP_COMMAND:New(_group,"Load crates",topcrates, self._LoadCratesNearby, self, _group, _unit) + local cratesmenu = MENU_GROUP:New(_group,"Get Crates",topcrates) + for _,_entry in pairs(self.Cargo_Crates) do + local entry = _entry -- #CTLD_CARGO + menucount = menucount + 1 + local menutext = string.format("Crate %s (%dkg)",entry.Name,entry.PerCrateMass or 0) + menus[menucount] = MENU_GROUP_COMMAND:New(_group,menutext,cratesmenu,self._GetCrates, self, _group, _unit, entry) + end + for _,_entry in pairs(self.Cargo_Statics) do + local entry = _entry -- #CTLD_CARGO + menucount = menucount + 1 + local menutext = string.format("Crate %s (%dkg)",entry.Name,entry.PerCrateMass or 0) + menus[menucount] = MENU_GROUP_COMMAND:New(_group,menutext,cratesmenu,self._GetCrates, self, _group, _unit, entry) + 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) + local repairmenu = MENU_GROUP_COMMAND:New(_group,"Repair",topcrates, self._RepairCrates, self, _group, _unit):Refresh() + end + if unittype == "Hercules" then + local hoverpars = MENU_GROUP_COMMAND:New(_group,"Show flight parameters",topmenu, self._ShowFlightParams, self, _group, _unit):Refresh() + else + local hoverpars = MENU_GROUP_COMMAND:New(_group,"Show hover parameters",topmenu, self._ShowHoverParams, self, _group, _unit):Refresh() + end + self.MenusDone[_unitName] = true + end -- end group + end -- end unit + else -- menu build check + self:T(self.lid .. " Menus already done for this group!") + end -- end menu build check + end -- end for + return self + end + +--- User function - Add *generic* troop type loadable as cargo. This type will load directly into the heli without crates. +-- @param #CTLD self +-- @param #string Name Unique name of this type of troop. E.g. "Anti-Air Small". +-- @param #table Templates Table of #string names of late activated Wrapper.Group#GROUP making up this troop. +-- @param #CTLD_CARGO.Enum Type Type of cargo, here TROOPS - these will move to a nearby destination zone when dropped/build. +-- @param #number NoTroops Size of the group in number of Units across combined templates (for loading). +-- @param #number PerTroopMass Mass in kg of each soldier +-- @param #number Stock Number of groups in stock. Nil for unlimited. +function CTLD:AddTroopsCargo(Name,Templates,Type,NoTroops,PerTroopMass,Stock) + self:T(self.lid .. " AddTroopsCargo") + self:T({Name,Templates,Type,NoTroops,PerTroopMass,Stock}) + self.CargoCounter = self.CargoCounter + 1 + -- Troops are directly loadable + local cargo = CTLD_CARGO:New(self.CargoCounter,Name,Templates,Type,false,true,NoTroops,nil,nil,PerTroopMass,Stock) + table.insert(self.Cargo_Troops,cargo) + return self +end + +--- User function - Add *generic* crate-type 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 #table Templates Table of #string names of late activated Wrapper.Group#GROUP building this cargo. +-- @param #CTLD_CARGO.Enum Type Type of cargo. I.e. VEHICLE or FOB. VEHICLE will move to destination zones when dropped/build, FOB stays put. +-- @param #number NoCrates Number of crates needed to build this cargo. +-- @param #number PerCrateMass Mass in kg of each crate +-- @param #number Stock Number of groups in stock. Nil for unlimited. +function CTLD:AddCratesCargo(Name,Templates,Type,NoCrates,PerCrateMass,Stock) + self:T(self.lid .. " AddCratesCargo") + self.CargoCounter = self.CargoCounter + 1 + -- Crates are not directly loadable + local cargo = CTLD_CARGO:New(self.CargoCounter,Name,Templates,Type,false,false,NoCrates,nil,nil,PerCrateMass,Stock) + table.insert(self.Cargo_Crates,cargo) + return self +end + +--- User function - Add *generic* static-type loadable as cargo. This type will create cargo that needs to be loaded, moved and dropped. +-- @param #CTLD self +-- @param #string Name Unique name of this type of cargo as set in the mission editor (note: UNIT name!), e.g. "Ammunition-1". +-- @param #number Mass Mass in kg of each static in kg, e.g. 100. +-- @param #number Stock Number of groups in stock. Nil for unlimited. +function CTLD:AddStaticsCargo(Name,Mass,Stock) + self:T(self.lid .. " AddStaticsCargo") + self.CargoCounter = self.CargoCounter + 1 + local type = CTLD_CARGO.Enum.STATIC + local template = STATIC:FindByName(Name,true):GetTypeName() + -- Crates are not directly loadable + local cargo = CTLD_CARGO:New(self.CargoCounter,Name,template,type,false,false,1,nil,nil,Mass,Stock) + table.insert(self.Cargo_Statics,cargo) + return self +end + +--- User function - Get a *generic* static-type loadable as #CTLD_CARGO object. +-- @param #CTLD self +-- @param #string Name Unique Unit(!) name of this type of cargo as set in the mission editor (not: GROUP name!), e.g. "Ammunition-1". +-- @param #number Mass Mass in kg of each static in kg, e.g. 100. +-- @return #CTLD_CARGO Cargo object +function CTLD:GetStaticsCargoFromTemplate(Name,Mass) + self:T(self.lid .. " GetStaticsCargoFromTemplate") + self.CargoCounter = self.CargoCounter + 1 + local type = CTLD_CARGO.Enum.STATIC + local template = STATIC:FindByName(Name,true):GetTypeName() + -- Crates are not directly loadable + local cargo = CTLD_CARGO:New(self.CargoCounter,Name,template,type,false,false,1,nil,nil,Mass,1) + --table.insert(self.Cargo_Statics,cargo) + return cargo +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. +-- @param #number PerCrateMass Mass in kg of each crate +-- @param #number Stock Number of groups in stock. Nil for unlimited. +function CTLD:AddCratesRepair(Name,Template,Type,NoCrates, PerCrateMass,Stock) + 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,nil,nil,PerCrateMass,Stock) + 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. +function CTLD:AddZone(Zone) + self:T(self.lid .. " AddZone") + local zone = Zone -- #CTLD.CargoZone + if zone.type == CTLD.CargoZoneType.LOAD then + table.insert(self.pickupZones,zone) + elseif zone.type == CTLD.CargoZoneType.DROP then + table.insert(self.dropOffZones,zone) + elseif zone.type == CTLD.CargoZoneType.SHIP then + table.insert(self.shipZones,zone) + else + table.insert(self.wpZones,zone) + end + return self +end + +--- User function - Activate Name #CTLD.CargoZone.Type ZoneType for this CTLD instance. +-- @param #CTLD self +-- @param #string Name Name of the zone to change in the ME. +-- @param #CTLD.CargoZoneType ZoneType Type of zone this belongs to. +-- @param #boolean NewState (Optional) Set to true to activate, false to switch off. +function CTLD:ActivateZone(Name,ZoneType,NewState) + self:T(self.lid .. " AddZone") + local newstate = true + -- set optional in case we\'re deactivating + if NewState ~= nil then + newstate = NewState + end + + -- get correct table + local table = {} + if ZoneType == CTLD.CargoZoneType.LOAD then + table = self.pickupZones + elseif ZoneType == CTLD.CargoZoneType.DROP then + table = self.dropOffZones + elseif ZoneType == CTLD.CargoZoneType.SHIP then + table = self.shipZones + else + table = self.wpZones + end + -- loop table + for _,_zone in pairs(table) do + local thiszone = _zone --#CTLD.CargoZone + if thiszone.name == Name then + thiszone.active = newstate + break + end + end + 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. +-- @param #CTLD.CargoZoneType ZoneType Type of zone this belongs to. +function CTLD:DeactivateZone(Name,ZoneType) + self:T(self.lid .. " AddZone") + self:ActivateZone(Name,ZoneType,false) + return self +end + +--- (Internal) Function to obtain a valid FM frequency. +-- @param #CTLD self +-- @param #string Name Name of zone. +-- @return #CTLD.ZoneBeacon Beacon Beacon table. +function CTLD:_GetFMBeacon(Name) + self:T(self.lid .. " _GetFMBeacon") + local beacon = {} -- #CTLD.ZoneBeacon + if #self.FreeFMFrequencies <= 1 then + self.FreeFMFrequencies = self.UsedFMFrequencies + self.UsedFMFrequencies = {} + end + --random + local FM = table.remove(self.FreeFMFrequencies, math.random(#self.FreeFMFrequencies)) + table.insert(self.UsedFMFrequencies, FM) + beacon.name = Name + beacon.frequency = FM / 1000000 + beacon.modulation = radio.modulation.FM + return beacon +end + +--- (Internal) Function to obtain a valid UHF frequency. +-- @param #CTLD self +-- @param #string Name Name of zone. +-- @return #CTLD.ZoneBeacon Beacon Beacon table. +function CTLD:_GetUHFBeacon(Name) + self:T(self.lid .. " _GetUHFBeacon") + local beacon = {} -- #CTLD.ZoneBeacon + if #self.FreeUHFFrequencies <= 1 then + self.FreeUHFFrequencies = self.UsedUHFFrequencies + self.UsedUHFFrequencies = {} + end + --random + local UHF = table.remove(self.FreeUHFFrequencies, math.random(#self.FreeUHFFrequencies)) + table.insert(self.UsedUHFFrequencies, UHF) + beacon.name = Name + beacon.frequency = UHF / 1000000 + beacon.modulation = radio.modulation.AM + + return beacon +end + +--- (Internal) Function to obtain a valid VHF frequency. +-- @param #CTLD self +-- @param #string Name Name of zone. +-- @return #CTLD.ZoneBeacon Beacon Beacon table. +function CTLD:_GetVHFBeacon(Name) + self:T(self.lid .. " _GetVHFBeacon") + local beacon = {} -- #CTLD.ZoneBeacon + if #self.FreeVHFFrequencies <= 3 then + self.FreeVHFFrequencies = self.UsedVHFFrequencies + self.UsedVHFFrequencies = {} + end + --get random + local VHF = table.remove(self.FreeVHFFrequencies, math.random(#self.FreeVHFFrequencies)) + table.insert(self.UsedVHFFrequencies, VHF) + beacon.name = Name + beacon.frequency = VHF / 1000000 + beacon.modulation = radio.modulation.FM + return beacon +end + + +--- User function - Crates and adds a #CTLD.CargoZone zone for this CTLD instance. +-- Zones of type LOAD: Players load crates and troops here. +-- Zones of type DROP: Players can drop crates here. Note that troops can be unloaded anywhere. +-- Zone of type MOVE: Dropped troops and vehicles will start moving to the nearest zone of this type (also see options). +-- @param #CTLD self +-- @param #string Name Name of this zone, as in Mission Editor. +-- @param #string Type Type of this zone, #CTLD.CargoZoneType +-- @param #number Color Smoke/Flare color e.g. #SMOKECOLOR.Red +-- @param #string Active Is this zone currently active? +-- @param #string HasBeacon Does this zone have a beacon if it is active? +-- @param #number Shiplength Length of Ship for shipzones +-- @param #number Shipwidth Width of Ship for shipzones +-- @return #CTLD self +function CTLD:AddCTLDZone(Name, Type, Color, Active, HasBeacon, Shiplength, Shipwidth) + self:T(self.lid .. " AddCTLDZone") + + local ctldzone = {} -- #CTLD.CargoZone + ctldzone.active = Active or false + ctldzone.color = Color or SMOKECOLOR.Red + ctldzone.name = Name or "NONE" + ctldzone.type = Type or CTLD.CargoZoneType.MOVE -- #CTLD.CargoZoneType + ctldzone.hasbeacon = HasBeacon or false + + if HasBeacon then + ctldzone.fmbeacon = self:_GetFMBeacon(Name) + ctldzone.uhfbeacon = self:_GetUHFBeacon(Name) + ctldzone.vhfbeacon = self:_GetVHFBeacon(Name) + else + ctldzone.fmbeacon = nil + ctldzone.uhfbeacon = nil + ctldzone.vhfbeacon = nil + end + + if Type == CTLD.CargoZoneType.SHIP then + ctldzone.shiplength = Shiplength or 100 + ctldzone.shipwidth = Shipwidth or 10 + end + + self:AddZone(ctldzone) + return self +end + +--- (Internal) Function to show list of radio beacons +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Unit#UNIT Unit +function CTLD:_ListRadioBeacons(Group, Unit) + self:T(self.lid .. " _ListRadioBeacons") + local report = REPORT:New("Active Zone Beacons") + report:Add("------------------------------------------------------------") + local zones = {[1] = self.pickupZones, [2] = self.wpZones, [3] = self.dropOffZones, [4] = self.shipZones} + for i=1,4 do + for index,cargozone in pairs(zones[i]) do + -- Get Beacon object from zone + local czone = cargozone -- #CTLD.CargoZone + if czone.active and czone.hasbeacon then + local FMbeacon = czone.fmbeacon -- #CTLD.ZoneBeacon + local VHFbeacon = czone.vhfbeacon -- #CTLD.ZoneBeacon + local UHFbeacon = czone.uhfbeacon -- #CTLD.ZoneBeacon + local Name = czone.name + local FM = FMbeacon.frequency -- MHz + local VHF = VHFbeacon.frequency * 1000 -- KHz + local UHF = UHFbeacon.frequency -- MHz + report:AddIndent(string.format(" %s | FM %s Mhz | VHF %s KHz | UHF %s Mhz ", Name, FM, VHF, UHF),"|") + end + end + end + if report:GetCount() == 1 then + report:Add(" N O N E") + end + report:Add("------------------------------------------------------------") + self:_SendMessage(report:Text(), 30, true, Group) + return self +end + +--- (Internal) Add radio beacon to zone. Runs 30 secs. +-- @param #CTLD self +-- @param #string Name Name of zone. +-- @param #string Sound Name of soundfile. +-- @param #number Mhz Frequency in Mhz. +-- @param #number Modulation Modulation AM or FM. +-- @param #boolean IsShip If true zone is a ship. +function CTLD:_AddRadioBeacon(Name, Sound, Mhz, Modulation, IsShip) + self:T(self.lid .. " _AddRadioBeacon") + local Zone = nil + if IsShip then + Zone = UNIT:FindByName(Name) + else + Zone = ZONE:FindByName(Name) + end + local Sound = Sound or "beacon.ogg" + if Zone then + local ZoneCoord = Zone:GetCoordinate() + local ZoneVec3 = ZoneCoord:GetVec3() + local Frequency = Mhz * 1000000 -- Freq in Hertz + local Sound = "l10n/DEFAULT/"..Sound + trigger.action.radioTransmission(Sound, ZoneVec3, Modulation, false, Frequency, 1000) -- Beacon in MP only runs for 30secs straight + end + return self +end + +--- (Internal) Function to refresh radio beacons +-- @param #CTLD self +function CTLD:_RefreshRadioBeacons() + self:T(self.lid .. " _RefreshRadioBeacons") + + local zones = {[1] = self.pickupZones, [2] = self.wpZones, [3] = self.dropOffZones, [4] = self.shipZones} + for i=1,4 do + local IsShip = false + if i == 4 then IsShip = true end + for index,cargozone in pairs(zones[i]) do + -- Get Beacon object from zone + local czone = cargozone -- #CTLD.CargoZone + local Sound = self.RadioSound + if czone.active and czone.hasbeacon then + local FMbeacon = czone.fmbeacon -- #CTLD.ZoneBeacon + local VHFbeacon = czone.vhfbeacon -- #CTLD.ZoneBeacon + local UHFbeacon = czone.uhfbeacon -- #CTLD.ZoneBeacon + local Name = czone.name + local FM = FMbeacon.frequency -- MHz + local VHF = VHFbeacon.frequency -- KHz + local UHF = UHFbeacon.frequency -- MHz + self:_AddRadioBeacon(Name,Sound,FM,radio.modulation.FM, IsShip) + self:_AddRadioBeacon(Name,Sound,VHF,radio.modulation.FM, IsShip) + self:_AddRadioBeacon(Name,Sound,UHF,radio.modulation.AM, IsShip) + end + end + end + return self +end + +--- (Internal) Function to see if a unit is in a specific zone type. +-- @param #CTLD self +-- @param Wrapper.Unit#UNIT Unit Unit +-- @param #CTLD.CargoZoneType Zonetype Zonetype +-- @return #boolean Outcome Is in zone or not +-- @return #string name Closest zone name +-- @return Core.Zone#ZONE zone Closest Core.Zone#ZONE object +-- @return #number distance Distance to closest zone +-- @return #number width Radius of zone or width of ship +function CTLD:IsUnitInZone(Unit,Zonetype) + self:T(self.lid .. " IsUnitInZone") + self:T(Zonetype) + local unitname = Unit:GetName() + local zonetable = {} + local outcome = false + if Zonetype == CTLD.CargoZoneType.LOAD then + zonetable = self.pickupZones -- #table + elseif Zonetype == CTLD.CargoZoneType.DROP then + zonetable = self.dropOffZones -- #table + elseif Zonetype == CTLD.CargoZoneType.SHIP then + zonetable = self.shipZones -- #table + else + zonetable = self.wpZones -- #table + end + --- now see if we\'re in + local zonecoord = nil + local colorret = nil + local maxdist = 1000000 -- 100km + local zoneret = nil + local zonewret = nil + local zonenameret = nil + for _,_cargozone in pairs(zonetable) do + local czone = _cargozone -- #CTLD.CargoZone + local unitcoord = Unit:GetCoordinate() + local zonename = czone.name + local active = czone.active + local color = czone.color + local zone = nil + local zoneradius = 100 + local zonewidth = 20 + if Zonetype == CTLD.CargoZoneType.SHIP then + self:T("Checking Type Ship: "..zonename) + zone = UNIT:FindByName(zonename) + zonecoord = zone:GetCoordinate() + zoneradius = czone.shiplength + zonewidth = czone.shipwidth + else + zone = ZONE:FindByName(zonename) + zonecoord = zone:GetCoordinate() + zoneradius = zone:GetRadius() + zonewidth = zoneradius + end + local distance = self:_GetDistance(zonecoord,unitcoord) + if distance <= zoneradius and active then + outcome = true + end + if maxdist > distance then + maxdist = distance + zoneret = zone + zonenameret = zonename + zonewret = zonewidth + colorret = color + end + end + if Zonetype == CTLD.CargoZoneType.SHIP then + return outcome, zonenameret, zoneret, maxdist, zonewret + else + return outcome, zonenameret, zoneret, maxdist + end +end + +--- User function - Start smoke in a zone close to the Unit. +-- @param #CTLD self +-- @param Wrapper.Unit#UNIT Unit The Unit. +-- @param #boolean Flare If true, flare instead. +function CTLD:SmokeZoneNearBy(Unit, Flare) + self:T(self.lid .. " SmokeZoneNearBy") + -- table of #CTLD.CargoZone table + local unitcoord = Unit:GetCoordinate() + local Group = Unit:GetGroup() + local smokedistance = self.smokedistance + local smoked = false + local zones = {[1] = self.pickupZones, [2] = self.wpZones, [3] = self.dropOffZones, [4] = self.shipZones} + for i=1,4 do + for index,cargozone in pairs(zones[i]) do + local CZone = cargozone --#CTLD.CargoZone + local zonename = CZone.name + local zone = nil + if i == 4 then + zone = UNIT:FindByName(zonename) + else + zone = ZONE:FindByName(zonename) + end + local zonecoord = zone:GetCoordinate() + local active = CZone.active + local color = CZone.color + local distance = self:_GetDistance(zonecoord,unitcoord) + if distance < smokedistance and active then + -- smoke zone since we\'re nearby + if not Flare then + zonecoord:Smoke(color or SMOKECOLOR.White) + else + if color == SMOKECOLOR.Blue then color = FLARECOLOR.White end + zonecoord:Flare(color or FLARECOLOR.White) + end + local txt = "smoking" + if Flare then txt = "flaring" end + self:_SendMessage(string.format("Roger, %s zone %s!",txt, zonename), 10, false, Group) + smoked = true + end + end + end + 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) + end + return self +end + + --- User - Function to add/adjust unittype capabilities. + -- @param #CTLD self + -- @param #string Unittype The unittype to adjust. If passed as Wrapper.Unit#UNIT, it will search for the unit in the mission. + -- @param #boolean Cancrates Unit can load crates. Default false. + -- @param #boolean Cantroops Unit can load troops. Default false. + -- @param #number Cratelimit Unit can carry number of crates. Default 0. + -- @param #number Trooplimit Unit can carry number of troops. Default 0. + -- @param #number Length Unit lenght (in mteres) for the load radius. Default 20. + function CTLD:UnitCapabilities(Unittype, Cancrates, Cantroops, Cratelimit, Trooplimit, Length) + self:T(self.lid .. " UnitCapabilities") + local unittype = nil + local unit = nil + if type(Unittype) == "string" then + unittype = Unittype + elseif type(Unittype) == "table" then + unit = UNIT:FindByName(Unittype) -- Wrapper.Unit#UNIT + unittype = unit:GetTypeName() + else + return self + end + -- set capabilities + local capabilities = {} -- #CTLD.UnitCapabilities + capabilities.type = unittype + capabilities.crates = Cancrates or false + capabilities.troops = Cantroops or false + capabilities.cratelimit = Cratelimit or 0 + capabilities.trooplimit = Trooplimit or 0 + capabilities.length = Length or 20 + self.UnitTypes[unittype] = capabilities + return self + end + + --- (Internal) Check if a unit is hovering *in parameters*. + -- @param #CTLD self + -- @param Wrapper.Unit#UNIT Unit + -- @return #boolean Outcome + function CTLD:IsCorrectHover(Unit) + self:T(self.lid .. " IsCorrectHover") + local outcome = false + -- see if we are in air and within parameters. + if self:IsUnitInAir(Unit) then + -- get speed and height + local uspeed = Unit:GetVelocityMPS() + local uheight = Unit:GetHeight() + local ucoord = Unit:GetCoordinate() + local gheight = ucoord:GetLandHeight() + local aheight = uheight - gheight -- height above ground + local maxh = self.maximumHoverHeight -- 15 + local minh = self.minimumHoverHeight -- 5 + local mspeed = 2 -- 2 m/s + if (uspeed <= mspeed) and (aheight <= maxh) and (aheight >= minh) then + -- yep within parameters + outcome = true + end + end + return outcome + end + + --- (Internal) Check if a Hercules is flying *in parameters* for air drops. + -- @param #CTLD self + -- @param Wrapper.Unit#UNIT Unit + -- @return #boolean Outcome + function CTLD:IsCorrectFlightParameters(Unit) + self:T(self.lid .. " IsCorrectFlightParameters") + local outcome = false + -- see if we are in air and within parameters. + if self:IsUnitInAir(Unit) then + -- get speed and height + local uspeed = Unit:GetVelocityMPS() + local uheight = Unit:GetHeight() + local ucoord = Unit:GetCoordinate() + local gheight = ucoord:GetLandHeight() + local aheight = uheight - gheight -- height above ground + local maxh = self.HercMinAngels-- 1500m + local minh = self.HercMaxAngels -- 5000m + 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 + end + return outcome + end + + --- (Internal) List if a unit is hovering *in parameters*. + -- @param #CTLD self + -- @param Wrapper.Group#GROUP Group + -- @param Wrapper.Unit#UNIT Unit + function CTLD:_ShowHoverParams(Group,Unit) + local inhover = self:IsCorrectHover(Unit) + local htxt = "true" + if not inhover then htxt = "false" end + 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) + return self + end + + --- (Internal) List if a Herc unit is flying *in parameters*. + -- @param #CTLD self + -- @param Wrapper.Group#GROUP Group + -- @param Wrapper.Unit#UNIT Unit + function CTLD:_ShowFlightParams(Group,Unit) + local inhover = self:IsCorrectFlightParameters(Unit) + local htxt = "true" + if not inhover then htxt = "false" end + 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) + return self + end + + + --- (Internal) Check if a unit is in a load zone and is hovering in parameters. + -- @param #CTLD self + -- @param Wrapper.Unit#UNIT Unit + -- @return #boolean Outcome + function CTLD:CanHoverLoad(Unit) + self:T(self.lid .. " CanHoverLoad") + if self:IsHercules(Unit) then return false end + local outcome = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) and self:IsCorrectHover(Unit) + if not outcome then + outcome = self:IsUnitInZone(Unit,CTLD.CargoZoneType.SHIP) --and self:IsCorrectHover(Unit) + end + return outcome + end + + --- (Internal) Check if a unit is above ground. + -- @param #CTLD self + -- @param Wrapper.Unit#UNIT Unit + -- @return #boolean Outcome + function CTLD:IsUnitInAir(Unit) + -- get speed and height + local minheight = self.minimumHoverHeight + if self.enableHercules and Unit:GetTypeName() == "Hercules" then + minheight = 5.1 -- herc is 5m AGL on the ground + end + local uheight = Unit:GetHeight() + local ucoord = Unit:GetCoordinate() + local gheight = ucoord:GetLandHeight() + local aheight = uheight - gheight -- height above ground + if aheight >= minheight then + return true + else + return false + end + end + + --- (Internal) Autoload if we can do crates, have capacity free and are in a load zone. + -- @param #CTLD self + -- @param Wrapper.Unit#UNIT Unit + -- @return #CTLD self + function CTLD:AutoHoverLoad(Unit) + self:T(self.lid .. " AutoHoverLoad") + -- get capabilities and current load + local unittype = Unit:GetTypeName() + local unitname = Unit:GetName() + local Group = Unit:GetGroup() + local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitCapabilities + local cancrates = capabilities.crates -- #boolean + local cratelimit = capabilities.cratelimit -- #number + if cancrates then + -- get load + local numberonboard = 0 + local loaded = {} + if self.Loaded_Cargo[unitname] then + loaded = self.Loaded_Cargo[unitname] -- #CTLD.LoadedCargo + numberonboard = loaded.Cratesloaded or 0 + end + local load = cratelimit - numberonboard + local canload = self:CanHoverLoad(Unit) + if canload and load > 0 then + self:_LoadCratesNearby(Group,Unit) + end + end + return self + end + + --- (Internal) Run through all pilots and see if we autoload. + -- @param #CTLD self + -- @return #CTLD self + function CTLD:CheckAutoHoverload() + if self.hoverautoloading then + for _,_pilot in pairs (self.CtldUnits) do + local Unit = UNIT:FindByName(_pilot) + if self:CanHoverLoad(Unit) then self:AutoHoverLoad(Unit) end + end + end + return self + end + + --- (Internal) Run through DroppedTroops and capture alive units + -- @param #CTLD self + -- @return #CTLD self + function CTLD:CleanDroppedTroops() + -- Troops + local troops = self.DroppedTroops + local newtable = {} + for _index, _group in pairs (troops) do + self:T({_group.ClassName}) + if _group and _group.ClassName == "GROUP" then + if _group:IsAlive() then + newtable[_index] = _group + end + end + end + self.DroppedTroops = newtable + -- Engineers + local engineers = self.EngineersInField + local engtable = {} + for _index, _group in pairs (engineers) do + self:T({_group.ClassName}) + if _group and _group:IsNotStatus("Stopped") then + engtable[_index] = _group + end + end + self.EngineersInField = engtable + return self + end + + --- User - function to add stock of a certain troops type + -- @param #CTLD self + -- @param #string Name Name as defined in the generic cargo. + -- @param #number Number Number of units/groups to add. + -- @return #CTLD self + function CTLD:AddStockTroops(Name, Number) + local name = Name or "none" + local number = Number or 1 + -- find right generic type + local gentroops = self.Cargo_Troops + for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO + if _troop.Name == name then + _troop:AddStock(number) + end + end + end + + --- User - function to add stock of a certain crates type + -- @param #CTLD self + -- @param #string Name Name as defined in the generic cargo. + -- @param #number Number Number of units/groups to add. + -- @return #CTLD self + function CTLD:AddStockCrates(Name, Number) + local name = Name or "none" + local number = Number or 1 + -- find right generic type + local gentroops = self.Cargo_Crates + for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO + if _troop.Name == name then + _troop:AddStock(number) + end + end + end + + --- User - function to remove stock of a certain troops type + -- @param #CTLD self + -- @param #string Name Name as defined in the generic cargo. + -- @param #number Number Number of units/groups to add. + -- @return #CTLD self + function CTLD:RemoveStockTroops(Name, Number) + local name = Name or "none" + local number = Number or 1 + -- find right generic type + local gentroops = self.Cargo_Troops + for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO + if _troop.Name == name then + _troop:RemoveStock(number) + end + end + end + + --- User - function to remove stock of a certain crates type + -- @param #CTLD self + -- @param #string Name Name as defined in the generic cargo. + -- @param #number Number Number of units/groups to add. + -- @return #CTLD self + function CTLD:RemoveStockCrates(Name, Number) + local name = Name or "none" + local number = Number or 1 + -- find right generic type + local gentroops = self.Cargo_Crates + for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO + if _troop.Name == name then + _troop:RemoveStock(number) + end + end + return self + end + + --- (Internal) Check on engineering teams + -- @param #CTLD self + -- @return #CTLD self + function CTLD:_CheckEngineers() + self:T(self.lid.." CheckEngineers") + local engtable = self.EngineersInField + for _ind,_engineers in pairs (engtable) do + local engineers = _engineers -- #CTLD_ENGINEERING + local wrenches = engineers.Group -- Wrapper.Group#GROUP + self:T(_engineers.lid .. _engineers:GetStatus()) + if wrenches and wrenches:IsAlive() then + if engineers:IsStatus("Running") or engineers:IsStatus("Searching") then + local crates,number = self:_FindCratesNearby(wrenches,nil, self.EngineerSearch) -- #table + engineers:Search(crates,number) + elseif engineers:IsStatus("Moving") then + engineers:Move() + elseif engineers:IsStatus("Arrived") then + engineers:Build() + local unit = wrenches:GetUnit(1) + self:_BuildCrates(wrenches,unit,true) + self:_RepairCrates(wrenches,unit,true) + engineers:Done() + end + else + engineers:Stop() + end + end + return self + end + + --- (User) Pre-populate troops in the field. + -- @param #CTLD self + -- @param Core.Zone#ZONE Zone The zone where to drop the troops. + -- @param Ops.CTLD#CTLD_CARGO Cargo The #CTLD_CARGO object to spawn. + -- @return #CTLD self + -- @usage Use this function to pre-populate the field with Troops or Engineers at a random coordinate in a zone: + -- -- create a matching #CTLD_CARGO type + -- local InjectTroopsType = CTLD_CARGO:New(nil,"Infantry",{"Inf12"},CTLD_CARGO.Enum.TROOPS,true,true,12,nil,false,80) + -- -- get a #ZONE object + -- local dropzone = ZONE:New("InjectZone") -- Core.Zone#ZONE + -- -- and go: + -- my_ctld:InjectTroops(dropzone,InjectTroopsType) + function CTLD:InjectTroops(Zone,Cargo) + self:T(self.lid.." InjectTroops") + local cargo = Cargo -- #CTLD_CARGO + + local function IsTroopsMatch(cargo) + local match = false + local cgotbl = self.Cargo_Troops + local name = cargo:GetName() + for _,_cgo in pairs (cgotbl) do + local cname = _cgo:GetName() + if name == cname then + match = true + break + end + end + return match + end + + if not IsTroopsMatch(cargo) then + self.CargoCounter = self.CargoCounter + 1 + cargo.ID = self.CargoCounter + cargo.Stock = 1 + table.insert(self.Cargo_Troops,cargo) + end + + local type = cargo:GetType() -- #CTLD_CARGO.Enum + if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS) then + -- unload + local name = cargo:GetName() or "none" + local temptable = cargo:GetTemplates() or {} + local factor = 1.5 + local zone = Zone + + local randomcoord = zone:GetRandomCoordinate(10,30*factor):GetVec2() + for _,_template in pairs(temptable) do + self.TroopCounter = self.TroopCounter + 1 + 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) + if self.movetroopstowpzone and type ~= CTLD_CARGO.Enum.ENGINEERS then + self:_MoveGroupToZone(self.DroppedTroops[self.TroopCounter]) + end + end -- template loop + cargo:SetWasDropped(true) + -- engineering group? + if type == CTLD_CARGO.Enum.ENGINEERS then + self.Engineers = self.Engineers + 1 + local grpname = self.DroppedTroops[self.TroopCounter]:GetName() + self.EngineersInField[self.Engineers] = CTLD_ENGINEERING:New(name, grpname) + --self:I(string.format("%s Injected Engineers %s into action!",self.lid, name)) + else + --self:I(string.format("%s Injected Troops %s into action!",self.lid, name)) + end + if self.eventoninject then + self:__TroopsDeployed(1,nil,nil,self.DroppedTroops[self.TroopCounter]) + end + end -- if type end + return self + end + + --- (User) Pre-populate vehicles in the field. + -- @param #CTLD self + -- @param Core.Zone#ZONE Zone The zone where to drop the troops. + -- @param Ops.CTLD#CTLD_CARGO Cargo The #CTLD_CARGO object to spawn. + -- @return #CTLD self + -- @usage Use this function to pre-populate the field with Vehicles or FOB at a random coordinate in a zone: + -- -- create a matching #CTLD_CARGO type + -- local InjectVehicleType = CTLD_CARGO:New(nil,"Humvee",{"Humvee"},CTLD_CARGO.Enum.VEHICLE,true,true,1,nil,false,1000) + -- -- get a #ZONE object + -- local dropzone = ZONE:New("InjectZone") -- Core.Zone#ZONE + -- -- and go: + -- my_ctld:InjectVehicles(dropzone,InjectVehicleType) + function CTLD:InjectVehicles(Zone,Cargo) + self:T(self.lid.." InjectVehicles") + local cargo = Cargo -- #CTLD_CARGO + + local function IsVehicMatch(cargo) + local match = false + local cgotbl = self.Cargo_Crates + local name = cargo:GetName() + for _,_cgo in pairs (cgotbl) do + local cname = _cgo:GetName() + if name == cname then + match = true + break + end + end + return match + end + + if not IsVehicMatch(cargo) then + self.CargoCounter = self.CargoCounter + 1 + cargo.ID = self.CargoCounter + cargo.Stock = 1 + table.insert(self.Cargo_Crates,cargo) + end + + local type = cargo:GetType() -- #CTLD_CARGO.Enum + if (type == CTLD_CARGO.Enum.VEHICLE or type == CTLD_CARGO.Enum.FOB) then + -- unload + local name = cargo:GetName() or "none" + local temptable = cargo:GetTemplates() or {} + local factor = 1.5 + local zone = Zone + local randomcoord = zone:GetRandomCoordinate(10,30*factor):GetVec2() + cargo:SetWasDropped(true) + local canmove = false + if type == CTLD_CARGO.Enum.VEHICLE then canmove = true end + for _,_template in pairs(temptable) do + self.TroopCounter = self.TroopCounter + 1 + local alias = string.format("%s-%d", _template, math.random(1,100000)) + if canmove then + 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) + :InitDelayOff() + :SpawnFromVec2(randomcoord) + end + if self.movetroopstowpzone and canmove then + self:_MoveGroupToZone(self.DroppedTroops[self.TroopCounter]) + end + if self.eventoninject then + self:__CratesBuild(1,nil,nil,self.DroppedTroops[self.TroopCounter]) + end + end -- end loop + end -- if type end + return self + end + +------------------------------------------------------------------- +-- FSM functions +------------------------------------------------------------------- + + --- (Internal) FSM Function onafterStart. + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @return #CTLD self + function CTLD:onafterStart(From, Event, To) + self:T({From, Event, To}) + self:I(self.lid .. "Started ("..self.version..")") + if self.useprefix or self.enableHercules then + local prefix = self.prefixes + if self.enableHercules then + self.PilotGroups = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(prefix):FilterStart() + else + self.PilotGroups = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(prefix):FilterCategories("helicopter"):FilterStart() + end + else + self.PilotGroups = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterCategories("helicopter"):FilterStart() + end + -- Events + self:HandleEvent(EVENTS.PlayerEnterAircraft, self._EventHandler) + self:HandleEvent(EVENTS.PlayerEnterUnit, self._EventHandler) + self:HandleEvent(EVENTS.PlayerLeaveUnit, self._EventHandler) + self:__Status(-5) + + -- AutoSave + if self.enableLoadSave then + local interval = self.saveinterval + local filename = self.filename + local filepath = self.filepath + self:__Save(interval,filepath,filename) + end + return self + end + + --- (Internal) FSM Function onbeforeStatus. + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @return #CTLD self + function CTLD:onbeforeStatus(From, Event, To) + self:T({From, Event, To}) + self:CleanDroppedTroops() + self:_RefreshF10Menus() + self:_RefreshRadioBeacons() + self:CheckAutoHoverload() + self:_CheckEngineers() + return self + end + + --- (Internal) FSM Function onafterStatus. + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @return #CTLD self + function CTLD:onafterStatus(From, Event, To) + self:T({From, Event, To}) + -- gather some stats + -- pilots + local pilots = 0 + for _,_pilot in pairs (self.CtldUnits) do + pilots = pilots + 1 + end + + -- spawned cargo boxes curr in field + local boxes = 0 + for _,_pilot in pairs (self.Spawned_Cargo) do + boxes = boxes + 1 + end + + local cc = self.CargoCounter + local tc = self.TroopCounter + + if self.debug or self.verbose > 0 then + local text = string.format("%s Pilots %d | Live Crates %d |\nCargo Counter %d | Troop Counter %d", self.lid, pilots, boxes, cc, tc) + local m = MESSAGE:New(text,10,"CTLD"):ToAll() + if self.verbose > 0 then + self:I(self.lid.."Cargo and Troops in Stock:") + for _,_troop in pairs (self.Cargo_Crates) do + local name = _troop:GetName() + local stock = _troop:GetStock() + self:I(string.format("-- %s \t\t\t %d", name, stock)) + end + for _,_troop in pairs (self.Cargo_Statics) do + local name = _troop:GetName() + local stock = _troop:GetStock() + self:I(string.format("-- %s \t\t\t %d", name, stock)) + end + for _,_troop in pairs (self.Cargo_Troops) do + local name = _troop:GetName() + local stock = _troop:GetStock() + self:I(string.format("-- %s \t\t %d", name, stock)) + end + end + end + self:__Status(-30) + return self + end + + --- (Internal) FSM Function onafterStop. + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @return #CTLD self + function CTLD:onafterStop(From, Event, To) + self:T({From, Event, To}) + self:UnhandleEvent(EVENTS.PlayerEnterAircraft) + self:UnhandleEvent(EVENTS.PlayerEnterUnit) + self:UnhandleEvent(EVENTS.PlayerLeaveUnit) + return self + end + + --- (Internal) FSM Function onbeforeTroopsPickedUp. + -- @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 crate. + -- @return #CTLD self + function CTLD:onbeforeTroopsPickedUp(From, Event, To, Group, Unit, Cargo) + self:T({From, Event, To}) + return self + end + + --- (Internal) FSM Function onbeforeCratesPickedUp. + -- @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 crate. + -- @return #CTLD self + function CTLD:onbeforeCratesPickedUp(From, Event, To, Group, Unit, Cargo) + self:T({From, Event, To}) + 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. + -- @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:onbeforeTroopsDeployed(From, Event, To, Group, Unit, Troops) + self:T({From, Event, To}) + return self + end + + --- (Internal) FSM Function onbeforeCratesDropped. + -- @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 #table Cargotable Table of #CTLD_CARGO objects dropped. + -- @return #CTLD self + function CTLD:onbeforeCratesDropped(From, Event, To, Group, Unit, Cargotable) + self:T({From, Event, To}) + return self + end + + --- (Internal) FSM Function onbeforeCratesBuild. + -- @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 build. + -- @return #CTLD self + function CTLD:onbeforeCratesBuild(From, Event, To, Group, Unit, Vehicle) + self:T({From, Event, To}) + return self + end + + --- (Internal) FSM Function onbeforeTroopsRTB. + -- @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. + -- @return #CTLD self + function CTLD:onbeforeTroopsRTB(From, Event, To, Group, Unit) + self:T({From, Event, To}) + return self + end + + --- On before "Save" event. Checks if io and lfs are available. + -- @param #CTLD self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string path (Optional) Path where the file is saved. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if the lfs module is desanitized. + -- @param #string filename (Optional) File name for saving. Default is "CTLD__Persist.csv". + function CTLD:onbeforeSave(From, Event, To, path, filename) + self:T({From, Event, To, path, filename}) + if not self.enableLoadSave then + return self + end + -- Thanks to @FunkyFranky + -- Check io module is available. + if not io then + self:E(self.lid.."ERROR: io not desanitized. Can't save current state.") + return false + end + + -- Check default path. + if path==nil and not lfs then + self:E(self.lid.."WARNING: lfs not desanitized. State will be saved in DCS installation root directory rather than your \"Saved Games\\DCS\" folder.") + end + + return true + end + + --- On after "Save" event. Player data is saved to file. + -- @param #CTLD self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string path Path where the file is saved. If nil, file is saved in the DCS root installtion directory or your "Saved Games" folder if lfs was desanitized. + -- @param #string filename (Optional) File name for saving. Default is Default is "CTLD__Persist.csv". + function CTLD:onafterSave(From, Event, To, path, filename) + self:T({From, Event, To, path, filename}) + -- Thanks to @FunkyFranky + if not self.enableLoadSave then + return self + end + --- Function that saves data to file + local function _savefile(filename, data) + local f = assert(io.open(filename, "wb")) + f:write(data) + f:close() + end + + -- Set path or default. + if lfs then + path=self.filepath or lfs.writedir() + end + + -- Set file name. + filename=filename or self.filename + + -- Set path. + if path~=nil then + filename=path.."\\"..filename + end + + local grouptable = self.DroppedTroops -- #table + local cgovehic = self.Cargo_Crates + local cgotable = self.Cargo_Troops + local stcstable = self.Spawned_Cargo + + local statics = nil + local statics = {} + self:I(self.lid.."Bulding Statics Table for Saving") + for _,_cargo in pairs (stcstable) do + local cargo = _cargo -- #CTLD_CARGO + local object = cargo:GetPositionable() -- Wrapper.Static#STATIC + if object and object:IsAlive() and cargo:WasDropped() then + self:I({_cargo}) + statics[#statics+1] = cargo + end + end + + -- find matching cargo + local function FindCargoType(name,table) + -- name matching a template in the table + local match = false + local cargo = nil + for _ind,_cargo in pairs (table) do + local thiscargo = _cargo -- #CTLD_CARGO + local template = thiscargo:GetTemplates() + if type(template) == "string" then + template = { template } + end + for _,_name in pairs (template) do + --self:I(string.format("*** Saving CTLD: Matching %s with %s",name,_name)) + if string.find(name,_name) and _cargo:GetType() ~= CTLD_CARGO.Enum.REPAIR then + match = true + cargo = thiscargo + end + end + if match then break end + end + return match, cargo + end + + + --local data = "LoadedData = {\n" + local data = "Group,x,y,z,CargoName,CargoTemplates,CargoType,CratesNeeded,CrateMass\n" + local n = 0 + for _,_grp in pairs(grouptable) do + local group = _grp -- Wrapper.Group#GROUP + if group and group:IsAlive() then + -- get template name + local name = group:GetName() + local template = string.gsub(name,"-(.+)$","") + if string.find(template,"#") then + template = string.gsub(name,"#(%d+)$","") + end + + local match, cargo = FindCargoType(template,cgotable) + if not match then + match, cargo = FindCargoType(template,cgovehic) + end + if match then + n = n + 1 + local cargo = cargo -- #CTLD_CARGO + local cgoname = cargo.Name + local cgotemp = cargo.Templates + local cgotype = cargo.CargoType + local cgoneed = cargo.CratesNeeded + local cgomass = cargo.PerCrateMass + + if type(cgotemp) == "table" then + local templates = "{" + for _,_tmpl in pairs(cgotemp) do + templates = templates .. _tmpl .. ";" + end + templates = templates .. "}" + cgotemp = templates + end + + local location = group:GetVec3() + local txt = string.format("%s,%d,%d,%d,%s,%s,%s,%d,%d\n" + ,template,location.x,location.y,location.z,cgoname,cgotemp,cgotype,cgoneed,cgomass) + data = data .. txt + end + end + end + + for _,_cgo in pairs(statics) do + local object = _cgo -- #CTLD_CARGO + local cgoname = object.Name + local cgotemp = object.Templates + + if type(cgotemp) == "table" then + local templates = "{" + for _,_tmpl in pairs(cgotemp) do + templates = templates .. _tmpl .. ";" + end + templates = templates .. "}" + cgotemp = templates + end + + local cgotype = object.CargoType + local cgoneed = object.CratesNeeded + local cgomass = object.PerCrateMass + local crateobj = object.Positionable + local location = crateobj:GetVec3() + local txt = string.format("%s,%d,%d,%d,%s,%s,%s,%d,%d\n" + ,"STATIC",location.x,location.y,location.z,cgoname,cgotemp,cgotype,cgoneed,cgomass) + data = data .. txt + end + + _savefile(filename, data) + + -- AutoSave + if self.enableLoadSave then + local interval = self.saveinterval + local filename = self.filename + local filepath = self.filepath + self:__Save(interval,filepath,filename) + end + return self + end + + --- On before "Load" event. Checks if io and lfs and the file are available. + -- @param #CTLD self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string path (Optional) Path where the file is located. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if the lfs module is desanitized. + -- @param #string filename (Optional) File name for loading. Default is "CTLD__Persist.csv". + function CTLD:onbeforeLoad(From, Event, To, path, filename) + self:T({From, Event, To, path, filename}) + if not self.enableLoadSave then + return self + end + --- Function that check if a file exists. + local function _fileexists(name) + local f=io.open(name,"r") + if f~=nil then + io.close(f) + return true + else + return false + end + end + + -- Set file name and path + filename=filename or self.filename + path = path or self.filepath + + -- Check io module is available. + if not io then + self:E(self.lid.."WARNING: io not desanitized. Cannot load file.") + return false + end + + -- Check default path. + if path==nil and not lfs then + self:E(self.lid.."WARNING: lfs not desanitized. State will be saved in DCS installation root directory rather than your \"Saved Games\\DCS\" folder.") + end + + -- Set path or default. + if lfs then + path=path or lfs.writedir() + end + + -- Set path. + if path~=nil then + filename=path.."\\"..filename + end + + -- Check if file exists. + local exists=_fileexists(filename) + + if exists then + return true + else + self:E(self.lid..string.format("WARNING: State file %s might not exist.", filename)) + return false + --return self + end + + end + + --- On after "Load" event. Loads dropped units from file. + -- @param #CTLD self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string path (Optional) Path where the file is located. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if the lfs module is desanitized. + -- @param #string filename (Optional) File name for loading. Default is "CTLD__Persist.csv". + function CTLD:onafterLoad(From, Event, To, path, filename) + self:T({From, Event, To, path, filename}) + if not self.enableLoadSave then + return self + end + --- Function that loads data from a file. + local function _loadfile(filename) + local f=assert(io.open(filename, "rb")) + local data=f:read("*all") + f:close() + return data + end + + -- Set file name and path + filename=filename or self.filename + path = path or self.filepath + + -- Set path or default. + if lfs then + path=path or lfs.writedir() + end + + -- Set path. + if path~=nil then + filename=path.."\\"..filename + end + + -- Info message. + local text=string.format("Loading CTLD state from file %s", filename) + MESSAGE:New(text,10):ToAllIf(self.Debug) + self:I(self.lid..text) + + local file=assert(io.open(filename, "rb")) + + local loadeddata = {} + for line in file:lines() do + --self:I({line=type(line)}) + loadeddata[#loadeddata+1] = line + end + file:close() + + -- remove header + table.remove(loadeddata, 1) + + for _id,_entry in pairs (loadeddata) do + local dataset = UTILS.Split(_entry,",") + -- 1=Group,2=x,3=y,4=z,5=CargoName,6=CargoTemplates,7=CargoType,8=CratesNeeded,9=CrateMass + local groupname = dataset[1] + local vec2 = {} + vec2.x = tonumber(dataset[2]) + vec2.y = tonumber(dataset[4]) + local cargoname = dataset[5] + local cargotype = dataset[7] + if type(groupname) == "string" and groupname ~= "STATIC" then + local cargotemplates = dataset[6] + cargotemplates = string.gsub(cargotemplates,"{","") + cargotemplates = string.gsub(cargotemplates,"}","") + cargotemplates = UTILS.Split(cargotemplates,";") + local size = tonumber(dataset[8]) + local mass = tonumber(dataset[9]) + --self:I({groupname,vec3,cargoname,cargotemplates,cargotype,size,mass}) + -- inject at Vec2 + local dropzone = ZONE_RADIUS:New("DropZone",vec2,20) + if cargotype == CTLD_CARGO.Enum.VEHICLE or cargotype == CTLD_CARGO.Enum.FOB then + local injectvehicle = CTLD_CARGO:New(nil,cargoname,cargotemplates,cargotype,true,true,size,nil,true,mass) + self:InjectVehicles(dropzone,injectvehicle) + elseif cargotype == CTLD_CARGO.Enum.TROOPS or cargotype == CTLD_CARGO.Enum.ENGINEERS then + local injecttroops = CTLD_CARGO:New(nil,cargoname,cargotemplates,cargotype,true,true,size,nil,true,mass) + self:InjectTroops(dropzone,injecttroops) + end + elseif (type(groupname) == "string" and groupname == "STATIC") or cargotype == CTLD_CARGO.Enum.REPAIR then + local cargotemplates = dataset[6] + local size = tonumber(dataset[8]) + local mass = tonumber(dataset[9]) + local dropzone = ZONE_RADIUS:New("DropZone",vec2,20) + -- STATIC,-84037,154,834021,Humvee,{Humvee;},Vehicle,1,100 + -- STATIC,-84036,154,834018,Ammunition-1,ammo_cargo,Static,1,500 + local injectstatic = nil + if cargotype == CTLD_CARGO.Enum.VEHICLE or cargotype == CTLD_CARGO.Enum.FOB then + cargotemplates = string.gsub(cargotemplates,"{","") + cargotemplates = string.gsub(cargotemplates,"}","") + cargotemplates = UTILS.Split(cargotemplates,";") + injectstatic = CTLD_CARGO:New(nil,cargoname,cargotemplates,cargotype,true,true,size,nil,true,mass) + elseif cargotype == CTLD_CARGO.Enum.STATIC or cargotype == CTLD_CARGO.Enum.REPAIR then + injectstatic = CTLD_CARGO:New(nil,cargoname,cargotemplates,cargotype,true,true,size,nil,true,mass) + end + if injectstatic then + self:InjectStatics(dropzone,injectstatic) + end + end + end + + return self + end +end -- end do +------------------------------------------------------------------- +-- End Ops.CTLD.lua +------------------------------------------------------------------- +--- **AI** -- Balance player slots with AI to create an engaging simulation environment, independent of the amount of players. +-- +-- **Features:** +-- +-- * Automatically spawn AI as a replacement of free player slots for a coalition. +-- * Make the AI to perform tasks. +-- * Define a maximum amount of AI to be active at the same time. +-- * Configure the behaviour of AI when a human joins a slot for which an AI is active. +-- +-- === +-- +-- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master-release/AIB%20-%20AI%20Balancing) +-- +-- === +-- +-- ### [YouTube Playlist](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl2CJVIrL1TdAumuVS8n64B7) +-- +-- === +-- +-- ### Author: **FlightControl** +-- ### Contributions: +-- +-- * **[Dutch_Baron](https://forums.eagle.ru/member.php?u=112075)**: Working together with James has resulted in the creation of the AI_BALANCER class. James has shared his ideas on balancing AI with air units, and together we made a first design which you can use now :-) +-- +-- === +-- +-- @module AI.AI_Balancer +-- @image AI_Balancing.JPG + +--- @type AI_BALANCER +-- @field Core.Set#SET_CLIENT SetClient +-- @field Core.Spawn#SPAWN SpawnAI +-- @field Wrapper.Group#GROUP Test +-- @extends Core.Fsm#FSM_SET + + +--- Monitors and manages as many replacement AI groups as there are +-- CLIENTS in a SET\_CLIENT collection, which are not occupied by human players. +-- In other words, use AI_BALANCER to simulate human behaviour by spawning in replacement AI in multi player missions. +-- +-- The parent class @{Core.Fsm#FSM_SET} manages the functionality to control the Finite State Machine (FSM). +-- The mission designer can tailor the behaviour of the AI_BALANCER, by defining event and state transition methods. +-- An explanation about state and event transition methods can be found in the @{FSM} module documentation. +-- +-- The mission designer can tailor the AI_BALANCER behaviour, by implementing a state or event handling method for the following: +-- +-- * @{#AI_BALANCER.OnAfterSpawned}( AISet, From, Event, To, AIGroup ): Define to add extra logic when an AI is spawned. +-- +-- ## 1. AI_BALANCER construction +-- +-- Create a new AI_BALANCER object with the @{#AI_BALANCER.New}() method: +-- +-- ## 2. AI_BALANCER is a FSM +-- +-- ![Process](..\Presentations\AI_Balancer\Dia13.JPG) +-- +-- ### 2.1. AI_BALANCER States +-- +-- * **Monitoring** ( Set ): Monitoring the Set if all AI is spawned for the Clients. +-- * **Spawning** ( Set, ClientName ): There is a new AI group spawned with ClientName as the name of reference. +-- * **Spawned** ( Set, AIGroup ): A new AI has been spawned. You can handle this event to customize the AI behaviour with other AI FSMs or own processes. +-- * **Destroying** ( Set, AIGroup ): The AI is being destroyed. +-- * **Returning** ( Set, AIGroup ): The AI is returning to the airbase specified by the ReturnToAirbase methods. Handle this state to customize the return behaviour of the AI, if any. +-- +-- ### 2.2. AI_BALANCER Events +-- +-- * **Monitor** ( Set ): Every 10 seconds, the Monitor event is triggered to monitor the Set. +-- * **Spawn** ( Set, ClientName ): Triggers when there is a new AI group to be spawned with ClientName as the name of reference. +-- * **Spawned** ( Set, AIGroup ): Triggers when a new AI has been spawned. You can handle this event to customize the AI behaviour with other AI FSMs or own processes. +-- * **Destroy** ( Set, AIGroup ): The AI is being destroyed. +-- * **Return** ( Set, AIGroup ): The AI is returning to the airbase specified by the ReturnToAirbase methods. +-- +-- ## 3. AI_BALANCER spawn interval for replacement AI +-- +-- Use the method @{#AI_BALANCER.InitSpawnInterval}() to set the earliest and latest interval in seconds that is waited until a new replacement AI is spawned. +-- +-- ## 4. AI_BALANCER returns AI to Airbases +-- +-- By default, When a human player joins a slot that is AI_BALANCED, the AI group will be destroyed by default. +-- However, there are 2 additional options that you can use to customize the destroy behaviour. +-- When a human player joins a slot, you can configure to let the AI return to: +-- +-- * @{#AI_BALANCER.ReturnToHomeAirbase}: Returns the AI to the **home** @{Wrapper.Airbase#AIRBASE}. +-- * @{#AI_BALANCER.ReturnToNearestAirbases}: Returns the AI to the **nearest friendly** @{Wrapper.Airbase#AIRBASE}. +-- +-- Note that when AI returns to an airbase, the AI_BALANCER will trigger the **Return** event and the AI will return, +-- otherwise the AI_BALANCER will trigger a **Destroy** event, and the AI will be destroyed. +-- +-- @field #AI_BALANCER +AI_BALANCER = { + ClassName = "AI_BALANCER", + PatrolZones = {}, + AIGroups = {}, + Earliest = 5, -- Earliest a new AI can be spawned is in 5 seconds. + Latest = 60, -- Latest a new AI can be spawned is in 60 seconds. +} + + + +--- Creates a new AI_BALANCER object +-- @param #AI_BALANCER self +-- @param Core.Set#SET_CLIENT SetClient A SET\_CLIENT object that will contain the CLIENT objects to be monitored if they are alive or not (joined by a player). +-- @param Core.Spawn#SPAWN SpawnAI The default Spawn object to spawn new AI Groups when needed. +-- @return #AI_BALANCER +function AI_BALANCER:New( SetClient, SpawnAI ) + + -- Inherits from BASE + local self = BASE:Inherit( self, FSM_SET:New( SET_GROUP:New() ) ) -- AI.AI_Balancer#AI_BALANCER + + -- TODO: Define the OnAfterSpawned event + self:SetStartState( "None" ) + self:AddTransition( "*", "Monitor", "Monitoring" ) + self:AddTransition( "*", "Spawn", "Spawning" ) + self:AddTransition( "Spawning", "Spawned", "Spawned" ) + self:AddTransition( "*", "Destroy", "Destroying" ) + self:AddTransition( "*", "Return", "Returning" ) + + self.SetClient = SetClient + self.SetClient:FilterOnce() + self.SpawnAI = SpawnAI + + self.SpawnQueue = {} + + self.ToNearestAirbase = false + self.ToHomeAirbase = false + + self:__Monitor( 1 ) + + return self +end + +--- Sets the earliest to the latest interval in seconds how long AI_BALANCER will wait to spawn a new AI. +-- Provide 2 identical seconds if the interval should be a fixed amount of seconds. +-- @param #AI_BALANCER self +-- @param #number Earliest The earliest a new AI can be spawned in seconds. +-- @param #number Latest The latest a new AI can be spawned in seconds. +-- @return self +function AI_BALANCER:InitSpawnInterval( Earliest, Latest ) + + self.Earliest = Earliest + self.Latest = Latest + + return self +end + +--- Returns the AI to the nearest friendly @{Wrapper.Airbase#AIRBASE}. +-- @param #AI_BALANCER self +-- @param DCS#Distance ReturnThresholdRange If there is an enemy @{Wrapper.Client#CLIENT} within the ReturnThresholdRange given in meters, the AI will not return to the nearest @{Wrapper.Airbase#AIRBASE}. +-- @param Core.Set#SET_AIRBASE ReturnAirbaseSet The SET of @{Core.Set#SET_AIRBASE}s to evaluate where to return to. +function AI_BALANCER:ReturnToNearestAirbases( ReturnThresholdRange, ReturnAirbaseSet ) + + self.ToNearestAirbase = true + self.ReturnThresholdRange = ReturnThresholdRange + self.ReturnAirbaseSet = ReturnAirbaseSet +end + +--- Returns the AI to the home @{Wrapper.Airbase#AIRBASE}. +-- @param #AI_BALANCER self +-- @param DCS#Distance ReturnThresholdRange If there is an enemy @{Wrapper.Client#CLIENT} within the ReturnThresholdRange given in meters, the AI will not return to the nearest @{Wrapper.Airbase#AIRBASE}. +function AI_BALANCER:ReturnToHomeAirbase( ReturnThresholdRange ) + + self.ToHomeAirbase = true + self.ReturnThresholdRange = ReturnThresholdRange +end + +--- @param #AI_BALANCER self +-- @param Core.Set#SET_GROUP SetGroup +-- @param #string ClientName +-- @param Wrapper.Group#GROUP AIGroup +function AI_BALANCER:onenterSpawning( SetGroup, From, Event, To, ClientName ) + + -- OK, Spawn a new group from the default SpawnAI object provided. + local AIGroup = self.SpawnAI:Spawn() -- Wrapper.Group#GROUP + if AIGroup then + AIGroup:T( { "Spawning new AIGroup", ClientName = ClientName } ) + --TODO: need to rework UnitName thing ... + + SetGroup:Remove( ClientName ) -- Ensure that the previously allocated AIGroup to ClientName is removed in the Set. + SetGroup:Add( ClientName, AIGroup ) + self.SpawnQueue[ClientName] = nil + + -- Fire the Spawned event. The first parameter is the AIGroup just Spawned. + -- Mission designers can catch this event to bind further actions to the AIGroup. + self:Spawned( AIGroup ) + end +end + +--- @param #AI_BALANCER self +-- @param Core.Set#SET_GROUP SetGroup +-- @param Wrapper.Group#GROUP AIGroup +function AI_BALANCER:onenterDestroying( SetGroup, From, Event, To, ClientName, AIGroup ) + + AIGroup:Destroy() + SetGroup:Flush( self ) + SetGroup:Remove( ClientName ) + SetGroup:Flush( self ) +end + +--- RTB +-- @param #AI_BALANCER self +-- @param Core.Set#SET_GROUP SetGroup +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Wrapper.Group#GROUP AIGroup +function AI_BALANCER:onenterReturning( SetGroup, From, Event, To, AIGroup ) + + local AIGroupTemplate = AIGroup:GetTemplate() + if self.ToHomeAirbase == true then + local WayPointCount = #AIGroupTemplate.route.points + local SwitchWayPointCommand = AIGroup:CommandSwitchWayPoint( 1, WayPointCount, 1 ) + AIGroup:SetCommand( SwitchWayPointCommand ) + AIGroup:MessageToRed( "Returning to home base ...", 30 ) + else + -- Okay, we need to send this Group back to the nearest base of the Coalition of the AI. + --TODO: i need to rework the POINT_VEC2 thing. + local PointVec2 = POINT_VEC2:New( AIGroup:GetVec2().x, AIGroup:GetVec2().y ) + local ClosestAirbase = self.ReturnAirbaseSet:FindNearestAirbaseFromPointVec2( PointVec2 ) + self:T( ClosestAirbase.AirbaseName ) + --[[ + AIGroup:MessageToRed( "Returning to " .. ClosestAirbase:GetName().. " ...", 30 ) + local RTBRoute = AIGroup:RouteReturnToAirbase( ClosestAirbase ) + AIGroupTemplate.route = RTBRoute + AIGroup:Respawn( AIGroupTemplate ) + ]] + AIGroup:RouteRTB(ClosestAirbase) + end + +end + + +--- @param #AI_BALANCER self +function AI_BALANCER:onenterMonitoring( SetGroup ) + + self:T2( { self.SetClient:Count() } ) + --self.SetClient:Flush() + + self.SetClient:ForEachClient( + --- @param Wrapper.Client#CLIENT Client + function( Client ) + self:T3(Client.ClientName) + + local AIGroup = self.Set:Get( Client.UnitName ) -- Wrapper.Group#GROUP + if AIGroup then self:T( { AIGroup = AIGroup:GetName(), IsAlive = AIGroup:IsAlive() } ) end + if Client:IsAlive() == true then + + if AIGroup and AIGroup:IsAlive() == true then + + if self.ToNearestAirbase == false and self.ToHomeAirbase == false then + self:Destroy( Client.UnitName, AIGroup ) + else + -- We test if there is no other CLIENT within the self.ReturnThresholdRange of the first unit of the AI group. + -- If there is a CLIENT, the AI stays engaged and will not return. + -- If there is no CLIENT within the self.ReturnThresholdRange, then the unit will return to the Airbase return method selected. + + local PlayerInRange = { Value = false } + local RangeZone = ZONE_RADIUS:New( 'RangeZone', AIGroup:GetVec2(), self.ReturnThresholdRange ) + + self:T2( RangeZone ) + + _DATABASE:ForEachPlayerUnit( + --- @param Wrapper.Unit#UNIT RangeTestUnit + function( RangeTestUnit, RangeZone, AIGroup, PlayerInRange ) + self:T2( { PlayerInRange, RangeTestUnit.UnitName, RangeZone.ZoneName } ) + if RangeTestUnit:IsInZone( RangeZone ) == true then + self:T2( "in zone" ) + if RangeTestUnit:GetCoalition() ~= AIGroup:GetCoalition() then + self:T2( "in range" ) + PlayerInRange.Value = true + end + end + end, + + --- @param Core.Zone#ZONE_RADIUS RangeZone + -- @param Wrapper.Group#GROUP AIGroup + function( RangeZone, AIGroup, PlayerInRange ) + if PlayerInRange.Value == false then + self:Return( AIGroup ) + end + end + , RangeZone, AIGroup, PlayerInRange + ) + + end + self.Set:Remove( Client.UnitName ) + end + else + if not AIGroup or not AIGroup:IsAlive() == true then + self:T( "Client " .. Client.UnitName .. " not alive." ) + self:T( { Queue = self.SpawnQueue[Client.UnitName] } ) + if not self.SpawnQueue[Client.UnitName] then + -- Spawn a new AI taking into account the spawn interval Earliest, Latest + self:__Spawn( math.random( self.Earliest, self.Latest ), Client.UnitName ) + self.SpawnQueue[Client.UnitName] = true + self:T( "New AI Spawned for Client " .. Client.UnitName ) + end + end + end + return true + end + ) + + self:__Monitor( 10 ) +end + + + +--- **AI** - Models the process of AI air operations. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_Air +-- @image MOOSE.JPG + +--- @type AI_AIR +-- @extends Core.Fsm#FSM_CONTROLLABLE + +--- The AI_AIR class implements the core functions to operate an AI @{Wrapper.Group}. +-- +-- +-- # 1) AI_AIR constructor +-- +-- * @{#AI_AIR.New}(): Creates a new AI_AIR object. +-- +-- # 2) AI_AIR is a Finite State Machine. +-- +-- This section must be read as follows. Each of the rows indicate a state transition, triggered through an event, and with an ending state of the event was executed. +-- The first column is the **From** state, the second column the **Event**, and the third column the **To** state. +-- +-- So, each of the rows have the following structure. +-- +-- * **From** => **Event** => **To** +-- +-- Important to know is that an event can only be executed if the **current state** is the **From** state. +-- This, when an **Event** that is being triggered has a **From** state that is equal to the **Current** state of the state machine, the event will be executed, +-- and the resulting state will be the **To** state. +-- +-- These are the different possible state transitions of this state machine implementation: +-- +-- * Idle => Start => Monitoring +-- +-- ## 2.1) AI_AIR States. +-- +-- * **Idle**: The process is idle. +-- +-- ## 2.2) AI_AIR Events. +-- +-- * **Start**: Start the transport process. +-- * **Stop**: Stop the transport process. +-- * **Monitor**: Monitor and take action. +-- +-- @field #AI_AIR +AI_AIR = { + ClassName = "AI_AIR", +} + +AI_AIR.TaskDelay = 0.5 -- The delay of each task given to the AI. + +--- Creates a new AI_AIR process. +-- @param #AI_AIR self +-- @param Wrapper.Group#GROUP AIGroup The group object to receive the A2G Process. +-- @return #AI_AIR +function AI_AIR:New( AIGroup ) + + -- Inherits from BASE + local self = BASE:Inherit( self, FSM_CONTROLLABLE:New() ) -- #AI_AIR + + self:SetControllable( AIGroup ) + + self:SetStartState( "Stopped" ) + + self:AddTransition( "*", "Queue", "Queued" ) + + self:AddTransition( "*", "Start", "Started" ) + + --- Start Handler OnBefore for AI_AIR + -- @function [parent=#AI_AIR] OnBeforeStart + -- @param #AI_AIR self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Start Handler OnAfter for AI_AIR + -- @function [parent=#AI_AIR] OnAfterStart + -- @param #AI_AIR self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Start Trigger for AI_AIR + -- @function [parent=#AI_AIR] Start + -- @param #AI_AIR self + + --- Start Asynchronous Trigger for AI_AIR + -- @function [parent=#AI_AIR] __Start + -- @param #AI_AIR self + -- @param #number Delay + + self:AddTransition( "*", "Stop", "Stopped" ) + +--- OnLeave Transition Handler for State Stopped. +-- @function [parent=#AI_AIR] OnLeaveStopped +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnEnter Transition Handler for State Stopped. +-- @function [parent=#AI_AIR] OnEnterStopped +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- OnBefore Transition Handler for Event Stop. +-- @function [parent=#AI_AIR] OnBeforeStop +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnAfter Transition Handler for Event Stop. +-- @function [parent=#AI_AIR] OnAfterStop +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- Synchronous Event Trigger for Event Stop. +-- @function [parent=#AI_AIR] Stop +-- @param #AI_AIR self + +--- Asynchronous Event Trigger for Event Stop. +-- @function [parent=#AI_AIR] __Stop +-- @param #AI_AIR self +-- @param #number Delay The delay in seconds. + + self:AddTransition( "*", "Status", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR. + +--- OnBefore Transition Handler for Event Status. +-- @function [parent=#AI_AIR] OnBeforeStatus +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnAfter Transition Handler for Event Status. +-- @function [parent=#AI_AIR] OnAfterStatus +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- Synchronous Event Trigger for Event Status. +-- @function [parent=#AI_AIR] Status +-- @param #AI_AIR self + +--- Asynchronous Event Trigger for Event Status. +-- @function [parent=#AI_AIR] __Status +-- @param #AI_AIR self +-- @param #number Delay The delay in seconds. + + self:AddTransition( "*", "RTB", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR. + +--- OnBefore Transition Handler for Event RTB. +-- @function [parent=#AI_AIR] OnBeforeRTB +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnAfter Transition Handler for Event RTB. +-- @function [parent=#AI_AIR] OnAfterRTB +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- Synchronous Event Trigger for Event RTB. +-- @function [parent=#AI_AIR] RTB +-- @param #AI_AIR self + +--- Asynchronous Event Trigger for Event RTB. +-- @function [parent=#AI_AIR] __RTB +-- @param #AI_AIR self +-- @param #number Delay The delay in seconds. + +--- OnLeave Transition Handler for State Returning. +-- @function [parent=#AI_AIR] OnLeaveReturning +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnEnter Transition Handler for State Returning. +-- @function [parent=#AI_AIR] OnEnterReturning +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + + self:AddTransition( "Patrolling", "Refuel", "Refuelling" ) + + --- Refuel Handler OnBefore for AI_AIR + -- @function [parent=#AI_AIR] OnBeforeRefuel + -- @param #AI_AIR self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Refuel Handler OnAfter for AI_AIR + -- @function [parent=#AI_AIR] OnAfterRefuel + -- @param #AI_AIR self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Refuel Trigger for AI_AIR + -- @function [parent=#AI_AIR] Refuel + -- @param #AI_AIR self + + --- Refuel Asynchronous Trigger for AI_AIR + -- @function [parent=#AI_AIR] __Refuel + -- @param #AI_AIR self + -- @param #number Delay + + self:AddTransition( "*", "Takeoff", "Airborne" ) + self:AddTransition( "*", "Return", "Returning" ) + self:AddTransition( "*", "Hold", "Holding" ) + self:AddTransition( "*", "Home", "Home" ) + self:AddTransition( "*", "LostControl", "LostControl" ) + self:AddTransition( "*", "Fuel", "Fuel" ) + self:AddTransition( "*", "Damaged", "Damaged" ) + self:AddTransition( "*", "Eject", "*" ) + self:AddTransition( "*", "Crash", "Crashed" ) + self:AddTransition( "*", "PilotDead", "*" ) + + self.IdleCount = 0 + + return self +end + +--- @param Wrapper.Group#GROUP self +-- @param Core.Event#EVENTDATA EventData +function GROUP:OnEventTakeoff( EventData, Fsm ) + Fsm:Takeoff() + self:UnHandleEvent( EVENTS.Takeoff ) +end + + + +function AI_AIR:SetDispatcher( Dispatcher ) + self.Dispatcher = Dispatcher +end + +function AI_AIR:GetDispatcher() + return self.Dispatcher +end + +function AI_AIR:SetTargetDistance( Coordinate ) + + local CurrentCoord = self.Controllable:GetCoordinate() + self.TargetDistance = CurrentCoord:Get2DDistance( Coordinate ) + + self.ClosestTargetDistance = ( not self.ClosestTargetDistance or self.ClosestTargetDistance > self.TargetDistance ) and self.TargetDistance or self.ClosestTargetDistance +end + + +function AI_AIR:ClearTargetDistance() + + self.TargetDistance = nil + self.ClosestTargetDistance = nil +end + + +--- Sets (modifies) the minimum and maximum speed of the patrol. +-- @param #AI_AIR self +-- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Controllable} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Controllable} in km/h. +-- @return #AI_AIR self +function AI_AIR:SetSpeed( PatrolMinSpeed, PatrolMaxSpeed ) + self:F2( { PatrolMinSpeed, PatrolMaxSpeed } ) + + self.PatrolMinSpeed = PatrolMinSpeed + self.PatrolMaxSpeed = PatrolMaxSpeed +end + + +--- Sets (modifies) the minimum and maximum RTB speed of the patrol. +-- @param #AI_AIR self +-- @param DCS#Speed RTBMinSpeed The minimum speed of the @{Wrapper.Controllable} in km/h. +-- @param DCS#Speed RTBMaxSpeed The maximum speed of the @{Wrapper.Controllable} in km/h. +-- @return #AI_AIR self +function AI_AIR:SetRTBSpeed( RTBMinSpeed, RTBMaxSpeed ) + self:F( { RTBMinSpeed, RTBMaxSpeed } ) + + self.RTBMinSpeed = RTBMinSpeed + self.RTBMaxSpeed = RTBMaxSpeed +end + + +--- Sets the floor and ceiling altitude of the patrol. +-- @param #AI_AIR self +-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @return #AI_AIR self +function AI_AIR:SetAltitude( PatrolFloorAltitude, PatrolCeilingAltitude ) + self:F2( { PatrolFloorAltitude, PatrolCeilingAltitude } ) + + self.PatrolFloorAltitude = PatrolFloorAltitude + self.PatrolCeilingAltitude = PatrolCeilingAltitude +end + + +--- Sets the home airbase. +-- @param #AI_AIR self +-- @param Wrapper.Airbase#AIRBASE HomeAirbase +-- @return #AI_AIR self +function AI_AIR:SetHomeAirbase( HomeAirbase ) + self:F2( { HomeAirbase } ) + + self.HomeAirbase = HomeAirbase +end + +--- Sets to refuel at the given tanker. +-- @param #AI_AIR self +-- @param Wrapper.Group#GROUP TankerName The group name of the tanker as defined within the Mission Editor or spawned. +-- @return #AI_AIR self +function AI_AIR:SetTanker( TankerName ) + self:F2( { TankerName } ) + + self.TankerName = TankerName +end + + +--- Sets the disengage range, that when engaging a target beyond the specified range, the engagement will be cancelled and the plane will RTB. +-- @param #AI_AIR self +-- @param #number DisengageRadius The disengage range. +-- @return #AI_AIR self +function AI_AIR:SetDisengageRadius( DisengageRadius ) + self:F2( { DisengageRadius } ) + + self.DisengageRadius = DisengageRadius +end + +--- Set the status checking off. +-- @param #AI_AIR self +-- @return #AI_AIR self +function AI_AIR:SetStatusOff() + self:F2() + + self.CheckStatus = false +end + + +--- When the AI is out of fuel, it is required that a new AI is started, before the old AI can return to the home base. +-- Therefore, with a parameter and a calculation of the distance to the home base, the fuel treshold is calculated. +-- When the fuel treshold is reached, the AI will continue for a given time its patrol task in orbit, while a new AIControllable is targetted to the AI_AIR. +-- Once the time is finished, the old AI will return to the base. +-- @param #AI_AIR self +-- @param #number FuelThresholdPercentage The treshold in percentage (between 0 and 1) when the AIControllable is considered to get out of fuel. +-- @param #number OutOfFuelOrbitTime The amount of seconds the out of fuel AIControllable will orbit before returning to the base. +-- @return #AI_AIR self +function AI_AIR:SetFuelThreshold( FuelThresholdPercentage, OutOfFuelOrbitTime ) + + self.FuelThresholdPercentage = FuelThresholdPercentage + self.OutOfFuelOrbitTime = OutOfFuelOrbitTime + + self.Controllable:OptionRTBBingoFuel( false ) + + return self +end + +--- When the AI is damaged beyond a certain treshold, it is required that the AI returns to the home base. +-- However, damage cannot be foreseen early on. +-- Therefore, when the damage treshold is reached, +-- the AI will return immediately to the home base (RTB). +-- Note that for groups, the average damage of the complete group will be calculated. +-- So, in a group of 4 airplanes, 2 lost and 2 with damage 0.2, the damage treshold will be 0.25. +-- @param #AI_AIR self +-- @param #number PatrolDamageThreshold The treshold in percentage (between 0 and 1) when the AI is considered to be damaged. +-- @return #AI_AIR self +function AI_AIR:SetDamageThreshold( PatrolDamageThreshold ) + + self.PatrolManageDamage = true + self.PatrolDamageThreshold = PatrolDamageThreshold + + return self +end + + + +--- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. +-- @param #AI_AIR self +-- @return #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_AIR:onafterStart( Controllable, From, Event, To ) + + self:__Status( 10 ) -- Check status status every 30 seconds. + + self:HandleEvent( EVENTS.PilotDead, self.OnPilotDead ) + self:HandleEvent( EVENTS.Crash, self.OnCrash ) + self:HandleEvent( EVENTS.Ejection, self.OnEjection ) + + Controllable:OptionROEHoldFire() + Controllable:OptionROTVertical() +end + +--- Coordinates the approriate returning action. +-- @param #AI_AIR self +-- @return #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_AIR:onafterReturn( Controllable, From, Event, To ) + + self:__RTB( self.TaskDelay ) + +end + +--- @param #AI_AIR self +function AI_AIR:onbeforeStatus() + + return self.CheckStatus +end + +--- @param #AI_AIR self +function AI_AIR:onafterStatus() + + if self.Controllable and self.Controllable:IsAlive() then + + local RTB = false + + local DistanceFromHomeBase = self.HomeAirbase:GetCoordinate():Get2DDistance( self.Controllable:GetCoordinate() ) + + if not self:Is( "Holding" ) and not self:Is( "Returning" ) then + local DistanceFromHomeBase = self.HomeAirbase:GetCoordinate():Get2DDistance( self.Controllable:GetCoordinate() ) + + if DistanceFromHomeBase > self.DisengageRadius then + self:I( self.Controllable:GetName() .. " is too far from home base, RTB!" ) + self:Hold( 300 ) + RTB = false + end + end + +-- I think this code is not requirement anymore after release 2.5. +-- if self:Is( "Fuel" ) or self:Is( "Damaged" ) or self:Is( "LostControl" ) then +-- if DistanceFromHomeBase < 5000 then +-- self:E( self.Controllable:GetName() .. " is near the home base, RTB!" ) +-- self:Home( "Destroy" ) +-- end +-- end + + + if not self:Is( "Fuel" ) and not self:Is( "Home" ) and not self:is( "Refuelling" )then + + local Fuel = self.Controllable:GetFuelMin() + + -- If the fuel in the controllable is below the treshold percentage, + -- then send for refuel in case of a tanker, otherwise RTB. + if Fuel < self.FuelThresholdPercentage then + + if self.TankerName then + self:I( self.Controllable:GetName() .. " is out of fuel: " .. Fuel .. " ... Refuelling at Tanker!" ) + self:Refuel() + else + self:I( self.Controllable:GetName() .. " is out of fuel: " .. Fuel .. " ... RTB!" ) + local OldAIControllable = self.Controllable + + local OrbitTask = OldAIControllable:TaskOrbitCircle( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ), self.PatrolMinSpeed ) + local TimedOrbitTask = OldAIControllable:TaskControlled( OrbitTask, OldAIControllable:TaskCondition(nil,nil,nil,nil,self.OutOfFuelOrbitTime,nil ) ) + OldAIControllable:SetTask( TimedOrbitTask, 10 ) + + self:Fuel() + RTB = true + end + else + end + end + + if self:Is( "Fuel" ) and not self:Is( "Home" ) and not self:is( "Refuelling" ) then + RTB = true + end + + -- TODO: Check GROUP damage function. + local Damage = self.Controllable:GetLife() + local InitialLife = self.Controllable:GetLife0() + + -- If the group is damaged, then RTB. + -- Note that a group can consist of more units, so if one unit is damaged of a group, the mission may continue. + -- The damaged unit will RTB due to DCS logic, and the others will continue to engage. + if ( Damage / InitialLife ) < self.PatrolDamageThreshold then + self:I( self.Controllable:GetName() .. " is damaged: " .. Damage .. " ... RTB!" ) + self:Damaged() + RTB = true + self:SetStatusOff() + end + + -- Check if planes went RTB and are out of control. + -- We only check if planes are out of control, when they are in duty. + if self.Controllable:HasTask() == false then + if not self:Is( "Started" ) and + not self:Is( "Stopped" ) and + not self:Is( "Fuel" ) and + not self:Is( "Damaged" ) and + not self:Is( "Home" ) then + if self.IdleCount >= 10 then + if Damage ~= InitialLife then + self:Damaged() + else + self:I( self.Controllable:GetName() .. " control lost! " ) + + self:LostControl() + end + else + self.IdleCount = self.IdleCount + 1 + end + end + else + self.IdleCount = 0 + end + + if RTB == true then + self:__RTB( self.TaskDelay ) + end + + if not self:Is("Home") then + self:__Status( 10 ) + end + + end +end + + +--- @param Wrapper.Group#GROUP AIGroup +function AI_AIR.RTBRoute( AIGroup, Fsm ) + + AIGroup:F( { "AI_AIR.RTBRoute:", AIGroup:GetName() } ) + + if AIGroup:IsAlive() then + Fsm:RTB() + end + +end + +--- @param Wrapper.Group#GROUP AIGroup +function AI_AIR.RTBHold( AIGroup, Fsm ) + + AIGroup:F( { "AI_AIR.RTBHold:", AIGroup:GetName() } ) + if AIGroup:IsAlive() then + Fsm:__RTB( Fsm.TaskDelay ) + Fsm:Return() + local Task = AIGroup:TaskOrbitCircle( 4000, 400 ) + AIGroup:SetTask( Task ) + end + +end + + +--- @param #AI_AIR self +-- @param Wrapper.Group#GROUP AIGroup +function AI_AIR:onafterRTB( AIGroup, From, Event, To ) + self:F( { AIGroup, From, Event, To } ) + + + if AIGroup and AIGroup:IsAlive() then + + self:I( "Group " .. AIGroup:GetName() .. " ... RTB! ( " .. self:GetState() .. " )" ) + + self:ClearTargetDistance() + --AIGroup:ClearTasks() + + local EngageRoute = {} + + --- Calculate the target route point. + + local FromCoord = AIGroup:GetCoordinate() + local ToTargetCoord = self.HomeAirbase:GetCoordinate() -- coordinate is on land height(!) + local ToTargetVec3 = ToTargetCoord:GetVec3() + ToTargetVec3.y = ToTargetCoord:GetLandHeight()+1000 -- let's set this 1000m/3000 feet above ground + local ToTargetCoord2 = COORDINATE:NewFromVec3( ToTargetVec3 ) + + if not self.RTBMinSpeed or not self.RTBMaxSpeed then + local RTBSpeedMax = AIGroup:GetSpeedMax() + self:SetRTBSpeed( RTBSpeedMax * 0.5, RTBSpeedMax * 0.6 ) + end + + local RTBSpeed = math.random( self.RTBMinSpeed, self.RTBMaxSpeed ) + --local ToAirbaseAngle = FromCoord:GetAngleDegrees( FromCoord:GetDirectionVec3( ToTargetCoord2 ) ) + + local Distance = FromCoord:Get2DDistance( ToTargetCoord2 ) + + --local ToAirbaseCoord = FromCoord:Translate( 5000, ToAirbaseAngle ) + local ToAirbaseCoord = ToTargetCoord2 + + if Distance < 5000 then + self:I( "RTB and near the airbase!" ) + self:Home() + return + end + + if not AIGroup:InAir() == true then + self:I( "Not anymore in the air, considered Home." ) + self:Home() + return + end + + + --- Create a route point of type air. + local FromRTBRoutePoint = FromCoord:WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + RTBSpeed, + true + ) + + --- Create a route point of type air. + local ToRTBRoutePoint = ToAirbaseCoord:WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + RTBSpeed, + true + ) + + EngageRoute[#EngageRoute+1] = FromRTBRoutePoint + EngageRoute[#EngageRoute+1] = ToRTBRoutePoint + + local Tasks = {} + Tasks[#Tasks+1] = AIGroup:TaskFunction( "AI_AIR.RTBRoute", self ) + + EngageRoute[#EngageRoute].task = AIGroup:TaskCombo( Tasks ) + + AIGroup:OptionROEHoldFire() + AIGroup:OptionROTEvadeFire() + + --- NOW ROUTE THE GROUP! + AIGroup:Route( EngageRoute, self.TaskDelay ) + + end + +end + +--- @param #AI_AIR self +-- @param Wrapper.Group#GROUP AIGroup +function AI_AIR:onafterHome( AIGroup, From, Event, To ) + self:F( { AIGroup, From, Event, To } ) + + self:I( "Group " .. self.Controllable:GetName() .. " ... Home! ( " .. self:GetState() .. " )" ) + + if AIGroup and AIGroup:IsAlive() then + end + +end + + + +--- @param #AI_AIR self +-- @param Wrapper.Group#GROUP AIGroup +function AI_AIR:onafterHold( AIGroup, From, Event, To, HoldTime ) + self:F( { AIGroup, From, Event, To } ) + + self:I( "Group " .. self.Controllable:GetName() .. " ... Holding! ( " .. self:GetState() .. " )" ) + + if AIGroup and AIGroup:IsAlive() then + local OrbitTask = AIGroup:TaskOrbitCircle( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ), self.PatrolMinSpeed ) + local TimedOrbitTask = AIGroup:TaskControlled( OrbitTask, AIGroup:TaskCondition( nil, nil, nil, nil, HoldTime , nil ) ) + + local RTBTask = AIGroup:TaskFunction( "AI_AIR.RTBHold", self ) + + local OrbitHoldTask = AIGroup:TaskOrbitCircle( 4000, self.PatrolMinSpeed ) + + --AIGroup:SetState( AIGroup, "AI_AIR", self ) + + AIGroup:SetTask( AIGroup:TaskCombo( { TimedOrbitTask, RTBTask, OrbitHoldTask } ), 1 ) + end + +end + +--- @param Wrapper.Group#GROUP AIGroup +function AI_AIR.Resume( AIGroup, Fsm ) + + AIGroup:I( { "AI_AIR.Resume:", AIGroup:GetName() } ) + if AIGroup:IsAlive() then + Fsm:__RTB( Fsm.TaskDelay ) + end + +end + +--- @param #AI_AIR self +-- @param Wrapper.Group#GROUP AIGroup +function AI_AIR:onafterRefuel( AIGroup, From, Event, To ) + self:F( { AIGroup, From, Event, To } ) + + if AIGroup and AIGroup:IsAlive() then + + -- Get tanker group. + local Tanker = GROUP:FindByName( self.TankerName ) + + if Tanker and Tanker:IsAlive() and Tanker:IsAirPlane() then + + self:I( "Group " .. self.Controllable:GetName() .. " ... Refuelling! State=" .. self:GetState() .. ", Refuelling tanker " .. self.TankerName ) + + local RefuelRoute = {} + + --- Calculate the target route point. + + local FromRefuelCoord = AIGroup:GetCoordinate() + local ToRefuelCoord = Tanker:GetCoordinate() + local ToRefuelSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) + + --- Create a route point of type air. + local FromRefuelRoutePoint = FromRefuelCoord:WaypointAir(self.PatrolAltType, POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, ToRefuelSpeed, true) + + --- Create a route point of type air. NOT used! + local ToRefuelRoutePoint = Tanker:GetCoordinate():WaypointAir(self.PatrolAltType, POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, ToRefuelSpeed, true) + + self:F( { ToRefuelSpeed = ToRefuelSpeed } ) + + RefuelRoute[#RefuelRoute+1] = FromRefuelRoutePoint + RefuelRoute[#RefuelRoute+1] = ToRefuelRoutePoint + + AIGroup:OptionROEHoldFire() + AIGroup:OptionROTEvadeFire() + + -- Get Class name for .Resume function + local classname=self:GetClassName() + + -- AI_A2A_CAP can call this function but does not have a .Resume function. Try to fix. + if classname=="AI_A2A_CAP" then + classname="AI_AIR_PATROL" + end + + env.info("FF refueling classname="..classname) + + local Tasks = {} + Tasks[#Tasks+1] = AIGroup:TaskRefueling() + Tasks[#Tasks+1] = AIGroup:TaskFunction( classname .. ".Resume", self ) + RefuelRoute[#RefuelRoute].task = AIGroup:TaskCombo( Tasks ) + + AIGroup:Route( RefuelRoute, self.TaskDelay ) + + else + + -- No tanker defined ==> RTB! + self:RTB() + + end + + end + +end + + + +--- @param #AI_AIR self +function AI_AIR:onafterDead() + self:SetStatusOff() +end + + +--- @param #AI_AIR self +-- @param Core.Event#EVENTDATA EventData +function AI_AIR:OnCrash( EventData ) + + if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then + if #self.Controllable:GetUnits() == 1 then + self:__Crash( self.TaskDelay, EventData ) + end + end +end + +--- @param #AI_AIR self +-- @param Core.Event#EVENTDATA EventData +function AI_AIR:OnEjection( EventData ) + + if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then + self:__Eject( self.TaskDelay, EventData ) + end +end + +--- @param #AI_AIR self +-- @param Core.Event#EVENTDATA EventData +function AI_AIR:OnPilotDead( EventData ) + + if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then + self:__PilotDead( self.TaskDelay, EventData ) + end +end +--- **AI** -- Models the process of A2G patrolling and engaging ground targets for airplanes and helicopters. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_Air_Patrol +-- @image AI_Air_To_Ground_Patrol.JPG + +--- @type AI_AIR_PATROL +-- @extends AI.AI_Air#AI_AIR + + +--- The AI_AIR_PATROL class implements the core functions to patrol a @{Zone} by an AI @{Wrapper.Group} or @{Wrapper.Group} +-- and automatically engage any airborne enemies that are within a certain range or within a certain zone. +-- +-- ![Process](..\Presentations\AI_CAP\Dia3.JPG) +-- +-- The AI_AIR_PATROL is assigned a @{Wrapper.Group} and this must be done before the AI_AIR_PATROL process can be started using the **Start** event. +-- +-- ![Process](..\Presentations\AI_CAP\Dia4.JPG) +-- +-- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits. +-- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. +-- +-- ![Process](..\Presentations\AI_CAP\Dia5.JPG) +-- +-- This cycle will continue. +-- +-- ![Process](..\Presentations\AI_CAP\Dia6.JPG) +-- +-- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event. +-- +-- ![Process](..\Presentations\AI_CAP\Dia9.JPG) +-- +-- When enemies are detected, the AI will automatically engage the enemy. +-- +-- ![Process](..\Presentations\AI_CAP\Dia10.JPG) +-- +-- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. +-- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. +-- +-- ![Process](..\Presentations\AI_CAP\Dia13.JPG) +-- +-- ## 1. AI_AIR_PATROL constructor +-- +-- * @{#AI_AIR_PATROL.New}(): Creates a new AI_AIR_PATROL object. +-- +-- ## 2. AI_AIR_PATROL is a FSM +-- +-- ![Process](..\Presentations\AI_CAP\Dia2.JPG) +-- +-- ### 2.1 AI_AIR_PATROL States +-- +-- * **None** ( Group ): The process is not started yet. +-- * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone. +-- * **Engaging** ( Group ): The AI is engaging the bogeys. +-- * **Returning** ( Group ): The AI is returning to Base.. +-- +-- ### 2.2 AI_AIR_PATROL Events +-- +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Start}**: Start the process. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.PatrolRoute}**: Route the AI to a new random 3D point within the Patrol Zone. +-- * **@{#AI_AIR_PATROL.Engage}**: Let the AI engage the bogeys. +-- * **@{#AI_AIR_PATROL.Abort}**: Aborts the engagement and return patrolling in the patrol zone. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.RTB}**: Route the AI to the home base. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detect}**: The AI is detecting targets. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets. +-- * **@{#AI_AIR_PATROL.Destroy}**: The AI has destroyed a bogey @{Wrapper.Unit}. +-- * **@{#AI_AIR_PATROL.Destroyed}**: The AI has destroyed all bogeys @{Wrapper.Unit}s assigned in the CAS task. +-- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. +-- +-- ## 3. Set the Range of Engagement +-- +-- ![Range](..\Presentations\AI_CAP\Dia11.JPG) +-- +-- An optional range can be set in meters, +-- that will define when the AI will engage with the detected airborne enemy targets. +-- The range can be beyond or smaller than the range of the Patrol Zone. +-- The range is applied at the position of the AI. +-- Use the method @{AI.AI_CAP#AI_AIR_PATROL.SetEngageRange}() to define that range. +-- +-- ## 4. Set the Zone of Engagement +-- +-- ![Zone](..\Presentations\AI_CAP\Dia12.JPG) +-- +-- An optional @{Zone} can be set, +-- that will define when the AI will engage with the detected airborne enemy targets. +-- Use the method @{AI.AI_Cap#AI_AIR_PATROL.SetEngageZone}() to define that Zone. +-- +-- === +-- +-- @field #AI_AIR_PATROL +AI_AIR_PATROL = { + ClassName = "AI_AIR_PATROL", +} + +--- Creates a new AI_AIR_PATROL object +-- @param #AI_AIR_PATROL self +-- @param AI.AI_Air#AI_AIR AI_Air The AI_AIR FSM. +-- @param Wrapper.Group#GROUP AIGroup The AI group. +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param DCS#Altitude PatrolFloorAltitude (optional, default = 1000m ) The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude (optional, default = 1500m ) The highest altitude in meters where to execute the patrol. +-- @param DCS#Speed PatrolMinSpeed (optional, default = 50% of max speed) The minimum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#Speed PatrolMaxSpeed (optional, default = 75% of max speed) The maximum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO. +-- @return #AI_AIR_PATROL +function AI_AIR_PATROL:New( AI_Air, AIGroup, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) + + -- Inherits from BASE + local self = BASE:Inherit( self, AI_Air ) -- #AI_AIR_PATROL + + local SpeedMax = AIGroup:GetSpeedMax() + + self.PatrolZone = PatrolZone + + self.PatrolFloorAltitude = PatrolFloorAltitude or 1000 + self.PatrolCeilingAltitude = PatrolCeilingAltitude or 1500 + self.PatrolMinSpeed = PatrolMinSpeed or SpeedMax * 0.5 + self.PatrolMaxSpeed = PatrolMaxSpeed or SpeedMax * 0.75 + + -- defafult PatrolAltType to "RADIO" if not specified + self.PatrolAltType = PatrolAltType or "RADIO" + + self:AddTransition( { "Started", "Airborne", "Refuelling" }, "Patrol", "Patrolling" ) + + --- OnBefore Transition Handler for Event Patrol. + -- @function [parent=#AI_AIR_PATROL] OnBeforePatrol + -- @param #AI_AIR_PATROL self + -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Patrol. + -- @function [parent=#AI_AIR_PATROL] OnAfterPatrol + -- @param #AI_AIR_PATROL self + -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Patrol. + -- @function [parent=#AI_AIR_PATROL] Patrol + -- @param #AI_AIR_PATROL self + + --- Asynchronous Event Trigger for Event Patrol. + -- @function [parent=#AI_AIR_PATROL] __Patrol + -- @param #AI_AIR_PATROL self + -- @param #number Delay The delay in seconds. + + --- OnLeave Transition Handler for State Patrolling. + -- @function [parent=#AI_AIR_PATROL] OnLeavePatrolling + -- @param #AI_AIR_PATROL self + -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnEnter Transition Handler for State Patrolling. + -- @function [parent=#AI_AIR_PATROL] OnEnterPatrolling + -- @param #AI_AIR_PATROL self + -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + self:AddTransition( "Patrolling", "PatrolRoute", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR_PATROL. + + --- OnBefore Transition Handler for Event PatrolRoute. + -- @function [parent=#AI_AIR_PATROL] OnBeforePatrolRoute + -- @param #AI_AIR_PATROL self + -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event PatrolRoute. + -- @function [parent=#AI_AIR_PATROL] OnAfterPatrolRoute + -- @param #AI_AIR_PATROL self + -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event PatrolRoute. + -- @function [parent=#AI_AIR_PATROL] PatrolRoute + -- @param #AI_AIR_PATROL self + + --- Asynchronous Event Trigger for Event PatrolRoute. + -- @function [parent=#AI_AIR_PATROL] __PatrolRoute + -- @param #AI_AIR_PATROL self + -- @param #number Delay The delay in seconds. + + + self:AddTransition( "*", "Reset", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR_PATROL. + + return self +end + + +--- Set the Engage Range when the AI will engage with airborne enemies. +-- @param #AI_AIR_PATROL self +-- @param #number EngageRange The Engage Range. +-- @return #AI_AIR_PATROL self +function AI_AIR_PATROL:SetEngageRange( EngageRange ) + self:F2() + + if EngageRange then + self.EngageRange = EngageRange + else + self.EngageRange = nil + end +end + +--- Set race track parameters. CAP flights will perform race track patterns rather than randomly patrolling the zone. +-- @param #AI_AIR_PATROL self +-- @param #number LegMin Min Length of the race track leg in meters. Default 10,000 m. +-- @param #number LegMax Max length of the race track leg in meters. Default 15,000 m. +-- @param #number HeadingMin Min heading of the race track in degrees. Default 0 deg, i.e. from South to North. +-- @param #number HeadingMax Max heading of the race track in degrees. Default 180 deg, i.e. from South to North. +-- @param #number DurationMin (Optional) Min duration before switching the orbit position. Default is keep same orbit until RTB or engage. +-- @param #number DurationMax (Optional) Max duration before switching the orbit position. Default is keep same orbit until RTB or engage. +-- @param #table CapCoordinates Table of coordinates of first race track point. Second point is determined by leg length and heading. +-- @return #AI_AIR_PATROL self +function AI_AIR_PATROL:SetRaceTrackPattern(LegMin, LegMax, HeadingMin, HeadingMax, DurationMin, DurationMax, CapCoordinates) + + self.racetrack=true + self.racetracklegmin=LegMin or 10000 + self.racetracklegmax=LegMax or 15000 + self.racetrackheadingmin=HeadingMin or 0 + self.racetrackheadingmax=HeadingMax or 180 + self.racetrackdurationmin=DurationMin + self.racetrackdurationmax=DurationMax + + if self.racetrackdurationmax and not self.racetrackdurationmin then + self.racetrackdurationmin=self.racetrackdurationmax + end + + self.racetrackcapcoordinates=CapCoordinates + +end + + + +--- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. +-- @param #AI_AIR_PATROL self +-- @return #AI_AIR_PATROL self +-- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_AIR_PATROL:onafterPatrol( AIPatrol, From, Event, To ) + self:F2() + + self:ClearTargetDistance() + + self:__PatrolRoute( self.TaskDelay ) + + AIPatrol:OnReSpawn( + function( PatrolGroup ) + self:__Reset( self.TaskDelay ) + self:__PatrolRoute( self.TaskDelay ) + end + ) +end + +--- This statis method is called from the route path within the last task at the last waaypoint of the AIPatrol. +-- Note that this method is required, as triggers the next route when patrolling for the AIPatrol. +-- @param Wrapper.Group#GROUP AIPatrol The AI group. +-- @param #AI_AIR_PATROL Fsm The FSM. +function AI_AIR_PATROL.___PatrolRoute( AIPatrol, Fsm ) + + AIPatrol:F( { "AI_AIR_PATROL.___PatrolRoute:", AIPatrol:GetName() } ) + + if AIPatrol and AIPatrol:IsAlive() then + Fsm:PatrolRoute() + end + +end + +--- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. +-- @param #AI_AIR_PATROL self +-- @param Wrapper.Group#GROUP AIPatrol The Group managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_AIR_PATROL:onafterPatrolRoute( AIPatrol, From, Event, To ) + + self:F2() + + -- When RTB, don't allow anymore the routing. + if From == "RTB" then + return + end + + + if AIPatrol and AIPatrol:IsAlive() then + + local PatrolRoute = {} + + --- Calculate the target route point. + + local CurrentCoord = AIPatrol:GetCoordinate() + + local altitude= math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ) + + local ToTargetCoord = self.PatrolZone:GetRandomPointVec2() + ToTargetCoord:SetAlt( altitude ) + self:SetTargetDistance( ToTargetCoord ) -- For RTB status check + + local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) + local speedkmh=ToTargetSpeed + + local FromWP = CurrentCoord:WaypointAir(self.PatrolAltType or "RADIO", POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, ToTargetSpeed, true) + PatrolRoute[#PatrolRoute+1] = FromWP + + if self.racetrack then + + -- Random heading. + local heading = math.random(self.racetrackheadingmin, self.racetrackheadingmax) + + -- Random leg length. + local leg=math.random(self.racetracklegmin, self.racetracklegmax) + + -- Random duration if any. + local duration = self.racetrackdurationmin + if self.racetrackdurationmax then + duration=math.random(self.racetrackdurationmin, self.racetrackdurationmax) + end + + -- CAP coordinate. + local c0=self.PatrolZone:GetRandomCoordinate() + if self.racetrackcapcoordinates and #self.racetrackcapcoordinates>0 then + c0=self.racetrackcapcoordinates[math.random(#self.racetrackcapcoordinates)] + end + + -- Race track points. + local c1=c0:SetAltitude(altitude) --Core.Point#COORDINATE + local c2=c1:Translate(leg, heading):SetAltitude(altitude) + + self:SetTargetDistance(c0) -- For RTB status check + + -- Debug: + self:T(string.format("Patrol zone race track: v=%.1f knots, h=%.1f ft, heading=%03d, leg=%d m, t=%s sec", UTILS.KmphToKnots(speedkmh), UTILS.MetersToFeet(altitude), heading, leg, tostring(duration))) + --c1:MarkToAll("Race track c1") + --c2:MarkToAll("Race track c2") + + -- Task to orbit. + local taskOrbit=AIPatrol:TaskOrbit(c1, altitude, UTILS.KmphToMps(speedkmh), c2) + + -- Task function to redo the patrol at other random position. + local taskPatrol=AIPatrol:TaskFunction("AI_AIR_PATROL.___PatrolRoute", self) + + -- Controlled task with task condition. + local taskCond=AIPatrol:TaskCondition(nil, nil, nil, nil, duration, nil) + local taskCont=AIPatrol:TaskControlled(taskOrbit, taskCond) + + -- Second waypoint + PatrolRoute[2]=c1:WaypointAirTurningPoint(self.PatrolAltType, speedkmh, {taskCont, taskPatrol}, "CAP Orbit") + + else + + --- Create a route point of type air. + local ToWP = ToTargetCoord:WaypointAir(self.PatrolAltType, POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, ToTargetSpeed, true) + PatrolRoute[#PatrolRoute+1] = ToWP + + local Tasks = {} + Tasks[#Tasks+1] = AIPatrol:TaskFunction("AI_AIR_PATROL.___PatrolRoute", self) + PatrolRoute[#PatrolRoute].task = AIPatrol:TaskCombo( Tasks ) + + end + + AIPatrol:OptionROEReturnFire() + AIPatrol:OptionROTEvadeFire() + + AIPatrol:Route( PatrolRoute, self.TaskDelay ) + + end + +end + +--- @param Wrapper.Group#GROUP AIPatrol +function AI_AIR_PATROL.Resume( AIPatrol, Fsm ) + + AIPatrol:F( { "AI_AIR_PATROL.Resume:", AIPatrol:GetName() } ) + if AIPatrol and AIPatrol:IsAlive() then + Fsm:__Reset( Fsm.TaskDelay ) + Fsm:__PatrolRoute( Fsm.TaskDelay ) + end + +end +--- **AI** -- Models the process of air to ground engagement for airplanes and helicopters. +-- +-- This is a class used in the @{AI_A2G_Dispatcher}. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_Air_Engage +-- @image AI_Air_To_Ground_Engage.JPG + + + +--- @type AI_AIR_ENGAGE +-- @extends AI.AI_AIR#AI_AIR + + +--- Implements the core functions to intercept intruders. Use the Engage trigger to intercept intruders. +-- +-- ![Process](..\Presentations\AI_GCI\Dia3.JPG) +-- +-- The AI_AIR_ENGAGE is assigned a @{Wrapper.Group} and this must be done before the AI_AIR_ENGAGE process can be started using the **Start** event. +-- +-- ![Process](..\Presentations\AI_GCI\Dia4.JPG) +-- +-- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits. +-- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. +-- +-- ![Process](..\Presentations\AI_GCI\Dia5.JPG) +-- +-- This cycle will continue. +-- +-- ![Process](..\Presentations\AI_GCI\Dia6.JPG) +-- +-- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event. +-- +-- ![Process](..\Presentations\AI_GCI\Dia9.JPG) +-- +-- When enemies are detected, the AI will automatically engage the enemy. +-- +-- ![Process](..\Presentations\AI_GCI\Dia10.JPG) +-- +-- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. +-- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. +-- +-- ![Process](..\Presentations\AI_GCI\Dia13.JPG) +-- +-- ## 1. AI_AIR_ENGAGE constructor +-- +-- * @{#AI_AIR_ENGAGE.New}(): Creates a new AI_AIR_ENGAGE object. +-- +-- ## 3. Set the Range of Engagement +-- +-- ![Range](..\Presentations\AI_GCI\Dia11.JPG) +-- +-- An optional range can be set in meters, +-- that will define when the AI will engage with the detected airborne enemy targets. +-- The range can be beyond or smaller than the range of the Patrol Zone. +-- The range is applied at the position of the AI. +-- Use the method @{AI.AI_GCI#AI_AIR_ENGAGE.SetEngageRange}() to define that range. +-- +-- ## 4. Set the Zone of Engagement +-- +-- ![Zone](..\Presentations\AI_GCI\Dia12.JPG) +-- +-- An optional @{Zone} can be set, +-- that will define when the AI will engage with the detected airborne enemy targets. +-- Use the method @{AI.AI_Cap#AI_AIR_ENGAGE.SetEngageZone}() to define that Zone. +-- +-- === +-- +-- @field #AI_AIR_ENGAGE +AI_AIR_ENGAGE = { + ClassName = "AI_AIR_ENGAGE", +} + + + +--- Creates a new AI_AIR_ENGAGE object +-- @param #AI_AIR_ENGAGE self +-- @param AI.AI_Air#AI_AIR AI_Air The AI_AIR FSM. +-- @param Wrapper.Group#GROUP AIGroup The AI group. +-- @param DCS#Speed EngageMinSpeed (optional, default = 50% of max speed) The minimum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Speed EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Altitude EngageFloorAltitude (optional, default = 1000m ) The lowest altitude in meters where to execute the engagement. +-- @param DCS#Altitude EngageCeilingAltitude (optional, default = 1500m ) The highest altitude in meters where to execute the engagement. +-- @param DCS#AltitudeType EngageAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO". +-- @return #AI_AIR_ENGAGE +function AI_AIR_ENGAGE:New( AI_Air, AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) + + -- Inherits from BASE + local self = BASE:Inherit( self, AI_Air ) -- #AI_AIR_ENGAGE + + self.Accomplished = false + self.Engaging = false + + local SpeedMax = AIGroup:GetSpeedMax() + + self.EngageMinSpeed = EngageMinSpeed or SpeedMax * 0.5 + self.EngageMaxSpeed = EngageMaxSpeed or SpeedMax * 0.75 + self.EngageFloorAltitude = EngageFloorAltitude or 1000 + self.EngageCeilingAltitude = EngageCeilingAltitude or 1500 + self.EngageAltType = EngageAltType or "RADIO" + + self:AddTransition( { "Started", "Engaging", "Returning", "Airborne", "Patrolling" }, "EngageRoute", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR_ENGAGE. + + --- OnBefore Transition Handler for Event EngageRoute. + -- @function [parent=#AI_AIR_ENGAGE] OnBeforeEngageRoute + -- @param #AI_AIR_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event EngageRoute. + -- @function [parent=#AI_AIR_ENGAGE] OnAfterEngageRoute + -- @param #AI_AIR_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event EngageRoute. + -- @function [parent=#AI_AIR_ENGAGE] EngageRoute + -- @param #AI_AIR_ENGAGE self + + --- Asynchronous Event Trigger for Event EngageRoute. + -- @function [parent=#AI_AIR_ENGAGE] __EngageRoute + -- @param #AI_AIR_ENGAGE self + -- @param #number Delay The delay in seconds. + +--- OnLeave Transition Handler for State Engaging. +-- @function [parent=#AI_AIR_ENGAGE] OnLeaveEngaging +-- @param #AI_AIR_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnEnter Transition Handler for State Engaging. +-- @function [parent=#AI_AIR_ENGAGE] OnEnterEngaging +-- @param #AI_AIR_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + + self:AddTransition( { "Started", "Engaging", "Returning", "Airborne", "Patrolling" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR_ENGAGE. + + --- OnBefore Transition Handler for Event Engage. + -- @function [parent=#AI_AIR_ENGAGE] OnBeforeEngage + -- @param #AI_AIR_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Engage. + -- @function [parent=#AI_AIR_ENGAGE] OnAfterEngage + -- @param #AI_AIR_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Engage. + -- @function [parent=#AI_AIR_ENGAGE] Engage + -- @param #AI_AIR_ENGAGE self + + --- Asynchronous Event Trigger for Event Engage. + -- @function [parent=#AI_AIR_ENGAGE] __Engage + -- @param #AI_AIR_ENGAGE self + -- @param #number Delay The delay in seconds. + +--- OnLeave Transition Handler for State Engaging. +-- @function [parent=#AI_AIR_ENGAGE] OnLeaveEngaging +-- @param #AI_AIR_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnEnter Transition Handler for State Engaging. +-- @function [parent=#AI_AIR_ENGAGE] OnEnterEngaging +-- @param #AI_AIR_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + + self:AddTransition( "Engaging", "Fired", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR_ENGAGE. + + --- OnBefore Transition Handler for Event Fired. + -- @function [parent=#AI_AIR_ENGAGE] OnBeforeFired + -- @param #AI_AIR_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Fired. + -- @function [parent=#AI_AIR_ENGAGE] OnAfterFired + -- @param #AI_AIR_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Fired. + -- @function [parent=#AI_AIR_ENGAGE] Fired + -- @param #AI_AIR_ENGAGE self + + --- Asynchronous Event Trigger for Event Fired. + -- @function [parent=#AI_AIR_ENGAGE] __Fired + -- @param #AI_AIR_ENGAGE self + -- @param #number Delay The delay in seconds. + + self:AddTransition( "*", "Destroy", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR_ENGAGE. + + --- OnBefore Transition Handler for Event Destroy. + -- @function [parent=#AI_AIR_ENGAGE] OnBeforeDestroy + -- @param #AI_AIR_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Destroy. + -- @function [parent=#AI_AIR_ENGAGE] OnAfterDestroy + -- @param #AI_AIR_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Destroy. + -- @function [parent=#AI_AIR_ENGAGE] Destroy + -- @param #AI_AIR_ENGAGE self + + --- Asynchronous Event Trigger for Event Destroy. + -- @function [parent=#AI_AIR_ENGAGE] __Destroy + -- @param #AI_AIR_ENGAGE self + -- @param #number Delay The delay in seconds. + + + self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR_ENGAGE. + + --- OnBefore Transition Handler for Event Abort. + -- @function [parent=#AI_AIR_ENGAGE] OnBeforeAbort + -- @param #AI_AIR_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Abort. + -- @function [parent=#AI_AIR_ENGAGE] OnAfterAbort + -- @param #AI_AIR_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Abort. + -- @function [parent=#AI_AIR_ENGAGE] Abort + -- @param #AI_AIR_ENGAGE self + + --- Asynchronous Event Trigger for Event Abort. + -- @function [parent=#AI_AIR_ENGAGE] __Abort + -- @param #AI_AIR_ENGAGE self + -- @param #number Delay The delay in seconds. + + self:AddTransition( "Engaging", "Accomplish", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR_ENGAGE. + + --- OnBefore Transition Handler for Event Accomplish. + -- @function [parent=#AI_AIR_ENGAGE] OnBeforeAccomplish + -- @param #AI_AIR_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Accomplish. + -- @function [parent=#AI_AIR_ENGAGE] OnAfterAccomplish + -- @param #AI_AIR_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Accomplish. + -- @function [parent=#AI_AIR_ENGAGE] Accomplish + -- @param #AI_AIR_ENGAGE self + + --- Asynchronous Event Trigger for Event Accomplish. + -- @function [parent=#AI_AIR_ENGAGE] __Accomplish + -- @param #AI_AIR_ENGAGE self + -- @param #number Delay The delay in seconds. + + self:AddTransition( { "Patrolling", "Engaging" }, "Refuel", "Refuelling" ) + + return self +end + +--- onafter event handler for Start event. +-- @param #AI_AIR_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The AI group managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_AIR_ENGAGE:onafterStart( AIGroup, From, Event, To ) + + self:GetParent( self, AI_AIR_ENGAGE ).onafterStart( self, AIGroup, From, Event, To ) + + AIGroup:HandleEvent( EVENTS.Takeoff, nil, self ) + +end + + + +--- onafter event handler for Engage event. +-- @param #AI_AIR_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The AI Group managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_AIR_ENGAGE:onafterEngage( AIGroup, From, Event, To ) + -- TODO: This function is overwritten below! + self:HandleEvent( EVENTS.Dead ) +end + +-- todo: need to fix this global function + + +--- onbefore event handler for Engage event. +-- @param #AI_AIR_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_AIR_ENGAGE:onbeforeEngage( AIGroup, From, Event, To ) + if self.Accomplished == true then + return false + end + return true +end + +--- onafter event handler for Abort event. +-- @param #AI_AIR_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The AI Group managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_AIR_ENGAGE:onafterAbort( AIGroup, From, Event, To ) + AIGroup:ClearTasks() + self:Return() +end + + +--- @param #AI_AIR_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_AIR_ENGAGE:onafterAccomplish( AIGroup, From, Event, To ) + self.Accomplished = true + --self:SetDetectionOff() +end + +--- @param #AI_AIR_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @param Core.Event#EVENTDATA EventData +function AI_AIR_ENGAGE:onafterDestroy( AIGroup, From, Event, To, EventData ) + + if EventData.IniUnit then + self.AttackUnits[EventData.IniUnit] = nil + end +end + +--- @param #AI_AIR_ENGAGE self +-- @param Core.Event#EVENTDATA EventData +function AI_AIR_ENGAGE:OnEventDead( EventData ) + self:F( { "EventDead", EventData } ) + + if EventData.IniDCSUnit then + if self.AttackUnits and self.AttackUnits[EventData.IniUnit] then + self:__Destroy( self.TaskDelay, EventData ) + end + end +end + + +--- @param Wrapper.Group#GROUP AIControllable +function AI_AIR_ENGAGE.___EngageRoute( AIGroup, Fsm, AttackSetUnit ) + Fsm:I(string.format("AI_AIR_ENGAGE.___EngageRoute: %s", tostring(AIGroup:GetName()))) + + if AIGroup and AIGroup:IsAlive() then + Fsm:__EngageRoute( Fsm.TaskDelay or 0.1, AttackSetUnit ) + end +end + + +--- @param #AI_AIR_ENGAGE self +-- @param Wrapper.Group#GROUP DefenderGroup The GroupGroup managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @param Core.Set#SET_UNIT AttackSetUnit Unit set to be attacked. +function AI_AIR_ENGAGE:onafterEngageRoute( DefenderGroup, From, Event, To, AttackSetUnit ) + self:I( { DefenderGroup, From, Event, To, AttackSetUnit } ) + + local DefenderGroupName = DefenderGroup:GetName() + + self.AttackSetUnit = AttackSetUnit -- Kept in memory in case of resume from refuel in air! + + local AttackCount = AttackSetUnit:CountAlive() + + if AttackCount > 0 then + + if DefenderGroup:IsAlive() then + + local EngageAltitude = math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude ) + local EngageSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed ) + + -- Determine the distance to the target. + -- If it is less than 10km, then attack without a route. + -- Otherwise perform a route attack. + + local DefenderCoord = DefenderGroup:GetPointVec3() + DefenderCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude. + + local TargetCoord = AttackSetUnit:GetFirst():GetPointVec3() + TargetCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude. + + local TargetDistance = DefenderCoord:Get2DDistance( TargetCoord ) + local EngageDistance = ( DefenderGroup:IsHelicopter() and 5000 ) or ( DefenderGroup:IsAirPlane() and 10000 ) + + -- TODO: A factor of * 3 is way too close. This causes the AI not to engange until merged sometimes! + if TargetDistance <= EngageDistance * 9 then + + self:I(string.format("AI_AIR_ENGAGE onafterEngageRoute ==> __Engage - target distance = %.1f km", TargetDistance/1000)) + self:__Engage( 0.1, AttackSetUnit ) + + else + + self:I(string.format("FF AI_AIR_ENGAGE onafterEngageRoute ==> Routing - target distance = %.1f km", TargetDistance/1000)) + + local EngageRoute = {} + local AttackTasks = {} + + --- Calculate the target route point. + + local FromWP = DefenderCoord:WaypointAir(self.PatrolAltType or "RADIO", POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, EngageSpeed, true) + + EngageRoute[#EngageRoute+1] = FromWP + + self:SetTargetDistance( TargetCoord ) -- For RTB status check + + local FromEngageAngle = DefenderCoord:GetAngleDegrees( DefenderCoord:GetDirectionVec3( TargetCoord ) ) + local ToCoord=DefenderCoord:Translate( EngageDistance, FromEngageAngle, true ) + + local ToWP = ToCoord:WaypointAir(self.PatrolAltType or "RADIO", POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, EngageSpeed, true) + + EngageRoute[#EngageRoute+1] = ToWP + + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_AIR_ENGAGE.___EngageRoute", self, AttackSetUnit ) + EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) + + DefenderGroup:OptionROEReturnFire() + DefenderGroup:OptionROTEvadeFire() + + DefenderGroup:Route( EngageRoute, self.TaskDelay or 0.1 ) + end + + end + else + -- TODO: This will make an A2A Dispatcher CAP flight to return rather than going back to patrolling! + self:I( DefenderGroupName .. ": No targets found -> Going RTB") + self:Return() + end +end + + +--- @param Wrapper.Group#GROUP AIControllable +function AI_AIR_ENGAGE.___Engage( AIGroup, Fsm, AttackSetUnit ) + + Fsm:I(string.format("AI_AIR_ENGAGE.___Engage: %s", tostring(AIGroup:GetName()))) + + if AIGroup and AIGroup:IsAlive() then + local delay=Fsm.TaskDelay or 0.1 + Fsm:__Engage(delay, AttackSetUnit) + end +end + + +--- @param #AI_AIR_ENGAGE self +-- @param Wrapper.Group#GROUP DefenderGroup The GroupGroup managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @param Core.Set#SET_UNIT AttackSetUnit Set of units to be attacked. +function AI_AIR_ENGAGE:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit ) + self:F( { DefenderGroup, From, Event, To, AttackSetUnit} ) + + local DefenderGroupName = DefenderGroup:GetName() + + self.AttackSetUnit = AttackSetUnit -- Kept in memory in case of resume from refuel in air! + + local AttackCount = AttackSetUnit:CountAlive() + self:T({AttackCount = AttackCount}) + + if AttackCount > 0 then + + if DefenderGroup and DefenderGroup:IsAlive() then + + local EngageAltitude = math.random( self.EngageFloorAltitude or 500, self.EngageCeilingAltitude or 1000 ) + local EngageSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed ) + + local DefenderCoord = DefenderGroup:GetPointVec3() + DefenderCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude. + + local TargetCoord = AttackSetUnit:GetFirst():GetPointVec3() + TargetCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude. + + local TargetDistance = DefenderCoord:Get2DDistance( TargetCoord ) + + local EngageDistance = ( DefenderGroup:IsHelicopter() and 5000 ) or ( DefenderGroup:IsAirPlane() and 10000 ) + + local EngageRoute = {} + local AttackTasks = {} + + local FromWP = DefenderCoord:WaypointAir(self.EngageAltType or "RADIO", POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, EngageSpeed, true) + EngageRoute[#EngageRoute+1] = FromWP + + self:SetTargetDistance( TargetCoord ) -- For RTB status check + + local FromEngageAngle = DefenderCoord:GetAngleDegrees( DefenderCoord:GetDirectionVec3( TargetCoord ) ) + local ToCoord=DefenderCoord:Translate( EngageDistance, FromEngageAngle, true ) + + local ToWP = ToCoord:WaypointAir(self.EngageAltType or "RADIO", POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, EngageSpeed, true) + EngageRoute[#EngageRoute+1] = ToWP + + -- TODO: A factor of * 3 this way too low. This causes the AI NOT to engage until very close or even merged sometimes. Some A2A missiles have a much longer range! Needs more frequent updates of the task! + if TargetDistance <= EngageDistance * 9 then + + local AttackUnitTasks = self:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, EngageAltitude ) -- Polymorphic + + if #AttackUnitTasks == 0 then + self:I( DefenderGroupName .. ": No valid targets found -> Going RTB") + self:Return() + return + else + local text=string.format("%s: Engaging targets at distance %.2f NM", DefenderGroupName, UTILS.MetersToNM(TargetDistance)) + self:I(text) + DefenderGroup:OptionROEOpenFire() + DefenderGroup:OptionROTEvadeFire() + DefenderGroup:OptionKeepWeaponsOnThreat() + + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskCombo( AttackUnitTasks ) + end + end + + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_AIR_ENGAGE.___Engage", self, AttackSetUnit ) + EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) + + DefenderGroup:Route( EngageRoute, self.TaskDelay or 0.1 ) + + end + else + -- TODO: This will make an A2A Dispatcher CAP flight to return rather than going back to patrolling! + self:I( DefenderGroupName .. ": No targets found -> returning.") + self:Return() + return + end +end + +--- @param Wrapper.Group#GROUP AIEngage +function AI_AIR_ENGAGE.Resume( AIEngage, Fsm ) + + AIEngage:F( { "Resume:", AIEngage:GetName() } ) + if AIEngage and AIEngage:IsAlive() then + Fsm:__Reset( Fsm.TaskDelay or 0.1 ) + Fsm:__EngageRoute( Fsm.TaskDelay or 0.2, Fsm.AttackSetUnit ) + end + +end +--- **AI** -- (R2.2) - Models the process of air patrol of airplanes. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_A2A_Patrol +-- @image AI_Air_Patrolling.JPG + + +--- @type AI_A2A_PATROL +-- @extends AI.AI_A2A#AI_A2A + +--- Implements the core functions to patrol a @{Zone} by an AI @{Wrapper.Group} or @{Wrapper.Group}. +-- +-- ![Process](..\Presentations\AI_PATROL\Dia3.JPG) +-- +-- The AI_A2A_PATROL is assigned a @{Wrapper.Group} and this must be done before the AI_A2A_PATROL process can be started using the **Start** event. +-- +-- ![Process](..\Presentations\AI_PATROL\Dia4.JPG) +-- +-- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits. +-- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. +-- +-- ![Process](..\Presentations\AI_PATROL\Dia5.JPG) +-- +-- This cycle will continue. +-- +-- ![Process](..\Presentations\AI_PATROL\Dia6.JPG) +-- +-- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event. +-- +-- ![Process](..\Presentations\AI_PATROL\Dia9.JPG) +-- +---- Note that the enemy is not engaged! To model enemy engagement, either tailor the **Detected** event, or +-- use derived AI_ classes to model AI offensive or defensive behaviour. +-- +-- ![Process](..\Presentations\AI_PATROL\Dia10.JPG) +-- +-- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. +-- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. +-- +-- ![Process](..\Presentations\AI_PATROL\Dia11.JPG) +-- +-- ## 1. AI_A2A_PATROL constructor +-- +-- * @{#AI_A2A_PATROL.New}(): Creates a new AI_A2A_PATROL object. +-- +-- ## 2. AI_A2A_PATROL is a FSM +-- +-- ![Process](..\Presentations\AI_PATROL\Dia2.JPG) +-- +-- ### 2.1. AI_A2A_PATROL States +-- +-- * **None** ( Group ): The process is not started yet. +-- * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone. +-- * **Returning** ( Group ): The AI is returning to Base. +-- * **Stopped** ( Group ): The process is stopped. +-- * **Crashed** ( Group ): The AI has crashed or is dead. +-- +-- ### 2.2. AI_A2A_PATROL Events +-- +-- * **Start** ( Group ): Start the process. +-- * **Stop** ( Group ): Stop the process. +-- * **Route** ( Group ): Route the AI to a new random 3D point within the Patrol Zone. +-- * **RTB** ( Group ): Route the AI to the home base. +-- * **Detect** ( Group ): The AI is detecting targets. +-- * **Detected** ( Group ): The AI has detected new targets. +-- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. +-- +-- ## 3. Set or Get the AI controllable +-- +-- * @{#AI_A2A_PATROL.SetControllable}(): Set the AIControllable. +-- * @{#AI_A2A_PATROL.GetControllable}(): Get the AIControllable. +-- +-- ## 4. Set the Speed and Altitude boundaries of the AI controllable +-- +-- * @{#AI_A2A_PATROL.SetSpeed}(): Set the patrol speed boundaries of the AI, for the next patrol. +-- * @{#AI_A2A_PATROL.SetAltitude}(): Set altitude boundaries of the AI, for the next patrol. +-- +-- ## 5. Manage the detection process of the AI controllable +-- +-- The detection process of the AI controllable can be manipulated. +-- Detection requires an amount of CPU power, which has an impact on your mission performance. +-- Only put detection on when absolutely necessary, and the frequency of the detection can also be set. +-- +-- * @{#AI_A2A_PATROL.SetDetectionOn}(): Set the detection on. The AI will detect for targets. +-- * @{#AI_A2A_PATROL.SetDetectionOff}(): Set the detection off, the AI will not detect for targets. The existing target list will NOT be erased. +-- +-- The detection frequency can be set with @{#AI_A2A_PATROL.SetRefreshTimeInterval}( seconds ), where the amount of seconds specify how much seconds will be waited before the next detection. +-- Use the method @{#AI_A2A_PATROL.GetDetectedUnits}() to obtain a list of the @{Wrapper.Unit}s detected by the AI. +-- +-- The detection can be filtered to potential targets in a specific zone. +-- Use the method @{#AI_A2A_PATROL.SetDetectionZone}() to set the zone where targets need to be detected. +-- Note that when the zone is too far away, or the AI is not heading towards the zone, or the AI is too high, no targets may be detected +-- according the weather conditions. +-- +-- ## 6. Manage the "out of fuel" in the AI_A2A_PATROL +-- +-- When the AI is out of fuel, it is required that a new AI is started, before the old AI can return to the home base. +-- Therefore, with a parameter and a calculation of the distance to the home base, the fuel treshold is calculated. +-- When the fuel treshold is reached, the AI will continue for a given time its patrol task in orbit, +-- while a new AI is targetted to the AI_A2A_PATROL. +-- Once the time is finished, the old AI will return to the base. +-- Use the method @{#AI_A2A_PATROL.ManageFuel}() to have this proces in place. +-- +-- ## 7. Manage "damage" behaviour of the AI in the AI_A2A_PATROL +-- +-- When the AI is damaged, it is required that a new Patrol is started. However, damage cannon be foreseen early on. +-- Therefore, when the damage treshold is reached, the AI will return immediately to the home base (RTB). +-- Use the method @{#AI_A2A_PATROL.ManageDamage}() to have this proces in place. +-- +-- === +-- +-- @field #AI_A2A_PATROL +AI_A2A_PATROL = { + ClassName = "AI_A2A_PATROL", +} + +--- Creates a new AI_A2A_PATROL object +-- @param #AI_A2A_PATROL self +-- @param Wrapper.Group#GROUP AIPatrol The patrol group object. +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to BARO +-- @return #AI_A2A_PATROL self +-- @usage +-- -- Define a new AI_A2A_PATROL Object. This PatrolArea will patrol a Group within PatrolZone between 3000 and 6000 meters, with a variying speed between 600 and 900 km/h. +-- PatrolZone = ZONE:New( 'PatrolZone' ) +-- PatrolSpawn = SPAWN:New( 'Patrol Group' ) +-- PatrolArea = AI_A2A_PATROL:New( PatrolZone, 3000, 6000, 600, 900 ) +function AI_A2A_PATROL:New( AIPatrol, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) + + local AI_Air = AI_AIR:New( AIPatrol ) + local AI_Air_Patrol = AI_AIR_PATROL:New( AI_Air, AIPatrol, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) + local self = BASE:Inherit( self, AI_Air_Patrol ) -- #AI_A2A_PATROL + + self:SetFuelThreshold( .2, 60 ) + self:SetDamageThreshold( 0.4 ) + self:SetDisengageRadius( 70000 ) + + + self.PatrolZone = PatrolZone + self.PatrolFloorAltitude = PatrolFloorAltitude + self.PatrolCeilingAltitude = PatrolCeilingAltitude + self.PatrolMinSpeed = PatrolMinSpeed + self.PatrolMaxSpeed = PatrolMaxSpeed + + -- defafult PatrolAltType to "BARO" if not specified + self.PatrolAltType = PatrolAltType or "BARO" + + self:AddTransition( { "Started", "Airborne", "Refuelling" }, "Patrol", "Patrolling" ) + +--- OnBefore Transition Handler for Event Patrol. +-- @function [parent=#AI_A2A_PATROL] OnBeforePatrol +-- @param #AI_A2A_PATROL self +-- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnAfter Transition Handler for Event Patrol. +-- @function [parent=#AI_A2A_PATROL] OnAfterPatrol +-- @param #AI_A2A_PATROL self +-- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- Synchronous Event Trigger for Event Patrol. +-- @function [parent=#AI_A2A_PATROL] Patrol +-- @param #AI_A2A_PATROL self + +--- Asynchronous Event Trigger for Event Patrol. +-- @function [parent=#AI_A2A_PATROL] __Patrol +-- @param #AI_A2A_PATROL self +-- @param #number Delay The delay in seconds. + +--- OnLeave Transition Handler for State Patrolling. +-- @function [parent=#AI_A2A_PATROL] OnLeavePatrolling +-- @param #AI_A2A_PATROL self +-- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnEnter Transition Handler for State Patrolling. +-- @function [parent=#AI_A2A_PATROL] OnEnterPatrolling +-- @param #AI_A2A_PATROL self +-- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + + self:AddTransition( "Patrolling", "Route", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_PATROL. + +--- OnBefore Transition Handler for Event Route. +-- @function [parent=#AI_A2A_PATROL] OnBeforeRoute +-- @param #AI_A2A_PATROL self +-- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnAfter Transition Handler for Event Route. +-- @function [parent=#AI_A2A_PATROL] OnAfterRoute +-- @param #AI_A2A_PATROL self +-- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- Synchronous Event Trigger for Event Route. +-- @function [parent=#AI_A2A_PATROL] Route +-- @param #AI_A2A_PATROL self + +--- Asynchronous Event Trigger for Event Route. +-- @function [parent=#AI_A2A_PATROL] __Route +-- @param #AI_A2A_PATROL self +-- @param #number Delay The delay in seconds. + + + + self:AddTransition( "*", "Reset", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_PATROL. + + return self +end + + + + +--- Sets (modifies) the minimum and maximum speed of the patrol. +-- @param #AI_A2A_PATROL self +-- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h. +-- @return #AI_A2A_PATROL self +function AI_A2A_PATROL:SetSpeed( PatrolMinSpeed, PatrolMaxSpeed ) + self:F2( { PatrolMinSpeed, PatrolMaxSpeed } ) + + self.PatrolMinSpeed = PatrolMinSpeed + self.PatrolMaxSpeed = PatrolMaxSpeed +end + + + +--- Sets the floor and ceiling altitude of the patrol. +-- @param #AI_A2A_PATROL self +-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @return #AI_A2A_PATROL self +function AI_A2A_PATROL:SetAltitude( PatrolFloorAltitude, PatrolCeilingAltitude ) + self:F2( { PatrolFloorAltitude, PatrolCeilingAltitude } ) + + self.PatrolFloorAltitude = PatrolFloorAltitude + self.PatrolCeilingAltitude = PatrolCeilingAltitude +end + + +--- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. +-- @param #AI_A2A_PATROL self +-- @return #AI_A2A_PATROL self +-- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2A_PATROL:onafterPatrol( AIPatrol, From, Event, To ) + self:F2() + + self:ClearTargetDistance() + + self:__Route( 1 ) + + AIPatrol:OnReSpawn( + function( PatrolGroup ) + self:__Reset( 1 ) + self:__Route( 5 ) + end + ) +end + + +--- This statis method is called from the route path within the last task at the last waaypoint of the AIPatrol. +-- Note that this method is required, as triggers the next route when patrolling for the AIPatrol. +-- @param Wrapper.Group#GROUP AIPatrol The AI group. +-- @param #AI_A2A_PATROL Fsm The FSM. +function AI_A2A_PATROL.PatrolRoute( AIPatrol, Fsm ) + + AIPatrol:F( { "AI_A2A_PATROL.PatrolRoute:", AIPatrol:GetName() } ) + + if AIPatrol and AIPatrol:IsAlive() then + Fsm:Route() + end + +end + + +--- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. +-- @param #AI_A2A_PATROL self +-- @param Wrapper.Group#GROUP AIPatrol The Group managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2A_PATROL:onafterRoute( AIPatrol, From, Event, To ) + self:F2() + + -- When RTB, don't allow anymore the routing. + if From == "RTB" then + return + end + + + if AIPatrol and AIPatrol:IsAlive() then + + local PatrolRoute = {} + + --- Calculate the target route point. + + local CurrentCoord = AIPatrol:GetCoordinate() + + -- Random altitude. + local altitude=math.random(self.PatrolFloorAltitude, self.PatrolCeilingAltitude) + + -- Random speed in km/h. + local speedkmh = math.random(self.PatrolMinSpeed, self.PatrolMaxSpeed) + + -- First waypoint is current position. + PatrolRoute[1]=CurrentCoord:WaypointAirTurningPoint(nil, speedkmh, {}, "Current") + + if self.racetrack then + + -- Random heading. + local heading = math.random(self.racetrackheadingmin, self.racetrackheadingmax) + + -- Random leg length. + local leg=math.random(self.racetracklegmin, self.racetracklegmax) + + -- Random duration if any. + local duration = self.racetrackdurationmin + if self.racetrackdurationmax then + duration=math.random(self.racetrackdurationmin, self.racetrackdurationmax) + end + + -- CAP coordinate. + local c0=self.PatrolZone:GetRandomCoordinate() + if self.racetrackcapcoordinates and #self.racetrackcapcoordinates>0 then + c0=self.racetrackcapcoordinates[math.random(#self.racetrackcapcoordinates)] + end + + -- Race track points. + local c1=c0:SetAltitude(altitude) --Core.Point#COORDINATE + local c2=c1:Translate(leg, heading):SetAltitude(altitude) + + self:SetTargetDistance(c0) -- For RTB status check + + -- Debug: + self:T(string.format("Patrol zone race track: v=%.1f knots, h=%.1f ft, heading=%03d, leg=%d m, t=%s sec", UTILS.KmphToKnots(speedkmh), UTILS.MetersToFeet(altitude), heading, leg, tostring(duration))) + --c1:MarkToAll("Race track c1") + --c2:MarkToAll("Race track c2") + + -- Task to orbit. + local taskOrbit=AIPatrol:TaskOrbit(c1, altitude, UTILS.KmphToMps(speedkmh), c2) + + -- Task function to redo the patrol at other random position. + local taskPatrol=AIPatrol:TaskFunction("AI_A2A_PATROL.PatrolRoute", self) + + -- Controlled task with task condition. + local taskCond=AIPatrol:TaskCondition(nil, nil, nil, nil, duration, nil) + local taskCont=AIPatrol:TaskControlled(taskOrbit, taskCond) + + -- Second waypoint + PatrolRoute[2]=c1:WaypointAirTurningPoint(self.PatrolAltType, speedkmh, {taskCont, taskPatrol}, "CAP Orbit") + + else + + -- Target coordinate. + local ToTargetCoord=self.PatrolZone:GetRandomCoordinate() --Core.Point#COORDINATE + ToTargetCoord:SetAltitude(altitude) + + self:SetTargetDistance( ToTargetCoord ) -- For RTB status check + + local taskReRoute=AIPatrol:TaskFunction( "AI_A2A_PATROL.PatrolRoute", self ) + + PatrolRoute[2]=ToTargetCoord:WaypointAirTurningPoint(self.PatrolAltType, speedkmh, {taskReRoute}, "Patrol Point") + + end + + -- ROE + AIPatrol:OptionROEReturnFire() + AIPatrol:OptionROTEvadeFire() + + -- Patrol. + AIPatrol:Route( PatrolRoute, 0.5) + end + +end + +--- **AI** -- (R2.2) - Models the process of Combat Air Patrol (CAP) for airplanes. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_A2A_Cap +-- @image AI_Combat_Air_Patrol.JPG + +--- @type AI_A2A_CAP +-- @extends AI.AI_Air_Patrol#AI_AIR_PATROL +-- @extends AI.AI_Air_Engage#AI_AIR_ENGAGE + + +--- The AI_A2A_CAP class implements the core functions to patrol a @{Zone} by an AI @{Wrapper.Group} or @{Wrapper.Group} +-- and automatically engage any airborne enemies that are within a certain range or within a certain zone. +-- +-- ![Process](..\Presentations\AI_CAP\Dia3.JPG) +-- +-- The AI_A2A_CAP is assigned a @{Wrapper.Group} and this must be done before the AI_A2A_CAP process can be started using the **Start** event. +-- +-- ![Process](..\Presentations\AI_CAP\Dia4.JPG) +-- +-- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits. +-- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. +-- +-- ![Process](..\Presentations\AI_CAP\Dia5.JPG) +-- +-- This cycle will continue. +-- +-- ![Process](..\Presentations\AI_CAP\Dia6.JPG) +-- +-- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event. +-- +-- ![Process](..\Presentations\AI_CAP\Dia9.JPG) +-- +-- When enemies are detected, the AI will automatically engage the enemy. +-- +-- ![Process](..\Presentations\AI_CAP\Dia10.JPG) +-- +-- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. +-- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. +-- +-- ![Process](..\Presentations\AI_CAP\Dia13.JPG) +-- +-- ## 1. AI_A2A_CAP constructor +-- +-- * @{#AI_A2A_CAP.New}(): Creates a new AI_A2A_CAP object. +-- +-- ## 2. AI_A2A_CAP is a FSM +-- +-- ![Process](..\Presentations\AI_CAP\Dia2.JPG) +-- +-- ### 2.1 AI_A2A_CAP States +-- +-- * **None** ( Group ): The process is not started yet. +-- * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone. +-- * **Engaging** ( Group ): The AI is engaging the bogeys. +-- * **Returning** ( Group ): The AI is returning to Base.. +-- +-- ### 2.2 AI_A2A_CAP Events +-- +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Start}**: Start the process. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Route}**: Route the AI to a new random 3D point within the Patrol Zone. +-- * **@{#AI_A2A_CAP.Engage}**: Let the AI engage the bogeys. +-- * **@{#AI_A2A_CAP.Abort}**: Aborts the engagement and return patrolling in the patrol zone. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.RTB}**: Route the AI to the home base. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detect}**: The AI is detecting targets. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets. +-- * **@{#AI_A2A_CAP.Destroy}**: The AI has destroyed a bogey @{Wrapper.Unit}. +-- * **@{#AI_A2A_CAP.Destroyed}**: The AI has destroyed all bogeys @{Wrapper.Unit}s assigned in the CAS task. +-- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. +-- +-- ## 3. Set the Range of Engagement +-- +-- ![Range](..\Presentations\AI_CAP\Dia11.JPG) +-- +-- An optional range can be set in meters, +-- that will define when the AI will engage with the detected airborne enemy targets. +-- The range can be beyond or smaller than the range of the Patrol Zone. +-- The range is applied at the position of the AI. +-- Use the method @{AI.AI_CAP#AI_A2A_CAP.SetEngageRange}() to define that range. +-- +-- ## 4. Set the Zone of Engagement +-- +-- ![Zone](..\Presentations\AI_CAP\Dia12.JPG) +-- +-- An optional @{Zone} can be set, +-- that will define when the AI will engage with the detected airborne enemy targets. +-- Use the method @{AI.AI_Cap#AI_A2A_CAP.SetEngageZone}() to define that Zone. +-- +-- === +-- +-- @field #AI_A2A_CAP +AI_A2A_CAP = { + ClassName = "AI_A2A_CAP", +} + +--- Creates a new AI_A2A_CAP object +-- @param #AI_A2A_CAP self +-- @param Wrapper.Group#GROUP AICap +-- @param DCS#Speed EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Speed EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement. +-- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement. +-- @param DCS#AltitudeType EngageAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO". +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO". +-- @return #AI_A2A_CAP +function AI_A2A_CAP:New2( AICap, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType, PatrolZone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType ) + + -- Multiple inheritance ... :-) + local AI_Air = AI_AIR:New( AICap ) + local AI_Air_Patrol = AI_AIR_PATROL:New( AI_Air, AICap, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) -- #AI_AIR_PATROL + local AI_Air_Engage = AI_AIR_ENGAGE:New( AI_Air_Patrol, AICap, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) + local self = BASE:Inherit( self, AI_Air_Engage ) --#AI_A2A_CAP + + self:SetFuelThreshold( .2, 60 ) + self:SetDamageThreshold( 0.4 ) + self:SetDisengageRadius( 70000 ) + + + return self +end + +--- Creates a new AI_A2A_CAP object +-- @param #AI_A2A_CAP self +-- @param Wrapper.Group#GROUP AICap +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#Speed EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Speed EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO +-- @return #AI_A2A_CAP +function AI_A2A_CAP:New( AICap, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, PatrolAltType ) + + return self:New2( AICap, EngageMinSpeed, EngageMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, PatrolZone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType ) + +end + +--- onafter State Transition for Event Patrol. +-- @param #AI_A2A_CAP self +-- @param Wrapper.Group#GROUP AICap The AI Group managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2A_CAP:onafterStart( AICap, From, Event, To ) + + self:GetParent( self, AI_A2A_CAP ).onafterStart( self, AICap, From, Event, To ) + AICap:HandleEvent( EVENTS.Takeoff, nil, self ) + +end + +--- Set the Engage Zone which defines where the AI will engage bogies. +-- @param #AI_A2A_CAP self +-- @param Core.Zone#ZONE EngageZone The zone where the AI is performing CAP. +-- @return #AI_A2A_CAP self +function AI_A2A_CAP:SetEngageZone( EngageZone ) + self:F2() + + if EngageZone then + self.EngageZone = EngageZone + else + self.EngageZone = nil + end +end + +--- Set the Engage Range when the AI will engage with airborne enemies. +-- @param #AI_A2A_CAP self +-- @param #number EngageRange The Engage Range. +-- @return #AI_A2A_CAP self +function AI_A2A_CAP:SetEngageRange( EngageRange ) + self:F2() + + if EngageRange then + self.EngageRange = EngageRange + else + self.EngageRange = nil + end +end + +--- Evaluate the attack and create an AttackUnitTask list. +-- @param #AI_A2A_CAP self +-- @param Core.Set#SET_UNIT AttackSetUnit The set of units to attack. +-- @param Wrappper.Group#GROUP DefenderGroup The group of defenders. +-- @param #number EngageAltitude The altitude to engage the targets. +-- @return #AI_A2A_CAP self +function AI_A2A_CAP:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, EngageAltitude ) + + local AttackUnitTasks = {} + + for AttackUnitID, AttackUnit in pairs( self.AttackSetUnit:GetSet() ) do + local AttackUnit = AttackUnit -- Wrapper.Unit#UNIT + if AttackUnit and AttackUnit:IsAlive() and AttackUnit:IsAir() then + -- TODO: Add coalition check? Only attack units of if AttackUnit:GetCoalition()~=AICap:GetCoalition() + -- Maybe the detected set also contains + self:T( { "Attacking Task:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsAir() } ) + AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit ) + end + end + + return AttackUnitTasks +end +--- **AI** -- (R2.2) - Models the process of Ground Controlled Interception (GCI) for airplanes. +-- +-- This is a class used in the @{AI_A2A_Dispatcher}. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_A2A_GCI +-- @image AI_Ground_Control_Intercept.JPG + + + +--- @type AI_A2A_GCI +-- @extends AI.AI_A2A#AI_A2A + + +--- Implements the core functions to intercept intruders. Use the Engage trigger to intercept intruders. +-- +-- ![Process](..\Presentations\AI_GCI\Dia3.JPG) +-- +-- The AI_A2A_GCI is assigned a @{Wrapper.Group} and this must be done before the AI_A2A_GCI process can be started using the **Start** event. +-- +-- ![Process](..\Presentations\AI_GCI\Dia4.JPG) +-- +-- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits. +-- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. +-- +-- ![Process](..\Presentations\AI_GCI\Dia5.JPG) +-- +-- This cycle will continue. +-- +-- ![Process](..\Presentations\AI_GCI\Dia6.JPG) +-- +-- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event. +-- +-- ![Process](..\Presentations\AI_GCI\Dia9.JPG) +-- +-- When enemies are detected, the AI will automatically engage the enemy. +-- +-- ![Process](..\Presentations\AI_GCI\Dia10.JPG) +-- +-- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. +-- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. +-- +-- ![Process](..\Presentations\AI_GCI\Dia13.JPG) +-- +-- ## 1. AI_A2A_GCI constructor +-- +-- * @{#AI_A2A_GCI.New}(): Creates a new AI_A2A_GCI object. +-- +-- ## 2. AI_A2A_GCI is a FSM +-- +-- ![Process](..\Presentations\AI_GCI\Dia2.JPG) +-- +-- ### 2.1 AI_A2A_GCI States +-- +-- * **None** ( Group ): The process is not started yet. +-- * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone. +-- * **Engaging** ( Group ): The AI is engaging the bogeys. +-- * **Returning** ( Group ): The AI is returning to Base.. +-- +-- ### 2.2 AI_A2A_GCI Events +-- +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Start}**: Start the process. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Route}**: Route the AI to a new random 3D point within the Patrol Zone. +-- * **@{#AI_A2A_GCI.Engage}**: Let the AI engage the bogeys. +-- * **@{#AI_A2A_GCI.Abort}**: Aborts the engagement and return patrolling in the patrol zone. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.RTB}**: Route the AI to the home base. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detect}**: The AI is detecting targets. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets. +-- * **@{#AI_A2A_GCI.Destroy}**: The AI has destroyed a bogey @{Wrapper.Unit}. +-- * **@{#AI_A2A_GCI.Destroyed}**: The AI has destroyed all bogeys @{Wrapper.Unit}s assigned in the CAS task. +-- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. +-- +-- ## 3. Set the Range of Engagement +-- +-- ![Range](..\Presentations\AI_GCI\Dia11.JPG) +-- +-- An optional range can be set in meters, +-- that will define when the AI will engage with the detected airborne enemy targets. +-- The range can be beyond or smaller than the range of the Patrol Zone. +-- The range is applied at the position of the AI. +-- Use the method @{AI.AI_GCI#AI_A2A_GCI.SetEngageRange}() to define that range. +-- +-- ## 4. Set the Zone of Engagement +-- +-- ![Zone](..\Presentations\AI_GCI\Dia12.JPG) +-- +-- An optional @{Zone} can be set, +-- that will define when the AI will engage with the detected airborne enemy targets. +-- Use the method @{AI.AI_Cap#AI_A2A_GCI.SetEngageZone}() to define that Zone. +-- +-- === +-- +-- @field #AI_A2A_GCI +AI_A2A_GCI = { + ClassName = "AI_A2A_GCI", +} + + + +--- Creates a new AI_A2A_GCI object +-- @param #AI_A2A_GCI self +-- @param Wrapper.Group#GROUP AIIntercept +-- @param DCS#Speed EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Speed EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement. +-- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement. +-- @param DCS#AltitudeType EngageAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO". +-- @return #AI_A2A_GCI +function AI_A2A_GCI:New2( AIIntercept, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) + + local AI_Air = AI_AIR:New( AIIntercept ) + local AI_Air_Engage = AI_AIR_ENGAGE:New( AI_Air, AIIntercept, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) + local self = BASE:Inherit( self, AI_Air_Engage ) -- #AI_A2A_GCI + + self:SetFuelThreshold( .2, 60 ) + self:SetDamageThreshold( 0.4 ) + self:SetDisengageRadius( 70000 ) + + return self +end + +--- Creates a new AI_A2A_GCI object +-- @param #AI_A2A_GCI self +-- @param Wrapper.Group#GROUP AIIntercept +-- @param DCS#Speed EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Speed EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement. +-- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement. +-- @param DCS#AltitudeType EngageAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO". +-- @return #AI_A2A_GCI +function AI_A2A_GCI:New( AIIntercept, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) + + return self:New2( AIIntercept, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) +end + +--- onafter State Transition for Event Patrol. +-- @param #AI_A2A_GCI self +-- @param Wrapper.Group#GROUP AIIntercept The AI Group managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2A_GCI:onafterStart( AIIntercept, From, Event, To ) + + self:GetParent( self, AI_A2A_GCI ).onafterStart( self, AIIntercept, From, Event, To ) +end + + +--- Evaluate the attack and create an AttackUnitTask list. +-- @param #AI_A2A_GCI self +-- @param Core.Set#SET_UNIT AttackSetUnit The set of units to attack. +-- @param Wrappper.Group#GROUP DefenderGroup The group of defenders. +-- @param #number EngageAltitude The altitude to engage the targets. +-- @return #AI_A2A_GCI self +function AI_A2A_GCI:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, EngageAltitude ) + + local AttackUnitTasks = {} + + for AttackUnitID, AttackUnit in pairs( self.AttackSetUnit:GetSet() ) do + local AttackUnit = AttackUnit -- Wrapper.Unit#UNIT + self:T( { "Attacking Unit:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsAir() } ) + if AttackUnit:IsAlive() and AttackUnit:IsAir() then + -- TODO: Add coalition check? Only attack units of if AttackUnit:GetCoalition()~=AICap:GetCoalition() + -- Maybe the detected set also contains + AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit ) + end + end + + return AttackUnitTasks +end +--- **AI** - (R2.2) - Manages the process of an automatic A2A defense system based on an EWR network targets and coordinating CAP and GCI. +-- +-- === +-- +-- Features: +-- +-- * Setup quickly an A2A defense system for a coalition. +-- * Setup (CAP) Control Air Patrols at defined zones to enhance your A2A defenses. +-- * Setup (GCI) Ground Control Intercept at defined airbases to enhance your A2A defenses. +-- * Define and use an EWR (Early Warning Radar) network. +-- * Define squadrons at airbases. +-- * Enable airbases for A2A defenses. +-- * Add different plane types to different squadrons. +-- * Add multiple squadrons to different airbases. +-- * Define different ranges to engage upon intruders. +-- * Establish an automatic in air refuel process for CAP using refuel tankers. +-- * Setup default settings for all squadrons and A2A defenses. +-- * Setup specific settings for specific squadrons. +-- * Quickly setup an A2A defense system using @{#AI_A2A_GCICAP}. +-- * Setup a more advanced defense system using @{#AI_A2A_DISPATCHER}. +-- +-- === +-- +-- ## Missions: +-- +-- [AID-A2A - AI A2A Dispatching](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AID%20-%20AI%20Dispatching/AID-A2A%20-%20AI%20A2A%20Dispatching) +-- +-- === +-- +-- ## YouTube Channel: +-- +-- [DCS WORLD - MOOSE - A2A GCICAP - Build an automatic A2A Defense System](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl0S4KMNUUJpaUs6zZHjLKNx) +-- +-- === +-- +-- # QUICK START GUIDE +-- +-- There are basically two classes available to model an A2A defense system. +-- +-- AI\_A2A\_DISPATCHER is the main A2A defense class that models the A2A defense system. +-- AI\_A2A\_GCICAP derives or inherits from AI\_A2A\_DISPATCHER and is a more **noob** user friendly class, but is less flexible. +-- +-- Before you start using the AI\_A2A\_DISPATCHER or AI\_A2A\_GCICAP ask youself the following questions. +-- +-- ## 0. Do I need AI\_A2A\_DISPATCHER or do I need AI\_A2A\_GCICAP? +-- +-- AI\_A2A\_GCICAP, automates a lot of the below questions using the mission editor and requires minimal lua scripting. +-- But the AI\_A2A\_GCICAP provides less flexibility and a lot of options are defaulted. +-- With AI\_A2A\_DISPATCHER you can setup a much more **fine grained** A2A defense mechanism, but some more (easy) lua scripting is required. +-- +-- ## 1. Which Coalition am I modeling an A2A defense system for? blue or red? +-- +-- One AI\_A2A\_DISPATCHER object can create a defense system for **one coalition**, which is blue or red. +-- If you want to create a **mutual defense system**, for both blue and red, then you need to create **two** AI\_A2A\_DISPATCHER **objects**, +-- each governing their defense system. +-- +-- +-- ## 2. Which type of EWR will I setup? Grouping based per AREA, per TYPE or per UNIT? (Later others will follow). +-- +-- The MOOSE framework leverages the @{Detection} classes to perform the EWR detection. +-- Several types of @{Detection} classes exist, and the most common characteristics of these classes is that they: +-- +-- * Perform detections from multiple FACs as one co-operating entity. +-- * Communicate with a Head Quarters, which consolidates each detection. +-- * Groups detections based on a method (per area, per type or per unit). +-- * Communicates detections. +-- +-- ## 3. Which EWR units will be used as part of the detection system? Only Ground or also Airborne? +-- +-- Typically EWR networks are setup using 55G6 EWR, 1L13 EWR, Hawk sr and Patriot str ground based radar units. +-- These radars have different ranges and 55G6 EWR and 1L13 EWR radars are Eastern Bloc units (eg Russia, Ukraine, Georgia) while the Hawk and Patriot radars are Western (eg US). +-- Additionally, ANY other radar capable unit can be part of the EWR network! Also AWACS airborne units, planes, helicopters can help to detect targets, as long as they have radar. +-- The position of these units is very important as they need to provide enough coverage +-- to pick up enemy aircraft as they approach so that CAP and GCI flights can be tasked to intercept them. +-- +-- ## 4. Is a border required? +-- +-- Is this a cold war or a hot war situation? In case of a cold war situation, a border can be set that will only trigger defenses +-- if the border is crossed by enemy units. +-- +-- ## 5. What maximum range needs to be checked to allow defenses to engage any attacker? +-- +-- A good functioning defense will have a "maximum range" evaluated to the enemy when CAP will be engaged or GCI will be spawned. +-- +-- ## 6. Which Airbases, Carrier Ships, Farps will take part in the defense system for the Coalition? +-- +-- Carefully plan which airbases will take part in the coalition. Color each airbase in the color of the coalition. +-- +-- ## 7. Which Squadrons will I create and which name will I give each Squadron? +-- +-- The defense system works with Squadrons. Each Squadron must be given a unique name, that forms the **key** to the defense system. +-- Several options and activities can be set per Squadron. +-- +-- ## 8. Where will the Squadrons be located? On Airbases? On Carrier Ships? On Farps? +-- +-- Squadrons are placed as the "home base" on an airfield, carrier or farp. +-- Carefully plan where each Squadron will be located as part of the defense system. +-- +-- ## 9. Which plane models will I assign for each Squadron? Do I need one plane model or more plane models per squadron? +-- +-- Per Squadron, one or multiple plane models can be allocated as **Templates**. +-- These are late activated groups with one airplane or helicopter that start with a specific name, called the **template prefix**. +-- The A2A defense system will select from the given templates a random template to spawn a new plane (group). +-- +-- ## 10. Which payloads, skills and skins will these plane models have? +-- +-- Per Squadron, even if you have one plane model, you can still allocate multiple templates of one plane model, +-- each having different payloads, skills and skins. +-- The A2A defense system will select from the given templates a random template to spawn a new plane (group). +-- +-- ## 11. For each Squadron, which will perform CAP? +-- +-- Per Squadron, evaluate which Squadrons will perform CAP. +-- Not all Squadrons need to perform CAP. +-- +-- ## 12. For each Squadron doing CAP, in which ZONE(s) will the CAP be performed? +-- +-- Per CAP, evaluate **where** the CAP will be performed, in other words, define the **zone**. +-- Near the border or a bit further away? +-- +-- ## 13. For each Squadron doing CAP, which zone types will I create? +-- +-- Per CAP zone, evaluate whether you want: +-- +-- * simple trigger zones +-- * polygon zones +-- * moving zones +-- +-- Depending on the type of zone selected, a different @{Zone} object needs to be created from a ZONE_ class. +-- +-- ## 14. For each Squadron doing CAP, what are the time intervals and CAP amounts to be performed? +-- +-- For each CAP: +-- +-- * **How many** CAP you want to have airborne at the same time? +-- * **How frequent** you want the defense mechanism to check whether to start a new CAP? +-- +-- ## 15. For each Squadron, which will perform GCI? +-- +-- For each Squadron, evaluate which Squadrons will perform GCI? +-- Not all Squadrons need to perform GCI. +-- +-- ## 16. For each Squadron, which takeoff method will I use? +-- +-- For each Squadron, evaluate which takeoff method will be used: +-- +-- * Straight from the air +-- * From the runway +-- * From a parking spot with running engines +-- * From a parking spot with cold engines +-- +-- **The default takeoff method is staight in the air.** +-- +-- ## 17. For each Squadron, which landing method will I use? +-- +-- For each Squadron, evaluate which landing method will be used: +-- +-- * Despawn near the airbase when returning +-- * Despawn after landing on the runway +-- * Despawn after engine shutdown after landing +-- +-- **The default landing method is despawn when near the airbase when returning.** +-- +-- ## 18. For each Squadron, which overhead will I use? +-- +-- For each Squadron, depending on the airplane type (modern, old) and payload, which overhead is required to provide any defense? +-- In other words, if **X** attacker airplanes are detected, how many **Y** defense airplanes need to be spawned per squadron? +-- The **Y** is dependent on the type of airplane (era), payload, fuel levels, skills etc. +-- The overhead is a **factor** that will calculate dynamically how many **Y** defenses will be required based on **X** attackers detected. +-- +-- **The default overhead is 1. A value greater than 1, like 1.5 will increase the overhead with 50%, a value smaller than 1, like 0.5 will decrease the overhead with 50%.** +-- +-- ## 19. For each Squadron, which grouping will I use? +-- +-- When multiple targets are detected, how will defense airplanes be grouped when multiple defense airplanes are spawned for multiple attackers? +-- Per one, two, three, four? +-- +-- **The default grouping is 1. That means, that each spawned defender will act individually.** +-- +-- === +-- +-- ### Authors: **FlightControl** rework of GCICAP + introduction of new concepts (squadrons). +-- ### Authors: **Stonehouse**, **SNAFU** in terms of the advice, documentation, and the original GCICAP script. +-- +-- @module AI.AI_A2A_Dispatcher +-- @image AI_Air_To_Air_Dispatching.JPG + + + +do -- AI_A2A_DISPATCHER + + --- AI_A2A_DISPATCHER class. + -- @type AI_A2A_DISPATCHER + -- @extends Tasking.DetectionManager#DETECTION_MANAGER + + --- Create an automatic air defence system for a coalition. + -- + -- === + -- + -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia3.JPG) + -- + -- It includes automatic spawning of Combat Air Patrol aircraft (CAP) and Ground Controlled Intercept aircraft (GCI) in response to enemy air movements that are detected by a ground based radar network. + -- CAP flights will take off and proceed to designated CAP zones where they will remain on station until the ground radars direct them to intercept detected enemy aircraft or they run short of fuel and must return to base (RTB). When a CAP flight leaves their zone to perform an interception or return to base a new CAP flight will spawn to take their place. + -- If all CAP flights are engaged or RTB then additional GCI interceptors will scramble to intercept unengaged enemy aircraft under ground radar control. + -- With a little time and with a little work it provides the mission designer with a convincing and completely automatic air defence system. + -- In short it is a plug in very flexible and configurable air defence module for DCS World. + -- + -- Note that in order to create a two way A2A defense system, two AI\_A2A\_DISPATCHER defense system may need to be created, for each coalition one. + -- This is a good implementation, because maybe in the future, more coalitions may become available in DCS world. + -- + -- === + -- + -- # USAGE GUIDE + -- + -- ## 1. AI\_A2A\_DISPATCHER constructor: + -- + -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_1.JPG) + -- + -- + -- The @{#AI_A2A_DISPATCHER.New}() method creates a new AI\_A2A\_DISPATCHER instance. + -- + -- ### 1.1. Define the **EWR network**: + -- + -- As part of the AI\_A2A\_DISPATCHER :New() constructor, an EWR network must be given as the first parameter. + -- An EWR network, or, Early Warning Radar network, is used to early detect potential airborne targets and to understand the position of patrolling targets of the enemy. + -- + -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia5.JPG) + -- + -- Typically EWR networks are setup using 55G6 EWR, 1L13 EWR, Hawk sr and Patriot str ground based radar units. + -- These radars have different ranges and 55G6 EWR and 1L13 EWR radars are Eastern Bloc units (eg Russia, Ukraine, Georgia) while the Hawk and Patriot radars are Western (eg US). + -- Additionally, ANY other radar capable unit can be part of the EWR network! Also AWACS airborne units, planes, helicopters can help to detect targets, as long as they have radar. + -- The position of these units is very important as they need to provide enough coverage + -- to pick up enemy aircraft as they approach so that CAP and GCI flights can be tasked to intercept them. + -- + -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia7.JPG) + -- + -- Additionally in a hot war situation where the border is no longer respected the placement of radars has a big effect on how fast the war escalates. + -- For example if they are a long way forward and can detect enemy planes on the ground and taking off + -- they will start to vector CAP and GCI flights to attack them straight away which will immediately draw a response from the other coalition. + -- Having the radars further back will mean a slower escalation because fewer targets will be detected and + -- therefore less CAP and GCI flights will spawn and this will tend to make just the border area active rather than a melee over the whole map. + -- It all depends on what the desired effect is. + -- + -- EWR networks are **dynamically constructed**, that is, they form part of the @{Functional.Detection#DETECTION_BASE} object that is given as the input parameter of the AI\_A2A\_DISPATCHER class. + -- By defining in a **smart way the names or name prefixes of the groups** with EWR capable units, these groups will be **automatically added or deleted** from the EWR network, + -- increasing or decreasing the radar coverage of the Early Warning System. + -- + -- See the following example to setup an EWR network containing EWR stations and AWACS. + -- + -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_2.JPG) + -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_3.JPG) + -- + -- -- Define a SET_GROUP object that builds a collection of groups that define the EWR network. + -- -- Here we build the network with all the groups that have a name starting with DF CCCP AWACS and DF CCCP EWR. + -- DetectionSetGroup = SET_GROUP:New() + -- DetectionSetGroup:FilterPrefixes( { "DF CCCP AWACS", "DF CCCP EWR" } ) + -- DetectionSetGroup:FilterStart() + -- + -- -- Setup the detection and group targets to a 30km range! + -- Detection = DETECTION_AREAS:New( DetectionSetGroup, 30000 ) + -- + -- -- Setup the A2A dispatcher, and initialize it. + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- + -- The above example creates a SET_GROUP instance, and stores this in the variable (object) **DetectionSetGroup**. + -- **DetectionSetGroup** is then being configured to filter all active groups with a group name starting with **DF CCCP AWACS** or **DF CCCP EWR** to be included in the Set. + -- **DetectionSetGroup** is then being ordered to start the dynamic filtering. Note that any destroy or new spawn of a group with the above names will be removed or added to the Set. + -- + -- Then a new Detection object is created from the class DETECTION_AREAS. A grouping radius of 30000 is choosen, which is 30km. + -- The **Detection** object is then passed to the @{#AI_A2A_DISPATCHER.New}() method to indicate the EWR network configuration and setup the A2A defense detection mechanism. + -- + -- You could build a **mutual defense system** like this: + -- + -- A2ADispatcher_Red = AI_A2A_DISPATCHER:New( EWR_Red ) + -- A2ADispatcher_Blue = AI_A2A_DISPATCHER:New( EWR_Blue ) + -- + -- ### 1.2. Define the detected **target grouping radius**: + -- + -- The target grouping radius is a property of the Detection object, that was passed to the AI\_A2A\_DISPATCHER object, but can be changed. + -- The grouping radius should not be too small, but also depends on the types of planes and the era of the simulation. + -- Fast planes like in the 80s, need a larger radius than WWII planes. + -- Typically I suggest to use 30000 for new generation planes and 10000 for older era aircraft. + -- + -- Note that detected targets are constantly re-grouped, that is, when certain detected aircraft are moving further than the group radius, then these aircraft will become a separate + -- group being detected. This may result in additional GCI being started by the dispatcher! So don't make this value too small! + -- + -- ## 3. Set the **Engage Radius**: + -- + -- Define the **Engage Radius** to **engage any target by airborne friendlies**, + -- which are executing **cap** or **returning** from an intercept mission. + -- + -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia10.JPG) + -- + -- If there is a target area detected and reported, + -- then any friendlies that are airborne near this target area, + -- will be commanded to (re-)engage that target when available (if no other tasks were commanded). + -- + -- For example, if **50000** or **50km** is given as a value, then any friendly that is airborne within **50km** from the detected target, + -- will be considered to receive the command to engage that target area. + -- + -- You need to evaluate the value of this parameter carefully: + -- + -- * If too small, more intercept missions may be triggered upon detected target areas. + -- * If too large, any airborne cap may not be able to reach the detected target area in time, because it is too far. + -- + -- The **default** Engage Radius is defined as **100000** or **100km**. + -- Use the method @{#AI_A2A_DISPATCHER.SetEngageRadius}() to set a specific Engage Radius. + -- **The Engage Radius is defined for ALL squadrons which are operational.** + -- + -- Demonstration Mission: [AID-019 - AI_A2A - Engage Range Test](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-019%20-%20AI_A2A%20-%20Engage%20Range%20Test) + -- + -- In this example an Engage Radius is set to various values. + -- + -- -- Set 50km as the radius to engage any target by airborne friendlies. + -- A2ADispatcher:SetEngageRadius( 50000 ) + -- + -- -- Set 100km as the radius to engage any target by airborne friendlies. + -- A2ADispatcher:SetEngageRadius() -- 100000 is the default value. + -- + -- + -- ## 4. Set the **Ground Controlled Intercept Radius** or **Gci radius**: + -- + -- When targets are detected that are still really far off, you don't want the AI_A2A_DISPATCHER to launch intercepts just yet. + -- You want it to wait until a certain Gci range is reached, which is the **distance of the closest airbase to target** + -- being **smaller** than the **Ground Controlled Intercept radius** or **Gci radius**. + -- + -- The **default** Gci radius is defined as **200000** or **200km**. Override the default Gci radius when the era of the warfare is early, or, + -- when you don't want to let the AI_A2A_DISPATCHER react immediately when a certain border or area is not being crossed. + -- + -- Use the method @{#AI_A2A_DISPATCHER.SetGciRadius}() to set a specific controlled ground intercept radius. + -- **The Ground Controlled Intercept radius is defined for ALL squadrons which are operational.** + -- + -- Demonstration Mission: [AID-013 - AI_A2A - Intercept Test](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-013%20-%20AI_A2A%20-%20Intercept%20Test) + -- + -- In these examples, the Gci Radius is set to various values: + -- + -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- + -- -- Set 100km as the radius to ground control intercept detected targets from the nearest airbase. + -- A2ADispatcher:SetGciRadius( 100000 ) + -- + -- -- Set 200km as the radius to ground control intercept. + -- A2ADispatcher:SetGciRadius() -- 200000 is the default value. + -- + -- ## 5. Set the **borders**: + -- + -- According to the tactical and strategic design of the mission broadly decide the shape and extent of red and blue territories. + -- They should be laid out such that a border area is created between the two coalitions. + -- + -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia4.JPG) + -- + -- **Define a border area to simulate a cold war scenario.** + -- Use the method @{#AI_A2A_DISPATCHER.SetBorderZone}() to create a border zone for the dispatcher. + -- + -- A **cold war** is one where CAP aircraft patrol their territory but will not attack enemy aircraft or launch GCI aircraft unless enemy aircraft enter their territory. In other words the EWR may detect an enemy aircraft but will only send aircraft to attack it if it crosses the border. + -- A **hot war** is one where CAP aircraft will intercept any detected enemy aircraft and GCI aircraft will launch against detected enemy aircraft without regard for territory. In other words if the ground radar can detect the enemy aircraft then it will send CAP and GCI aircraft to attack it. + -- + -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia9.JPG) + -- + -- If it's a cold war then the **borders of red and blue territory** need to be defined using a @{zone} object derived from @{Core.Zone#ZONE_BASE}. + -- If a hot war is chosen then **no borders** actually need to be defined using the helicopter units other than + -- it makes it easier sometimes for the mission maker to envisage where the red and blue territories roughly are. + -- In a hot war the borders are effectively defined by the ground based radar coverage of a coalition. + -- + -- Demonstration Mission: [AID-009 - AI_A2A - Border Test](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-009 - AI_A2A - Border Test) + -- + -- In this example a border is set for the CCCP A2A dispatcher: + -- + -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_4.JPG) + -- + -- -- Setup the A2A dispatcher, and initialize it. + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- + -- -- Setup the border. + -- -- Initialize the dispatcher, setting up a border zone. This is a polygon, + -- -- which takes the waypoints of a late activated group with the name CCCP Border as the boundaries of the border area. + -- -- Any enemy crossing this border will be engaged. + -- + -- CCCPBorderZone = ZONE_POLYGON:New( "CCCP Border", GROUP:FindByName( "CCCP Border" ) ) + -- A2ADispatcher:SetBorderZone( CCCPBorderZone ) + -- + -- ## 6. Squadrons: + -- + -- The AI\_A2A\_DISPATCHER works with **Squadrons**, that need to be defined using the different methods available. + -- + -- Use the method @{#AI_A2A_DISPATCHER.SetSquadron}() to **setup a new squadron** active at an airfield, + -- while defining which plane types are being used by the squadron and how many resources are available. + -- + -- Squadrons: + -- + -- * Have name (string) that is the identifier or key of the squadron. + -- * Have specific plane types. + -- * Are located at one airbase. + -- * Optionally have a limited set of resources. The default is that squadrons have **unlimited resources**. + -- + -- The name of the squadron given acts as the **squadron key** in the AI\_A2A\_DISPATCHER:Squadron...() methods. + -- + -- Additionally, squadrons have specific configuration options to: + -- + -- * Control how new aircraft are taking off from the airfield (in the air, cold, hot, at the runway). + -- * Control how returning aircraft are landing at the airfield (in the air near the airbase, after landing, after engine shutdown). + -- * Control the **grouping** of new aircraft spawned at the airfield. If there is more than one aircraft to be spawned, these may be grouped. + -- * Control the **overhead** or defensive strength of the squadron. Depending on the types of planes and amount of resources, the mission designer can choose to increase or reduce the amount of planes spawned. + -- + -- For performance and bug workaround reasons within DCS, squadrons have different methods to spawn new aircraft or land returning or damaged aircraft. + -- + -- This example defines a couple of squadrons. Note the templates defined within the Mission Editor. + -- + -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_5.JPG) + -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_6.JPG) + -- + -- -- Setup the squadrons. + -- A2ADispatcher:SetSquadron( "Mineralnye", AIRBASE.Caucasus.Mineralnye_Vody, { "SQ CCCP SU-27" }, 20 ) + -- A2ADispatcher:SetSquadron( "Maykop", AIRBASE.Caucasus.Maykop_Khanskaya, { "SQ CCCP MIG-31" }, 20 ) + -- A2ADispatcher:SetSquadron( "Mozdok", AIRBASE.Caucasus.Mozdok, { "SQ CCCP MIG-31" }, 20 ) + -- A2ADispatcher:SetSquadron( "Sochi", AIRBASE.Caucasus.Sochi_Adler, { "SQ CCCP SU-27" }, 20 ) + -- A2ADispatcher:SetSquadron( "Novo", AIRBASE.Caucasus.Novorossiysk, { "SQ CCCP SU-27" }, 20 ) + -- + -- ### 6.1. Set squadron take-off methods + -- + -- Use the various SetSquadronTakeoff... methods to control how squadrons are taking-off from the airfield: + -- + -- * @{#AI_A2A_DISPATCHER.SetSquadronTakeoff}() is the generic configuration method to control takeoff from the air, hot, cold or from the runway. See the method for further details. + -- * @{#AI_A2A_DISPATCHER.SetSquadronTakeoffInAir}() will spawn new aircraft from the squadron directly in the air. + -- * @{#AI_A2A_DISPATCHER.SetSquadronTakeoffFromParkingCold}() will spawn new aircraft in without running engines at a parking spot at the airfield. + -- * @{#AI_A2A_DISPATCHER.SetSquadronTakeoffFromParkingHot}() will spawn new aircraft in with running engines at a parking spot at the airfield. + -- * @{#AI_A2A_DISPATCHER.SetSquadronTakeoffFromRunway}() will spawn new aircraft at the runway at the airfield. + -- + -- **The default landing method is to spawn new aircraft directly in the air.** + -- + -- Use these methods to fine-tune for specific airfields that are known to create bottlenecks, or have reduced airbase efficiency. + -- The more and the longer aircraft need to taxi at an airfield, the more risk there is that: + -- + -- * aircraft will stop waiting for each other or for a landing aircraft before takeoff. + -- * aircraft may get into a "dead-lock" situation, where two aircraft are blocking each other. + -- * aircraft may collide at the airbase. + -- * aircraft may be awaiting the landing of a plane currently in the air, but never lands ... + -- + -- Currently within the DCS engine, the airfield traffic coordination is erroneous and contains a lot of bugs. + -- If you experience while testing problems with aircraft take-off or landing, please use one of the above methods as a solution to workaround these issues! + -- + -- This example sets the default takeoff method to be from the runway. + -- And for a couple of squadrons overrides this default method. + -- + -- -- Setup the Takeoff methods + -- + -- -- The default takeoff + -- A2ADispatcher:SetDefaultTakeOffFromRunway() + -- + -- -- The individual takeoff per squadron + -- A2ADispatcher:SetSquadronTakeoff( "Mineralnye", AI_A2A_DISPATCHER.Takeoff.Air ) + -- A2ADispatcher:SetSquadronTakeoffInAir( "Sochi" ) + -- A2ADispatcher:SetSquadronTakeoffFromRunway( "Mozdok" ) + -- A2ADispatcher:SetSquadronTakeoffFromParkingCold( "Maykop" ) + -- A2ADispatcher:SetSquadronTakeoffFromParkingHot( "Novo" ) + -- + -- + -- ### 6.1. Set Squadron takeoff altitude when spawning new aircraft in the air. + -- + -- In the case of the @{#AI_A2A_DISPATCHER.SetSquadronTakeoffInAir}() there is also an other parameter that can be applied. + -- That is modifying or setting the **altitude** from where planes spawn in the air. + -- Use the method @{#AI_A2A_DISPATCHER.SetSquadronTakeoffInAirAltitude}() to set the altitude for a specific squadron. + -- The default takeoff altitude can be modified or set using the method @{#AI_A2A_DISPATCHER.SetSquadronTakeoffInAirAltitude}(). + -- As part of the method @{#AI_A2A_DISPATCHER.SetSquadronTakeoffInAir}() a parameter can be specified to set the takeoff altitude. + -- If this parameter is not specified, then the default altitude will be used for the squadron. + -- + -- ### 6.2. Set squadron landing methods + -- + -- In analogy with takeoff, the landing methods are to control how squadrons land at the airfield: + -- + -- * @{#AI_A2A_DISPATCHER.SetSquadronLanding}() is the generic configuration method to control landing, namely despawn the aircraft near the airfield in the air, right after landing, or at engine shutdown. + -- * @{#AI_A2A_DISPATCHER.SetSquadronLandingNearAirbase}() will despawn the returning aircraft in the air when near the airfield. + -- * @{#AI_A2A_DISPATCHER.SetSquadronLandingAtRunway}() will despawn the returning aircraft directly after landing at the runway. + -- * @{#AI_A2A_DISPATCHER.SetSquadronLandingAtEngineShutdown}() will despawn the returning aircraft when the aircraft has returned to its parking spot and has turned off its engines. + -- + -- You can use these methods to minimize the airbase coodination overhead and to increase the airbase efficiency. + -- When there are lots of aircraft returning for landing, at the same airbase, the takeoff process will be halted, which can cause a complete failure of the + -- A2A defense system, as no new CAP or GCI planes can takeoff. + -- Note that the method @{#AI_A2A_DISPATCHER.SetSquadronLandingNearAirbase}() will only work for returning aircraft, not for damaged or out of fuel aircraft. + -- Damaged or out-of-fuel aircraft are returning to the nearest friendly airbase and will land, and are out of control from ground control. + -- + -- This example defines the default landing method to be at the runway. + -- And for a couple of squadrons overrides this default method. + -- + -- -- Setup the Landing methods + -- + -- -- The default landing method + -- A2ADispatcher:SetDefaultLandingAtRunway() + -- + -- -- The individual landing per squadron + -- A2ADispatcher:SetSquadronLandingAtRunway( "Mineralnye" ) + -- A2ADispatcher:SetSquadronLandingNearAirbase( "Sochi" ) + -- A2ADispatcher:SetSquadronLandingAtEngineShutdown( "Mozdok" ) + -- A2ADispatcher:SetSquadronLandingNearAirbase( "Maykop" ) + -- A2ADispatcher:SetSquadronLanding( "Novo", AI_A2A_DISPATCHER.Landing.AtRunway ) + -- + -- + -- ### 6.3. Set squadron grouping + -- + -- Use the method @{#AI_A2A_DISPATCHER.SetSquadronGrouping}() to set the grouping of CAP or GCI flights that will take-off when spawned. + -- + -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia12.JPG) + -- + -- In the case of GCI, the @{#AI_A2A_DISPATCHER.SetSquadronGrouping}() method has additional behaviour. When there aren't enough CAP flights airborne, a GCI will be initiated for the remaining + -- targets to be engaged. Depending on the grouping parameter, the spawned flights for GCI are grouped into this setting. + -- For example with a group setting of 2, if 3 targets are detected and cannot be engaged by CAP or any airborne flight, + -- a GCI needs to be started, the GCI flights will be grouped as follows: Group 1 of 2 flights and Group 2 of one flight! + -- + -- Even more ... If one target has been detected, and the overhead is 1.5, grouping is 1, then two groups of planes will be spawned, with one unit each! + -- + -- The **grouping value is set for a Squadron**, and can be **dynamically adjusted** during mission execution, so to adjust the defense flights grouping when the tactical situation changes. + -- + -- ### 6.4. Overhead and Balance the effectiveness of the air defenses in case of GCI. + -- + -- The effectiveness can be set with the **overhead parameter**. This is a number that is used to calculate the amount of Units that dispatching command will allocate to GCI in surplus of detected amount of units. + -- The **default value** of the overhead parameter is 1.0, which means **equal balance**. + -- + -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia11.JPG) + -- + -- However, depending on the (type of) aircraft (strength and payload) in the squadron and the amount of resources available, this parameter can be changed. + -- + -- The @{#AI_A2A_DISPATCHER.SetSquadronOverhead}() method can be used to tweak the defense strength, + -- taking into account the plane types of the squadron. + -- + -- For example, a MIG-31 with full long-distance A2A missiles payload, may still be less effective than a F-15C with short missiles... + -- So in this case, one may want to use the @{#AI_A2A_DISPATCHER.SetOverhead}() method to allocate more defending planes as the amount of detected attacking planes. + -- The overhead must be given as a decimal value with 1 as the neutral value, which means that overhead values: + -- + -- * Higher than 1.0, for example 1.5, will increase the defense unit amounts. For 4 planes detected, 6 planes will be spawned. + -- * Lower than 1, for example 0.75, will decrease the defense unit amounts. For 4 planes detected, only 3 planes will be spawned. + -- + -- The amount of defending units is calculated by multiplying the amount of detected attacking planes as part of the detected group + -- multiplied by the Overhead and rounded up to the smallest integer. + -- + -- For example ... If one target has been detected, and the overhead is 1.5, grouping is 1, then two groups of planes will be spawned, with one unit each! + -- + -- The **overhead value is set for a Squadron**, and can be **dynamically adjusted** during mission execution, so to adjust the defense overhead when the tactical situation changes. + -- + -- ## 6.5. Squadron fuel treshold. + -- + -- When an airplane gets **out of fuel** to a certain %-tage, which is by default **15% (0.15)**, there are two possible actions that can be taken: + -- - The defender will go RTB, and will be replaced with a new defender if possible. + -- - The defender will refuel at a tanker, if a tanker has been specified for the squadron. + -- + -- Use the method @{#AI_A2A_DISPATCHER.SetSquadronFuelThreshold}() to set the **squadron fuel treshold** of spawned airplanes for all squadrons. + -- + -- ## 7. Setup a squadron for CAP + -- + -- ### 7.1. Set the CAP zones + -- + -- CAP zones are patrol areas where Combat Air Patrol (CAP) flights loiter until they either return to base due to low fuel or are assigned an interception task by ground control. + -- + -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia6.JPG) + -- + -- * As the CAP flights wander around within the zone waiting to be tasked, these zones need to be large enough that the aircraft are not constantly turning + -- but do not have to be big and numerous enough to completely cover a border. + -- + -- * CAP zones can be of any type, and are derived from the @{Core.Zone#ZONE_BASE} class. Zones can be @{Core.Zone#ZONE}, @{Core.Zone#ZONE_POLYGON}, @{Core.Zone#ZONE_UNIT}, @{Core.Zone#ZONE_GROUP}, etc. + -- This allows to setup **static, moving and/or complex zones** wherein aircraft will perform the CAP. + -- + -- * Typically 20000-50000 metres width is used and they are spaced so that aircraft in the zone waiting for tasks don't have to far to travel to protect their coalitions important targets. + -- These targets are chosen as part of the mission design and might be an important airfield or town etc. + -- Zone size is also determined somewhat by territory size, plane types + -- (eg WW2 aircraft might mean smaller zones or more zones because they are slower and take longer to intercept enemy aircraft). + -- + -- * In a **cold war** it is important to make sure a CAP zone doesn't intrude into enemy territory as otherwise CAP flights will likely cross borders + -- and spark a full scale conflict which will escalate rapidly. + -- + -- * CAP flights do not need to be in the CAP zone before they are "on station" and ready for tasking. + -- + -- * Typically if a CAP flight is tasked and therefore leaves their zone empty while they go off and intercept their target another CAP flight will spawn to take their place. + -- + -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia7.JPG) + -- + -- The following example illustrates how CAP zones are coded: + -- + -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_8.JPG) + -- + -- -- CAP Squadron execution. + -- CAPZoneEast = ZONE_POLYGON:New( "CAP Zone East", GROUP:FindByName( "CAP Zone East" ) ) + -- A2ADispatcher:SetSquadronCap( "Mineralnye", CAPZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- A2ADispatcher:SetSquadronCapInterval( "Mineralnye", 2, 30, 60, 1 ) + -- + -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_7.JPG) + -- + -- CAPZoneWest = ZONE_POLYGON:New( "CAP Zone West", GROUP:FindByName( "CAP Zone West" ) ) + -- A2ADispatcher:SetSquadronCap( "Sochi", CAPZoneWest, 4000, 8000, 600, 800, 800, 1200, "BARO" ) + -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) + -- + -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_9.JPG) + -- + -- CAPZoneMiddle = ZONE:New( "CAP Zone Middle") + -- A2ADispatcher:SetSquadronCap( "Maykop", CAPZoneMiddle, 4000, 8000, 600, 800, 800, 1200, "RADIO" ) + -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) + -- + -- Note the different @{Zone} MOOSE classes being used to create zones of different types. Please click the @{Zone} link for more information about the different zone types. + -- Zones can be circles, can be setup in the mission editor using trigger zones, but can also be setup in the mission editor as polygons and in this case GROUP objects are being used! + -- + -- ## 7.2. Set the squadron to execute CAP: + -- + -- The method @{#AI_A2A_DISPATCHER.SetSquadronCap}() defines a CAP execution for a squadron. + -- + -- Setting-up a CAP zone also requires specific parameters: + -- + -- * The minimum and maximum altitude + -- * The minimum speed and maximum patrol speed + -- * The minimum and maximum engage speed + -- * The type of altitude measurement + -- + -- These define how the squadron will perform the CAP while partrolling. Different terrain types requires different types of CAP. + -- + -- The @{#AI_A2A_DISPATCHER.SetSquadronCapInterval}() method specifies **how much** and **when** CAP flights will takeoff. + -- + -- It is recommended not to overload the air defense with CAP flights, as these will decrease the performance of the overall system. + -- + -- For example, the following setup will create a CAP for squadron "Sochi": + -- + -- A2ADispatcher:SetSquadronCap( "Sochi", CAPZoneWest, 4000, 8000, 600, 800, 800, 1200, "BARO" ) + -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) + -- + -- ## 7.3. Squadron tanker to refuel when executing CAP and defender is out of fuel. + -- + -- Instead of sending CAP to RTB when out of fuel, you can let CAP refuel in mid air using a tanker. + -- This greatly increases the efficiency of your CAP operations. + -- + -- In the mission editor, setup a group with task Refuelling. A tanker unit of the correct coalition will be automatically selected. + -- Then, use the method @{#AI_A2A_DISPATCHER.SetDefaultTanker}() to set the default tanker for the refuelling. + -- You can also specify a specific tanker for refuelling for a squadron by using the method @{#AI_A2A_DISPATCHER.SetSquadronTanker}(). + -- + -- When the tanker specified is alive and in the air, the tanker will be used for refuelling. + -- + -- For example, the following setup will create a CAP for squadron "Gelend" with a refuel task for the squadron: + -- + -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_10.JPG) + -- + -- -- Define the CAP + -- A2ADispatcher:SetSquadron( "Gelend", AIRBASE.Caucasus.Gelendzhik, { "SQ CCCP SU-30" }, 20 ) + -- A2ADispatcher:SetSquadronCap( "Gelend", ZONE:New( "PatrolZoneGelend" ), 4000, 8000, 600, 800, 1000, 1300 ) + -- A2ADispatcher:SetSquadronCapInterval( "Gelend", 2, 30, 600, 1 ) + -- A2ADispatcher:SetSquadronGci( "Gelend", 900, 1200 ) + -- + -- -- Setup the Refuelling for squadron "Gelend", at tanker (group) "TankerGelend" when the fuel in the tank of the CAP defenders is less than 80%. + -- A2ADispatcher:SetSquadronFuelThreshold( "Gelend", 0.8 ) + -- A2ADispatcher:SetSquadronTanker( "Gelend", "TankerGelend" ) + -- + -- ## 7.4 Set up race track pattern + -- + -- By default, flights patrol randomly within the CAP zone. It is also possible to let them fly a race track pattern using the + -- @{#AI_A2A_DISPATCHER.SetDefaultCapRacetrack}(*LeglengthMin*, *LeglengthMax*, *HeadingMin*, *HeadingMax*, *DurationMin*, *DurationMax*) or + -- @{#AI_A2A_DISPATCHER.SetSquadronCapRacetrack}(*SquadronName*, *LeglengthMin*, *LeglengthMax*, *HeadingMin*, *HeadingMax*, *DurationMin*, *DurationMax*) functions. + -- The first function enables this for all squadrons, the latter only for specific squadrons. For example, + -- + -- -- Enable race track pattern for CAP squadron "Mineralnye". + -- A2ADispatcher:SetSquadronCapRacetrack("Mineralnye", 10000, 20000, 90, 180, 10*60, 20*60) + -- + -- In this case the squadron "Mineralnye" will a race track pattern at a random point in the CAP zone. The leg length will be randomly selected between 10,000 and 20,000 meters. The heading + -- of the race track will randomly selected between 90 (West to East) and 180 (North to South) degrees. + -- After a random duration between 10 and 20 minutes, the flight will get a new random orbit location. + -- + -- Note that all parameters except the squadron name are optional. If not specified, default values are taken. Speed and altitude are taken from the CAP command used earlier on, e.g. + -- + -- A2ADispatcher:SetSquadronCap( "Mineralnye", CAPZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- + -- Also note that the center of the race track pattern is chosen randomly within the patrol zone and can be close the the boarder of the zone. Hence, it cannot be guaranteed that the + -- whole pattern lies within the patrol zone. + -- + -- ## 8. Setup a squadron for GCI: + -- + -- The method @{#AI_A2A_DISPATCHER.SetSquadronGci}() defines a GCI execution for a squadron. + -- + -- Setting-up a GCI readiness also requires specific parameters: + -- + -- * The minimum speed and maximum patrol speed + -- + -- Essentially this controls how many flights of GCI aircraft can be active at any time. + -- Note allowing large numbers of active GCI flights can adversely impact mission performance on low or medium specification hosts/servers. + -- GCI needs to be setup at strategic airbases. Too far will mean that the aircraft need to fly a long way to reach the intruders, + -- too short will mean that the intruders may have alraedy passed the ideal interception point! + -- + -- For example, the following setup will create a GCI for squadron "Sochi": + -- + -- A2ADispatcher:SetSquadronGci( "Mozdok", 900, 1200 ) + -- + -- ## 9. Other configuration options + -- + -- ### 9.1. Set a tactical display panel: + -- + -- Every 30 seconds, a tactical display panel can be shown that illustrates what the status is of the different groups controlled by AI\_A2A\_DISPATCHER. + -- Use the method @{#AI_A2A_DISPATCHER.SetTacticalDisplay}() to switch on the tactical display panel. The default will not show this panel. + -- Note that there may be some performance impact if this panel is shown. + -- + -- ## 10. Defaults settings. + -- + -- This provides a good overview of the different parameters that are setup or hardcoded by default. + -- For some default settings, a method is available that allows you to tweak the defaults. + -- + -- ## 10.1. Default takeoff method. + -- + -- The default **takeoff method** is set to **in the air**, which means that new spawned airplanes will be spawned directly in the air above the airbase by default. + -- + -- **The default takeoff method can be set for ALL squadrons that don't have an individual takeoff method configured.** + -- + -- * @{#AI_A2A_DISPATCHER.SetDefaultTakeoff}() is the generic configuration method to control takeoff by default from the air, hot, cold or from the runway. See the method for further details. + -- * @{#AI_A2A_DISPATCHER.SetDefaultTakeoffInAir}() will spawn by default new aircraft from the squadron directly in the air. + -- * @{#AI_A2A_DISPATCHER.SetDefaultTakeoffFromParkingCold}() will spawn by default new aircraft in without running engines at a parking spot at the airfield. + -- * @{#AI_A2A_DISPATCHER.SetDefaultTakeoffFromParkingHot}() will spawn by default new aircraft in with running engines at a parking spot at the airfield. + -- * @{#AI_A2A_DISPATCHER.SetDefaultTakeoffFromRunway}() will spawn by default new aircraft at the runway at the airfield. + -- + -- ## 10.2. Default landing method. + -- + -- The default **landing method** is set to **near the airbase**, which means that returning airplanes will be despawned directly in the air by default. + -- + -- The default landing method can be set for ALL squadrons that don't have an individual landing method configured. + -- + -- * @{#AI_A2A_DISPATCHER.SetDefaultLanding}() is the generic configuration method to control by default landing, namely despawn the aircraft near the airfield in the air, right after landing, or at engine shutdown. + -- * @{#AI_A2A_DISPATCHER.SetDefaultLandingNearAirbase}() will despawn by default the returning aircraft in the air when near the airfield. + -- * @{#AI_A2A_DISPATCHER.SetDefaultLandingAtRunway}() will despawn by default the returning aircraft directly after landing at the runway. + -- * @{#AI_A2A_DISPATCHER.SetDefaultLandingAtEngineShutdown}() will despawn by default the returning aircraft when the aircraft has returned to its parking spot and has turned off its engines. + -- + -- ## 10.3. Default overhead. + -- + -- The default **overhead** is set to **1**. That essentially means that there isn't any overhead set by default. + -- + -- The default overhead value can be set for ALL squadrons that don't have an individual overhead value configured. + -- + -- Use the @{#AI_A2A_DISPATCHER.SetDefaultOverhead}() method can be used to set the default overhead or defense strength for ALL squadrons. + -- + -- ## 10.4. Default grouping. + -- + -- The default **grouping** is set to **one airplane**. That essentially means that there won't be any grouping applied by default. + -- + -- The default grouping value can be set for ALL squadrons that don't have an individual grouping value configured. + -- + -- Use the method @{#AI_A2A_DISPATCHER.SetDefaultGrouping}() to set the **default grouping** of spawned airplanes for all squadrons. + -- + -- ## 10.5. Default RTB fuel treshold. + -- + -- When an airplane gets **out of fuel** to a certain %-tage, which is **15% (0.15)**, it will go RTB, and will be replaced with a new airplane when applicable. + -- + -- Use the method @{#AI_A2A_DISPATCHER.SetDefaultFuelThreshold}() to set the **default fuel treshold** of spawned airplanes for all squadrons. + -- + -- ## 10.6. Default RTB damage treshold. + -- + -- When an airplane is **damaged** to a certain %-tage, which is **40% (0.40)**, it will go RTB, and will be replaced with a new airplane when applicable. + -- + -- Use the method @{#AI_A2A_DISPATCHER.SetDefaultDamageThreshold}() to set the **default damage treshold** of spawned airplanes for all squadrons. + -- + -- ## 10.7. Default settings for CAP. + -- + -- ### 10.7.1. Default CAP Time Interval. + -- + -- CAP is time driven, and will evaluate in random time intervals if a new CAP needs to be spawned. + -- The **default CAP time interval** is between **180** and **600** seconds. + -- + -- Use the method @{#AI_A2A_DISPATCHER.SetDefaultCapTimeInterval}() to set the **default CAP time interval** of spawned airplanes for all squadrons. + -- Note that you can still change the CAP limit and CAP time intervals for each CAP individually using the @{#AI_A2A_DISPATCHER.SetSquadronCapTimeInterval}() method. + -- + -- ### 10.7.2. Default CAP limit. + -- + -- Multiple CAP can be airborne at the same time for one squadron, which is controlled by the **CAP limit**. + -- The **default CAP limit** is 1 CAP per squadron to be airborne at the same time. + -- Note that the default CAP limit is used when a Squadron CAP is defined, and cannot be changed afterwards. + -- So, ensure that you set the default CAP limit **before** you spawn the Squadron CAP. + -- + -- Use the method @{#AI_A2A_DISPATCHER.SetDefaultCapTimeInterval}() to set the **default CAP time interval** of spawned airplanes for all squadrons. + -- Note that you can still change the CAP limit and CAP time intervals for each CAP individually using the @{#AI_A2A_DISPATCHER.SetSquadronCapTimeInterval}() method. + -- + -- ## 10.7.3. Default tanker for refuelling when executing CAP. + -- + -- Instead of sending CAP to RTB when out of fuel, you can let CAP refuel in mid air using a tanker. + -- This greatly increases the efficiency of your CAP operations. + -- + -- In the mission editor, setup a group with task Refuelling. A tanker unit of the correct coalition will be automatically selected. + -- Then, use the method @{#AI_A2A_DISPATCHER.SetDefaultTanker}() to set the tanker for the dispatcher. + -- Use the method @{#AI_A2A_DISPATCHER.SetDefaultFuelThreshold}() to set the %-tage left in the defender airplane tanks when a refuel action is needed. + -- + -- When the tanker specified is alive and in the air, the tanker will be used for refuelling. + -- + -- For example, the following setup will set the default refuel tanker to "Tanker": + -- + -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_11.JPG) + -- + -- -- Define the CAP + -- A2ADispatcher:SetSquadron( "Sochi", AIRBASE.Caucasus.Sochi_Adler, { "SQ CCCP SU-34" }, 20 ) + -- A2ADispatcher:SetSquadronCap( "Sochi", ZONE:New( "PatrolZone" ), 4000, 8000, 600, 800, 1000, 1300 ) + -- A2ADispatcher:SetSquadronCapInterval("Sochi", 2, 30, 600, 1 ) + -- A2ADispatcher:SetSquadronGci( "Sochi", 900, 1200 ) + -- + -- -- Set the default tanker for refuelling to "Tanker", when the default fuel treshold has reached 90% fuel left. + -- A2ADispatcher:SetDefaultFuelThreshold( 0.9 ) + -- A2ADispatcher:SetDefaultTanker( "Tanker" ) + -- + -- ## 10.8. Default settings for GCI. + -- + -- ## 10.8.1. Optimal intercept point calculation. + -- + -- When intruders are detected, the intrusion path of the attackers can be monitored by the EWR. + -- Although defender planes might be on standby at the airbase, it can still take some time to get the defenses up in the air if there aren't any defenses airborne. + -- This time can easily take 2 to 3 minutes, and even then the defenders still need to fly towards the target, which takes also time. + -- + -- Therefore, an optimal **intercept point** is calculated which takes a couple of parameters: + -- + -- * The average bearing of the intruders for an amount of seconds. + -- * The average speed of the intruders for an amount of seconds. + -- * An assumed time it takes to get planes operational at the airbase. + -- + -- The **intercept point** will determine: + -- + -- * If there are any friendlies close to engage the target. These can be defenders performing CAP or defenders in RTB. + -- * The optimal airbase from where defenders will takeoff for GCI. + -- + -- Use the method @{#AI_A2A_DISPATCHER.SetIntercept}() to modify the assumed intercept delay time to calculate a valid interception. + -- + -- ## 10.8.2. Default Disengage Radius. + -- + -- The radius to **disengage any target** when the **distance** of the defender to the **home base** is larger than the specified meters. + -- The default Disengage Radius is **300km** (300000 meters). Note that the Disengage Radius is applicable to ALL squadrons! + -- + -- Use the method @{#AI_A2A_DISPATCHER.SetDisengageRadius}() to modify the default Disengage Radius to another distance setting. + -- + -- ## 11. Airbase capture: + -- + -- Different squadrons can be located at one airbase. + -- If the airbase gets captured, that is, when there is an enemy unit near the airbase, and there aren't anymore friendlies at the airbase, the airbase will change coalition ownership. + -- As a result, the GCI and CAP will stop! + -- However, the squadron will still stay alive. Any airplane that is airborne will continue its operations until all airborne airplanes + -- of the squadron will be destroyed. This to keep consistency of air operations not to confuse the players. + -- + -- ## 12. Q & A: + -- + -- ### 12.1. Which countries will be selected for each coalition? + -- + -- Which countries are assigned to a coalition influences which units are available to the coalition. + -- For example because the mission calls for a EWR radar on the blue side the Ukraine might be chosen as a blue country + -- so that the 55G6 EWR radar unit is available to blue. + -- Some countries assign different tasking to aircraft, for example Germany assigns the CAP task to F-4E Phantoms but the USA does not. + -- Therefore if F4s are wanted as a coalition's CAP or GCI aircraft Germany will need to be assigned to that coalition. + -- + -- ### 12.2. Country, type, load out, skill and skins for CAP and GCI aircraft? + -- + -- * Note these can be from any countries within the coalition but must be an aircraft with one of the main tasks being "CAP". + -- * Obviously skins which are selected must be available to all players that join the mission otherwise they will see a default skin. + -- * Load outs should be appropriate to a CAP mission eg perhaps drop tanks for CAP flights and extra missiles for GCI flights. + -- * These decisions will eventually lead to template aircraft units being placed as late activation units that the script will use as templates for spawning CAP and GCI flights. Up to 4 different aircraft configurations can be chosen for each coalition. The spawned aircraft will inherit the characteristics of the template aircraft. + -- * The selected aircraft type must be able to perform the CAP tasking for the chosen country. + -- + -- + -- @field #AI_A2A_DISPATCHER + AI_A2A_DISPATCHER = { + ClassName = "AI_A2A_DISPATCHER", + Detection = nil, + } + + + --- Squadron data structure. + -- @type AI_A2A_DISPATCHER.Squadron + -- @field #string Name Name of the squadron. + -- @field #number ResourceCount Number of resources. + -- @field #string AirbaseName Name of the home airbase. + -- @field Wrapper.Airbase#AIRBASE Airbase The home airbase of the squadron. + -- @field #boolean Captured If true, airbase of the squadron was captured. + -- @field #table Resources Flight group resources Resources[TemplateID][GroupName] = SpawnGroup. + -- @field #boolean Uncontrolled If true, flight groups are spawned uncontrolled and later activated. + -- @field #table Gci GCI. + -- @field #number Overhead Squadron overhead. + -- @field #number Grouping Squadron flight group size. + -- @field #number Takeoff Takeoff type. + -- @field #number TakeoffAltitude Altitude in meters for spawn in air. + -- @field #number Landing Landing type. + -- @field #number FuelThreshold Fuel threshold [0,1] for RTB. + -- @field #string TankerName Name of the refuelling tanker. + -- @field #table Table of template group names of the squadron. + -- @field #table Spawn Table of spaws Core.Spawn#SPAWN. + -- @field #table TemplatePrefixes + -- @field #boolean Racetrack If true, CAP flights will perform a racetrack pattern rather than randomly patrolling the zone. + -- @field #number RacetrackLengthMin Min Length of race track in meters. Default 10,000 m. + -- @field #number RacetrackLengthMax Max Length of race track in meters. Default 15,000 m. + -- @field #number RacetrackHeadingMin Min heading of race track in degrees. Default 0 deg, i.e. from South to North. + -- @field #number RacetrackHeadingMax Max heading of race track in degrees. Default 180 deg, i.e. from North to South. + -- @field #number RacetrackDurationMin Min duration in seconds before the CAP flight changes its orbit position. Default never. + -- @field #number RacetrackDurationMax Max duration in seconds before the CAP flight changes its orbit position. Default never. + + --- Enumerator for spawns at airbases + -- @type AI_A2A_DISPATCHER.Takeoff + -- @extends Wrapper.Group#GROUP.Takeoff + + --- @field #AI_A2A_DISPATCHER.Takeoff Takeoff + AI_A2A_DISPATCHER.Takeoff = GROUP.Takeoff + + --- Defnes Landing location. + -- @field Landing + AI_A2A_DISPATCHER.Landing = { + NearAirbase = 1, + AtRunway = 2, + AtEngineShutdown = 3, + } + + --- AI_A2A_DISPATCHER constructor. + -- This is defining the A2A DISPATCHER for one coaliton. + -- The Dispatcher works with a @{Functional.Detection#DETECTION_BASE} object that is taking of the detection of targets using the EWR units. + -- The Detection object is polymorphic, depending on the type of detection object choosen, the detection will work differently. + -- @param #AI_A2A_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE Detection The DETECTION object that will detects targets using the the Early Warning Radar network. + -- @return #AI_A2A_DISPATCHER self + -- @usage + -- + -- -- Setup the Detection, using DETECTION_AREAS. + -- -- First define the SET of GROUPs that are defining the EWR network. + -- -- Here with prefixes DF CCCP AWACS, DF CCCP EWR. + -- DetectionSetGroup = SET_GROUP:New() + -- DetectionSetGroup:FilterPrefixes( { "DF CCCP AWACS", "DF CCCP EWR" } ) + -- DetectionSetGroup:FilterStart() + -- + -- -- Define the DETECTION_AREAS, using the DetectionSetGroup, with a 30km grouping radius. + -- Detection = DETECTION_AREAS:New( DetectionSetGroup, 30000 ) + -- + -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) -- + -- + function AI_A2A_DISPATCHER:New( Detection ) + + -- Inherits from DETECTION_MANAGER + local self = BASE:Inherit( self, DETECTION_MANAGER:New( nil, Detection ) ) -- #AI_A2A_DISPATCHER + + self.Detection = Detection -- Functional.Detection#DETECTION_AREAS + + -- This table models the DefenderSquadron templates. + self.DefenderSquadrons = {} -- The Defender Squadrons. + self.DefenderSpawns = {} + self.DefenderTasks = {} -- The Defenders Tasks. + self.DefenderDefault = {} -- The Defender Default Settings over all Squadrons. + + self.SetSendPlayerMessages = false --#boolean Flash messages to player + + -- TODO: Check detection through radar. + self.Detection:FilterCategories( { Unit.Category.AIRPLANE, Unit.Category.HELICOPTER } ) + --self.Detection:InitDetectRadar( true ) + self.Detection:SetRefreshTimeInterval( 30 ) + + self:SetEngageRadius() + self:SetGciRadius() + self:SetIntercept( 300 ) -- A default intercept delay time of 300 seconds. + self:SetDisengageRadius( 300000 ) -- The default Disengage Radius is 300 km. + + self:SetDefaultTakeoff( AI_A2A_DISPATCHER.Takeoff.Air ) + self:SetDefaultTakeoffInAirAltitude( 500 ) -- Default takeoff is 500 meters above the ground. + self:SetDefaultLanding( AI_A2A_DISPATCHER.Landing.NearAirbase ) + self:SetDefaultOverhead( 1 ) + self:SetDefaultGrouping( 1 ) + self:SetDefaultFuelThreshold( 0.15, 0 ) -- 15% of fuel remaining in the tank will trigger the airplane to return to base or refuel. + self:SetDefaultDamageThreshold( 0.4 ) -- When 40% of damage, go RTB. + self:SetDefaultCapTimeInterval( 180, 600 ) -- Between 180 and 600 seconds. + self:SetDefaultCapLimit( 1 ) -- Maximum one CAP per squadron. + + self:AddTransition( "Started", "Assign", "Started" ) + + --- OnAfter Transition Handler for Event Assign. + -- @function [parent=#AI_A2A_DISPATCHER] OnAfterAssign + -- @param #AI_A2A_DISPATCHER self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @param Tasking.Task_A2A#AI_A2A Task + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param #string PlayerName + + self:AddTransition( "*", "CAP", "*" ) + + --- CAP Handler OnBefore for AI_A2A_DISPATCHER + -- @function [parent=#AI_A2A_DISPATCHER] OnBeforeCAP + -- @param #AI_A2A_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- CAP Handler OnAfter for AI_A2A_DISPATCHER + -- @function [parent=#AI_A2A_DISPATCHER] OnAfterCAP + -- @param #AI_A2A_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- CAP Trigger for AI_A2A_DISPATCHER + -- @function [parent=#AI_A2A_DISPATCHER] CAP + -- @param #AI_A2A_DISPATCHER self + + --- CAP Asynchronous Trigger for AI_A2A_DISPATCHER + -- @function [parent=#AI_A2A_DISPATCHER] __CAP + -- @param #AI_A2A_DISPATCHER self + -- @param #number Delay + + self:AddTransition( "*", "GCI", "*" ) + + --- GCI Handler OnBefore for AI_A2A_DISPATCHER + -- @function [parent=#AI_A2A_DISPATCHER] OnBeforeGCI + -- @param #AI_A2A_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- GCI Handler OnAfter for AI_A2A_DISPATCHER + -- @function [parent=#AI_A2A_DISPATCHER] OnAfterGCI + -- @param #AI_A2A_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item. + -- @param #number DefendersMissing Number of missing defenders. + -- @param #table DefenderFriendlies Friendly defenders. + + --- GCI Trigger for AI_A2A_DISPATCHER + -- @function [parent=#AI_A2A_DISPATCHER] GCI + -- @param #AI_A2A_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item. + -- @param #number DefendersMissing Number of missing defenders. + -- @param #table DefenderFriendlies Friendly defenders. + + --- GCI Asynchronous Trigger for AI_A2A_DISPATCHER + -- @function [parent=#AI_A2A_DISPATCHER] __GCI + -- @param #AI_A2A_DISPATCHER self + -- @param #number Delay + -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item. + -- @param #number DefendersMissing Number of missing defenders. + -- @param #table DefenderFriendlies Friendly defenders. + + self:AddTransition( "*", "ENGAGE", "*" ) + + --- ENGAGE Handler OnBefore for AI_A2A_DISPATCHER + -- @function [parent=#AI_A2A_DISPATCHER] OnBeforeENGAGE + -- @param #AI_A2A_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item. + -- @param #table Defenders Defenders table. + -- @return #boolean + + --- ENGAGE Handler OnAfter for AI_A2A_DISPATCHER + -- @function [parent=#AI_A2A_DISPATCHER] OnAfterENGAGE + -- @param #AI_A2A_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item. + -- @param #table Defenders Defenders table. + + --- ENGAGE Trigger for AI_A2A_DISPATCHER + -- @function [parent=#AI_A2A_DISPATCHER] ENGAGE + -- @param #AI_A2A_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item. + -- @param #table Defenders Defenders table. + + --- ENGAGE Asynchronous Trigger for AI_A2A_DISPATCHER + -- @function [parent=#AI_A2A_DISPATCHER] __ENGAGE + -- @param #AI_A2A_DISPATCHER self + -- @param #number Delay + -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item. + -- @param #table Defenders Defenders table. + + + -- Subscribe to the CRASH event so that when planes are shot + -- by a Unit from the dispatcher, they will be removed from the detection... + -- This will avoid the detection to still "know" the shot unit until the next detection. + -- Otherwise, a new intercept or engage may happen for an already shot plane! + + + self:HandleEvent( EVENTS.Crash, self.OnEventCrashOrDead ) + self:HandleEvent( EVENTS.Dead, self.OnEventCrashOrDead ) + --self:HandleEvent( EVENTS.RemoveUnit, self.OnEventCrashOrDead ) + + + self:HandleEvent( EVENTS.Land ) + self:HandleEvent( EVENTS.EngineShutdown ) + + -- Handle the situation where the airbases are captured. + self:HandleEvent( EVENTS.BaseCaptured ) + + self:SetTacticalDisplay( false ) + + self.DefenderCAPIndex = 0 + + self:__Start( 5 ) + + return self + end + + + --- On after "Start" event. + -- @param #AI_A2A_DISPATCHER self + function AI_A2A_DISPATCHER:onafterStart( From, Event, To ) + + self:GetParent( self, AI_A2A_DISPATCHER ).onafterStart( self, From, Event, To ) + + -- Spawn the resources. + for SquadronName,_DefenderSquadron in pairs( self.DefenderSquadrons ) do + local DefenderSquadron=_DefenderSquadron --#AI_A2A_DISPATCHER.Squadron + DefenderSquadron.Resources = {} + if DefenderSquadron.ResourceCount then + for Resource = 1, DefenderSquadron.ResourceCount do + self:ParkDefender( DefenderSquadron ) + end + end + end + end + + + --- Park defender. + -- @param #AI_A2A_DISPATCHER self + -- @param #AI_A2A_DISPATCHER.Squadron DefenderSquadron The squadron. + function AI_A2A_DISPATCHER:ParkDefender( DefenderSquadron ) + + local TemplateID = math.random( 1, #DefenderSquadron.Spawn ) + + local Spawn = DefenderSquadron.Spawn[ TemplateID ] -- Core.Spawn#SPAWN + + Spawn:InitGrouping( 1 ) + + local SpawnGroup + + if self:IsSquadronVisible( DefenderSquadron.Name ) then + + local Grouping=DefenderSquadron.Grouping or self.DefenderDefault.Grouping + + Grouping=1 + + Spawn:InitGrouping(Grouping) + + SpawnGroup = Spawn:SpawnAtAirbase( DefenderSquadron.Airbase, SPAWN.Takeoff.Cold ) + + local GroupName = SpawnGroup:GetName() + + DefenderSquadron.Resources = DefenderSquadron.Resources or {} + + DefenderSquadron.Resources[TemplateID] = DefenderSquadron.Resources[TemplateID] or {} + DefenderSquadron.Resources[TemplateID][GroupName] = {} + DefenderSquadron.Resources[TemplateID][GroupName] = SpawnGroup + + self.uncontrolled=self.uncontrolled or {} + self.uncontrolled[DefenderSquadron.Name]=self.uncontrolled[DefenderSquadron.Name] or {} + + table.insert(self.uncontrolled[DefenderSquadron.Name], {group=SpawnGroup, name=GroupName, grouping=Grouping}) + end + + end + + + --- Event base captured. + -- @param #AI_A2A_DISPATCHER self + -- @param Core.Event#EVENTDATA EventData + function AI_A2A_DISPATCHER:OnEventBaseCaptured( EventData ) + + local AirbaseName = EventData.PlaceName -- The name of the airbase that was captured. + + self:I( "Captured " .. AirbaseName ) + + -- Now search for all squadrons located at the airbase, and sanatize them. + for SquadronName, Squadron in pairs( self.DefenderSquadrons ) do + if Squadron.AirbaseName == AirbaseName then + Squadron.ResourceCount = -999 -- The base has been captured, and the resources are eliminated. No more spawning. + Squadron.Captured = true + self:I( "Squadron " .. SquadronName .. " captured." ) + end + end + end + + --- Event dead or crash. + -- @param #AI_A2A_DISPATCHER self + -- @param Core.Event#EVENTDATA EventData + function AI_A2A_DISPATCHER:OnEventCrashOrDead( EventData ) + self.Detection:ForgetDetectedUnit( EventData.IniUnitName ) + end + + --- Event land. + -- @param #AI_A2A_DISPATCHER self + -- @param Core.Event#EVENTDATA EventData + function AI_A2A_DISPATCHER:OnEventLand( EventData ) + self:F( "Landed" ) + local DefenderUnit = EventData.IniUnit + local Defender = EventData.IniGroup + local Squadron = self:GetSquadronFromDefender( Defender ) + if Squadron then + self:F( { SquadronName = Squadron.Name } ) + local LandingMethod = self:GetSquadronLanding( Squadron.Name ) + if LandingMethod == AI_A2A_DISPATCHER.Landing.AtRunway then + local DefenderSize = Defender:GetSize() + if DefenderSize == 1 then + self:RemoveDefenderFromSquadron( Squadron, Defender ) + end + DefenderUnit:Destroy() + self:ParkDefender( Squadron ) + return + end + if DefenderUnit:GetLife() ~= DefenderUnit:GetLife0() then + -- Damaged units cannot be repaired anymore. + DefenderUnit:Destroy() + return + end + end + end + + --- Event engine shutdown. + -- @param #AI_A2A_DISPATCHER self + -- @param Core.Event#EVENTDATA EventData + function AI_A2A_DISPATCHER:OnEventEngineShutdown( EventData ) + local DefenderUnit = EventData.IniUnit + local Defender = EventData.IniGroup + local Squadron = self:GetSquadronFromDefender( Defender ) + if Squadron then + self:F( { SquadronName = Squadron.Name } ) + local LandingMethod = self:GetSquadronLanding( Squadron.Name ) + if LandingMethod == AI_A2A_DISPATCHER.Landing.AtEngineShutdown and + not DefenderUnit:InAir() then + local DefenderSize = Defender:GetSize() + if DefenderSize == 1 then + self:RemoveDefenderFromSquadron( Squadron, Defender ) + end + DefenderUnit:Destroy() + self:ParkDefender( Squadron ) + end + end + end + + --- Define the radius to engage any target by airborne friendlies, which are executing cap or returning from an intercept mission. + -- If there is a target area detected and reported, then any friendlies that are airborne near this target area, + -- will be commanded to (re-)engage that target when available (if no other tasks were commanded). + -- + -- For example, if 100000 is given as a value, then any friendly that is airborne within 100km from the detected target, + -- will be considered to receive the command to engage that target area. + -- + -- You need to evaluate the value of this parameter carefully: + -- + -- * If too small, more intercept missions may be triggered upon detected target areas. + -- * If too large, any airborne cap may not be able to reach the detected target area in time, because it is too far. + -- + -- **Use the method @{#AI_A2A_DISPATCHER.SetEngageRadius}() to modify the default Engage Radius for ALL squadrons.** + -- + -- Demonstration Mission: [AID-019 - AI_A2A - Engage Range Test](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-019%20-%20AI_A2A%20-%20Engage%20Range%20Test) + -- + -- @param #AI_A2A_DISPATCHER self + -- @param #number EngageRadius (Optional, Default = 100000) The radius to report friendlies near the target. + -- @return #AI_A2A_DISPATCHER + -- @usage + -- + -- -- Set 50km as the radius to engage any target by airborne friendlies. + -- A2ADispatcher:SetEngageRadius( 50000 ) + -- + -- -- Set 100km as the radius to engage any target by airborne friendlies. + -- A2ADispatcher:SetEngageRadius() -- 100000 is the default value. + -- + function AI_A2A_DISPATCHER:SetEngageRadius( EngageRadius ) + + self.Detection:SetFriendliesRange( EngageRadius or 100000 ) + + return self + end + + --- Define the radius to disengage any target when the distance to the home base is larger than the specified meters. + -- @param #AI_A2A_DISPATCHER self + -- @param #number DisengageRadius (Optional, Default = 300000) The radius in meters to disengage a target when too far from the home base. + -- @return #AI_A2A_DISPATCHER + -- @usage + -- + -- -- Set 50km as the Disengage Radius. + -- A2ADispatcher:SetDisengageRadius( 50000 ) + -- + -- -- Set 100km as the Disengage Radius. + -- A2ADispatcher:SetDisngageRadius() -- 300000 is the default value. + -- + function AI_A2A_DISPATCHER:SetDisengageRadius( DisengageRadius ) + + self.DisengageRadius = DisengageRadius or 300000 + + return self + end + + + --- Define the radius to check if a target can be engaged by an ground controlled intercept. + -- When targets are detected that are still really far off, you don't want the AI_A2A_DISPATCHER to launch intercepts just yet. + -- You want it to wait until a certain Gci range is reached, which is the **distance of the closest airbase to target** + -- being **smaller** than the **Ground Controlled Intercept radius** or **Gci radius**. + -- + -- The **default** Gci radius is defined as **200000** or **200km**. Override the default Gci radius when the era of the warfare is early, or, + -- when you don't want to let the AI_A2A_DISPATCHER react immediately when a certain border or area is not being crossed. + -- + -- Use the method @{#AI_A2A_DISPATCHER.SetGciRadius}() to set a specific controlled ground intercept radius. + -- **The Ground Controlled Intercept radius is defined for ALL squadrons which are operational.** + -- + -- Demonstration Mission: [AID-013 - AI_A2A - Intercept Test](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-013%20-%20AI_A2A%20-%20Intercept%20Test) + -- + -- @param #AI_A2A_DISPATCHER self + -- @param #number GciRadius (Optional, Default = 200000) The radius to ground control intercept detected targets from the nearest airbase. + -- @return #AI_A2A_DISPATCHER self + -- @usage + -- + -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- + -- -- Set 100km as the radius to ground control intercept detected targets from the nearest airbase. + -- A2ADispatcher:SetGciRadius( 100000 ) + -- + -- -- Set 200km as the radius to ground control intercept. + -- A2ADispatcher:SetGciRadius() -- 200000 is the default value. + -- + function AI_A2A_DISPATCHER:SetGciRadius( GciRadius ) + + self.GciRadius = GciRadius or 200000 + + return self + end + + + + --- Define a border area to simulate a **cold war** scenario. + -- A **cold war** is one where CAP aircraft patrol their territory but will not attack enemy aircraft or launch GCI aircraft unless enemy aircraft enter their territory. In other words the EWR may detect an enemy aircraft but will only send aircraft to attack it if it crosses the border. + -- A **hot war** is one where CAP aircraft will intercept any detected enemy aircraft and GCI aircraft will launch against detected enemy aircraft without regard for territory. In other words if the ground radar can detect the enemy aircraft then it will send CAP and GCI aircraft to attack it. + -- If it's a cold war then the **borders of red and blue territory** need to be defined using a @{zone} object derived from @{Core.Zone#ZONE_BASE}. This method needs to be used for this. + -- If a hot war is chosen then **no borders** actually need to be defined using the helicopter units other than it makes it easier sometimes for the mission maker to envisage where the red and blue territories roughly are. In a hot war the borders are effectively defined by the ground based radar coverage of a coalition. Set the noborders parameter to 1 + -- @param #AI_A2A_DISPATCHER self + -- @param Core.Zone#ZONE_BASE BorderZone An object derived from ZONE_BASE, or a list of objects derived from ZONE_BASE. + -- @return #AI_A2A_DISPATCHER self + -- @usage + -- + -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- + -- -- Set one ZONE_POLYGON object as the border for the A2A dispatcher. + -- local BorderZone = ZONE_POLYGON( "CCCP Border", GROUP:FindByName( "CCCP Border" ) ) -- The GROUP object is a late activate helicopter unit. + -- A2ADispatcher:SetBorderZone( BorderZone ) + -- + -- or + -- + -- -- Set two ZONE_POLYGON objects as the border for the A2A dispatcher. + -- local BorderZone1 = ZONE_POLYGON( "CCCP Border1", GROUP:FindByName( "CCCP Border1" ) ) -- The GROUP object is a late activate helicopter unit. + -- local BorderZone2 = ZONE_POLYGON( "CCCP Border2", GROUP:FindByName( "CCCP Border2" ) ) -- The GROUP object is a late activate helicopter unit. + -- A2ADispatcher:SetBorderZone( { BorderZone1, BorderZone2 } ) + -- + -- + function AI_A2A_DISPATCHER:SetBorderZone( BorderZone ) + + self.Detection:SetAcceptZones( BorderZone ) + + return self + end + + --- Display a tactical report every 30 seconds about which aircraft are: + -- * Patrolling + -- * Engaging + -- * Returning + -- * Damaged + -- * Out of Fuel + -- * ... + -- @param #AI_A2A_DISPATCHER self + -- @param #boolean TacticalDisplay Provide a value of **true** to display every 30 seconds a tactical overview. + -- @return #AI_A2A_DISPATCHER self + -- @usage + -- + -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the Tactical Display for debug mode. + -- A2ADispatcher:SetTacticalDisplay( true ) + -- + function AI_A2A_DISPATCHER:SetTacticalDisplay( TacticalDisplay ) + + self.TacticalDisplay = TacticalDisplay + + return self + end + + + --- Set the default damage treshold when defenders will RTB. + -- The default damage treshold is by default set to 40%, which means that when the airplane is 40% damaged, it will go RTB. + -- @param #AI_A2A_DISPATCHER self + -- @param #number DamageThreshold A decimal number between 0 and 1, that expresses the %-tage of the damage treshold before going RTB. + -- @return #AI_A2A_DISPATCHER self + -- @usage + -- + -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default damage treshold. + -- A2ADispatcher:SetDefaultDamageThreshold( 0.90 ) -- Go RTB when the airplane 90% damaged. + -- + function AI_A2A_DISPATCHER:SetDefaultDamageThreshold( DamageThreshold ) + + self.DefenderDefault.DamageThreshold = DamageThreshold + + return self + end + + + --- Set the default CAP time interval for squadrons, which will be used to determine a random CAP timing. + -- The default CAP time interval is between 180 and 600 seconds. + -- @param #AI_A2A_DISPATCHER self + -- @param #number CapMinSeconds The minimum amount of seconds for the random time interval. + -- @param #number CapMaxSeconds The maximum amount of seconds for the random time interval. + -- @return #AI_A2A_DISPATCHER self + -- @usage + -- + -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default CAP time interval. + -- A2ADispatcher:SetDefaultCapTimeInterval( 300, 1200 ) -- Between 300 and 1200 seconds. + -- + function AI_A2A_DISPATCHER:SetDefaultCapTimeInterval( CapMinSeconds, CapMaxSeconds ) + + self.DefenderDefault.CapMinSeconds = CapMinSeconds + self.DefenderDefault.CapMaxSeconds = CapMaxSeconds + + return self + end + + + --- Set the default CAP limit for squadrons, which will be used to determine how many CAP can be airborne at the same time for the squadron. + -- The default CAP limit is 1 CAP, which means one CAP group being spawned. + -- @param #AI_A2A_DISPATCHER self + -- @param #number CapLimit The maximum amount of CAP that can be airborne at the same time for the squadron. + -- @return #AI_A2A_DISPATCHER self + -- @usage + -- + -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default CAP limit. + -- A2ADispatcher:SetDefaultCapLimit( 2 ) -- Maximum 2 CAP per squadron. + -- + function AI_A2A_DISPATCHER:SetDefaultCapLimit( CapLimit ) + + self.DefenderDefault.CapLimit = CapLimit + + return self + end + + --- Set intercept. + -- @param #AI_A2A_DISPATCHER self + -- @param #number InterceptDelay Delay in seconds before intercept. + -- @return #AI_A2A_DISPATCHER self + function AI_A2A_DISPATCHER:SetIntercept( InterceptDelay ) + + self.DefenderDefault.InterceptDelay = InterceptDelay + + local Detection = self.Detection -- Functional.Detection#DETECTION_AREAS + Detection:SetIntercept( true, InterceptDelay ) + + return self + end + + + --- Calculates which AI friendlies are nearby the area + -- @param #AI_A2A_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem + -- @return #table A list of the friendlies nearby. + function AI_A2A_DISPATCHER:GetAIFriendliesNearBy( DetectedItem ) + + local FriendliesNearBy = self.Detection:GetFriendliesDistance( DetectedItem ) + + return FriendliesNearBy + end + + --- Return the defender tasks table. + -- @param #AI_A2A_DISPATCHER self + -- @return #table Defender tasks as table. + function AI_A2A_DISPATCHER:GetDefenderTasks() + return self.DefenderTasks or {} + end + + --- Get defender task. + -- @param #AI_A2A_DISPATCHER self + -- @param Wrapper.Group#GROUP Defender The defender group. + -- @return #table Defender task. + function AI_A2A_DISPATCHER:GetDefenderTask( Defender ) + return self.DefenderTasks[Defender] + end + + --- Get defender task FSM. + -- @param #AI_A2A_DISPATCHER self + -- @param Wrapper.Group#GROUP Defender The defender group. + -- @return Core.Fsm#FSM The FSM. + function AI_A2A_DISPATCHER:GetDefenderTaskFsm( Defender ) + return self:GetDefenderTask( Defender ).Fsm + end + + --- Get target of defender. + -- @param #AI_A2A_DISPATCHER self + -- @param Wrapper.Group#GROUP Defender The defender group. + -- @return Target + function AI_A2A_DISPATCHER:GetDefenderTaskTarget( Defender ) + return self:GetDefenderTask( Defender ).Target + end + + --- + -- @param #AI_A2A_DISPATCHER self + -- @param Wrapper.Group#GROUP Defender The defender group. + -- @return #string Squadron name of the defender task. + function AI_A2A_DISPATCHER:GetDefenderTaskSquadronName( Defender ) + return self:GetDefenderTask( Defender ).SquadronName + end + + --- + -- @param #AI_A2A_DISPATCHER self + -- @param Wrapper.Group#GROUP Defender The defender group. + function AI_A2A_DISPATCHER:ClearDefenderTask( Defender ) + if Defender and Defender:IsAlive() and self.DefenderTasks[Defender] then + local Target = self.DefenderTasks[Defender].Target + local Message = "Clearing (" .. self.DefenderTasks[Defender].Type .. ") " + Message = Message .. Defender:GetName() + if Target then + Message = Message .. ( Target and ( " from " .. Target.Index .. " [" .. Target.Set:Count() .. "]" ) ) or "" + end + self:F( { Target = Message } ) + end + self.DefenderTasks[Defender] = nil + return self + end + + --- + -- @param #AI_A2A_DISPATCHER self + -- @param Wrapper.Group#GROUP Defender The defender group. + function AI_A2A_DISPATCHER:ClearDefenderTaskTarget( Defender ) + + local DefenderTask = self:GetDefenderTask( Defender ) + + if Defender and Defender:IsAlive() and DefenderTask then + local Target = DefenderTask.Target + local Message = "Clearing (" .. DefenderTask.Type .. ") " + Message = Message .. Defender:GetName() + if Target then + Message = Message .. ( Target and ( " from " .. Target.Index .. " [" .. Target.Set:Count() .. "]" ) ) or "" + end + self:F( { Target = Message } ) + end + if Defender and DefenderTask and DefenderTask.Target then + DefenderTask.Target = nil + end +-- if Defender and DefenderTask then +-- if DefenderTask.Fsm:Is( "Fuel" ) +-- or DefenderTask.Fsm:Is( "LostControl") +-- or DefenderTask.Fsm:Is( "Damaged" ) then +-- self:ClearDefenderTask( Defender ) +-- end +-- end + return self + end + + + --- Set defender task. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName Name of the squadron. + -- @param Wrapper.Group#GROUP Defender The defender group. + -- @param #table Type Type of the defender task + -- @param Core.Fsm#FSM Fsm The defender task FSM. + -- @param Functional.Detection#DETECTION_BASE.DetectedItem Target The defender detected item. + -- @return #AI_A2A_DISPATCHER self + function AI_A2A_DISPATCHER:SetDefenderTask( SquadronName, Defender, Type, Fsm, Target ) + + self:F( { SquadronName = SquadronName, Defender = Defender:GetName(), Type=Type, Target=Target } ) + + self.DefenderTasks[Defender] = self.DefenderTasks[Defender] or {} + self.DefenderTasks[Defender].Type = Type + self.DefenderTasks[Defender].Fsm = Fsm + self.DefenderTasks[Defender].SquadronName = SquadronName + + if Target then + self:SetDefenderTaskTarget( Defender, Target ) + end + return self + end + + + --- Set defender task target. + -- @param #AI_A2A_DISPATCHER self + -- @param Wrapper.Group#GROUP Defender The defender group. + -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection The detection object. + -- @return #AI_A2A_DISPATCHER self + function AI_A2A_DISPATCHER:SetDefenderTaskTarget( Defender, AttackerDetection ) + + local Message = "(" .. self.DefenderTasks[Defender].Type .. ") " + Message = Message .. Defender:GetName() + Message = Message .. ( AttackerDetection and ( " target " .. AttackerDetection.Index .. " [" .. AttackerDetection.Set:Count() .. "]" ) ) or "" + self:F( { AttackerDetection = Message } ) + if AttackerDetection then + self.DefenderTasks[Defender].Target = AttackerDetection + end + return self + end + + + --- This is the main method to define Squadrons programmatically. + -- Squadrons: + -- + -- * Have a **name or key** that is the identifier or key of the squadron. + -- * Have **specific plane types** defined by **templates**. + -- * Are **located at one specific airbase**. Multiple squadrons can be located at one airbase through. + -- * Optionally have a limited set of **resources**. The default is that squadrons have unlimited resources. + -- + -- The name of the squadron given acts as the **squadron key** in the AI\_A2A\_DISPATCHER:Squadron...() methods. + -- + -- Additionally, squadrons have specific configuration options to: + -- + -- * Control how new aircraft are **taking off** from the airfield (in the air, cold, hot, at the runway). + -- * Control how returning aircraft are **landing** at the airfield (in the air near the airbase, after landing, after engine shutdown). + -- * Control the **grouping** of new aircraft spawned at the airfield. If there is more than one aircraft to be spawned, these may be grouped. + -- * Control the **overhead** or defensive strength of the squadron. Depending on the types of planes and amount of resources, the mission designer can choose to increase or reduce the amount of planes spawned. + -- + -- For performance and bug workaround reasons within DCS, squadrons have different methods to spawn new aircraft or land returning or damaged aircraft. + -- + -- @param #AI_A2A_DISPATCHER self + -- + -- @param #string SquadronName A string (text) that defines the squadron identifier or the key of the Squadron. + -- It can be any name, for example `"104th Squadron"` or `"SQ SQUADRON1"`, whatever. + -- As long as you remember that this name becomes the identifier of your squadron you have defined. + -- You need to use this name in other methods too! + -- + -- @param #string AirbaseName The airbase name where you want to have the squadron located. + -- You need to specify here EXACTLY the name of the airbase as you see it in the mission editor. + -- Examples are `"Batumi"` or `"Tbilisi-Lochini"`. + -- EXACTLY the airbase name, between quotes `""`. + -- To ease the airbase naming when using the LDT editor and IntelliSense, the @{Wrapper.Airbase#AIRBASE} class contains enumerations of the airbases of each map. + -- + -- * Caucasus: @{Wrapper.Airbase#AIRBASE.Caucaus} + -- * Nevada or NTTR: @{Wrapper.Airbase#AIRBASE.Nevada} + -- * Normandy: @{Wrapper.Airbase#AIRBASE.Normandy} + -- + -- @param #string TemplatePrefixes A string or an array of strings specifying the **prefix names of the templates** (not going to explain what is templates here again). + -- Examples are `{ "104th", "105th" }` or `"104th"` or `"Template 1"` or `"BLUE PLANES"`. + -- Just remember that your template (groups late activated) need to start with the prefix you have specified in your code. + -- If you have only one prefix name for a squadron, you don't need to use the `{ }`, otherwise you need to use the brackets. + -- + -- @param #number ResourceCount (optional) A number that specifies how many resources are in stock of the squadron. If not specified, the squadron will have infinite resources available. + -- + -- @usage + -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- + -- @usage + -- -- This will create squadron "Squadron1" at "Batumi" airbase, and will use plane types "SQ1" and has 40 planes in stock... + -- A2ADispatcher:SetSquadron( "Squadron1", "Batumi", "SQ1", 40 ) + -- + -- @usage + -- -- This will create squadron "Sq 1" at "Batumi" airbase, and will use plane types "Mig-29" and "Su-27" and has 20 planes in stock... + -- -- Note that in this implementation, the A2A dispatcher will select a random plane type when a new plane (group) needs to be spawned for defenses. + -- -- Note the usage of the {} for the airplane templates list. + -- A2ADispatcher:SetSquadron( "Sq 1", "Batumi", { "Mig-29", "Su-27" }, 40 ) + -- + -- @usage + -- -- This will create 2 squadrons "104th" and "23th" at "Batumi" airbase, and will use plane types "Mig-29" and "Su-27" respectively and each squadron has 10 planes in stock... + -- A2ADispatcher:SetSquadron( "104th", "Batumi", "Mig-29", 10 ) + -- A2ADispatcher:SetSquadron( "23th", "Batumi", "Su-27", 10 ) + -- + -- @usage + -- -- This is an example like the previous, but now with infinite resources. + -- -- The ResourceCount parameter is not given in the SetSquadron method. + -- A2ADispatcher:SetSquadron( "104th", "Batumi", "Mig-29" ) + -- A2ADispatcher:SetSquadron( "23th", "Batumi", "Su-27" ) + -- + -- + -- @return #AI_A2A_DISPATCHER self + function AI_A2A_DISPATCHER:SetSquadron( SquadronName, AirbaseName, TemplatePrefixes, ResourceCount ) + + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + + local DefenderSquadron = self.DefenderSquadrons[SquadronName] --#AI_A2A_DISPATCHER.Squadron + + DefenderSquadron.Name = SquadronName + DefenderSquadron.Airbase = AIRBASE:FindByName( AirbaseName ) + DefenderSquadron.AirbaseName = DefenderSquadron.Airbase:GetName() + if not DefenderSquadron.Airbase then + error( "Cannot find airbase with name:" .. AirbaseName ) + end + + DefenderSquadron.Spawn = {} + if type( TemplatePrefixes ) == "string" then + local SpawnTemplate = TemplatePrefixes + self.DefenderSpawns[SpawnTemplate] = self.DefenderSpawns[SpawnTemplate] or SPAWN:New( SpawnTemplate ) -- :InitCleanUp( 180 ) + DefenderSquadron.Spawn[1] = self.DefenderSpawns[SpawnTemplate] + else + for TemplateID, SpawnTemplate in pairs( TemplatePrefixes ) do + self.DefenderSpawns[SpawnTemplate] = self.DefenderSpawns[SpawnTemplate] or SPAWN:New( SpawnTemplate ) -- :InitCleanUp( 180 ) + DefenderSquadron.Spawn[#DefenderSquadron.Spawn+1] = self.DefenderSpawns[SpawnTemplate] + end + end + DefenderSquadron.ResourceCount = ResourceCount + DefenderSquadron.TemplatePrefixes = TemplatePrefixes + DefenderSquadron.Captured = false -- Not captured. This flag will be set to true, when the airbase where the squadron is located, is captured. + + self:SetSquadronLanguage( SquadronName, "EN" ) -- Squadrons speak English by default. + + self:F( { Squadron = {SquadronName, AirbaseName, TemplatePrefixes, ResourceCount } } ) + + return self + end + + --- Get an item from the Squadron table. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName Name of the squadron. + -- @return #AI_A2A_DISPATCHER.Squadron Defender squadron table. + function AI_A2A_DISPATCHER:GetSquadron( SquadronName ) + local DefenderSquadron = self.DefenderSquadrons[SquadronName] + + if not DefenderSquadron then + error( "Unknown Squadron:" .. SquadronName ) + end + + return DefenderSquadron + end + + + --- Set the Squadron visible before startup of the dispatcher. + -- All planes will be spawned as uncontrolled on the parking spot. + -- They will lock the parking spot. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @return #AI_A2A_DISPATCHER self + -- @usage + -- + -- -- Set the Squadron visible before startup of dispatcher. + -- A2ADispatcher:SetSquadronVisible( "Mineralnye" ) + -- + function AI_A2A_DISPATCHER:SetSquadronVisible( SquadronName ) + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + + local DefenderSquadron = self:GetSquadron( SquadronName ) --#AI_A2A_DISPATCHER.Squadron + + DefenderSquadron.Uncontrolled = true + + -- For now, grouping is forced to 1 due to other parts of the class which would not work well with grouping>1. + DefenderSquadron.Grouping=1 + + -- Get free parking for fighter aircraft. + local nfreeparking=DefenderSquadron.Airbase:GetFreeParkingSpotsNumber(AIRBASE.TerminalType.FighterAircraft, true) + + -- Take number of free parking spots if no resource count was specifed. + DefenderSquadron.ResourceCount=DefenderSquadron.ResourceCount or nfreeparking + + -- Check that resource count is not larger than free parking spots. + DefenderSquadron.ResourceCount=math.min(DefenderSquadron.ResourceCount, nfreeparking) + + -- Set uncontrolled spawning option. + for SpawnTemplate,_DefenderSpawn in pairs( self.DefenderSpawns ) do + local DefenderSpawn=_DefenderSpawn --Core.Spawn#SPAWN + DefenderSpawn:InitUnControlled(true) + end + + end + + --- Check if the Squadron is visible before startup of the dispatcher. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @return #boolean true if visible. + -- @usage + -- + -- -- Set the Squadron visible before startup of dispatcher. + -- local IsVisible = A2ADispatcher:IsSquadronVisible( "Mineralnye" ) + -- + function AI_A2A_DISPATCHER:IsSquadronVisible( SquadronName ) + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + + local DefenderSquadron = self:GetSquadron( SquadronName ) --#AI_A2A_DISPATCHER.Squadron + + if DefenderSquadron then + return DefenderSquadron.Uncontrolled == true + end + + return nil + + end + + --- Set a CAP for a Squadron. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageMinSpeed The minimum speed at which the engage can be executed. + -- @param #number EngageMaxSpeed The maximum speed at which the engage can be executed. + -- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement. + -- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement. + -- @param #number EngageAltType The altitude type to engage, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the CAP will be executed. + -- @param #number PatrolMinSpeed The minimum speed at which the cap can be executed. + -- @param #number PatrolMaxSpeed The maximum speed at which the cap can be executed. + -- @param #number PatrolFloorAltitude The minimum altitude at which the cap can be executed. + -- @param #number PatrolCeilingAltitude the maximum altitude at which the cap can be executed. + -- @param #number PatrolAltType The altitude type to patrol, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @return #AI_A2A_DISPATCHER + -- @usage + -- + -- -- CAP Squadron execution. + -- CAPZoneEast = ZONE_POLYGON:New( "CAP Zone East", GROUP:FindByName( "CAP Zone East" ) ) + -- -- Setup a CAP, engaging between 800 and 900 km/h, altitude 30 (above the sea), radio altitude measurement, + -- -- patrolling speed between 500 and 600 km/h, altitude between 4000 and 10000 meters, barometric altitude measurement. + -- A2ADispatcher:SetSquadronCapV2( "Mineralnye", 800, 900, 30, 30, "RADIO", CAPZoneEast, 500, 600, 4000, 10000, "BARO" ) + -- A2ADispatcher:SetSquadronCapInterval( "Mineralnye", 2, 30, 60, 1 ) + -- + -- CAPZoneWest = ZONE_POLYGON:New( "CAP Zone West", GROUP:FindByName( "CAP Zone West" ) ) + -- -- Setup a CAP, engaging between 800 and 1200 km/h, altitude between 4000 and 10000 meters, radio altitude measurement, + -- -- patrolling speed between 600 and 800 km/h, altitude between 4000 and 8000, barometric altitude measurement. + -- A2ADispatcher:SetSquadronCapV2( "Sochi", 800, 1200, 2000, 3000, "RADIO", CAPZoneWest, 600, 800, 4000, 8000, "BARO" ) + -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) + -- + -- CAPZoneMiddle = ZONE:New( "CAP Zone Middle") + -- -- Setup a CAP, engaging between 800 and 1200 km/h, altitude between 5000 and 8000 meters, barometric altitude measurement, + -- -- patrolling speed between 600 and 800 km/h, altitude between 4000 and 8000, radio altitude. + -- A2ADispatcher:SetSquadronCapV2( "Maykop", 800, 1200, 5000, 8000, "BARO", CAPZoneMiddle, 600, 800, 4000, 8000, "RADIO" ) + -- A2ADispatcher:SetSquadronCapInterval( "Maykop", 2, 30, 120, 1 ) + -- + function AI_A2A_DISPATCHER:SetSquadronCap2( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType, Zone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType ) + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + self.DefenderSquadrons[SquadronName].Cap = self.DefenderSquadrons[SquadronName].Cap or {} + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + local Cap = self.DefenderSquadrons[SquadronName].Cap + Cap.Name = SquadronName + Cap.EngageMinSpeed = EngageMinSpeed + Cap.EngageMaxSpeed = EngageMaxSpeed + Cap.EngageFloorAltitude = EngageFloorAltitude + Cap.EngageCeilingAltitude = EngageCeilingAltitude + Cap.Zone = Zone + Cap.PatrolMinSpeed = PatrolMinSpeed + Cap.PatrolMaxSpeed = PatrolMaxSpeed + Cap.PatrolFloorAltitude = PatrolFloorAltitude + Cap.PatrolCeilingAltitude = PatrolCeilingAltitude + Cap.PatrolAltType = PatrolAltType + Cap.EngageAltType = EngageAltType + + self:SetSquadronCapInterval( SquadronName, self.DefenderDefault.CapLimit, self.DefenderDefault.CapMinSeconds, self.DefenderDefault.CapMaxSeconds, 1 ) + + self:I( { CAP = { SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, Zone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, EngageAltType } } ) + + -- Add the CAP to the EWR network. + + local RecceSet = self.Detection:GetDetectionSet() + RecceSet:FilterPrefixes( DefenderSquadron.TemplatePrefixes ) + RecceSet:FilterStart() + + self.Detection:SetFriendlyPrefixes( DefenderSquadron.TemplatePrefixes ) + + return self + end + + --- Set a CAP for a Squadron. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the CAP will be executed. + -- @param #number PatrolFloorAltitude The minimum altitude at which the cap can be executed. + -- @param #number PatrolCeilingAltitude the maximum altitude at which the cap can be executed. + -- @param #number PatrolMinSpeed The minimum speed at which the cap can be executed. + -- @param #number PatrolMaxSpeed The maximum speed at which the cap can be executed. + -- @param #number EngageMinSpeed The minimum speed at which the engage can be executed. + -- @param #number EngageMaxSpeed The maximum speed at which the engage can be executed. + -- @param #number AltType The altitude type, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @return #AI_A2A_DISPATCHER + -- @usage + -- + -- -- CAP Squadron execution. + -- CAPZoneEast = ZONE_POLYGON:New( "CAP Zone East", GROUP:FindByName( "CAP Zone East" ) ) + -- A2ADispatcher:SetSquadronCap( "Mineralnye", CAPZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- A2ADispatcher:SetSquadronCapInterval( "Mineralnye", 2, 30, 60, 1 ) + -- + -- CAPZoneWest = ZONE_POLYGON:New( "CAP Zone West", GROUP:FindByName( "CAP Zone West" ) ) + -- A2ADispatcher:SetSquadronCap( "Sochi", CAPZoneWest, 4000, 8000, 600, 800, 800, 1200, "BARO" ) + -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) + -- + -- CAPZoneMiddle = ZONE:New( "CAP Zone Middle") + -- A2ADispatcher:SetSquadronCap( "Maykop", CAPZoneMiddle, 4000, 8000, 600, 800, 800, 1200, "RADIO" ) + -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) + -- + function AI_A2A_DISPATCHER:SetSquadronCap( SquadronName, Zone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, AltType ) + + return self:SetSquadronCap2( SquadronName, EngageMinSpeed, EngageMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, AltType, Zone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, AltType ) + end + + --- Set the squadron CAP parameters. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number CapLimit (optional) The maximum amount of CAP groups to be spawned. Note that a CAP is a group, so can consist out of 1 to 4 airplanes. The default is 1 CAP group. + -- @param #number LowInterval (optional) The minimum time boundary in seconds when a new CAP will be spawned. The default is 180 seconds. + -- @param #number HighInterval (optional) The maximum time boundary in seconds when a new CAP will be spawned. The default is 600 seconds. + -- @param #number Probability Is not in use, you can skip this parameter. + -- @return #AI_A2A_DISPATCHER + -- @usage + -- + -- -- CAP Squadron execution. + -- CAPZoneEast = ZONE_POLYGON:New( "CAP Zone East", GROUP:FindByName( "CAP Zone East" ) ) + -- A2ADispatcher:SetSquadronCap( "Mineralnye", CAPZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- A2ADispatcher:SetSquadronCapInterval( "Mineralnye", 2, 30, 60, 1 ) + -- + -- CAPZoneWest = ZONE_POLYGON:New( "CAP Zone West", GROUP:FindByName( "CAP Zone West" ) ) + -- A2ADispatcher:SetSquadronCap( "Sochi", CAPZoneWest, 4000, 8000, 600, 800, 800, 1200, "BARO" ) + -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) + -- + -- CAPZoneMiddle = ZONE:New( "CAP Zone Middle") + -- A2ADispatcher:SetSquadronCap( "Maykop", CAPZoneMiddle, 4000, 8000, 600, 800, 800, 1200, "RADIO" ) + -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) + -- + function AI_A2A_DISPATCHER:SetSquadronCapInterval( SquadronName, CapLimit, LowInterval, HighInterval, Probability ) + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + self.DefenderSquadrons[SquadronName].Cap = self.DefenderSquadrons[SquadronName].Cap or {} + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + local Cap = self.DefenderSquadrons[SquadronName].Cap + if Cap then + Cap.LowInterval = LowInterval or 180 + Cap.HighInterval = HighInterval or 600 + Cap.Probability = Probability or 1 + Cap.CapLimit = CapLimit or 1 + Cap.Scheduler = Cap.Scheduler or SCHEDULER:New( self ) + local Scheduler = Cap.Scheduler -- Core.Scheduler#SCHEDULER + local ScheduleID = Cap.ScheduleID + local Variance = ( Cap.HighInterval - Cap.LowInterval ) / 2 + local Repeat = Cap.LowInterval + Variance + local Randomization = Variance / Repeat + local Start = math.random( 1, Cap.HighInterval ) + + if ScheduleID then + Scheduler:Stop( ScheduleID ) + end + + Cap.ScheduleID = Scheduler:Schedule( self, self.SchedulerCAP, { SquadronName }, Start, Repeat, Randomization ) + else + error( "This squadron does not exist:" .. SquadronName ) + end + + end + + --- + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @return #AI_A2A_DISPATCHER self + function AI_A2A_DISPATCHER:GetCAPDelay( SquadronName ) + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + self.DefenderSquadrons[SquadronName].Cap = self.DefenderSquadrons[SquadronName].Cap or {} + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + local Cap = self.DefenderSquadrons[SquadronName].Cap + if Cap then + return math.random( Cap.LowInterval, Cap.HighInterval ) + else + error( "This squadron does not exist:" .. SquadronName ) + end + end + + --- Check if squadron can do CAP. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @return #AI_A2A_DISPATCHER.Squadron DefenderSquadron + function AI_A2A_DISPATCHER:CanCAP( SquadronName ) + self:F({SquadronName = SquadronName}) + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + self.DefenderSquadrons[SquadronName].Cap = self.DefenderSquadrons[SquadronName].Cap or {} + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + if DefenderSquadron.Captured == false then -- We can only spawn new CAP if the base has not been captured. + + if ( not DefenderSquadron.ResourceCount ) or ( DefenderSquadron.ResourceCount and DefenderSquadron.ResourceCount > 0 ) then -- And, if there are sufficient resources. + + local Cap = DefenderSquadron.Cap + if Cap then + local CapCount = self:CountCapAirborne( SquadronName ) + self:F( { CapCount = CapCount } ) + if CapCount < Cap.CapLimit then + local Probability = math.random() + if Probability <= Cap.Probability then + return DefenderSquadron + end + end + end + end + end + return nil + end + + + --- Set race track pattern as default when any squadron is performing CAP. + -- @param #AI_A2A_DISPATCHER self + -- @param #number LeglengthMin Min length of the race track leg in meters. Default 10,000 m. + -- @param #number LeglengthMax Max length of the race track leg in meters. Default 15,000 m. + -- @param #number HeadingMin Min heading of the race track in degrees. Default 0 deg, i.e. counter clockwise from South to North. + -- @param #number HeadingMax Max heading of the race track in degrees. Default 180 deg, i.e. counter clockwise from North to South. + -- @param #number DurationMin (Optional) Min duration in seconds before switching the orbit position. Default is keep same orbit until RTB or engage. + -- @param #number DurationMax (Optional) Max duration in seconds before switching the orbit position. Default is keep same orbit until RTB or engage. + -- @param #table CapCoordinates Table of coordinates of first race track point. Second point is determined by leg length and heading. + -- @return #AI_A2A_DISPATCHER self + function AI_A2A_DISPATCHER:SetDefaultCapRacetrack(LeglengthMin, LeglengthMax, HeadingMin, HeadingMax, DurationMin, DurationMax, CapCoordinates) + + self.DefenderDefault.Racetrack=true + self.DefenderDefault.RacetrackLengthMin=LeglengthMin + self.DefenderDefault.RacetrackLengthMax=LeglengthMax + self.DefenderDefault.RacetrackHeadingMin=HeadingMin + self.DefenderDefault.RacetrackHeadingMax=HeadingMax + self.DefenderDefault.RacetrackDurationMin=DurationMin + self.DefenderDefault.RacetrackDurationMax=DurationMax + self.DefenderDefault.RacetrackCoordinates=CapCoordinates + + return self + end + + --- Set race track pattern when squadron is performing CAP. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName Name of the squadron. + -- @param #number LeglengthMin Min length of the race track leg in meters. Default 10,000 m. + -- @param #number LeglengthMax Max length of the race track leg in meters. Default 15,000 m. + -- @param #number HeadingMin Min heading of the race track in degrees. Default 0 deg, i.e. from South to North. + -- @param #number HeadingMax Max heading of the race track in degrees. Default 180 deg, i.e. from North to South. + -- @param #number DurationMin (Optional) Min duration in seconds before switching the orbit position. Default is keep same orbit until RTB or engage. + -- @param #number DurationMax (Optional) Max duration in seconds before switching the orbit position. Default is keep same orbit until RTB or engage. + -- @param #table CapCoordinates Table of coordinates of first race track point. Second point is determined by leg length and heading. + -- @return #AI_A2A_DISPATCHER self + function AI_A2A_DISPATCHER:SetSquadronCapRacetrack(SquadronName, LeglengthMin, LeglengthMax, HeadingMin, HeadingMax, DurationMin, DurationMax, CapCoordinates) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + if DefenderSquadron then + DefenderSquadron.Racetrack=true + DefenderSquadron.RacetrackLengthMin=LeglengthMin + DefenderSquadron.RacetrackLengthMax=LeglengthMax + DefenderSquadron.RacetrackHeadingMin=HeadingMin + DefenderSquadron.RacetrackHeadingMax=HeadingMax + DefenderSquadron.RacetrackDurationMin=DurationMin + DefenderSquadron.RacetrackDurationMax=DurationMax + DefenderSquadron.RacetrackCoordinates=CapCoordinates + end + + return self + end + + + --- Check if squadron can do GCI. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @return #table DefenderSquadron + function AI_A2A_DISPATCHER:CanGCI( SquadronName ) + self:F({SquadronName = SquadronName}) + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + self.DefenderSquadrons[SquadronName].Gci = self.DefenderSquadrons[SquadronName].Gci or {} + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + if DefenderSquadron.Captured == false then -- We can only spawn new CAP if the base has not been captured. + + if ( not DefenderSquadron.ResourceCount ) or ( DefenderSquadron.ResourceCount and DefenderSquadron.ResourceCount > 0 ) then -- And, if there are sufficient resources. + local Gci = DefenderSquadron.Gci + if Gci then + return DefenderSquadron + end + end + end + return nil + end + + --- Set squadron GCI. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageMinSpeed The minimum speed [km/h] at which the GCI can be executed. + -- @param #number EngageMaxSpeed The maximum speed [km/h] at which the GCI can be executed. + -- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement. + -- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement. + -- @param DCS#AltitudeType EngageAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO". + -- @usage + -- + -- -- GCI Squadron execution. + -- A2ADispatcher:SetSquadronGci2( "Mozdok", 900, 1200, 5000, 5000, "BARO" ) + -- A2ADispatcher:SetSquadronGci2( "Novo", 900, 2100, 30, 30, "RADIO" ) + -- A2ADispatcher:SetSquadronGci2( "Maykop", 900, 1200, 100, 300, "RADIO" ) + -- + -- @return #AI_A2A_DISPATCHER + function AI_A2A_DISPATCHER:SetSquadronGci2( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + self.DefenderSquadrons[SquadronName].Gci = self.DefenderSquadrons[SquadronName].Gci or {} + + local Intercept = self.DefenderSquadrons[SquadronName].Gci + Intercept.Name = SquadronName + Intercept.EngageMinSpeed = EngageMinSpeed + Intercept.EngageMaxSpeed = EngageMaxSpeed + Intercept.EngageFloorAltitude = EngageFloorAltitude + Intercept.EngageCeilingAltitude = EngageCeilingAltitude + Intercept.EngageAltType = EngageAltType + + self:I( { GCI = { SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } ) + end + + --- Set squadron GCI. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageMinSpeed The minimum speed [km/h] at which the GCI can be executed. + -- @param #number EngageMaxSpeed The maximum speed [km/h] at which the GCI can be executed. + -- @usage + -- + -- -- GCI Squadron execution. + -- A2ADispatcher:SetSquadronGci( "Mozdok", 900, 1200 ) + -- A2ADispatcher:SetSquadronGci( "Novo", 900, 2100 ) + -- A2ADispatcher:SetSquadronGci( "Maykop", 900, 1200 ) + -- + -- @return #AI_A2A_DISPATCHER + function AI_A2A_DISPATCHER:SetSquadronGci( SquadronName, EngageMinSpeed, EngageMaxSpeed ) + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + self.DefenderSquadrons[SquadronName].Gci = self.DefenderSquadrons[SquadronName].Gci or {} + + local Intercept = self.DefenderSquadrons[SquadronName].Gci + Intercept.Name = SquadronName + Intercept.EngageMinSpeed = EngageMinSpeed + Intercept.EngageMaxSpeed = EngageMaxSpeed + + self:F( { GCI = { SquadronName, EngageMinSpeed, EngageMaxSpeed } } ) + end + + --- Defines the default amount of extra planes that will take-off as part of the defense system. + -- @param #AI_A2A_DISPATCHER self + -- @param #number Overhead The %-tage of Units that dispatching command will allocate to intercept in surplus of detected amount of units. + -- The default overhead is 1, so equal balance. The @{#AI_A2A_DISPATCHER.SetOverhead}() method can be used to tweak the defense strength, + -- taking into account the plane types of the squadron. For example, a MIG-31 with full long-distance A2A missiles payload, may still be less effective than a F-15C with short missiles... + -- So in this case, one may want to use the Overhead method to allocate more defending planes as the amount of detected attacking planes. + -- The overhead must be given as a decimal value with 1 as the neutral value, which means that Overhead values: + -- + -- * Higher than 1, will increase the defense unit amounts. + -- * Lower than 1, will decrease the defense unit amounts. + -- + -- The amount of defending units is calculated by multiplying the amount of detected attacking planes as part of the detected group + -- multiplied by the Overhead and rounded up to the smallest integer. + -- + -- The Overhead value set for a Squadron, can be programmatically adjusted (by using this SetOverhead method), to adjust the defense overhead during mission execution. + -- + -- See example below. + -- + -- @usage: + -- + -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) + -- + -- -- An overhead of 1,5 with 1 planes detected, will allocate 2 planes ( 1 * 1,5 ) = 1,5 => rounded up gives 2. + -- -- An overhead of 1,5 with 2 planes detected, will allocate 3 planes ( 2 * 1,5 ) = 3 => rounded up gives 3. + -- -- An overhead of 1,5 with 3 planes detected, will allocate 5 planes ( 3 * 1,5 ) = 4,5 => rounded up gives 5 planes. + -- -- An overhead of 1,5 with 4 planes detected, will allocate 6 planes ( 4 * 1,5 ) = 6 => rounded up gives 6 planes. + -- + -- A2ADispatcher:SetDefaultOverhead( 1.5 ) + -- + -- @return #AI_A2A_DISPATCHER + function AI_A2A_DISPATCHER:SetDefaultOverhead( Overhead ) + + self.DefenderDefault.Overhead = Overhead + + return self + end + + + --- Defines the amount of extra planes that will take-off as part of the defense system. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number Overhead The %-tage of Units that dispatching command will allocate to intercept in surplus of detected amount of units. + -- The default overhead is 1, so equal balance. The @{#AI_A2A_DISPATCHER.SetOverhead}() method can be used to tweak the defense strength, + -- taking into account the plane types of the squadron. For example, a MIG-31 with full long-distance A2A missiles payload, may still be less effective than a F-15C with short missiles... + -- So in this case, one may want to use the Overhead method to allocate more defending planes as the amount of detected attacking planes. + -- The overhead must be given as a decimal value with 1 as the neutral value, which means that Overhead values: + -- + -- * Higher than 1, will increase the defense unit amounts. + -- * Lower than 1, will decrease the defense unit amounts. + -- + -- The amount of defending units is calculated by multiplying the amount of detected attacking planes as part of the detected group + -- multiplied by the Overhead and rounded up to the smallest integer. + -- + -- The Overhead value set for a Squadron, can be programmatically adjusted (by using this SetOverhead method), to adjust the defense overhead during mission execution. + -- + -- See example below. + -- + -- @usage: + -- + -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) + -- + -- -- An overhead of 1,5 with 1 planes detected, will allocate 2 planes ( 1 * 1,5 ) = 1,5 => rounded up gives 2. + -- -- An overhead of 1,5 with 2 planes detected, will allocate 3 planes ( 2 * 1,5 ) = 3 => rounded up gives 3. + -- -- An overhead of 1,5 with 3 planes detected, will allocate 5 planes ( 3 * 1,5 ) = 4,5 => rounded up gives 5 planes. + -- -- An overhead of 1,5 with 4 planes detected, will allocate 6 planes ( 4 * 1,5 ) = 6 => rounded up gives 6 planes. + -- + -- A2ADispatcher:SetSquadronOverhead( "SquadronName", 1.5 ) + -- + -- @return #AI_A2A_DISPATCHER self + function AI_A2A_DISPATCHER:SetSquadronOverhead( SquadronName, Overhead ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.Overhead = Overhead + + return self + end + + + --- Sets the default grouping of new airplanes spawned. + -- Grouping will trigger how new airplanes will be grouped if more than one airplane is spawned for defense. + -- @param #AI_A2A_DISPATCHER self + -- @param #number Grouping The level of grouping that will be applied of the CAP or GCI defenders. + -- @usage: + -- + -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) + -- + -- -- Set a grouping by default per 2 airplanes. + -- A2ADispatcher:SetDefaultGrouping( 2 ) + -- + -- + -- @return #AI_A2A_DISPATCHER self + function AI_A2A_DISPATCHER:SetDefaultGrouping( Grouping ) + + self.DefenderDefault.Grouping = Grouping + + return self + end + + + --- Sets the grouping of new airplanes spawned. + -- Grouping will trigger how new airplanes will be grouped if more than one airplane is spawned for defense. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number Grouping The level of grouping that will be applied of the CAP or GCI defenders. + -- @usage: + -- + -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) + -- + -- -- Set a grouping per 2 airplanes. + -- A2ADispatcher:SetSquadronGrouping( "SquadronName", 2 ) + -- + -- + -- @return #AI_A2A_DISPATCHER self + function AI_A2A_DISPATCHER:SetSquadronGrouping( SquadronName, Grouping ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.Grouping = Grouping + + return self + end + + + --- Defines the default method at which new flights will spawn and take-off as part of the defense system. + -- @param #AI_A2A_DISPATCHER self + -- @param #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. + -- @usage: + -- + -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default take-off in the air. + -- A2ADispatcher:SetDefaultTakeoff( AI_A2A_Dispatcher.Takeoff.Air ) + -- + -- -- Let new flights by default take-off from the runway. + -- A2ADispatcher:SetDefaultTakeoff( AI_A2A_Dispatcher.Takeoff.Runway ) + -- + -- -- Let new flights by default take-off from the airbase hot. + -- A2ADispatcher:SetDefaultTakeoff( AI_A2A_Dispatcher.Takeoff.Hot ) + -- + -- -- Let new flights by default take-off from the airbase cold. + -- A2ADispatcher:SetDefaultTakeoff( AI_A2A_Dispatcher.Takeoff.Cold ) + -- + -- + -- @return #AI_A2A_DISPATCHER self + -- + function AI_A2A_DISPATCHER:SetDefaultTakeoff( Takeoff ) + + self.DefenderDefault.Takeoff = Takeoff + + return self + end + + --- Defines the method at which new flights will spawn and take-off as part of the defense system. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. + -- @usage: + -- + -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off in the air. + -- A2ADispatcher:SetSquadronTakeoff( "SquadronName", AI_A2A_Dispatcher.Takeoff.Air ) + -- + -- -- Let new flights take-off from the runway. + -- A2ADispatcher:SetSquadronTakeoff( "SquadronName", AI_A2A_Dispatcher.Takeoff.Runway ) + -- + -- -- Let new flights take-off from the airbase hot. + -- A2ADispatcher:SetSquadronTakeoff( "SquadronName", AI_A2A_Dispatcher.Takeoff.Hot ) + -- + -- -- Let new flights take-off from the airbase cold. + -- A2ADispatcher:SetSquadronTakeoff( "SquadronName", AI_A2A_Dispatcher.Takeoff.Cold ) + -- + -- + -- @return #AI_A2A_DISPATCHER self + -- + function AI_A2A_DISPATCHER:SetSquadronTakeoff( SquadronName, Takeoff ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.Takeoff = Takeoff + + return self + end + + + --- Gets the default method at which new flights will spawn and take-off as part of the defense system. + -- @param #AI_A2A_DISPATCHER self + -- @return #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. + -- @usage: + -- + -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default take-off in the air. + -- local TakeoffMethod = A2ADispatcher:GetDefaultTakeoff() + -- if TakeOffMethod == , AI_A2A_Dispatcher.Takeoff.InAir then + -- ... + -- end + -- + function AI_A2A_DISPATCHER:GetDefaultTakeoff( ) + + return self.DefenderDefault.Takeoff + end + + --- Gets the method at which new flights will spawn and take-off as part of the defense system. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @return #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. + -- @usage: + -- + -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off in the air. + -- local TakeoffMethod = A2ADispatcher:GetSquadronTakeoff( "SquadronName" ) + -- if TakeOffMethod == , AI_A2A_Dispatcher.Takeoff.InAir then + -- ... + -- end + -- + function AI_A2A_DISPATCHER:GetSquadronTakeoff( SquadronName ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + return DefenderSquadron.Takeoff or self.DefenderDefault.Takeoff + end + + + --- Sets flights to default take-off in the air, as part of the defense system. + -- @param #AI_A2A_DISPATCHER self + -- @usage: + -- + -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default take-off in the air. + -- A2ADispatcher:SetDefaultTakeoffInAir() + -- + -- @return #AI_A2A_DISPATCHER self + -- + function AI_A2A_DISPATCHER:SetDefaultTakeoffInAir() + + self:SetDefaultTakeoff( AI_A2A_DISPATCHER.Takeoff.Air ) + + return self + end + + --- Set flashing player messages on or off + -- @param #AI_A2G_DISPATCHER self + -- @param #boolean onoff Set messages on (true) or off (false) + function AI_A2A_DISPATCHER:SetSendMessages( onoff ) + self.SetSendPlayerMessages = onoff + end + + --- Sets flights to take-off in the air, as part of the defense system. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number TakeoffAltitude (optional) The altitude in meters above the ground. If not given, the default takeoff altitude will be used. + -- @usage: + -- + -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off in the air. + -- A2ADispatcher:SetSquadronTakeoffInAir( "SquadronName" ) + -- + -- @return #AI_A2A_DISPATCHER self + -- + function AI_A2A_DISPATCHER:SetSquadronTakeoffInAir( SquadronName, TakeoffAltitude ) + + self:SetSquadronTakeoff( SquadronName, AI_A2A_DISPATCHER.Takeoff.Air ) + + if TakeoffAltitude then + self:SetSquadronTakeoffInAirAltitude( SquadronName, TakeoffAltitude ) + end + + return self + end + + + --- Sets flights by default to take-off from the runway, as part of the defense system. + -- @param #AI_A2A_DISPATCHER self + -- @usage: + -- + -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default take-off from the runway. + -- A2ADispatcher:SetDefaultTakeoffFromRunway() + -- + -- @return #AI_A2A_DISPATCHER self + -- + function AI_A2A_DISPATCHER:SetDefaultTakeoffFromRunway() + + self:SetDefaultTakeoff( AI_A2A_DISPATCHER.Takeoff.Runway ) + + return self + end + + + --- Sets flights to take-off from the runway, as part of the defense system. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off from the runway. + -- A2ADispatcher:SetSquadronTakeoffFromRunway( "SquadronName" ) + -- + -- @return #AI_A2A_DISPATCHER self + -- + function AI_A2A_DISPATCHER:SetSquadronTakeoffFromRunway( SquadronName ) + + self:SetSquadronTakeoff( SquadronName, AI_A2A_DISPATCHER.Takeoff.Runway ) + + return self + end + + + --- Sets flights by default to take-off from the airbase at a hot location, as part of the defense system. + -- @param #AI_A2A_DISPATCHER self + -- @usage: + -- + -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default take-off at a hot parking spot. + -- A2ADispatcher:SetDefaultTakeoffFromParkingHot() + -- + -- @return #AI_A2A_DISPATCHER self + -- + function AI_A2A_DISPATCHER:SetDefaultTakeoffFromParkingHot() + + self:SetDefaultTakeoff( AI_A2A_DISPATCHER.Takeoff.Hot ) + + return self + end + + --- Sets flights to take-off from the airbase at a hot location, as part of the defense system. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off in the air. + -- A2ADispatcher:SetSquadronTakeoffFromParkingHot( "SquadronName" ) + -- + -- @return #AI_A2A_DISPATCHER self + -- + function AI_A2A_DISPATCHER:SetSquadronTakeoffFromParkingHot( SquadronName ) + + self:SetSquadronTakeoff( SquadronName, AI_A2A_DISPATCHER.Takeoff.Hot ) + + return self + end + + + --- Sets flights to by default take-off from the airbase at a cold location, as part of the defense system. + -- @param #AI_A2A_DISPATCHER self + -- @usage: + -- + -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off from a cold parking spot. + -- A2ADispatcher:SetDefaultTakeoffFromParkingCold() + -- + -- @return #AI_A2A_DISPATCHER self + -- + function AI_A2A_DISPATCHER:SetDefaultTakeoffFromParkingCold() + + self:SetDefaultTakeoff( AI_A2A_DISPATCHER.Takeoff.Cold ) + + return self + end + + + --- Sets flights to take-off from the airbase at a cold location, as part of the defense system. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off from a cold parking spot. + -- A2ADispatcher:SetSquadronTakeoffFromParkingCold( "SquadronName" ) + -- + -- @return #AI_A2A_DISPATCHER self + -- + function AI_A2A_DISPATCHER:SetSquadronTakeoffFromParkingCold( SquadronName ) + + self:SetSquadronTakeoff( SquadronName, AI_A2A_DISPATCHER.Takeoff.Cold ) + + return self + end + + + --- Defines the default altitude where airplanes will spawn in the air and take-off as part of the defense system, when the take-off in the air method has been selected. + -- @param #AI_A2A_DISPATCHER self + -- @param #number TakeoffAltitude The altitude in meters above the ground. + -- @usage: + -- + -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) + -- + -- -- Set the default takeoff altitude when taking off in the air. + -- A2ADispatcher:SetDefaultTakeoffInAirAltitude( 2000 ) -- This makes planes start at 2000 meters above the ground. + -- + -- @return #AI_A2A_DISPATCHER self + -- + function AI_A2A_DISPATCHER:SetDefaultTakeoffInAirAltitude( TakeoffAltitude ) + + self.DefenderDefault.TakeoffAltitude = TakeoffAltitude + + return self + end + + --- Defines the default altitude where airplanes will spawn in the air and take-off as part of the defense system, when the take-off in the air method has been selected. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number TakeoffAltitude The altitude in meters above the ground. + -- @usage: + -- + -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) + -- + -- -- Set the default takeoff altitude when taking off in the air. + -- A2ADispatcher:SetSquadronTakeoffInAirAltitude( "SquadronName", 2000 ) -- This makes planes start at 2000 meters above the ground. + -- + -- @return #AI_A2A_DISPATCHER self + -- + function AI_A2A_DISPATCHER:SetSquadronTakeoffInAirAltitude( SquadronName, TakeoffAltitude ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.TakeoffAltitude = TakeoffAltitude + + return self + end + + + --- Defines the default method at which flights will land and despawn as part of the defense system. + -- @param #AI_A2A_DISPATCHER self + -- @param #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown + -- @usage: + -- + -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default despawn near the airbase when returning. + -- A2ADispatcher:SetDefaultLanding( AI_A2A_Dispatcher.Landing.NearAirbase ) + -- + -- -- Let new flights by default despawn after landing land at the runway. + -- A2ADispatcher:SetDefaultLanding( AI_A2A_Dispatcher.Landing.AtRunway ) + -- + -- -- Let new flights by default despawn after landing and parking, and after engine shutdown. + -- A2ADispatcher:SetDefaultLanding( AI_A2A_Dispatcher.Landing.AtEngineShutdown ) + -- + -- @return #AI_A2A_DISPATCHER self + function AI_A2A_DISPATCHER:SetDefaultLanding( Landing ) + + self.DefenderDefault.Landing = Landing + + return self + end + + + --- Defines the method at which flights will land and despawn as part of the defense system. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown + -- @usage: + -- + -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) + -- + -- -- Let new flights despawn near the airbase when returning. + -- A2ADispatcher:SetSquadronLanding( "SquadronName", AI_A2A_Dispatcher.Landing.NearAirbase ) + -- + -- -- Let new flights despawn after landing land at the runway. + -- A2ADispatcher:SetSquadronLanding( "SquadronName", AI_A2A_Dispatcher.Landing.AtRunway ) + -- + -- -- Let new flights despawn after landing and parking, and after engine shutdown. + -- A2ADispatcher:SetSquadronLanding( "SquadronName", AI_A2A_Dispatcher.Landing.AtEngineShutdown ) + -- + -- @return #AI_A2A_DISPATCHER self + function AI_A2A_DISPATCHER:SetSquadronLanding( SquadronName, Landing ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.Landing = Landing + + return self + end + + + --- Gets the default method at which flights will land and despawn as part of the defense system. + -- @param #AI_A2A_DISPATCHER self + -- @return #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown + -- @usage: + -- + -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default despawn near the airbase when returning. + -- local LandingMethod = A2ADispatcher:GetDefaultLanding( AI_A2A_Dispatcher.Landing.NearAirbase ) + -- if LandingMethod == AI_A2A_Dispatcher.Landing.NearAirbase then + -- ... + -- end + -- + function AI_A2A_DISPATCHER:GetDefaultLanding() + + return self.DefenderDefault.Landing + end + + + --- Gets the method at which flights will land and despawn as part of the defense system. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @return #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown + -- @usage: + -- + -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) + -- + -- -- Let new flights despawn near the airbase when returning. + -- local LandingMethod = A2ADispatcher:GetSquadronLanding( "SquadronName", AI_A2A_Dispatcher.Landing.NearAirbase ) + -- if LandingMethod == AI_A2A_Dispatcher.Landing.NearAirbase then + -- ... + -- end + -- + function AI_A2A_DISPATCHER:GetSquadronLanding( SquadronName ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + return DefenderSquadron.Landing or self.DefenderDefault.Landing + end + + + --- Sets flights by default to land and despawn near the airbase in the air, as part of the defense system. + -- @param #AI_A2A_DISPATCHER self + -- @usage: + -- + -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) + -- + -- -- Let flights by default to land near the airbase and despawn. + -- A2ADispatcher:SetDefaultLandingNearAirbase() + -- + -- @return #AI_A2A_DISPATCHER self + function AI_A2A_DISPATCHER:SetDefaultLandingNearAirbase() + + self:SetDefaultLanding( AI_A2A_DISPATCHER.Landing.NearAirbase ) + + return self + end + + + --- Sets flights to land and despawn near the airbase in the air, as part of the defense system. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) + -- + -- -- Let flights to land near the airbase and despawn. + -- A2ADispatcher:SetSquadronLandingNearAirbase( "SquadronName" ) + -- + -- @return #AI_A2A_DISPATCHER self + function AI_A2A_DISPATCHER:SetSquadronLandingNearAirbase( SquadronName ) + + self:SetSquadronLanding( SquadronName, AI_A2A_DISPATCHER.Landing.NearAirbase ) + + return self + end + + + --- Sets flights by default to land and despawn at the runway, as part of the defense system. + -- @param #AI_A2A_DISPATCHER self + -- @usage: + -- + -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) + -- + -- -- Let flights by default land at the runway and despawn. + -- A2ADispatcher:SetDefaultLandingAtRunway() + -- + -- @return #AI_A2A_DISPATCHER self + function AI_A2A_DISPATCHER:SetDefaultLandingAtRunway() + + self:SetDefaultLanding( AI_A2A_DISPATCHER.Landing.AtRunway ) + + return self + end + + + --- Sets flights to land and despawn at the runway, as part of the defense system. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) + -- + -- -- Let flights land at the runway and despawn. + -- A2ADispatcher:SetSquadronLandingAtRunway( "SquadronName" ) + -- + -- @return #AI_A2A_DISPATCHER self + function AI_A2A_DISPATCHER:SetSquadronLandingAtRunway( SquadronName ) + + self:SetSquadronLanding( SquadronName, AI_A2A_DISPATCHER.Landing.AtRunway ) + + return self + end + + + --- Sets flights by default to land and despawn at engine shutdown, as part of the defense system. + -- @param #AI_A2A_DISPATCHER self + -- @usage: + -- + -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) + -- + -- -- Let flights by default land and despawn at engine shutdown. + -- A2ADispatcher:SetDefaultLandingAtEngineShutdown() + -- + -- @return #AI_A2A_DISPATCHER self + function AI_A2A_DISPATCHER:SetDefaultLandingAtEngineShutdown() + + self:SetDefaultLanding( AI_A2A_DISPATCHER.Landing.AtEngineShutdown ) + + return self + end + + + --- Sets flights to land and despawn at engine shutdown, as part of the defense system. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) + -- + -- -- Let flights land and despawn at engine shutdown. + -- A2ADispatcher:SetSquadronLandingAtEngineShutdown( "SquadronName" ) + -- + -- @return #AI_A2A_DISPATCHER self + function AI_A2A_DISPATCHER:SetSquadronLandingAtEngineShutdown( SquadronName ) + + self:SetSquadronLanding( SquadronName, AI_A2A_DISPATCHER.Landing.AtEngineShutdown ) + + return self + end + + --- Set the default fuel treshold when defenders will RTB or Refuel in the air. + -- The fuel treshold is by default set to 15%, which means that an airplane will stay in the air until 15% of its fuel has been consumed. + -- @param #AI_A2A_DISPATCHER self + -- @param #number FuelThreshold A decimal number between 0 and 1, that expresses the %-tage of the treshold of fuel remaining in the tank when the plane will go RTB or Refuel. + -- @return #AI_A2A_DISPATCHER self + -- @usage + -- + -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default fuel treshold. + -- A2ADispatcher:SetDefaultFuelThreshold( 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. + -- + function AI_A2A_DISPATCHER:SetDefaultFuelThreshold( FuelThreshold ) + + self.DefenderDefault.FuelThreshold = FuelThreshold + + return self + end + + + --- Set the fuel treshold for the squadron when defenders will RTB or Refuel in the air. + -- The fuel treshold is by default set to 15%, which means that an airplane will stay in the air until 15% of its fuel has been consumed. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number FuelThreshold A decimal number between 0 and 1, that expresses the %-tage of the treshold of fuel remaining in the tank when the plane will go RTB or Refuel. + -- @return #AI_A2A_DISPATCHER self + -- @usage + -- + -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default fuel treshold. + -- A2ADispatcher:SetSquadronFuelThreshold( "SquadronName", 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. + -- + function AI_A2A_DISPATCHER:SetSquadronFuelThreshold( SquadronName, FuelThreshold ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.FuelThreshold = FuelThreshold + + return self + end + + --- Set the default tanker where defenders will Refuel in the air. + -- @param #AI_A2A_DISPATCHER self + -- @param #string TankerName A string defining the group name of the Tanker as defined within the Mission Editor. + -- @return #AI_A2A_DISPATCHER self + -- @usage + -- + -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default fuel treshold. + -- A2ADispatcher:SetDefaultFuelThreshold( 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. + -- + -- -- Now Setup the default tanker. + -- A2ADispatcher:SetDefaultTanker( "Tanker" ) -- The group name of the tanker is "Tanker" in the Mission Editor. + function AI_A2A_DISPATCHER:SetDefaultTanker( TankerName ) + + self.DefenderDefault.TankerName = TankerName + + return self + end + + + --- Set the squadron tanker where defenders will Refuel in the air. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #string TankerName A string defining the group name of the Tanker as defined within the Mission Editor. + -- @return #AI_A2A_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the squadron fuel treshold. + -- A2ADispatcher:SetSquadronFuelThreshold( "SquadronName", 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. + -- + -- -- Now Setup the squadron tanker. + -- A2ADispatcher:SetSquadronTanker( "SquadronName", "Tanker" ) -- The group name of the tanker is "Tanker" in the Mission Editor. + function AI_A2A_DISPATCHER:SetSquadronTanker( SquadronName, TankerName ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.TankerName = TankerName + + return self + end + + + --- Set the squadron language. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #string Language A string defining the language to be embedded within the miz file. + -- @return #AI_A2A_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- + -- -- Set for English. + -- A2ADispatcher:SetSquadronLanguage( "SquadronName", "EN" ) -- This squadron speaks English. + -- + -- -- Set for Russian. + -- A2ADispatcher:SetSquadronLanguage( "SquadronName", "RU" ) -- This squadron speaks Russian. + function AI_A2A_DISPATCHER:SetSquadronLanguage( SquadronName, Language ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.Language = Language + + if DefenderSquadron.RadioQueue then + DefenderSquadron.RadioQueue:SetLanguage( Language ) + end + + return self + end + + + --- Set the frequency of communication and the mode of communication for voice overs. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number RadioFrequency The frequency of communication. + -- @param #number RadioModulation The modulation of communication. + -- @param #number RadioPower The power in Watts of communication. + function AI_A2A_DISPATCHER:SetSquadronRadioFrequency( SquadronName, RadioFrequency, RadioModulation, RadioPower ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.RadioFrequency = RadioFrequency + DefenderSquadron.RadioModulation = RadioModulation or radio.modulation.AM + DefenderSquadron.RadioPower = RadioPower or 100 + + if DefenderSquadron.RadioQueue then + DefenderSquadron.RadioQueue:Stop() + end + + DefenderSquadron.RadioQueue = nil + + DefenderSquadron.RadioQueue = RADIOSPEECH:New( DefenderSquadron.RadioFrequency, DefenderSquadron.RadioModulation ) + DefenderSquadron.RadioQueue.power = DefenderSquadron.RadioPower + DefenderSquadron.RadioQueue:Start( 0.5 ) + + DefenderSquadron.RadioQueue:SetLanguage( DefenderSquadron.Language ) + end + + --- Add defender to squadron. Resource count will get smaller. + -- @param #AI_A2A_DISPATCHER self + -- @param #AI_A2A_DISPATCHER.Squadron Squadron The squadron. + -- @param Wrapper.Group#GROUP Defender The defender group. + -- @param #number Size Size of the group. + function AI_A2A_DISPATCHER:AddDefenderToSquadron( Squadron, Defender, Size ) + self.Defenders = self.Defenders or {} + local DefenderName = Defender:GetName() + self.Defenders[ DefenderName ] = Squadron + if Squadron.ResourceCount then + Squadron.ResourceCount = Squadron.ResourceCount - Size + end + self:F( { DefenderName = DefenderName, SquadronResourceCount = Squadron.ResourceCount } ) + end + + --- Remove defender from squadron. Resource count will increase. + -- @param #AI_A2A_DISPATCHER self + -- @param #AI_A2A_DISPATCHER.Squadron Squadron The squadron. + -- @param Wrapper.Group#GROUP Defender The defender group. + function AI_A2A_DISPATCHER:RemoveDefenderFromSquadron( Squadron, Defender ) + self.Defenders = self.Defenders or {} + local DefenderName = Defender:GetName() + if Squadron.ResourceCount then + Squadron.ResourceCount = Squadron.ResourceCount + Defender:GetSize() + end + self.Defenders[ DefenderName ] = nil + self:F( { DefenderName = DefenderName, SquadronResourceCount = Squadron.ResourceCount } ) + end + + --- Get squadron from defender. + -- @param #AI_A2A_DISPATCHER self + -- @param Wrapper.Group#GROUP Defender The defender group. + -- @return #AI_A2A_DISPATCHER.Squadron Squadron The squadron. + function AI_A2A_DISPATCHER:GetSquadronFromDefender( Defender ) + self.Defenders = self.Defenders or {} + if Defender ~= nil then + local DefenderName = Defender:GetName() + self:F( { DefenderName = DefenderName } ) + return self.Defenders[ DefenderName ] + else + return nil + end + end + + + --- Creates an SWEEP task when there are targets for it. + -- @param #AI_A2A_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem + -- @return Core.Set#SET_UNIT TargetSetUnit: The target set of units. + function AI_A2A_DISPATCHER:EvaluateSWEEP( DetectedItem ) + self:F( { DetectedItem.ItemID } ) + + local DetectedSet = DetectedItem.Set + local DetectedZone = DetectedItem.Zone + + + if DetectedItem.IsDetected == false then + + -- Here we're doing something advanced... We're copying the DetectedSet. + local TargetSetUnit = SET_UNIT:New() + TargetSetUnit:SetDatabase( DetectedSet ) + TargetSetUnit:FilterOnce() -- Filter but don't do any events!!! Elements are added manually upon each detection. + + return TargetSetUnit + end + + return nil + end + + --- Count number of airborne CAP flights. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName Name of the squadron. + -- @return #number Number of defender CAP groups. + function AI_A2A_DISPATCHER:CountCapAirborne( SquadronName ) + + local CapCount = 0 + + local DefenderSquadron = self.DefenderSquadrons[SquadronName] + if DefenderSquadron then + for AIGroup, DefenderTask in pairs( self:GetDefenderTasks() ) do + if DefenderTask.SquadronName == SquadronName then + if DefenderTask.Type == "CAP" then + if AIGroup and AIGroup:IsAlive() then + -- Check if the CAP is patrolling or engaging. If not, this is not a valid CAP, even if it is alive! + -- The CAP could be damaged, lost control, or out of fuel! + --env.info("FF fsm state "..tostring(DefenderTask.Fsm:GetState())) + if DefenderTask.Fsm:Is( "Patrolling" ) or DefenderTask.Fsm:Is( "Engaging" ) or DefenderTask.Fsm:Is( "Refuelling" ) or DefenderTask.Fsm:Is( "Started" ) then + --env.info("FF capcount "..CapCount) + CapCount = CapCount + 1 + end + end + end + end + end + end + + return CapCount + end + + + --- Count number of engaging defender groups. + -- @param #AI_A2A_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detection object. + -- @return #number Number of defender groups engaging. + function AI_A2A_DISPATCHER:CountDefendersEngaged( AttackerDetection ) + + -- First, count the active AIGroups Units, targetting the DetectedSet + local DefenderCount = 0 + + local DetectedSet = AttackerDetection.Set + --DetectedSet:Flush() + + local DefenderTasks = self:GetDefenderTasks() + + for DefenderGroup, DefenderTask in pairs( DefenderTasks ) do + local Defender = DefenderGroup -- Wrapper.Group#GROUP + local DefenderTaskTarget = DefenderTask.Target --Functional.Detection#DETECTION_BASE.DetectedItem + local DefenderSquadronName = DefenderTask.SquadronName + + if DefenderTaskTarget and DefenderTaskTarget.Index == AttackerDetection.Index then + local Squadron = self:GetSquadron( DefenderSquadronName ) + local SquadronOverhead = Squadron.Overhead or self.DefenderDefault.Overhead + + local DefenderSize = Defender:GetInitialSize() + if DefenderSize then + DefenderCount = DefenderCount + DefenderSize / SquadronOverhead + self:F( "Defender Group Name: " .. Defender:GetName() .. ", Size: " .. DefenderSize ) + else + DefenderCount = 0 + end + end + end + + self:F( { DefenderCount = DefenderCount } ) + + return DefenderCount + end + + --- Count defenders to be engaged if number of attackers larger than number of defenders. + -- @param #AI_A2A_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item. + -- @param #number DefenderCount Number of defenders. + -- @return #table Table of friendly groups. + function AI_A2A_DISPATCHER:CountDefendersToBeEngaged( AttackerDetection, DefenderCount ) + + local Friendlies = nil + + local AttackerSet = AttackerDetection.Set + local AttackerCount = AttackerSet:Count() + + local DefenderFriendlies = self:GetAIFriendliesNearBy( AttackerDetection ) + + for FriendlyDistance, AIFriendly in UTILS.spairs( DefenderFriendlies or {} ) do + -- We only allow to ENGAGE targets as long as the Units on both sides are balanced. + if AttackerCount > DefenderCount then + local Friendly = AIFriendly:GetGroup() -- Wrapper.Group#GROUP + if Friendly and Friendly:IsAlive() then + -- Ok, so we have a friendly near the potential target. + -- Now we need to check if the AIGroup has a Task. + local DefenderTask = self:GetDefenderTask( Friendly ) + if DefenderTask then + -- The Task should be CAP or GCI + if DefenderTask.Type == "CAP" or DefenderTask.Type == "GCI" then + -- If there is no target, then add the AIGroup to the ResultAIGroups for Engagement to the AttackerSet + if DefenderTask.Target == nil then + if DefenderTask.Fsm:Is( "Returning" ) or DefenderTask.Fsm:Is( "Patrolling" ) then + Friendlies = Friendlies or {} + Friendlies[Friendly] = Friendly + DefenderCount = DefenderCount + Friendly:GetSize() + self:F( { Friendly = Friendly:GetName(), FriendlyDistance = FriendlyDistance } ) + end + end + end + end + end + else + break + end + end + + return Friendlies + end + + + --- Activate resource. + -- @param #AI_A2A_DISPATCHER self + -- @param #AI_A2A_DISPATCHER.Squadron DefenderSquadron The defender squadron. + -- @param #number DefendersNeeded Number of defenders needed. Default 4. + -- @return Wrapper.Group#GROUP The defender group. + -- @return #boolean Grouping. + function AI_A2A_DISPATCHER:ResourceActivate( DefenderSquadron, DefendersNeeded ) + + local SquadronName = DefenderSquadron.Name + + DefendersNeeded = DefendersNeeded or 4 + + local DefenderGrouping = DefenderSquadron.Grouping or self.DefenderDefault.Grouping + + DefenderGrouping = ( DefenderGrouping < DefendersNeeded ) and DefenderGrouping or DefendersNeeded + + --env.info(string.format("FF resource activate: Squadron=%s grouping=%d needed=%d visible=%s", SquadronName, DefenderGrouping, DefendersNeeded, tostring(self:IsSquadronVisible( SquadronName )))) + + if self:IsSquadronVisible( SquadronName ) then + + local n=#self.uncontrolled[SquadronName] + + if n>0 then + -- Random number 1,...n + local id=math.random(n) + + -- Pick a random defender group. + local Defender=self.uncontrolled[SquadronName][id].group --Wrapper.Group#GROUP + + -- Start uncontrolled group. + Defender:StartUncontrolled() + + -- Get grouping. + DefenderGrouping=self.uncontrolled[SquadronName][id].grouping + + -- Add defender to squadron. + self:AddDefenderToSquadron( DefenderSquadron, Defender, DefenderGrouping ) + + -- Remove defender from uncontrolled table. + table.remove(self.uncontrolled[SquadronName], id) + + return Defender, DefenderGrouping + else + return nil,0 + end + + -- Here we CAP the new planes. + -- The Resources table is filled in advance. + local TemplateID = math.random( 1, #DefenderSquadron.Spawn ) -- Choose the template. + + --[[ + -- We determine the grouping based on the parameters set. + self:F( { DefenderGrouping = DefenderGrouping } ) + + -- New we will form the group to spawn in. + -- We search for the first free resource matching the template. + local DefenderUnitIndex = 1 + local DefenderCAPTemplate = nil + local DefenderName = nil + for GroupName, DefenderGroup in pairs( DefenderSquadron.Resources[TemplateID] or {} ) do + self:F( { GroupName = GroupName } ) + local DefenderTemplate = _DATABASE:GetGroupTemplate( GroupName ) + if DefenderUnitIndex == 1 then + DefenderCAPTemplate = UTILS.DeepCopy( DefenderTemplate ) + self.DefenderCAPIndex = self.DefenderCAPIndex + 1 + DefenderCAPTemplate.name = SquadronName .. "#" .. self.DefenderCAPIndex .. "#" .. GroupName + DefenderName = DefenderCAPTemplate.name + else + -- Add the unit in the template to the DefenderCAPTemplate. + local DefenderUnitTemplate = DefenderTemplate.units[1] + DefenderCAPTemplate.units[DefenderUnitIndex] = DefenderUnitTemplate + end + DefenderUnitIndex = DefenderUnitIndex + 1 + DefenderSquadron.Resources[TemplateID][GroupName] = nil + if DefenderUnitIndex > DefenderGrouping then + break + end + + end + + if DefenderCAPTemplate then + local TakeoffMethod = self:GetSquadronTakeoff( SquadronName ) + local SpawnGroup = GROUP:Register( DefenderName ) + DefenderCAPTemplate.lateActivation = nil + DefenderCAPTemplate.uncontrolled = nil + local Takeoff = self:GetSquadronTakeoff( SquadronName ) + DefenderCAPTemplate.route.points[1].type = GROUPTEMPLATE.Takeoff[Takeoff][1] -- type + DefenderCAPTemplate.route.points[1].action = GROUPTEMPLATE.Takeoff[Takeoff][2] -- action + local Defender = _DATABASE:Spawn( DefenderCAPTemplate ) + + self:AddDefenderToSquadron( DefenderSquadron, Defender, DefenderGrouping ) + return Defender, DefenderGrouping + end + ]] + + else + + ---------------------------- + --- Squadron not visible --- + ---------------------------- + + local Spawn = DefenderSquadron.Spawn[ math.random( 1, #DefenderSquadron.Spawn ) ] -- Core.Spawn#SPAWN + + if DefenderGrouping then + Spawn:InitGrouping( DefenderGrouping ) + else + Spawn:InitGrouping() + end + + local TakeoffMethod = self:GetSquadronTakeoff( SquadronName ) + + local Defender = Spawn:SpawnAtAirbase( DefenderSquadron.Airbase, TakeoffMethod, DefenderSquadron.TakeoffAltitude or self.DefenderDefault.TakeoffAltitude ) -- Wrapper.Group#GROUP + + self:AddDefenderToSquadron( DefenderSquadron, Defender, DefenderGrouping ) + + return Defender, DefenderGrouping + end + + return nil, nil + end + + --- On after "CAP" event. + -- @param #AI_A2A_DISPATCHER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string SquadronName Name of the squadron. + function AI_A2A_DISPATCHER:onafterCAP( From, Event, To, SquadronName ) + + self:F({SquadronName = SquadronName}) + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + self.DefenderSquadrons[SquadronName].Cap = self.DefenderSquadrons[SquadronName].Cap or {} + + local DefenderSquadron = self:CanCAP( SquadronName ) + + if DefenderSquadron then + + local Cap = DefenderSquadron.Cap + + if Cap then + + local DefenderCAP, DefenderGrouping = self:ResourceActivate( DefenderSquadron ) + + if DefenderCAP then + + local AI_A2A_Fsm = AI_A2A_CAP:New2( DefenderCAP, Cap.EngageMinSpeed, Cap.EngageMaxSpeed, Cap.EngageFloorAltitude, Cap.EngageCeilingAltitude, Cap.EngageAltType, Cap.Zone, Cap.PatrolMinSpeed, Cap.PatrolMaxSpeed, Cap.PatrolFloorAltitude, Cap.PatrolCeilingAltitude, Cap.PatrolAltType ) + AI_A2A_Fsm:SetDispatcher( self ) + AI_A2A_Fsm:SetHomeAirbase( DefenderSquadron.Airbase ) + AI_A2A_Fsm:SetFuelThreshold( DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold, 60 ) + AI_A2A_Fsm:SetDamageThreshold( self.DefenderDefault.DamageThreshold ) + AI_A2A_Fsm:SetDisengageRadius( self.DisengageRadius ) + AI_A2A_Fsm:SetTanker( DefenderSquadron.TankerName or self.DefenderDefault.TankerName ) + if DefenderSquadron.Racetrack or self.DefenderDefault.Racetrack then + AI_A2A_Fsm:SetRaceTrackPattern(DefenderSquadron.RacetrackLengthMin or self.DefenderDefault.RacetrackLengthMin, + DefenderSquadron.RacetrackLengthMax or self.DefenderDefault.RacetrackLengthMax, + DefenderSquadron.RacetrackHeadingMin or self.DefenderDefault.RacetrackHeadingMin, + DefenderSquadron.RacetrackHeadingMax or self.DefenderDefault.RacetrackHeadingMax, + DefenderSquadron.RacetrackDurationMin or self.DefenderDefault.RacetrackDurationMin, + DefenderSquadron.RacetrackDurationMax or self.DefenderDefault.RacetrackDurationMax, + DefenderSquadron.RacetrackCoordinates or self.DefenderDefault.RacetrackCoordinates) + end + AI_A2A_Fsm:Start() + + self:SetDefenderTask( SquadronName, DefenderCAP, "CAP", AI_A2A_Fsm ) + + function AI_A2A_Fsm:onafterTakeoff( DefenderGroup, From, Event, To ) + -- Issue GetCallsign() returns nil, see https://github.com/FlightControl-Master/MOOSE/issues/1228 + if DefenderGroup and DefenderGroup:IsAlive() then + self:F({"CAP Takeoff", DefenderGroup:GetName()}) + --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = AI_A2A_Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + + if Squadron then + if self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. " Wheels up.", DefenderGroup ) + end + AI_A2A_Fsm:__Patrol( 2 ) -- Start Patrolling + end + end + end + + function AI_A2A_Fsm:onafterPatrolRoute( DefenderGroup, From, Event, To ) + if DefenderGroup and DefenderGroup:IsAlive() then + self:F({"CAP PatrolRoute", DefenderGroup:GetName()}) + self:GetParent(self).onafterPatrolRoute( self, DefenderGroup, From, Event, To ) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + if Squadron and self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", patrolling.", DefenderGroup ) + end + + Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) + end + end + + function AI_A2A_Fsm:onafterRTB( DefenderGroup, From, Event, To ) + if DefenderGroup and DefenderGroup:IsAlive() then + self:F({"CAP RTB", DefenderGroup:GetName()}) + + self:GetParent(self).onafterRTB( self, DefenderGroup, From, Event, To ) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + if Squadron and self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. " returning to base.", DefenderGroup ) + end + Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) + end + end + + --- @param #AI_A2A_DISPATCHER self + function AI_A2A_Fsm:onafterHome( Defender, From, Event, To, Action ) + if Defender and Defender:IsAlive() then + self:F({"CAP Home", Defender:GetName()}) + self:GetParent(self).onafterHome( self, Defender, From, Event, To ) + + local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + + if Action and Action == "Destroy" then + Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) + Defender:Destroy() + end + + if Dispatcher:GetSquadronLanding( Squadron.Name ) == AI_A2A_DISPATCHER.Landing.NearAirbase then + Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) + Defender:Destroy() + Dispatcher:ParkDefender( Squadron ) + end + end + end + end + end + end + + end + + + --- On after "ENGAGE" event. + -- @param #AI_A2A_DISPATCHER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item. + -- @param #table Defenders Defenders table. + function AI_A2A_DISPATCHER:onafterENGAGE( From, Event, To, AttackerDetection, Defenders ) + self:F("ENGAGING Detection ID="..tostring(AttackerDetection.ID)) + + if Defenders then + + for DefenderID, Defender in pairs( Defenders ) do + + local Fsm = self:GetDefenderTaskFsm( Defender ) + + Fsm:EngageRoute( AttackerDetection.Set ) -- Engage on the TargetSetUnit + + self:SetDefenderTaskTarget( Defender, AttackerDetection ) + + end + + end + end + + --- On after "GCI" event. + -- @param #AI_A2A_DISPATCHER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item. + -- @param #number DefendersMissing Number of missing defenders. + -- @param #table DefenderFriendlies Friendly defenders. + function AI_A2A_DISPATCHER:onafterGCI( From, Event, To, AttackerDetection, DefendersMissing, DefenderFriendlies ) + + self:F("GCI Detection ID="..tostring(AttackerDetection.ID)) + + self:F( { From, Event, To, AttackerDetection.Index, DefendersMissing, DefenderFriendlies } ) + + local AttackerSet = AttackerDetection.Set + local AttackerUnit = AttackerSet:GetFirst() + + if AttackerUnit and AttackerUnit:IsAlive() then + local AttackerCount = AttackerSet:Count() + local DefenderCount = 0 + + for DefenderID, DefenderGroup in pairs( DefenderFriendlies or {} ) do + + local Fsm = self:GetDefenderTaskFsm( DefenderGroup ) + Fsm:__EngageRoute( 0.1, AttackerSet ) -- Engage on the TargetSetUnit + + self:SetDefenderTaskTarget( DefenderGroup, AttackerDetection ) + + DefenderCount = DefenderCount + DefenderGroup:GetSize() + end + + self:F( { DefenderCount = DefenderCount, DefendersMissing = DefendersMissing } ) + DefenderCount = DefendersMissing + + local ClosestDistance = 0 + local ClosestDefenderSquadronName = nil + + local BreakLoop = false + + while( DefenderCount > 0 and not BreakLoop ) do + + self:F( { DefenderSquadrons = self.DefenderSquadrons } ) + + for SquadronName, DefenderSquadron in pairs( self.DefenderSquadrons or {} ) do + + self:F( { GCI = DefenderSquadron.Gci } ) + + for InterceptID, Intercept in pairs( DefenderSquadron.Gci or {} ) do + + self:F( { DefenderSquadron } ) + local SpawnCoord = DefenderSquadron.Airbase:GetCoordinate() -- Core.Point#COORDINATE + local AttackerCoord = AttackerUnit:GetCoordinate() + local InterceptCoord = AttackerDetection.InterceptCoord + self:F( { InterceptCoord = InterceptCoord } ) + if InterceptCoord then + local InterceptDistance = SpawnCoord:Get2DDistance( InterceptCoord ) + local AirbaseDistance = SpawnCoord:Get2DDistance( AttackerCoord ) + self:F( { InterceptDistance = InterceptDistance, AirbaseDistance = AirbaseDistance, InterceptCoord = InterceptCoord } ) + + if ClosestDistance == 0 or InterceptDistance < ClosestDistance then + + -- Only intercept if the distance to target is smaller or equal to the GciRadius limit. + if AirbaseDistance <= self.GciRadius then + ClosestDistance = InterceptDistance + ClosestDefenderSquadronName = SquadronName + end + end + end + end + end + + if ClosestDefenderSquadronName then + + local DefenderSquadron = self:CanGCI( ClosestDefenderSquadronName ) + + if DefenderSquadron then + + local Gci = self.DefenderSquadrons[ClosestDefenderSquadronName].Gci + + if Gci then + + local DefenderOverhead = DefenderSquadron.Overhead or self.DefenderDefault.Overhead + local DefenderGrouping = DefenderSquadron.Grouping or self.DefenderDefault.Grouping + local DefendersNeeded = math.ceil( DefenderCount * DefenderOverhead ) + + self:F( { Overhead = DefenderOverhead, SquadronOverhead = DefenderSquadron.Overhead , DefaultOverhead = self.DefenderDefault.Overhead } ) + self:F( { Grouping = DefenderGrouping, SquadronGrouping = DefenderSquadron.Grouping, DefaultGrouping = self.DefenderDefault.Grouping } ) + self:F( { DefendersCount = DefenderCount, DefendersNeeded = DefendersNeeded } ) + + -- DefenderSquadron.ResourceCount can have the value nil, which expresses unlimited resources. + -- DefendersNeeded cannot exceed DefenderSquadron.ResourceCount! + if DefenderSquadron.ResourceCount and DefendersNeeded > DefenderSquadron.ResourceCount then + DefendersNeeded = DefenderSquadron.ResourceCount + BreakLoop = true + end + + while ( DefendersNeeded > 0 ) do + + local DefenderGCI, DefenderGrouping = self:ResourceActivate( DefenderSquadron, DefendersNeeded ) + + DefendersNeeded = DefendersNeeded - DefenderGrouping + + if DefenderGCI then + + DefenderCount = DefenderCount - DefenderGrouping / DefenderOverhead + + local Fsm = AI_A2A_GCI:New2( DefenderGCI, Gci.EngageMinSpeed, Gci.EngageMaxSpeed, Gci.EngageFloorAltitude, Gci.EngageCeilingAltitude, Gci.EngageAltType ) + Fsm:SetDispatcher( self ) + Fsm:SetHomeAirbase( DefenderSquadron.Airbase ) + Fsm:SetFuelThreshold( DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold, 60 ) + Fsm:SetDamageThreshold( self.DefenderDefault.DamageThreshold ) + Fsm:SetDisengageRadius( self.DisengageRadius ) + Fsm:Start() + + + self:SetDefenderTask( ClosestDefenderSquadronName, DefenderGCI, "GCI", Fsm, AttackerDetection ) + + + function Fsm:onafterTakeoff( DefenderGroup, From, Event, To ) + self:F({"GCI Birth", DefenderGroup:GetName()}) + --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + local DefenderTarget = Dispatcher:GetDefenderTaskTarget( DefenderGroup ) + + if DefenderTarget then + if Squadron.Language == "EN" and self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. " wheels up.", DefenderGroup ) + elseif Squadron.Language == "RU" and self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. " колёса вверх.", DefenderGroup ) + end + --Fsm:__Engage( 2, DefenderTarget.Set ) -- Engage on the TargetSetUnit + Fsm:EngageRoute( DefenderTarget.Set ) -- Engage on the TargetSetUnit + end + end + + function Fsm:onafterEngageRoute( DefenderGroup, From, Event, To, AttackSetUnit ) + self:F({"GCI Route", DefenderGroup:GetName()}) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + + if Squadron and AttackSetUnit:Count() > 0 then + local FirstUnit = AttackSetUnit:GetFirst() + local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE + + if Squadron.Language == "EN" and self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", intercepting bogeys at " .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup ) + elseif Squadron.Language == "RU" and self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", перехватывая боги в " .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup ) + elseif Squadron.Language == "DE" and self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", Eindringlinge abfangen bei" .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup ) + end + end + self:GetParent( Fsm ).onafterEngageRoute( self, DefenderGroup, From, Event, To, AttackSetUnit ) + end + + function Fsm:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit ) + self:F({"GCI Engage", DefenderGroup:GetName()}) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + + if Squadron and AttackSetUnit:Count() > 0 then + local FirstUnit = AttackSetUnit:GetFirst() + local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE + + if Squadron.Language == "EN" and self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", engaging bogeys at " .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup ) + elseif Squadron.Language == "RU" and self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", задействуя боги в " .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup ) + end + end + self:GetParent( Fsm ).onafterEngage( self, DefenderGroup, From, Event, To, AttackSetUnit ) + end + + function Fsm:onafterRTB( DefenderGroup, From, Event, To ) + self:F({"GCI RTB", DefenderGroup:GetName()}) + self:GetParent(self).onafterRTB( self, DefenderGroup, From, Event, To ) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + + if Squadron then + if Squadron.Language == "EN" and self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. " returning to base.", DefenderGroup ) + elseif Squadron.Language == "RU" and self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", возвращение на базу.", DefenderGroup ) + end + end + Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) + end + + --- @param #AI_A2A_DISPATCHER self + function Fsm:onafterLostControl( Defender, From, Event, To ) + self:F({"GCI LostControl", Defender:GetName()}) + self:GetParent(self).onafterHome( self, Defender, From, Event, To ) + + local Dispatcher = Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + if Defender:IsAboveRunway() then + Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) + Defender:Destroy() + end + end + + --- @param #AI_A2A_DISPATCHER self + function Fsm:onafterHome( DefenderGroup, From, Event, To, Action ) + self:F({"GCI Home", DefenderGroup:GetName()}) + self:GetParent(self).onafterHome( self, DefenderGroup, From, Event, To ) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + + if Squadron.Language == "EN" and self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. " landing at base.", DefenderGroup ) + elseif Squadron.Language == "RU" and self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", посадка на базу.", DefenderGroup ) + end + + if Action and Action == "Destroy" then + Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup ) + DefenderGroup:Destroy() + end + + if Dispatcher:GetSquadronLanding( Squadron.Name ) == AI_A2A_DISPATCHER.Landing.NearAirbase then + Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup ) + DefenderGroup:Destroy() + Dispatcher:ParkDefender( Squadron ) + end + end + + end -- if DefenderGCI then + end -- while ( DefendersNeeded > 0 ) do + end + else + -- No more resources, try something else. + -- Subject for a later enhancement to try to depart from another squadron and disable this one. + BreakLoop = true + break + end + else + -- There isn't any closest airbase anymore, break the loop. + break + end + end -- if DefenderSquadron then + end -- if AttackerUnit + end + + + + --- Creates an ENGAGE task when there are human friendlies airborne near the targets. + -- @param #AI_A2A_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem The detected item. + -- @return Core.Set#SET_UNIT TargetSetUnit: The target set of units or nil. + function AI_A2A_DISPATCHER:EvaluateENGAGE( DetectedItem ) + self:F( { DetectedItem.ItemID } ) + + -- First, count the active AIGroups Units, targetting the DetectedSet + local DefenderCount = self:CountDefendersEngaged( DetectedItem ) + local DefenderGroups = self:CountDefendersToBeEngaged( DetectedItem, DefenderCount ) + + self:F( { DefenderCount = DefenderCount } ) + + -- Only allow ENGAGE when: + -- 1. There are friendly units near the detected attackers. + -- 2. There is sufficient fuel + -- 3. There is sufficient ammo + -- 4. The plane is not damaged + if DefenderGroups and DetectedItem.IsDetected == true then + return DefenderGroups + end + + return nil + end + + --- Creates an GCI task when there are targets for it. + -- @param #AI_A2A_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem The detected item. + -- @return Core.Set#SET_UNIT TargetSetUnit: The target set of units or nil if there are no targets to be set. + -- @return #table Table of friendly groups. + function AI_A2A_DISPATCHER:EvaluateGCI( DetectedItem ) + self:F( { DetectedItem.ItemID } ) + + local AttackerSet = DetectedItem.Set + local AttackerCount = AttackerSet:Count() + + -- First, count the active AIGroups Units, targetting the DetectedSet + local DefenderCount = self:CountDefendersEngaged( DetectedItem ) + local DefendersMissing = AttackerCount - DefenderCount + self:F( { AttackerCount = AttackerCount, DefenderCount = DefenderCount, DefendersMissing = DefendersMissing } ) + + local Friendlies = self:CountDefendersToBeEngaged( DetectedItem, DefenderCount ) + + if DetectedItem.IsDetected == true then + + return DefendersMissing, Friendlies + end + + return nil, nil + end + + + --- Assigns A2G AI Tasks in relation to the detected items. + -- @param #AI_A2G_DISPATCHER self + function AI_A2A_DISPATCHER:Order( DetectedItem ) + + local detection=self.Detection -- Functional.Detection#DETECTION_AREAS + + local ShortestDistance = 999999999 + + -- Get coordinate (or nil). + local AttackCoordinate = detection:GetDetectedItemCoordinate( DetectedItem ) + + -- Issue https://github.com/FlightControl-Master/MOOSE/issues/1232 + if AttackCoordinate then + + for DefenderSquadronName, DefenderSquadron in pairs( self.DefenderSquadrons ) do + + self:T( { DefenderSquadron = DefenderSquadron.Name } ) + + local Airbase = DefenderSquadron.Airbase + local AirbaseCoordinate = Airbase:GetCoordinate() + + local EvaluateDistance = AttackCoordinate:Get2DDistance( AirbaseCoordinate ) + + if EvaluateDistance <= ShortestDistance then + ShortestDistance = EvaluateDistance + end + end + + end + + return ShortestDistance + end + + + --- Shows the tactical display. + -- @param #AI_A2A_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE Detection The detection created by the @{Functional.Detection#DETECTION_BASE} derived object. + function AI_A2A_DISPATCHER:ShowTacticalDisplay( Detection ) + + local AreaMsg = {} + local TaskMsg = {} + local ChangeMsg = {} + + local TaskReport = REPORT:New() + + local Report = REPORT:New( "Tactical Overview:" ) + + local DefenderGroupCount = 0 + + -- Now that all obsolete tasks are removed, loop through the detected targets. + --for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do + for DetectedItemID, DetectedItem in UTILS.spairs( Detection:GetDetectedItems(), function( t, a, b ) return self:Order(t[a]) < self:Order(t[b]) end ) do + + local DetectedItem = DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem + local DetectedSet = DetectedItem.Set -- Core.Set#SET_UNIT + local DetectedCount = DetectedSet:Count() + local DetectedZone = DetectedItem.Zone + + self:F( { "Target ID", DetectedItem.ItemID } ) + DetectedSet:Flush( self ) + + local DetectedID = DetectedItem.ID + local DetectionIndex = DetectedItem.Index + local DetectedItemChanged = DetectedItem.Changed + + -- Show tactical situation + Report:Add( string.format( "\n- Target %s (%s): (#%d) %s" , DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Set:GetObjectNames() ) ) + for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do + local Defender = Defender -- Wrapper.Group#GROUP + if DefenderTask.Target and DefenderTask.Target.Index == DetectedItem.Index then + if Defender and Defender:IsAlive() then + DefenderGroupCount = DefenderGroupCount + 1 + local Fuel = Defender:GetFuelMin() * 100 + local Damage = Defender:GetLife() / Defender:GetLife0() * 100 + Report:Add( string.format( " - %s*%d/%d (%s - %s): (#%d) F: %3d, D:%3d - %s", + Defender:GetName(), + Defender:GetSize(), + Defender:GetInitialSize(), + DefenderTask.Type, + DefenderTask.Fsm:GetState(), + Defender:GetSize(), + Fuel, + Damage, + Defender:HasTask() == true and "Executing" or "Idle" ) ) + end + end + end + end + + Report:Add( "\n- No Targets:") + local TaskCount = 0 + for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do + TaskCount = TaskCount + 1 + local Defender = Defender -- Wrapper.Group#GROUP + if not DefenderTask.Target then + if Defender:IsAlive() then + local DefenderHasTask = Defender:HasTask() + local Fuel = Defender:GetFuelMin() * 100 + local Damage = Defender:GetLife() / Defender:GetLife0() * 100 + DefenderGroupCount = DefenderGroupCount + 1 + Report:Add( string.format( " - %s*%d/%d (%s - %s): (#%d) F: %3d, D:%3d - %s", + Defender:GetName(), + Defender:GetSize(), + Defender:GetInitialSize(), + DefenderTask.Type, + DefenderTask.Fsm:GetState(), + Defender:GetSize(), + Fuel, + Damage, + Defender:HasTask() == true and "Executing" or "Idle" ) ) + end + end + end + Report:Add( string.format( "\n- %d Tasks - %d Defender Groups", TaskCount, DefenderGroupCount ) ) + + self:F( Report:Text( "\n" ) ) + trigger.action.outText( Report:Text( "\n" ), 25 ) + + return true + + end + + --- Assigns A2A AI Tasks in relation to the detected items. + -- @param #AI_A2A_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE Detection The detection created by the @{Functional.Detection#DETECTION_BASE} derived object. + -- @return #boolean Return true if you want the task assigning to continue... false will cancel the loop. + function AI_A2A_DISPATCHER:ProcessDetected( Detection ) + + local AreaMsg = {} + local TaskMsg = {} + local ChangeMsg = {} + + local TaskReport = REPORT:New() + + + for AIGroup, DefenderTask in pairs( self:GetDefenderTasks() ) do + local AIGroup = AIGroup -- Wrapper.Group#GROUP + if not AIGroup:IsAlive() then + local DefenderTaskFsm = self:GetDefenderTaskFsm( AIGroup ) + self:F( { Defender = AIGroup:GetName(), DefenderState = DefenderTaskFsm:GetState() } ) + if not DefenderTaskFsm:Is( "Started" ) then + self:ClearDefenderTask( AIGroup ) + end + else + if DefenderTask.Target then + local AttackerItem = Detection:GetDetectedItemByIndex( DefenderTask.Target.Index ) + if not AttackerItem then + self:F( { "Removing obsolete Target:", DefenderTask.Target.Index } ) + self:ClearDefenderTaskTarget( AIGroup ) + else + if DefenderTask.Target.Set then + local AttackerCount = DefenderTask.Target.Set:Count() + if AttackerCount == 0 then + self:F( { "All Targets destroyed in Target, removing:", DefenderTask.Target.Index } ) + self:ClearDefenderTaskTarget( AIGroup ) + end + end + end + end + end + end + + local Report = REPORT:New( "Tactical Overviews" ) + + local DefenderGroupCount = 0 + + -- Now that all obsolete tasks are removed, loop through the detected targets. + -- Closest detected targets to be considered first! + --for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do + for DetectedItemID, DetectedItem in UTILS.spairs( Detection:GetDetectedItems(), function( t, a, b ) return self:Order(t[a]) < self:Order(t[b]) end ) do + + local DetectedItem = DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem + local DetectedSet = DetectedItem.Set -- Core.Set#SET_UNIT + local DetectedCount = DetectedSet:Count() + local DetectedZone = DetectedItem.Zone + + self:F( { "Target ID", DetectedItem.ItemID } ) + DetectedSet:Flush( self ) + + local DetectedID = DetectedItem.ID + local DetectionIndex = DetectedItem.Index + local DetectedItemChanged = DetectedItem.Changed + + do + local Friendlies = self:EvaluateENGAGE( DetectedItem ) -- Returns a SetUnit if there are targets to be GCIed... + if Friendlies then + self:F( { AIGroups = Friendlies } ) + self:ENGAGE( DetectedItem, Friendlies ) + end + end + + do + local DefendersMissing, Friendlies = self:EvaluateGCI( DetectedItem ) + if DefendersMissing and DefendersMissing > 0 then + self:F( { DefendersMissing = DefendersMissing } ) + self:GCI( DetectedItem, DefendersMissing, Friendlies ) + end + end + end + + if self.TacticalDisplay then + self:ShowTacticalDisplay( Detection ) + end + + return true + end + +end + +do + + --- Calculates which HUMAN friendlies are nearby the area. + -- @param #AI_A2A_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem The detected item. + -- @return #number, Core.Report#REPORT The amount of friendlies and a text string explaining which friendlies of which type. + function AI_A2A_DISPATCHER:GetPlayerFriendliesNearBy( DetectedItem ) + + local DetectedSet = DetectedItem.Set + local PlayersNearBy = self.Detection:GetPlayersNearBy( DetectedItem ) + + local PlayerTypes = {} + local PlayersCount = 0 + + if PlayersNearBy then + local DetectedTreatLevel = DetectedSet:CalculateThreatLevelA2G() + for PlayerUnitName, PlayerUnitData in pairs( PlayersNearBy ) do + local PlayerUnit = PlayerUnitData -- Wrapper.Unit#UNIT + local PlayerName = PlayerUnit:GetPlayerName() + --self:F( { PlayerName = PlayerName, PlayerUnit = PlayerUnit } ) + if PlayerUnit:IsAirPlane() and PlayerName ~= nil then + local FriendlyUnitThreatLevel = PlayerUnit:GetThreatLevel() + PlayersCount = PlayersCount + 1 + local PlayerType = PlayerUnit:GetTypeName() + PlayerTypes[PlayerName] = PlayerType + if DetectedTreatLevel < FriendlyUnitThreatLevel + 2 then + end + end + end + + end + + --self:F( { PlayersCount = PlayersCount } ) + + local PlayerTypesReport = REPORT:New() + + if PlayersCount > 0 then + for PlayerName, PlayerType in pairs( PlayerTypes ) do + PlayerTypesReport:Add( string.format('"%s" in %s', PlayerName, PlayerType ) ) + end + else + PlayerTypesReport:Add( "-" ) + end + + + return PlayersCount, PlayerTypesReport + end + + --- Calculates which friendlies are nearby the area. + -- @param #AI_A2A_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem The detected item. + -- @return #number, Core.Report#REPORT The amount of friendlies and a text string explaining which friendlies of which type. + function AI_A2A_DISPATCHER:GetFriendliesNearBy( DetectedItem ) + + local DetectedSet = DetectedItem.Set + local FriendlyUnitsNearBy = self.Detection:GetFriendliesNearBy( DetectedItem ) + + local FriendlyTypes = {} + local FriendliesCount = 0 + + if FriendlyUnitsNearBy then + local DetectedTreatLevel = DetectedSet:CalculateThreatLevelA2G() + for FriendlyUnitName, FriendlyUnitData in pairs( FriendlyUnitsNearBy ) do + local FriendlyUnit = FriendlyUnitData -- Wrapper.Unit#UNIT + if FriendlyUnit:IsAirPlane() then + local FriendlyUnitThreatLevel = FriendlyUnit:GetThreatLevel() + FriendliesCount = FriendliesCount + 1 + local FriendlyType = FriendlyUnit:GetTypeName() + FriendlyTypes[FriendlyType] = FriendlyTypes[FriendlyType] and ( FriendlyTypes[FriendlyType] + 1 ) or 1 + if DetectedTreatLevel < FriendlyUnitThreatLevel + 2 then + end + end + end + + end + + --self:F( { FriendliesCount = FriendliesCount } ) + + local FriendlyTypesReport = REPORT:New() + + if FriendliesCount > 0 then + for FriendlyType, FriendlyTypeCount in pairs( FriendlyTypes ) do + FriendlyTypesReport:Add( string.format("%d of %s", FriendlyTypeCount, FriendlyType ) ) + end + else + FriendlyTypesReport:Add( "-" ) + end + + + return FriendliesCount, FriendlyTypesReport + end + + --- Schedules a new CAP for the given SquadronName. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName The squadron name. + function AI_A2A_DISPATCHER:SchedulerCAP( SquadronName ) + self:CAP( SquadronName ) + end + +end + +do + + --- @type AI_A2A_GCICAP + -- @extends #AI_A2A_DISPATCHER + + --- Create an automatic air defence system for a coalition setting up GCI and CAP air defenses. + -- The class derives from @{#AI_A2A_DISPATCHER} and thus, all the methods that are defined in the @{#AI_A2A_DISPATCHER} class, can be used also in AI\_A2A\_GCICAP. + -- + -- === + -- + -- # Demo Missions + -- + -- ### [AI\_A2A\_GCICAP for Caucasus](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-200%20-%20AI_A2A%20-%20GCICAP%20Demonstration) + -- ### [AI\_A2A\_GCICAP for NTTR](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-210%20-%20NTTR%20AI_A2A_GCICAP%20Demonstration) + -- ### [AI\_A2A\_GCICAP for Normandy](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-220%20-%20NORMANDY%20AI_A2A_GCICAP%20Demonstration) + -- + -- ### [AI\_A2A\_GCICAP for beta testers](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AID%20-%20AI%20Dispatching) + -- + -- === + -- + -- # YouTube Channel + -- + -- ### [DCS WORLD - MOOSE - A2A GCICAP - Build an automatic A2A Defense System](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl0S4KMNUUJpaUs6zZHjLKNx) + -- + -- === + -- + -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia3.JPG) + -- + -- AI\_A2A\_GCICAP includes automatic spawning of Combat Air Patrol aircraft (CAP) and Ground Controlled Intercept aircraft (GCI) in response to enemy + -- air movements that are detected by an airborne or ground based radar network. + -- + -- With a little time and with a little work it provides the mission designer with a convincing and completely automatic air defence system. + -- + -- The AI_A2A_GCICAP provides a lightweight configuration method using the mission editor. Within a very short time, and with very little coding, + -- the mission designer is able to configure a complete A2A defense system for a coalition using the DCS Mission Editor available functions. + -- Using the DCS Mission Editor, you define borders of the coalition which are guarded by GCICAP, + -- configure airbases to belong to the coalition, define squadrons flying certain types of planes or payloads per airbase, and define CAP zones. + -- **Very little lua needs to be applied, a one liner**, which is fully explained below, which can be embedded + -- right in a DO SCRIPT trigger action or in a larger DO SCRIPT FILE trigger action. + -- + -- CAP flights will take off and proceed to designated CAP zones where they will remain on station until the ground radars direct them to intercept + -- detected enemy aircraft or they run short of fuel and must return to base (RTB). + -- + -- When a CAP flight leaves their zone to perform a GCI or return to base a new CAP flight will spawn to take its place. + -- If all CAP flights are engaged or RTB then additional GCI interceptors will scramble to intercept unengaged enemy aircraft under ground radar control. + -- + -- In short it is a plug in very flexible and configurable air defence module for DCS World. + -- + -- === + -- + -- # The following actions need to be followed when using AI\_A2A\_GCICAP in your mission: + -- + -- ## 1) Configure a working AI\_A2A\_GCICAP defense system for ONE coalition. + -- + -- ### 1.1) Define which airbases are for which coalition. + -- + -- ![Mission Editor Action](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_GCICAP-ME_1.JPG) + -- + -- Color the airbases red or blue. You can do this by selecting the airbase on the map, and select the coalition blue or red. + -- + -- ### 1.2) Place groups of units given a name starting with a **EWR prefix** of your choice to build your EWR network. + -- + -- ![Mission Editor Action](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_GCICAP-ME_2.JPG) + -- + -- **All EWR groups starting with the EWR prefix (text) will be included in the detection system.** + -- + -- An EWR network, or, Early Warning Radar network, is used to early detect potential airborne targets and to understand the position of patrolling targets of the enemy. + -- Typically EWR networks are setup using 55G6 EWR, 1L13 EWR, Hawk sr and Patriot str ground based radar units. + -- These radars have different ranges and 55G6 EWR and 1L13 EWR radars are Eastern Bloc units (eg Russia, Ukraine, Georgia) while the Hawk and Patriot radars are Western (eg US). + -- Additionally, ANY other radar capable unit can be part of the EWR network! + -- Also AWACS airborne units, planes, helicopters can help to detect targets, as long as they have radar. + -- The position of these units is very important as they need to provide enough coverage + -- to pick up enemy aircraft as they approach so that CAP and GCI flights can be tasked to intercept them. + -- + -- Additionally in a hot war situation where the border is no longer respected the placement of radars has a big effect on how fast the war escalates. + -- For example if they are a long way forward and can detect enemy planes on the ground and taking off + -- they will start to vector CAP and GCI flights to attack them straight away which will immediately draw a response from the other coalition. + -- Having the radars further back will mean a slower escalation because fewer targets will be detected and + -- therefore less CAP and GCI flights will spawn and this will tend to make just the border area active rather than a melee over the whole map. + -- It all depends on what the desired effect is. + -- + -- EWR networks are **dynamically maintained**. By defining in a **smart way the names or name prefixes of the groups** with EWR capable units, these groups will be **automatically added or deleted** from the EWR network, + -- increasing or decreasing the radar coverage of the Early Warning System. + -- + -- ### 1.3) Place Airplane or Helicopter Groups with late activation switched on above the airbases to define Squadrons. + -- + -- ![Mission Editor Action](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_GCICAP-ME_3.JPG) + -- + -- These are **templates**, with a given name starting with a **Template prefix** above each airbase that you wanna have a squadron. + -- These **templates** need to be within 1.5km from the airbase center. They don't need to have a slot at the airplane, they can just be positioned above the airbase, + -- without a route, and should only have ONE unit. + -- + -- ![Mission Editor Action](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_GCICAP-ME_4.JPG) + -- + -- **All airplane or helicopter groups that are starting with any of the choosen Template Prefixes will result in a squadron created at the airbase.** + -- + -- ### 1.4) Place floating helicopters to create the CAP zones defined by its route points. + -- + -- ![Mission Editor Action](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_GCICAP-ME_5.JPG) + -- + -- **All airplane or helicopter groups that are starting with any of the choosen Template Prefixes will result in a squadron created at the airbase.** + -- + -- The helicopter indicates the start of the CAP zone. + -- The route points define the form of the CAP zone polygon. + -- + -- ![Mission Editor Action](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_GCICAP-ME_6.JPG) + -- + -- **The place of the helicopter is important, as the airbase closest to the helicopter will be the airbase from where the CAP planes will take off for CAP.** + -- + -- ## 2) There are a lot of defaults set, which can be further modified using the methods in @{#AI_A2A_DISPATCHER}: + -- + -- ### 2.1) Planes are taking off in the air from the airbases. + -- + -- This prevents airbases to get cluttered with airplanes taking off, it also reduces the risk of human players colliding with taxiiing airplanes, + -- resulting in the airbase to halt operations. + -- + -- You can change the way how planes take off by using the inherited methods from AI\_A2A\_DISPATCHER: + -- + -- * @{#AI_A2A_DISPATCHER.SetSquadronTakeoff}() is the generic configuration method to control takeoff from the air, hot, cold or from the runway. See the method for further details. + -- * @{#AI_A2A_DISPATCHER.SetSquadronTakeoffInAir}() will spawn new aircraft from the squadron directly in the air. + -- * @{#AI_A2A_DISPATCHER.SetSquadronTakeoffFromParkingCold}() will spawn new aircraft in without running engines at a parking spot at the airfield. + -- * @{#AI_A2A_DISPATCHER.SetSquadronTakeoffFromParkingHot}() will spawn new aircraft in with running engines at a parking spot at the airfield. + -- * @{#AI_A2A_DISPATCHER.SetSquadronTakeoffFromRunway}() will spawn new aircraft at the runway at the airfield. + -- + -- Use these methods to fine-tune for specific airfields that are known to create bottlenecks, or have reduced airbase efficiency. + -- The more and the longer aircraft need to taxi at an airfield, the more risk there is that: + -- + -- * aircraft will stop waiting for each other or for a landing aircraft before takeoff. + -- * aircraft may get into a "dead-lock" situation, where two aircraft are blocking each other. + -- * aircraft may collide at the airbase. + -- * aircraft may be awaiting the landing of a plane currently in the air, but never lands ... + -- + -- Currently within the DCS engine, the airfield traffic coordination is erroneous and contains a lot of bugs. + -- If you experience while testing problems with aircraft take-off or landing, please use one of the above methods as a solution to workaround these issues! + -- + -- ### 2.2) Planes return near the airbase or will land if damaged. + -- + -- When damaged airplanes return to the airbase, they will be routed and will dissapear in the air when they are near the airbase. + -- There are exceptions to this rule, airplanes that aren't "listening" anymore due to damage or out of fuel, will return to the airbase and land. + -- + -- You can change the way how planes land by using the inherited methods from AI\_A2A\_DISPATCHER: + -- + -- * @{#AI_A2A_DISPATCHER.SetSquadronLanding}() is the generic configuration method to control landing, namely despawn the aircraft near the airfield in the air, right after landing, or at engine shutdown. + -- * @{#AI_A2A_DISPATCHER.SetSquadronLandingNearAirbase}() will despawn the returning aircraft in the air when near the airfield. + -- * @{#AI_A2A_DISPATCHER.SetSquadronLandingAtRunway}() will despawn the returning aircraft directly after landing at the runway. + -- * @{#AI_A2A_DISPATCHER.SetSquadronLandingAtEngineShutdown}() will despawn the returning aircraft when the aircraft has returned to its parking spot and has turned off its engines. + -- + -- You can use these methods to minimize the airbase coodination overhead and to increase the airbase efficiency. + -- When there are lots of aircraft returning for landing, at the same airbase, the takeoff process will be halted, which can cause a complete failure of the + -- A2A defense system, as no new CAP or GCI planes can takeoff. + -- Note that the method @{#AI_A2A_DISPATCHER.SetSquadronLandingNearAirbase}() will only work for returning aircraft, not for damaged or out of fuel aircraft. + -- Damaged or out-of-fuel aircraft are returning to the nearest friendly airbase and will land, and are out of control from ground control. + -- + -- ### 2.3) CAP operations setup for specific airbases, will be executed with the following parameters: + -- + -- * The altitude will range between 6000 and 10000 meters. + -- * The CAP speed will vary between 500 and 800 km/h. + -- * The engage speed between 800 and 1200 km/h. + -- + -- You can change or add a CAP zone by using the inherited methods from AI\_A2A\_DISPATCHER: + -- + -- The method @{#AI_A2A_DISPATCHER.SetSquadronCap}() defines a CAP execution for a squadron. + -- + -- Setting-up a CAP zone also requires specific parameters: + -- + -- * The minimum and maximum altitude + -- * The minimum speed and maximum patrol speed + -- * The minimum and maximum engage speed + -- * The type of altitude measurement + -- + -- These define how the squadron will perform the CAP while partrolling. Different terrain types requires different types of CAP. + -- + -- The @{#AI_A2A_DISPATCHER.SetSquadronCapInterval}() method specifies **how much** and **when** CAP flights will takeoff. + -- + -- It is recommended not to overload the air defense with CAP flights, as these will decrease the performance of the overall system. + -- + -- For example, the following setup will create a CAP for squadron "Sochi": + -- + -- A2ADispatcher:SetSquadronCap( "Sochi", CAPZoneWest, 4000, 8000, 600, 800, 800, 1200, "BARO" ) + -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) + -- + -- ### 2.4) Each airbase will perform GCI when required, with the following parameters: + -- + -- * The engage speed is between 800 and 1200 km/h. + -- + -- You can change or add a GCI parameters by using the inherited methods from AI\_A2A\_DISPATCHER: + -- + -- The method @{#AI_A2A_DISPATCHER.SetSquadronGci}() defines a GCI execution for a squadron. + -- + -- Setting-up a GCI readiness also requires specific parameters: + -- + -- * The minimum speed and maximum patrol speed + -- + -- Essentially this controls how many flights of GCI aircraft can be active at any time. + -- Note allowing large numbers of active GCI flights can adversely impact mission performance on low or medium specification hosts/servers. + -- GCI needs to be setup at strategic airbases. Too far will mean that the aircraft need to fly a long way to reach the intruders, + -- too short will mean that the intruders may have alraedy passed the ideal interception point! + -- + -- For example, the following setup will create a GCI for squadron "Sochi": + -- + -- A2ADispatcher:SetSquadronGci( "Mozdok", 900, 1200 ) + -- + -- ### 2.5) Grouping or detected targets. + -- + -- Detected targets are constantly re-grouped, that is, when certain detected aircraft are moving further than the group radius, then these aircraft will become a separate + -- group being detected. + -- + -- Targets will be grouped within a radius of 30km by default. + -- + -- The radius indicates that detected targets need to be grouped within a radius of 30km. + -- The grouping radius should not be too small, but also depends on the types of planes and the era of the simulation. + -- Fast planes like in the 80s, need a larger radius than WWII planes. + -- Typically I suggest to use 30000 for new generation planes and 10000 for older era aircraft. + -- + -- ## 3) Additional notes: + -- + -- In order to create a two way A2A defense system, **two AI\_A2A\_GCICAP defense systems must need to be created**, for each coalition one. + -- Each defense system needs its own EWR network setup, airplane templates and CAP configurations. + -- + -- This is a good implementation, because maybe in the future, more coalitions may become available in DCS world. + -- + -- ## 4) Coding examples how to use the AI\_A2A\_GCICAP class: + -- + -- ### 4.1) An easy setup: + -- + -- -- Setup the AI_A2A_GCICAP dispatcher for one coalition, and initialize it. + -- GCI_Red = AI_A2A_GCICAP:New( "EWR CCCP", "SQUADRON CCCP", "CAP CCCP", 2 ) + -- -- + -- The following parameters were given to the :New method of AI_A2A_GCICAP, and mean the following: + -- + -- * `"EWR CCCP"`: Groups of the blue coalition are placed that define the EWR network. These groups start with the name `EWR CCCP`. + -- * `"SQUADRON CCCP"`: Late activated Groups objects of the red coalition are placed above the relevant airbases that will contain these templates in the squadron. + -- These late activated Groups start with the name `SQUADRON CCCP`. Each Group object contains only one Unit, and defines the weapon payload, skin and skill level. + -- * `"CAP CCCP"`: CAP Zones are defined using floating, late activated Helicopter Group objects, where the route points define the route of the polygon of the CAP Zone. + -- These Helicopter Group objects start with the name `CAP CCCP`, and will be the locations wherein CAP will be performed. + -- * `2` Defines how many CAP airplanes are patrolling in each CAP zone defined simulateneously. + -- + -- + -- ### 4.2) A more advanced setup: + -- + -- -- Setup the AI_A2A_GCICAP dispatcher for the blue coalition. + -- + -- A2A_GCICAP_Blue = AI_A2A_GCICAP:New( { "BLUE EWR" }, { "104th", "105th", "106th" }, { "104th CAP" }, 4 ) + -- + -- The following parameters for the :New method have the following meaning: + -- + -- * `{ "BLUE EWR" }`: An array of the group name prefixes of the groups of the blue coalition are placed that define the EWR network. These groups start with the name `BLUE EWR`. + -- * `{ "104th", "105th", "106th" } `: An array of the group name prefixes of the Late activated Groups objects of the blue coalition are + -- placed above the relevant airbases that will contain these templates in the squadron. + -- These late activated Groups start with the name `104th` or `105th` or `106th`. + -- * `{ "104th CAP" }`: An array of the names of the CAP zones are defined using floating, late activated helicopter group objects, + -- where the route points define the route of the polygon of the CAP Zone. + -- These Helicopter Group objects start with the name `104th CAP`, and will be the locations wherein CAP will be performed. + -- * `4` Defines how many CAP airplanes are patrolling in each CAP zone defined simulateneously. + -- + -- @field #AI_A2A_GCICAP + AI_A2A_GCICAP = { + ClassName = "AI_A2A_GCICAP", + Detection = nil, + } + + + --- AI_A2A_GCICAP constructor. + -- @param #AI_A2A_GCICAP self + -- @param #string EWRPrefixes A list of prefixes that of groups that setup the Early Warning Radar network. + -- @param #string TemplatePrefixes A list of template prefixes. + -- @param #string CapPrefixes A list of CAP zone prefixes (polygon zones). + -- @param #number CapLimit A number of how many CAP maximum will be spawned. + -- @param #number GroupingRadius The radius in meters wherein detected planes are being grouped as one target area. + -- For airplanes, 6000 (6km) is recommended, and is also the default value of this parameter. + -- @param #number EngageRadius The radius in meters wherein detected airplanes will be engaged by airborne defenders without a task. + -- @param #number GciRadius The radius in meters wherein detected airplanes will GCI. + -- @param #number ResourceCount The amount of resources that will be allocated to each squadron. + -- @return #AI_A2A_GCICAP + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object. Each squadron has unlimited resources. + -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The CAP Zone prefix is "CAP Zone". + -- -- The CAP Limit is 2. + -- A2ADispatcher = AI_A2A_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2 ) + -- + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object. Each squadron has unlimited resources. + -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The CAP Zone prefix is "CAP Zone". + -- -- The CAP Limit is 2. + -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. + -- A2ADispatcher = AI_A2A_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2, 20000 ) + -- + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object. Each squadron has unlimited resources. + -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The CAP Zone prefix is "CAP Zone". + -- -- The CAP Limit is 2. + -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. + -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, + -- -- will be considered a defense task if the target is within 60km from the defender. + -- A2ADispatcher = AI_A2A_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2, 20000, 60000 ) + -- + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object. Each squadron has unlimited resources. + -- -- The EWR network group prefix is DF CCCP. All groups starting with DF CCCP will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The CAP Zone prefix is "CAP Zone". + -- -- The CAP Limit is 2. + -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. + -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, + -- -- will be considered a defense task if the target is within 60km from the defender. + -- -- The GCI Radius is set to 150000. Any target detected within 150km will be considered for GCI engagement. + -- A2ADispatcher = AI_A2A_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2, 20000, 60000, 150000 ) + -- + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object. Each squadron has 30 resources. + -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The CAP Zone prefix is "CAP Zone". + -- -- The CAP Limit is 2. + -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. + -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, + -- -- will be considered a defense task if the target is within 60km from the defender. + -- -- The GCI Radius is set to 150000. Any target detected within 150km will be considered for GCI engagement. + -- -- The amount of resources for each squadron is set to 30. Thus about 30 resources are allocated to each squadron created. + -- + -- A2ADispatcher = AI_A2A_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2, 20000, 60000, 150000, 30 ) + -- + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object. Each squadron has 30 resources. + -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The CAP Zone prefix is nil. No CAP is created. + -- -- The CAP Limit is nil. + -- -- The Grouping Radius is nil. The default range of 6km radius will be grouped as a group of targets. + -- -- The Engage Radius is set nil. The default Engage Radius will be used to consider a defenser being assigned to a task. + -- -- The GCI Radius is nil. Any target detected within the default GCI Radius will be considered for GCI engagement. + -- -- The amount of resources for each squadron is set to 30. Thus about 30 resources are allocated to each squadron created. + -- + -- A2ADispatcher = AI_A2A_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, nil, nil, nil, nil, nil, 30 ) + -- + function AI_A2A_GCICAP:New( EWRPrefixes, TemplatePrefixes, CapPrefixes, CapLimit, GroupingRadius, EngageRadius, GciRadius, ResourceCount ) + + local EWRSetGroup = SET_GROUP:New() + EWRSetGroup:FilterPrefixes( EWRPrefixes ) + EWRSetGroup:FilterStart() + + local Detection = DETECTION_AREAS:New( EWRSetGroup, GroupingRadius or 30000 ) + + local self = BASE:Inherit( self, AI_A2A_DISPATCHER:New( Detection ) ) -- #AI_A2A_GCICAP + + self:SetEngageRadius( EngageRadius ) + self:SetGciRadius( GciRadius ) + + -- Determine the coalition of the EWRNetwork, this will be the coalition of the GCICAP. + local EWRFirst = EWRSetGroup:GetFirst() -- Wrapper.Group#GROUP + local EWRCoalition = EWRFirst:GetCoalition() + + -- Determine the airbases belonging to the coalition. + local AirbaseNames = {} -- #list<#string> + for AirbaseID, AirbaseData in pairs( _DATABASE.AIRBASES ) do + local Airbase = AirbaseData -- Wrapper.Airbase#AIRBASE + local AirbaseName = Airbase:GetName() + if Airbase:GetCoalition() == EWRCoalition then + table.insert( AirbaseNames, AirbaseName ) + end + end + + self.Templates = SET_GROUP + :New() + :FilterPrefixes( TemplatePrefixes ) + :FilterOnce() + + -- Setup squadrons + + self:I( { Airbases = AirbaseNames } ) + + self:I( "Defining Templates for Airbases ..." ) + for AirbaseID, AirbaseName in pairs( AirbaseNames ) do + local Airbase = _DATABASE:FindAirbase( AirbaseName ) -- Wrapper.Airbase#AIRBASE + local AirbaseName = Airbase:GetName() + local AirbaseCoord = Airbase:GetCoordinate() + local AirbaseZone = ZONE_RADIUS:New( "Airbase", AirbaseCoord:GetVec2(), 3000 ) + local Templates = nil + self:I( { Airbase = AirbaseName } ) + for TemplateID, Template in pairs( self.Templates:GetSet() ) do + local Template = Template -- Wrapper.Group#GROUP + local TemplateCoord = Template:GetCoordinate() + if AirbaseZone:IsVec2InZone( TemplateCoord:GetVec2() ) then + Templates = Templates or {} + table.insert( Templates, Template:GetName() ) + self:I( { Template = Template:GetName() } ) + end + end + if Templates then + self:SetSquadron( AirbaseName, AirbaseName, Templates, ResourceCount ) + end + end + + -- Setup CAP. + -- Find for each CAP the nearest airbase to the (start or center) of the zone. + -- CAP will be launched from there. + + self.CAPTemplates = SET_GROUP:New() + self.CAPTemplates:FilterPrefixes( CapPrefixes ) + self.CAPTemplates:FilterOnce() + + self:I( "Setting up CAP ..." ) + for CAPID, CAPTemplate in pairs( self.CAPTemplates:GetSet() ) do + local CAPZone = ZONE_POLYGON:New( CAPTemplate:GetName(), CAPTemplate ) + -- Now find the closest airbase from the ZONE (start or center) + local AirbaseDistance = 99999999 + local AirbaseClosest = nil -- Wrapper.Airbase#AIRBASE + self:I( { CAPZoneGroup = CAPID } ) + for AirbaseID, AirbaseName in pairs( AirbaseNames ) do + local Airbase = _DATABASE:FindAirbase( AirbaseName ) -- Wrapper.Airbase#AIRBASE + local AirbaseName = Airbase:GetName() + local AirbaseCoord = Airbase:GetCoordinate() + local Squadron = self.DefenderSquadrons[AirbaseName] + if Squadron then + local Distance = AirbaseCoord:Get2DDistance( CAPZone:GetCoordinate() ) + self:I( { AirbaseDistance = Distance } ) + if Distance < AirbaseDistance then + AirbaseDistance = Distance + AirbaseClosest = Airbase + end + end + end + if AirbaseClosest then + self:I( { CAPAirbase = AirbaseClosest:GetName() } ) + self:SetSquadronCap( AirbaseClosest:GetName(), CAPZone, 6000, 10000, 500, 800, 800, 1200, "RADIO" ) + self:SetSquadronCapInterval( AirbaseClosest:GetName(), CapLimit, 300, 600, 1 ) + end + end + + -- Setup GCI. + -- GCI is setup for all Squadrons. + self:I( "Setting up GCI ..." ) + for AirbaseID, AirbaseName in pairs( AirbaseNames ) do + local Airbase = _DATABASE:FindAirbase( AirbaseName ) -- Wrapper.Airbase#AIRBASE + local AirbaseName = Airbase:GetName() + local Squadron = self.DefenderSquadrons[AirbaseName] + self:F( { Airbase = AirbaseName } ) + if Squadron then + self:I( { GCIAirbase = AirbaseName } ) + self:SetSquadronGci( AirbaseName, 800, 1200 ) + end + end + + self:__Start( 5 ) + + self:HandleEvent( EVENTS.Crash, self.OnEventCrashOrDead ) + self:HandleEvent( EVENTS.Dead, self.OnEventCrashOrDead ) + --self:HandleEvent( EVENTS.RemoveUnit, self.OnEventCrashOrDead ) + + self:HandleEvent( EVENTS.Land ) + self:HandleEvent( EVENTS.EngineShutdown ) + + return self + end + + --- AI_A2A_GCICAP constructor with border. + -- @param #AI_A2A_GCICAP self + -- @param #string EWRPrefixes A list of prefixes that of groups that setup the Early Warning Radar network. + -- @param #string TemplatePrefixes A list of template prefixes. + -- @param #string BorderPrefix A Border Zone Prefix. + -- @param #string CapPrefixes A list of CAP zone prefixes (polygon zones). + -- @param #number CapLimit A number of how many CAP maximum will be spawned. + -- @param #number GroupingRadius The radius in meters wherein detected planes are being grouped as one target area. + -- For airplanes, 6000 (6km) is recommended, and is also the default value of this parameter. + -- @param #number EngageRadius The radius in meters wherein detected airplanes will be engaged by airborne defenders without a task. + -- @param #number GciRadius The radius in meters wherein detected airplanes will GCI. + -- @param #number ResourceCount The amount of resources that will be allocated to each squadron. + -- @return #AI_A2A_GCICAP + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object with a border. Each squadron has unlimited resources. + -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The CAP Zone prefix is "CAP Zone". + -- -- The CAP Limit is 2. + -- + -- A2ADispatcher = AI_A2A_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2 ) + -- + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object with a border. Each squadron has unlimited resources. + -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The Border prefix is "Border". This will setup a border using the group defined within the mission editor with the name Border. + -- -- The CAP Zone prefix is "CAP Zone". + -- -- The CAP Limit is 2. + -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. + -- + -- A2ADispatcher = AI_A2A_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2, 20000 ) + -- + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object with a border. Each squadron has unlimited resources. + -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The Border prefix is "Border". This will setup a border using the group defined within the mission editor with the name Border. + -- -- The CAP Zone prefix is "CAP Zone". + -- -- The CAP Limit is 2. + -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. + -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, + -- -- will be considered a defense task if the target is within 60km from the defender. + -- + -- A2ADispatcher = AI_A2A_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2, 20000, 60000 ) + -- + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object with a border. Each squadron has unlimited resources. + -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The Border prefix is "Border". This will setup a border using the group defined within the mission editor with the name Border. + -- -- The CAP Zone prefix is "CAP Zone". + -- -- The CAP Limit is 2. + -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. + -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, + -- -- will be considered a defense task if the target is within 60km from the defender. + -- -- The GCI Radius is set to 150000. Any target detected within 150km will be considered for GCI engagement. + -- + -- A2ADispatcher = AI_A2A_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2, 20000, 60000, 150000 ) + -- + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object with a border. Each squadron has 30 resources. + -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The Border prefix is "Border". This will setup a border using the group defined within the mission editor with the name Border. + -- -- The CAP Zone prefix is "CAP Zone". + -- -- The CAP Limit is 2. + -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. + -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, + -- -- will be considered a defense task if the target is within 60km from the defender. + -- -- The GCI Radius is set to 150000. Any target detected within 150km will be considered for GCI engagement. + -- -- The amount of resources for each squadron is set to 30. Thus about 30 resources are allocated to each squadron created. + -- + -- A2ADispatcher = AI_A2A_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2, 20000, 60000, 150000, 30 ) + -- + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object with a border. Each squadron has 30 resources. + -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The Border prefix is "Border". This will setup a border using the group defined within the mission editor with the name Border. + -- -- The CAP Zone prefix is nil. No CAP is created. + -- -- The CAP Limit is nil. + -- -- The Grouping Radius is nil. The default range of 6km radius will be grouped as a group of targets. + -- -- The Engage Radius is set nil. The default Engage Radius will be used to consider a defenser being assigned to a task. + -- -- The GCI Radius is nil. Any target detected within the default GCI Radius will be considered for GCI engagement. + -- -- The amount of resources for each squadron is set to 30. Thus about 30 resources are allocated to each squadron created. + -- + -- A2ADispatcher = AI_A2A_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", nil, nil, nil, nil, nil, 30 ) + -- + function AI_A2A_GCICAP:NewWithBorder( EWRPrefixes, TemplatePrefixes, BorderPrefix, CapPrefixes, CapLimit, GroupingRadius, EngageRadius, GciRadius, ResourceCount ) + + local self = AI_A2A_GCICAP:New( EWRPrefixes, TemplatePrefixes, CapPrefixes, CapLimit, GroupingRadius, EngageRadius, GciRadius, ResourceCount ) + + if BorderPrefix then + self:SetBorderZone( ZONE_POLYGON:New( BorderPrefix, GROUP:FindByName( BorderPrefix ) ) ) + end + + return self + + end + +end +--- **AI** -- Models the process of air to ground BAI engagement for airplanes and helicopters. +-- +-- This is a class used in the @{AI_A2G_Dispatcher}. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_A2G_BAI +-- @image AI_Air_To_Ground_Engage.JPG + + + +--- @type AI_A2G_BAI +-- @extends AI.AI_A2A_Engage#AI_A2A_Engage + + +--- Implements the core functions to intercept intruders. Use the Engage trigger to intercept intruders. +-- +-- === +-- +-- @field #AI_A2G_BAI +AI_A2G_BAI = { + ClassName = "AI_A2G_BAI", +} + + + +--- Creates a new AI_A2G_BAI object +-- @param #AI_A2G_BAI self +-- @param Wrapper.Group#GROUP AIGroup +-- @param DCS#Speed EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Speed EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement. +-- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement. +-- @param DCS#AltitudeType EngageAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO". +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO +-- @return #AI_A2G_BAI +function AI_A2G_BAI:New2( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) + + local AI_Air = AI_AIR:New( AIGroup ) + local AI_Air_Patrol = AI_AIR_PATROL:New( AI_Air, AIGroup, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) -- #AI_AIR_PATROL + local AI_Air_Engage = AI_AIR_ENGAGE:New( AI_Air_Patrol, AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) + local self = BASE:Inherit( self, AI_Air_Engage ) + + return self +end + + +--- Creates a new AI_A2G_BAI object +-- @param #AI_A2G_BAI self +-- @param Wrapper.Group#GROUP AIGroup +-- @param DCS#Speed EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Speed EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement. +-- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement. +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO +-- @return #AI_A2G_BAI +function AI_A2G_BAI:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) + + return self:New2( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolAltType, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType) +end + +--- Evaluate the attack and create an AttackUnitTask list. +-- @param #AI_A2G_BAI self +-- @param Core.Set#SET_UNIT AttackSetUnit The set of units to attack. +-- @param Wrappper.Group#GROUP DefenderGroup The group of defenders. +-- @param #number EngageAltitude The altitude to engage the targets. +-- @return #AI_A2G_BAI self +function AI_A2G_BAI:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, EngageAltitude ) + + local AttackUnitTasks = {} + + local AttackSetUnitPerThreatLevel = AttackSetUnit:GetSetPerThreatLevel( 10, 0 ) + for AttackUnitIndex, AttackUnit in ipairs( AttackSetUnitPerThreatLevel or {} ) do + if AttackUnit then + if AttackUnit:IsAlive() and AttackUnit:IsGround() then + self:T( { "BAI Unit:", AttackUnit:GetName() } ) + AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, true, false, nil, nil, EngageAltitude ) + end + end + end + + return AttackUnitTasks +end + + +--- **AI** -- Models the process of air to ground engagement for airplanes and helicopters. +-- +-- This is a class used in the @{AI_A2G_Dispatcher}. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_A2G_CAS +-- @image AI_Air_To_Ground_Engage.JPG + + + +--- @type AI_A2G_CAS +-- @extends AI.AI_A2G_Patrol#AI_AIR_PATROL + + +--- Implements the core functions to intercept intruders. Use the Engage trigger to intercept intruders. +-- +-- === +-- +-- @field #AI_A2G_CAS +AI_A2G_CAS = { + ClassName = "AI_A2G_CAS", +} + + + +--- Creates a new AI_A2G_CAS object +-- @param #AI_A2G_CAS self +-- @param Wrapper.Group#GROUP AIGroup +-- @param DCS#Speed EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Speed EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement. +-- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement. +-- @param DCS#AltitudeType EngageAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO". +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO +-- @return #AI_A2G_CAS +function AI_A2G_CAS:New2( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) + + local AI_Air = AI_AIR:New( AIGroup ) + local AI_Air_Patrol = AI_AIR_PATROL:New( AI_Air, AIGroup, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) -- #AI_AIR_PATROL + local AI_Air_Engage = AI_AIR_ENGAGE:New( AI_Air_Patrol, AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) + local self = BASE:Inherit( self, AI_Air_Engage ) + + return self +end + + +--- Creates a new AI_A2G_CAS object +-- @param #AI_A2G_CAS self +-- @param Wrapper.Group#GROUP AIGroup +-- @param DCS#Speed EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Speed EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement. +-- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement. +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO +-- @return #AI_A2G_CAS +function AI_A2G_CAS:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) + + return self:New2( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolAltType, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType) +end + +--- Evaluate the attack and create an AttackUnitTask list. +-- @param #AI_A2G_CAS self +-- @param Core.Set#SET_UNIT AttackSetUnit The set of units to attack. +-- @param Wrappper.Group#GROUP DefenderGroup The group of defenders. +-- @param #number EngageAltitude The altitude to engage the targets. +-- @return #AI_A2G_CAS self +function AI_A2G_CAS:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, EngageAltitude ) + + local AttackUnitTasks = {} + + local AttackSetUnitPerThreatLevel = AttackSetUnit:GetSetPerThreatLevel( 10, 0 ) + for AttackUnitIndex, AttackUnit in ipairs( AttackSetUnitPerThreatLevel or {} ) do + if AttackUnit then + if AttackUnit:IsAlive() and AttackUnit:IsGround() then + self:T( { "CAS Unit:", AttackUnit:GetName() } ) + AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, true, false, nil, nil, EngageAltitude ) + end + end + end + + return AttackUnitTasks +end + + + +--- **AI** -- Models the process of air to ground SEAD engagement for airplanes and helicopters. +-- +-- This is a class used in the @{AI_A2G_Dispatcher}. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_A2G_SEAD +-- @image AI_Air_To_Ground_Engage.JPG + + + +--- @type AI_A2G_SEAD +-- @extends AI.AI_A2G_Patrol#AI_AIR_PATROL + + +--- Implements the core functions to SEAD intruders. Use the Engage trigger to intercept intruders. +-- +-- ![Process](..\Presentations\AI_GCI\Dia3.JPG) +-- +-- The AI_A2G_SEAD is assigned a @{Wrapper.Group} and this must be done before the AI_A2G_SEAD process can be started using the **Start** event. +-- +-- ![Process](..\Presentations\AI_GCI\Dia4.JPG) +-- +-- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits. +-- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. +-- +-- ![Process](..\Presentations\AI_GCI\Dia5.JPG) +-- +-- This cycle will continue. +-- +-- ![Process](..\Presentations\AI_GCI\Dia6.JPG) +-- +-- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event. +-- +-- ![Process](..\Presentations\AI_GCI\Dia9.JPG) +-- +-- When enemies are detected, the AI will automatically engage the enemy. +-- +-- ![Process](..\Presentations\AI_GCI\Dia10.JPG) +-- +-- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. +-- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. +-- +-- ![Process](..\Presentations\AI_GCI\Dia13.JPG) +-- +-- ## 1. AI_A2G_SEAD constructor +-- +-- * @{#AI_A2G_SEAD.New}(): Creates a new AI_A2G_SEAD object. +-- +-- ## 3. Set the Range of Engagement +-- +-- ![Range](..\Presentations\AI_GCI\Dia11.JPG) +-- +-- An optional range can be set in meters, +-- that will define when the AI will engage with the detected airborne enemy targets. +-- The range can be beyond or smaller than the range of the Patrol Zone. +-- The range is applied at the position of the AI. +-- Use the method @{AI.AI_GCI#AI_A2G_SEAD.SetEngageRange}() to define that range. +-- +-- ## 4. Set the Zone of Engagement +-- +-- ![Zone](..\Presentations\AI_GCI\Dia12.JPG) +-- +-- An optional @{Zone} can be set, +-- that will define when the AI will engage with the detected airborne enemy targets. +-- Use the method @{AI.AI_Cap#AI_A2G_SEAD.SetEngageZone}() to define that Zone. +-- +-- === +-- +-- @field #AI_A2G_SEAD +AI_A2G_SEAD = { + ClassName = "AI_A2G_SEAD", +} + + + +--- Creates a new AI_A2G_SEAD object +-- @param #AI_A2G_SEAD self +-- @param Wrapper.Group#GROUP AIGroup +-- @param DCS#Speed EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Speed EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement. +-- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement. +-- @param DCS#AltitudeType EngageAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO". +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO +-- @return #AI_A2G_SEAD +function AI_A2G_SEAD:New2( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) + + local AI_Air = AI_AIR:New( AIGroup ) + local AI_Air_Patrol = AI_AIR_PATROL:New( AI_Air, AIGroup, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) -- #AI_AIR_PATROL + local AI_Air_Engage = AI_AIR_ENGAGE:New( AI_Air_Patrol, AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) + local self = BASE:Inherit( self, AI_Air_Engage ) + + return self +end + + +--- Creates a new AI_A2G_SEAD object +-- @param #AI_A2G_SEAD self +-- @param Wrapper.Group#GROUP AIGroup +-- @param DCS#Speed EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Speed EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement. +-- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement. +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO +-- @return #AI_A2G_SEAD +function AI_A2G_SEAD:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) + + return self:New2( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolAltType, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) +end + + +--- Evaluate the attack and create an AttackUnitTask list. +-- @param #AI_A2G_SEAD self +-- @param Core.Set#SET_UNIT AttackSetUnit The set of units to attack. +-- @param Wrappper.Group#GROUP DefenderGroup The group of defenders. +-- @param #number EngageAltitude The altitude to engage the targets. +-- @return #AI_A2G_SEAD self +function AI_A2G_SEAD:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, EngageAltitude ) + + local AttackUnitTasks = {} + + local AttackSetUnitPerThreatLevel = AttackSetUnit:GetSetPerThreatLevel( 10, 0 ) + for AttackUnitID, AttackUnit in ipairs( AttackSetUnitPerThreatLevel ) do + if AttackUnit then + if AttackUnit:IsAlive() and AttackUnit:IsGround() then + local HasRadar = AttackUnit:HasSEAD() + if HasRadar then + self:F( { "SEAD Unit:", AttackUnit:GetName() } ) + AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, true, false, nil, nil, EngageAltitude ) + end + end + end + end + + return AttackUnitTasks +end + +--- **AI** - Create an automated A2G defense system based on a detection network of reconnaissance vehicles and air units, coordinating SEAD, BAI and CAP operations. +-- +-- === +-- +-- Features: +-- +-- * Setup quickly an A2G defense system for a coalition. +-- * Setup multiple defense zones to defend specific coordinates in your battlefield. +-- * Setup (SEAD) Suppression of Air Defense squadrons, to gain control in the air of enemy grounds. +-- * Setup (CAS) Controlled Air Support squadrons, to attack closeby enemy ground units near friendly installations. +-- * Setup (BAI) Battleground Air Interdiction squadrons to attack remote enemy ground units and targets. +-- * Define and use a detection network controlled by recce. +-- * Define A2G defense squadrons at airbases, farps and carriers. +-- * Enable airbases for A2G defenses. +-- * Add different planes and helicopter templates to squadrons. +-- * Assign squadrons to execute a specific engagement type depending on threat level of the detected ground enemy unit composition. +-- * Add multiple squadrons to different airbases, farps or carriers. +-- * Define different ranges to engage upon. +-- * Establish an automatic in air refuel process for planes using refuel tankers. +-- * Setup default settings for all squadrons and A2G defenses. +-- * Setup specific settings for specific squadrons. +-- +-- === +-- +-- ## Missions: +-- +-- [AID-A2G - AI A2G Dispatching](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AID%20-%20AI%20Dispatching/AID-A2G%20-%20AI%20A2G%20Dispatching) +-- +-- === +-- +-- ## YouTube Channel: +-- +-- [DCS WORLD - MOOSE - A2G GCICAP - Build an automatic A2G Defense System](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl0S4KMNUUJpaUs6zZHjLKNx) +-- +-- === +-- +-- # QUICK START GUIDE +-- +-- The following class is available to model an A2G defense system. +-- +-- AI_A2G_DISPATCHER is the main A2G defense class that models the A2G defense system. +-- +-- Before you start using the AI_A2G_DISPATCHER, ask youself the following questions. +-- +-- +-- ## 1. Which coalition am I modeling an A2G defense system for? blue or red? +-- +-- One AI_A2G_DISPATCHER object can create a defense system for **one coalition**, which is blue or red. +-- If you want to create a **mutual defense system**, for both blue and red, then you need to create **two** AI_A2G_DISPATCHER **objects**, +-- each governing their defense system for one coalition. +-- +-- +-- ## 2. Which type of detection will I setup? Grouping based per AREA, per TYPE or per UNIT? (Later others will follow). +-- +-- The MOOSE framework leverages the @{Functional.Detection} classes to perform the reconnaissance, detecting enemy units +-- and reporting them to the head quarters. +-- Several types of @{Functional.Detection} classes exist, and the most common characteristics of these classes is that they: +-- +-- * Perform detections from multiple recce as one co-operating entity. +-- * Communicate with a @{Tasking.CommandCenter}, which consolidates each detection. +-- * Groups detections based on a method (per area, per type or per unit). +-- * Communicates detections. +-- +-- +-- ## 3. Which recce units can be used as part of the detection system? Only ground based, or also airborne? +-- +-- Depending on the type of mission you want to achieve, different types of units can be engaged to perform ground enemy targets reconnaissance. +-- Ground recce (FAC) are very useful units to determine the position of enemy ground targets when they spread out over the battlefield at strategic positions. +-- Using their varying detection technology, and especially those ground units which have spotting technology, can be extremely effective at +-- detecting targets at great range. The terrain elevation characteristics are a big tool in making ground recce to be more effective. +-- Unfortunately, they lack sometimes the visibility to detect targets at greater range, or when scenery is preventing line of sight. +-- If you succeed to position recce at higher level terrain providing a broad and far overview of the lower terrain in the distance, then +-- the recce will be very effective at detecting approaching enemy targets. Therefore, always use the terrain very carefully! +-- +-- Airborne recce (AFAC) are also very effective. The are capable of patrolling at a functional detection altitude, +-- having an overview of the whole battlefield. However, airborne recce can be vulnerable to air to ground attacks, +-- so you need air superiority to make them effective. +-- Airborne recce will also have varying ground detection technology, which plays a big role in the effectiveness of the reconnaissance. +-- Certain helicopter or plane types have ground searching radars or advanced ground scanning technology, and are very effective +-- compared to air units having only visual detection capabilities. +-- For example, for the red coalition, the Mi-28N and the Su-34; and for the blue side, the reaper, are such effective airborne recce units. +-- +-- Typically, don't want these recce units to engage with the enemy, you want to keep them at position. Therefore, it is a good practice +-- to set the ROE for these recce to hold weapons, and make them invisible from the enemy. +-- +-- It is not possible to perform a recce function as a player (unit). +-- +-- +-- ## 4. How do the defenses decide **when and where to engage** on approaching enemy units? +-- +-- The A2G dispacher needs you to setup (various) defense coordinates, which are strategic positions in the battle field to be defended. +-- Any ground based enemy approaching within the proximity of such a defense point, may trigger for a defensive action by friendly air units. +-- +-- There are 2 important parameters that play a role in the defensive decision making: defensiveness and reactivity. +-- +-- The A2G dispatcher provides various parameters to setup the **defensiveness**, +-- which models the decision **when** a defender will engage with the approaching enemy. +-- Defensiveness is calculated by a probability distribution model when to trigger a defense action, +-- depending on the distance of the enemy unit from the defense coordinates, and a **defensiveness factor**. +-- +-- The other parameter considered for defensive action is **where the enemy is located**, thus the distance from a defense coordinate, +-- which we call the **reactive distance**. By default, the reactive distance is set to 60km, but can be changed by the mission designer +-- using the available method explained further below. +-- The combination of the defensiveness and reactivity results in a model that, the closer the attacker is to the defense point, +-- the higher the probability will be that a defense action will be launched! +-- +-- +-- ## 5. Are defense coordinates and defense reactivity the only parameters? +-- +-- No, depending on the target type, and the threat level of the target, the probability of defense will be higher. +-- In other words, when a SAM-10 radar emitter is detected, its probabilty for defense will be much higher than when a BMP-1 vehicle is +-- detected, even when both enemies are at the same distance from a defense coordinate. +-- This will ensure optimal defenses, SEAD tasks will be launched much more quicker against engaging radar emitters, to ensure air superiority. +-- Approaching main battle tanks will be engaged much faster, than a group of approaching trucks. +-- +-- +-- ## 6. Which Squadrons will I create and which name will I give each Squadron? +-- +-- The A2G defense system works with **Squadrons**. Each Squadron must be given a unique name, that forms the **key** to the squadron. +-- Several options and activities can be set per Squadron. A free format name can be given, but always ensure that the name is meaningfull +-- for your mission, and remember that squadron names are used for communication to the players of your mission. +-- +-- There are mainly 3 types of defenses: **SEAD**, **CAS** and **BAI**. +-- +-- Suppression of Air Defenses (SEAD) are effective agains radar emitters. Close Air Support (CAS) is launched when the enemy is close near friendly units. +-- Battleground Air Interdiction (BAI) tasks are launched when there are no friendlies around. +-- +-- Depending on the defense type, different payloads will be needed. See further points on squadron definition. +-- +-- +-- ## 7. Where will the Squadrons be located? On Airbases? On Carrier Ships? On Farps? +-- +-- Squadrons are placed at the **home base** on an **airfield**, **carrier** or **farp**. +-- Carefully plan where each Squadron will be located as part of the defense system required for mission effective defenses. +-- If the home base of the squadron is too far from assumed enemy positions, then the defenses will be too late. +-- The home bases must be **behind** enemy lines, you want to prevent your home bases to be engaged by enemies! +-- Depending on the units applied for defenses, the home base can be further or closer to the enemies. +-- Any airbase, farp or carrier can act as the launching platform for A2G defenses. +-- Carefully plan which airbases will take part in the coalition. Color each airbase **in the color of the coalition**, using the mission editor, +-- or your air units will not return for landing at the airbase! +-- +-- +-- ## 8. Which helicopter or plane models will I assign for each Squadron? Do I need one plane model or more plane models per squadron? +-- +-- Per Squadron, one or multiple helicopter or plane models can be allocated as **Templates**. +-- These are late activated groups with one airplane or helicopter that start with a specific name, called the **template prefix**. +-- The A2G defense system will select from the given templates a random template to spawn a new plane (group). +-- +-- A squadron will perform specific task types (SEAD, CAS or BAI). So, squadrons will require specific templates for the +-- task types it will perform. A squadron executing SEAD defenses, will require a payload with long range anti-radar seeking missiles. +-- +-- +-- ## 9. Which payloads, skills and skins will these plane models have? +-- +-- Per Squadron, even if you have one plane model, you can still allocate multiple templates of one plane model, +-- each having different payloads, skills and skins. +-- The A2G defense system will select from the given templates a random template to spawn a new plane (group). +-- +-- +-- ## 10. How to squadrons engage in a defensive action? +-- +-- There are two ways how squadrons engage and execute your A2G defenses. +-- Squadrons can start the defense directly from the airbase, farp or carrier. When a squadron launches a defensive group, that group +-- will start directly from the airbase. The other way is to launch early on in the mission a patrolling mechanism. +-- Squadrons will launch air units to patrol in specific zone(s), so that when ground enemy targets are detected, that the airborne +-- A2G defenses can come immediately into action. +-- +-- +-- ## 11. For each Squadron doing a patrol, which zone types will I create? +-- +-- Per zone, evaluate whether you want: +-- +-- * simple trigger zones +-- * polygon zones +-- * moving zones +-- +-- Depending on the type of zone selected, a different @{Zone} object needs to be created from a ZONE_ class. +-- +-- +-- ## 12. Are moving defense coordinates possible? +-- +-- Yes, different COORDINATE types are possible to be used. +-- The COORDINATE_UNIT will help you to specify a defense coodinate that is attached to a moving unit. +-- +-- +-- ## 13. How much defense coordinates do I need to create? +-- +-- It depends, but the idea is to define only the necessary defense points that drive your mission. +-- If you define too much defense points, the performance of your mission may decrease. Per defense point defined, +-- all the possible enemies are evaluated. Note that each defense coordinate has a reach depending on the size of the defense radius. +-- The default defense radius is about 60km, and depending on the defense reactivity, defenses will be launched when the enemy is at +-- close or greater distance from the defense coordinate. +-- +-- +-- ## 14. For each Squadron doing patrols, what are the time intervals and patrol amounts to be performed? +-- +-- For each patrol: +-- +-- * **How many** patrol you want to have airborne at the same time? +-- * **How frequent** you want the defense mechanism to check whether to start a new patrol? +-- +-- other considerations: +-- +-- * **How far** is the patrol area from the engagement "hot zone". You want to ensure that the enemy is reached on time! +-- * **How safe** is the patrol area taking into account air superiority. Is it well defended, are there nearby A2A bases? +-- +-- +-- ## 15. For each Squadron, which takeoff method will I use? +-- +-- For each Squadron, evaluate which takeoff method will be used: +-- +-- * Straight from the air +-- * From the runway +-- * From a parking spot with running engines +-- * From a parking spot with cold engines +-- +-- **The default takeoff method is staight in the air.** +-- This takeoff method is the most useful if you want to avoid airplane clutter at airbases! +-- But it is the least realistic one! +-- +-- +-- ## 16. For each Squadron, which landing method will I use? +-- +-- For each Squadron, evaluate which landing method will be used: +-- +-- * Despawn near the airbase when returning +-- * Despawn after landing on the runway +-- * Despawn after engine shutdown after landing +-- +-- **The default landing method is despawn when near the airbase when returning.** +-- This landing method is the most useful if you want to avoid airplane clutter at airbases! +-- But it is the least realistic one! +-- +-- +-- ## 19. For each Squadron, which **defense overhead** will I use? +-- +-- For each Squadron, depending on the helicopter or airplane type (modern, old) and payload, which overhead is required to provide any defense? +-- +-- In other words, if **X** enemy ground units are detected, how many **Y** defense helicpters or airplanes need to engage (per squadron)? +-- The **Y** is dependent on the type of airplane (era), payload, fuel levels, skills etc. +-- But the most important factor is the payload, which is the amount of A2G weapons the defense can carry to attack the enemy ground units. +-- For example, a Ka-50 can carry 16 vikrs, that means, that it potentially can destroy at least 8 ground units without a reload of ammunication. +-- That means, that one defender can destroy more enemy ground units. +-- Thus, the overhead is a **factor** that will calculate dynamically how many **Y** defenses will be required based on **X** attackers detected. +-- +-- **The default overhead is 1. A smaller value than 1, like 0.25 will decrease the overhead to a 1 / 4 ratio, meaning, +-- one defender for each 4 detected ground enemy units. ** +-- +-- +-- ## 19. For each Squadron, which grouping will I use? +-- +-- When multiple targets are detected, how will defenses be grouped when multiple defense air units are spawned for multiple enemy ground units? +-- Per one, two, three, four? +-- +-- **The default grouping is 1. That means, that each spawned defender will act individually.** +-- But you can specify a number between 1 and 4, so that the defenders will act as a group. +-- +-- === +-- +-- ### Author: **FlightControl** rework of GCICAP + introduction of new concepts (squadrons). +-- +-- @module AI.AI_A2G_Dispatcher +-- @image AI_Air_To_Ground_Dispatching.JPG + + + +do -- AI_A2G_DISPATCHER + + --- AI_A2G_DISPATCHER class. + -- @type AI_A2G_DISPATCHER + -- @extends Tasking.DetectionManager#DETECTION_MANAGER + + --- Create an automated A2G defense system based on a detection network of reconnaissance vehicles and air units, coordinating SEAD, BAI and CAP operations. + -- + -- === + -- + -- When your mission is in the need to take control of the AI to automate and setup a process of air to ground defenses, this is the module you need. + -- The defense system work through the definition of defense coordinates, which are points in your friendly area within the battle field, that your mission need to have defended. + -- Multiple defense coordinates can be setup. Defense coordinates can be strategic or tactical positions or references to strategic units or scenery. + -- The A2G dispatcher will evaluate every x seconds the tactical situation around each defense coordinate. When a defense coordinate + -- is under threat, it will communicate through the command center that defensive actions need to be taken and will launch groups of air units for defense. + -- The level of threat to the defense coordinate varyies upon the strength and types of the enemy units, the distance to the defense point, and the defensiveness parameters. + -- Defensive actions are taken through probability, but the closer and the more threat the enemy poses to the defense coordinate, the faster it will be attacked by friendly A2G units. + -- + -- Please study carefully the underlying explanations how to setup and use this module, as it has many features. + -- It also requires a little study to ensure that you get a good understanding of the defense mechanisms, to ensure a strong + -- defense for your missions. + -- + -- === + -- + -- # USAGE GUIDE + -- + -- ## 1. AI\_A2G\_DISPATCHER constructor: + -- + -- ![Banner Image](..\Presentations\AI_A2G_DISPATCHER\AI_A2G_DISPATCHER-ME_1.JPG) + -- + -- + -- The @{#AI_A2G_DISPATCHER.New}() method creates a new AI_A2G_DISPATCHER instance. + -- + -- ### 1.1. Define the **reconnaissance network**: + -- + -- As part of the AI_A2G_DISPATCHER :New() constructor, a reconnaissance network must be given as the first parameter. + -- A reconnaissance network is provided by passing a @{Functional.Detection} object. + -- The most effective reconnaissance for the A2G dispatcher would be to use the @{Functional.Detection#DETECTION_AREAS} object. + -- + -- A reconnaissance network, is used to detect enemy ground targets, + -- potentially group them into areas, and to understand the position, level of threat of the enemy. + -- + -- ![Banner Image](..\Presentations\AI_A2G_DISPATCHER\Dia5.JPG) + -- + -- As explained in the introduction, depending on the type of mission you want to achieve, different types of units can be applied to detect ground enemy targets. + -- Ground based units are very useful to act as a reconnaissance, but they lack sometimes the visibility to detect targets at greater range. + -- Recce are very useful to acquire the position of enemy ground targets when spread out over the battlefield at strategic positions. + -- Ground units also have varying detectors, and especially the ground units which have laser guiding missiles can be extremely effective at + -- detecting targets at great range. The terrain elevation characteristics are a big tool in making ground recce to be more effective. + -- If you succeed to position recce at higher level terrain providing a broad and far overview of the lower terrain in the distance, then + -- the recce will be very effective at detecting approaching enemy targets. Therefore, always use the terrain very carefully! + -- + -- Beside ground level units to use for reconnaissance, air units are also very effective. The are capable of patrolling at great speed + -- covering a large terrain. However, airborne recce can be vulnerable to air to ground attacks, and you need air superiority to make then + -- effective. Also the instruments available at the air units play a big role in the effectiveness of the reconnaissance. + -- Air units which have ground detection capabilities will be much more effective than air units with only visual detection capabilities. + -- For the red coalition, the Mi-28N and for the blue side, the reaper are such effective reconnaissance airborne units. + -- + -- Reconnaissance networks are **dynamically constructed**, that is, they form part of the @{Functional.Detection} instance that is given as the first parameter to the A2G dispatcher. + -- By defining in a **smart way the names or name prefixes of the reconnaissance groups**, these groups will be **automatically added or removed** to or from the reconnaissance network, + -- when these groups are spawned in or destroyed during the ongoing battle. + -- By spawning in dynamically additional recce, you can ensure that there is sufficient reconnaissance coverage so the defense mechanism is continuously + -- alerted of new enemy ground targets. + -- + -- The following example defens a new reconnaissance network using a @{Functional.Detection#DETECTION_AREAS} object. + -- + -- -- Define a SET_GROUP object that builds a collection of groups that define the recce network. + -- -- Here we build the network with all the groups that have a name starting with CCCP Recce. + -- DetectionSetGroup = SET_GROUP:New() -- Defene a set of group objects, caled DetectionSetGroup. + -- + -- DetectionSetGroup:FilterPrefixes( { "CCCP Recce" } ) -- The DetectionSetGroup will search for groups that start with the name "CCCP Recce". + -- + -- -- This command will start the dynamic filtering, so when groups spawn in or are destroyed, + -- -- which have a group name starting with "CCCP Recce", then these will be automatically added or removed from the set. + -- DetectionSetGroup:FilterStart() + -- + -- -- This command defines the reconnaissance network. + -- -- It will group any detected ground enemy targets within a radius of 1km. + -- -- It uses the DetectionSetGroup, which defines the set of reconnaissance groups to detect for enemy ground targets. + -- Detection = DETECTION_AREAS:New( DetectionSetGroup, 1000 ) + -- + -- -- Setup the A2A dispatcher, and initialize it. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- + -- The above example creates a SET_GROUP instance, and stores this in the variable (object) **DetectionSetGroup**. + -- **DetectionSetGroup** is then being configured to filter all active groups with a group name starting with `"CCCP Recce"` to be included in the set. + -- **DetectionSetGroup** is then calling `FilterStart()`, which is starting the dynamic filtering or inclusion of these groups. + -- Note that any destroy or new spawn of a group having a name, starting with the above prefix, will be removed or added to the set. + -- + -- Then a new detection object is created from the class `DETECTION_AREAS`. A grouping radius of 1000 meters (1km) is choosen. + -- + -- The `Detection` object is then passed to the @{#AI_A2G_DISPATCHER.New}() method to indicate the reconnaissance network + -- configuration and setup the A2G defense detection mechanism. + -- + -- ### 1.2. Setup the A2G dispatcher for both a red and blue coalition. + -- + -- Following the above described procedure, you'll need to create for each coalition an separate detection network, and a separate A2G dispatcher. + -- Ensure that while doing so, that you name the objects differently both for red and blue coalition. + -- + -- For example like this for the red coalition: + -- + -- DetectionRed = DETECTION_AREAS:New( DetectionSetGroupRed, 1000 ) + -- A2GDispatcherRed = AI_A2G_DISPATCHER:New( DetectionRed ) + -- + -- And for the blue coalition: + -- + -- DetectionBlue = DETECTION_AREAS:New( DetectionSetGroupBlue, 1000 ) + -- A2GDispatcherBlue = AI_A2G_DISPATCHER:New( DetectionBlue ) + -- + -- + -- Note: Also the SET_GROUP objects should be created for each coalition separately, containing each red and blue recce respectively! + -- + -- ### 1.3. Define the enemy ground target **grouping radius**, in case you use DETECTION_AREAS: + -- + -- The target grouping radius is a property of the DETECTION_AREAS class, that was passed to the AI_A2G_DISPATCHER:New() method, + -- but can be changed. The grouping radius should not be too small, but also depends on the types of ground forces and the way you want your mission to evolve. + -- A large radius will mean large groups of enemy ground targets, while making smaller groups will result in a more fragmented defense system. + -- Typically I suggest a grouping radius of 1km. This is the right balance to create efficient defenses. + -- + -- Note that detected targets are constantly re-grouped, that is, when certain detected enemy ground units are moving further than the group radius, + -- then these units will become a separate area being detected. This may result in additional defenses being started by the dispatcher! + -- So don't make this value too small! Again, I advise about 1km or 1000 meters. + -- + -- ## 2. Setup (a) **Defense Coordinate(s)**. + -- + -- As explained above, defense coordinates are the center of your defense operations. + -- The more threat to the defense coordinate, the higher it is likely a defensive action will be launched. + -- + -- Find below an example how to add defense coordinates: + -- + -- -- Add defense coordinates. + -- A2GDispatcher:AddDefenseCoordinate( "HQ", GROUP:FindByName( "HQ" ):GetCoordinate() ) + -- + -- In this example, the coordinate of a group called `"HQ"` is retrieved, using `:GetCoordinate()` + -- This returns a COORDINATE object, pointing to the first unit within the GROUP object. + -- + -- The method @{#AI_A2G_DISPATCHER.AddDefenseCoordinate}() adds a new defense coordinate to the `A2GDispatcher` object. + -- The first parameter is the key of the defense coordinate, the second the coordinate itself. + -- + -- Later, a COORDINATE_UNIT will be added to the framework, which can be used to assign "moving" coordinates to an A2G dispatcher. + -- + -- **REMEMBER!** + -- + -- - **Defense coordinates are the center of the A2G dispatcher defense system!** + -- - **You can define more defense coordinates to defend a larger area.** + -- - **Detected enemy ground targets are not immediately engaged, but are engaged with a reactivity or probability calculation!** + -- + -- But, there is more to it ... + -- + -- + -- ### 2.1. The **Defense Radius**. + -- + -- The defense radius defines the maximum radius that a defense will be initiated around each defense coordinate. + -- So even when there are targets further away than the defense radius, then these targets won't be engaged upon. + -- By default, the defense radius is set to 100km (100.000 meters), but can be changed using the @{#AI_A2G_DISPATCHER.SetDefenseRadius}() method. + -- Note that the defense radius influences the defense reactivity also! The larger the defense radius, the more reactive the defenses will be. + -- + -- For example: + -- + -- A2GDispatcher:SetDefenseRadius( 30000 ) + -- + -- This defines an A2G dispatcher which will engage on enemy ground targets within 30km radius around the defense coordinate. + -- Note that the defense radius **applies to all defense coordinates** defined within the A2G dispatcher. + -- + -- ### 2.2. The **Defense Reactivity**. + -- + -- There are three levels that can be configured to tweak the defense reactivity. As explained above, the threat to a defense coordinate is + -- also determined by the distance of the enemy ground target to the defense coordinate. + -- If you want to have a **low** defense reactivity, that is, the probability that an A2G defense will engage to the enemy ground target, then + -- use the @{#AI_A2G_DISPATCHER.SetDefenseReactivityLow}() method. For medium and high reactivity, use the methods + -- @{#AI_A2G_DISPATCHER.SetDefenseReactivityMedium}() and @{#AI_A2G_DISPATCHER.SetDefenseReactivityHigh}() respectively. + -- + -- Note that the reactivity of defenses is always in relation to the Defense Radius! the shorter the distance, + -- the less reactive the defenses will be in terms of distance to enemy ground targets! + -- + -- For example: + -- + -- A2GDispatcher:SetDefenseReactivityHigh() + -- + -- This defines an A2G dispatcher with high defense reactivity. + -- + -- ## 3. **Squadrons**. + -- + -- The A2G dispatcher works with **Squadrons**, that need to be defined using the different methods available. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetSquadron}() to **setup a new squadron** active at an airfield, farp or carrier, + -- while defining which helicopter or plane **templates** are being used by the squadron and how many **resources** are available. + -- + -- **Multiple squadrons** can be defined within one A2G dispatcher, each having specific defense tasks and defense parameter settings! + -- + -- Squadrons: + -- + -- * Have name (string) that is the identifier or **key** of the squadron. + -- * Have specific helicopter or plane **templates**. + -- * Are located at **one** airbase, farp or carrier. + -- * Optionally have a **limited set of resources**. The default is that squadrons have **unlimited resources**. + -- + -- The name of the squadron given acts as the **squadron key** in all `A2GDispatcher:SetSquadron...()` or `A2GDispatcher:GetSquadron...()` methods. + -- + -- Additionally, squadrons have specific configuration options to: + -- + -- * Control how new helicopters or aircraft are taking off from the airfield, farp or carrier (in the air, cold, hot, at the runway). + -- * Control how returning helicopters or aircraft are landing at the airfield, farp or carrier (in the air near the airbase, after landing, after engine shutdown). + -- * Control the **grouping** of new helicopters or aircraft spawned at the airfield, farp or carrier. If there is more than one helicopter or aircraft to be spawned, these may be grouped. + -- * Control the **overhead** or defensive strength of the squadron. Depending on the types of helicopters, planes, amount of resources and payload (weapon configuration) chosen, + -- the mission designer can choose to increase or reduce the amount of planes spawned. + -- + -- The method @{#AI_A2G_DISPATCHER.SetSquadron}() defines for you a new squadron. + -- The provided parameters are the squadron name, airbase name and a list of template prefixe, and a number that indicates the amount of resources. + -- + -- For example, this defines 3 new squadrons: + -- + -- A2GDispatcher:SetSquadron( "Maykop SEAD", AIRBASE.Caucasus.Maykop_Khanskaya, { "CCCP KA-50" }, 10 ) + -- A2GDispatcher:SetSquadron( "Maykop CAS", "CAS", { "CCCP KA-50" }, 10 ) + -- A2GDispatcher:SetSquadron( "Maykop BAI", "BAI", { "CCCP KA-50" }, 10 ) + -- + -- The latter 2 will depart from FARPs, which bare the name `"CAS"` and `"BAI"`. + -- + -- + -- ### 3.1. Squadrons **Tasking**. + -- + -- Squadrons can be commanded to execute 3 types of tasks, as explained above: + -- + -- - SEAD: Suppression of Air Defenses, which are ground targets that have medium or long range radar emitters. + -- - CAS : Close Air Support, when there are enemy ground targets close to friendly units. + -- - BAI : Battlefield Air Interdiction, which are targets further away from the frond-line. + -- + -- You need to configure each squadron which task types you want it to perform. Read on ... + -- + -- ### 3.2. Squadrons enemy ground target **engagement types**. + -- + -- There are two ways how targets can be engaged: directly **on call** from the airfield, farp or carrier, or through a **patrol**. + -- + -- Patrols are extremely handy, as these will airborne your helicopters or airplanes in advance. They will patrol in defined zones outlined, + -- and will engage with the targets once commanded. If the patrol zone is close enough to the enemy ground targets, then the time required + -- to engage is heavily minimized! + -- + -- However; patrols come with a side effect: since your resources are airborne, they will be vulnerable to incoming air attacks from the enemy. + -- + -- The mission designer needs to carefully balance the need for patrols or the need for engagement on call from the airfields. + -- + -- ### 3.3. Squadron **on call** engagement. + -- + -- So to make squadrons engage targets from the airfields, use the following methods: + -- + -- - For SEAD, use the @{#AI_A2G_DISPATCHER.SetSquadronSead}() method. + -- - For CAS, use the @{#AI_A2G_DISPATCHER.SetSquadronCas}() method. + -- - For BAI, use the @{#AI_A2G_DISPATCHER.SetSquadronBai}() method. + -- + -- Note that for the tasks, specific helicopter or airplane templates are required to be used, which you can configure using your mission editor. + -- Especially the payload (weapons configuration) is important to get right. + -- + -- For example, the following will define for the squadrons different tasks: + -- + -- A2GDispatcher:SetSquadron( "Maykop SEAD", AIRBASE.Caucasus.Maykop_Khanskaya, { "CCCP KA-50 SEAD" }, 10 ) + -- A2GDispatcher:SetSquadronSead( "Maykop SEAD", 120, 250 ) + -- + -- A2GDispatcher:SetSquadron( "Maykop CAS", "CAS", { "CCCP KA-50 CAS" }, 10 ) + -- A2GDispatcher:SetSquadronCas( "Maykop CAS", 120, 250 ) + -- + -- A2GDispatcher:SetSquadron( "Maykop BAI", "BAI", { "CCCP KA-50 BAI" }, 10 ) + -- A2GDispatcher:SetSquadronBai( "Maykop BAI", 120, 250 ) + -- + -- ### 3.4. Squadron **on patrol engagement**. + -- + -- Squadrons can be setup to patrol in the air near the engagement hot zone. + -- When needed, the A2G defense units will be close to the battle area, and can engage quickly. + -- + -- So to make squadrons engage targets from a patrol zone, use the following methods: + -- + -- - For SEAD, use the @{#AI_A2G_DISPATCHER.SetSquadronSeadPatrol}() method. + -- - For CAS, use the @{#AI_A2G_DISPATCHER.SetSquadronCasPatrol}() method. + -- - For BAI, use the @{#AI_A2G_DISPATCHER.SetSquadronBaiPatrol}() method. + -- + -- Because a patrol requires more parameters, the following methods must be used to fine-tune the patrols for each squadron. + -- + -- - For SEAD, use the @{#AI_A2G_DISPATCHER.SetSquadronSeadPatrolInterval}() method. + -- - For CAS, use the @{#AI_A2G_DISPATCHER.SetSquadronCasPatrolInterval}() method. + -- - For BAI, use the @{#AI_A2G_DISPATCHER.SetSquadronBaiPatrolInterval}() method. + -- + -- Here an example to setup patrols of various task types: + -- + -- A2GDispatcher:SetSquadron( "Maykop SEAD", AIRBASE.Caucasus.Maykop_Khanskaya, { "CCCP KA-50 SEAD" }, 10 ) + -- A2GDispatcher:SetSquadronSeadPatrol( "Maykop SEAD", PatrolZone, 300, 500, 50, 80, 250, 300 ) + -- A2GDispatcher:SetSquadronPatrolInterval( "Maykop SEAD", 2, 30, 60, 1, "SEAD" ) + -- + -- A2GDispatcher:SetSquadron( "Maykop CAS", "CAS", { "CCCP KA-50 CAS" }, 10 ) + -- A2GDispatcher:SetSquadronCasPatrol( "Maykop CAS", PatrolZone, 600, 700, 50, 80, 250, 300 ) + -- A2GDispatcher:SetSquadronPatrolInterval( "Maykop CAS", 2, 30, 60, 1, "CAS" ) + -- + -- A2GDispatcher:SetSquadron( "Maykop BAI", "BAI", { "CCCP KA-50 BAI" }, 10 ) + -- A2GDispatcher:SetSquadronBaiPatrol( "Maykop BAI", PatrolZone, 800, 900, 50, 80, 250, 300 ) + -- A2GDispatcher:SetSquadronPatrolInterval( "Maykop BAI", 2, 30, 60, 1, "BAI" ) + -- + -- + -- ### 3.5. Set squadron take-off methods + -- + -- Use the various SetSquadronTakeoff... methods to control how squadrons are taking-off from the home airfield, FARP or ship. + -- + -- * @{#AI_A2G_DISPATCHER.SetSquadronTakeoff}() is the generic configuration method to control takeoff from the air, hot, cold or from the runway. See the method for further details. + -- * @{#AI_A2G_DISPATCHER.SetSquadronTakeoffInAir}() will spawn new aircraft from the squadron directly in the air. + -- * @{#AI_A2G_DISPATCHER.SetSquadronTakeoffFromParkingCold}() will spawn new aircraft in without running engines at a parking spot at the airfield. + -- * @{#AI_A2G_DISPATCHER.SetSquadronTakeoffFromParkingHot}() will spawn new aircraft in with running engines at a parking spot at the airfield. + -- * @{#AI_A2G_DISPATCHER.SetSquadronTakeoffFromRunway}() will spawn new aircraft at the runway at the airfield. + -- + -- **The default landing method is to spawn new aircraft directly in the air.** + -- + -- Use these methods to fine-tune for specific airfields that are known to create bottlenecks, or have reduced airbase efficiency. + -- The more and the longer aircraft need to taxi at an airfield, the more risk there is that: + -- + -- * aircraft will stop waiting for each other or for a landing aircraft before takeoff. + -- * aircraft may get into a "dead-lock" situation, where two aircraft are blocking each other. + -- * aircraft may collide at the airbase. + -- * aircraft may be awaiting the landing of a plane currently in the air, but never lands ... + -- + -- Currently within the DCS engine, the airfield traffic coordination is erroneous and contains a lot of bugs. + -- If you experience while testing problems with aircraft take-off or landing, please use one of the above methods as a solution to workaround these issues! + -- + -- This example sets the default takeoff method to be from the runway. + -- And for a couple of squadrons overrides this default method. + -- + -- -- Setup the Takeoff methods + -- + -- -- The default takeoff + -- A2ADispatcher:SetDefaultTakeOffFromRunway() + -- + -- -- The individual takeoff per squadron + -- A2ADispatcher:SetSquadronTakeoff( "Mineralnye", AI_A2G_DISPATCHER.Takeoff.Air ) + -- A2ADispatcher:SetSquadronTakeoffInAir( "Sochi" ) + -- A2ADispatcher:SetSquadronTakeoffFromRunway( "Mozdok" ) + -- A2ADispatcher:SetSquadronTakeoffFromParkingCold( "Maykop" ) + -- A2ADispatcher:SetSquadronTakeoffFromParkingHot( "Novo" ) + -- + -- + -- ### 3.5.1. Set Squadron takeoff altitude when spawning new aircraft in the air. + -- + -- In the case of the @{#AI_A2G_DISPATCHER.SetSquadronTakeoffInAir}() there is also an other parameter that can be applied. + -- That is modifying or setting the **altitude** from where planes spawn in the air. + -- Use the method @{#AI_A2G_DISPATCHER.SetSquadronTakeoffInAirAltitude}() to set the altitude for a specific squadron. + -- The default takeoff altitude can be modified or set using the method @{#AI_A2G_DISPATCHER.SetSquadronTakeoffInAirAltitude}(). + -- As part of the method @{#AI_A2G_DISPATCHER.SetSquadronTakeoffInAir}() a parameter can be specified to set the takeoff altitude. + -- If this parameter is not specified, then the default altitude will be used for the squadron. + -- + -- ### 3.5.2. Set Squadron takeoff interval. + -- + -- The different types of available airfields have different amounts of available launching platforms: + -- + -- - Airbases typically have a lot of platforms. + -- - FARPs have 4 platforms. + -- - Ships have 2 to 4 platforms. + -- + -- Depending on the demand of requested takeoffs by the A2G dispatcher, an airfield can become overloaded. Too many aircraft need to be taken + -- off at the same time, which will result in clutter as described above. In order to better control this behaviour, a takeoff scheduler is implemented, + -- which can be used to control how many aircraft are ordered for takeoff between specific time intervals. + -- The takeff intervals can be specified per squadron, which make sense, as each squadron have a "home" airfield. + -- + -- For this purpose, the method @{#AI_A2G_DISPATCHER.SetSquadronTakeOffInterval}() can be used to specify the takeoff intervals of + -- aircraft groups per squadron to avoid cluttering of aircraft at airbases. + -- This is especially useful for FARPs and ships. Each takeoff dispatch is queued by the dispatcher and when the interval time + -- has been reached, a new group will be spawned or activated for takeoff. + -- + -- The interval needs to be estimated, and depends on the time needed for the aircraft group to actually depart from the launch platform, and + -- the way how the aircraft are starting up. Cold starts take the longest duration, hot starts a few seconds, and runway takeoff also a few seconds for FARPs and ships. + -- + -- See the underlying example: + -- + -- -- Imagine a squadron launched from a FARP, with a grouping of 4. + -- -- Aircraft will cold start from the FARP, and thus, a maximum of 4 aircraft can be launched at the same time. + -- -- Additionally, depending on the group composition of the aircraft, defending units will be ordered for takeoff together. + -- -- It takes about 3 to 4 minutes to takeoff helicopters from FARPs in cold start. + -- A2ADispatcher:SetSquadronTakeOffInterval( "Mineralnye", 60 * 4 ) + -- + -- + -- ### 3.6. Set squadron landing methods + -- + -- In analogy with takeoff, the landing methods are to control how squadrons land at the airfield: + -- + -- * @{#AI_A2G_DISPATCHER.SetSquadronLanding}() is the generic configuration method to control landing, namely despawn the aircraft near the airfield in the air, right after landing, or at engine shutdown. + -- * @{#AI_A2G_DISPATCHER.SetSquadronLandingNearAirbase}() will despawn the returning aircraft in the air when near the airfield. + -- * @{#AI_A2G_DISPATCHER.SetSquadronLandingAtRunway}() will despawn the returning aircraft directly after landing at the runway. + -- * @{#AI_A2G_DISPATCHER.SetSquadronLandingAtEngineShutdown}() will despawn the returning aircraft when the aircraft has returned to its parking spot and has turned off its engines. + -- + -- You can use these methods to minimize the airbase coodination overhead and to increase the airbase efficiency. + -- When there are lots of aircraft returning for landing, at the same airbase, the takeoff process will be halted, which can cause a complete failure of the + -- A2A defense system, as no new CAP or GCI planes can takeoff. + -- Note that the method @{#AI_A2G_DISPATCHER.SetSquadronLandingNearAirbase}() will only work for returning aircraft, not for damaged or out of fuel aircraft. + -- Damaged or out-of-fuel aircraft are returning to the nearest friendly airbase and will land, and are out of control from ground control. + -- + -- This example defines the default landing method to be at the runway. + -- And for a couple of squadrons overrides this default method. + -- + -- -- Setup the Landing methods + -- + -- -- The default landing method + -- A2ADispatcher:SetDefaultLandingAtRunway() + -- + -- -- The individual landing per squadron + -- A2ADispatcher:SetSquadronLandingAtRunway( "Mineralnye" ) + -- A2ADispatcher:SetSquadronLandingNearAirbase( "Sochi" ) + -- A2ADispatcher:SetSquadronLandingAtEngineShutdown( "Mozdok" ) + -- A2ADispatcher:SetSquadronLandingNearAirbase( "Maykop" ) + -- A2ADispatcher:SetSquadronLanding( "Novo", AI_A2G_DISPATCHER.Landing.AtRunway ) + -- + -- + -- ### 3.7. Set squadron **grouping**. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetSquadronGrouping}() to set the grouping of aircraft when spawned in. + -- + -- ![Banner Image](..\Presentations\AI_A2G_DISPATCHER\Dia12.JPG) + -- + -- In the case of **on call** engagement, the @{#AI_A2G_DISPATCHER.SetSquadronGrouping}() method has additional behaviour. + -- When there aren't enough patrol flights airborne, a on call will be initiated for the remaining + -- targets to be engaged. Depending on the grouping parameter, the spawned flights for on call aircraft are grouped into this setting. + -- For example with a group setting of 2, if 3 targets are detected and cannot be engaged by the available patrols or any airborne flight, + -- an additional on call flight needs to be started. + -- + -- The **grouping value is set for a Squadron**, and can be **dynamically adjusted** during mission execution, so to adjust the defense flights grouping when the tactical situation changes. + -- + -- ### 3.8. Set the squadron **overhead** to balance the effectiveness of the A2G defenses. + -- + -- The effectiveness can be set with the **overhead parameter**. This is a number that is used to calculate the amount of Units that dispatching command will allocate to GCI in surplus of detected amount of units. + -- The **default value** of the overhead parameter is 1.0, which means **equal balance**. + -- + -- ![Banner Image](..\Presentations\AI_A2G_DISPATCHER\Dia11.JPG) + -- + -- However, depending on the (type of) aircraft (strength and payload) in the squadron and the amount of resources available, this parameter can be changed. + -- + -- The @{#AI_A2G_DISPATCHER.SetSquadronOverhead}() method can be used to tweak the defense strength, + -- taking into account the plane types of the squadron. + -- + -- For example, a A-10C with full long-distance A2G missiles payload, may still be less effective than a Su-23 with short range A2G missiles... + -- So in this case, one may want to use the @{#AI_A2G_DISPATCHER.SetOverhead}() method to allocate more defending planes as the amount of detected attacking ground units. + -- The overhead must be given as a decimal value with 1 as the neutral value, which means that overhead values: + -- + -- * Higher than 1.0, for example 1.5, will increase the defense unit amounts. For 4 attacking ground units detected, 6 aircraft will be spawned. + -- * Lower than 1, for example 0.75, will decrease the defense unit amounts. For 4 attacking ground units detected, only 3 aircraft will be spawned. + -- + -- The amount of defending units is calculated by multiplying the amount of detected attacking ground units as part of the detected group + -- multiplied by the overhead parameter, and rounded up to the smallest integer. + -- + -- Typically, for A2G defenses, values small than 1 will be used. Here are some good values for a couple of aircraft to support CAS operations: + -- + -- - A-10C: 0.15 + -- - Su-34: 0.15 + -- - A-10A: 0.25 + -- - SU-25T: 0.10 + -- + -- So generically, the amount of missiles that an aircraft can take will determine its attacking effectiveness. The longer the range of the missiles, + -- the less risk that the defender may be destroyed by the enemy, thus, the less aircraft needs to be activated in a defense. + -- + -- The **overhead value is set for a Squadron**, and can be **dynamically adjusted** during mission execution, so to adjust the defense overhead when the tactical situation changes. + -- + -- ### 3.8. Set the squadron **engage limit**. + -- + -- To limit the amount of aircraft to defend against a large group of intruders, an **engage limit** can be defined per squadron. + -- This limit will avoid an extensive amount of aircraft to engage with the enemy if the attacking ground forces are enormous. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetSquadronEngageLimit}() to limit the amount of aircraft that will engage with the enemy, per squadron. + -- + -- ## 4. Set the **fuel treshold**. + -- + -- When aircraft get **out of fuel** to a certain %-tage, which is by default **15% (0.15)**, there are two possible actions that can be taken: + -- - The aircraft will go RTB, and will be replaced with a new aircraft if possible. + -- - The aircraft will refuel at a tanker, if a tanker has been specified for the squadron. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetSquadronFuelThreshold}() to set the **squadron fuel treshold** of the aircraft for all squadrons. + -- + -- ## 6. Other configuration options + -- + -- ### 6.1. Set a tactical display panel. + -- + -- Every 30 seconds, a tactical display panel can be shown that illustrates what the status is of the different groups controlled by AI_A2G_DISPATCHER. + -- Use the method @{#AI_A2G_DISPATCHER.SetTacticalDisplay}() to switch on the tactical display panel. The default will not show this panel. + -- Note that there may be some performance impact if this panel is shown. + -- + -- ## 10. Default settings. + -- + -- Default settings configure the standard behaviour of the squadrons. + -- This section a good overview of the different parameters that setup the behaviour of **ALL** the squadrons by default. + -- Note that default behaviour can be tweaked, and thus, this will change the behaviour of all the squadrons. + -- Unless there is a specific behaviour set for a specific squadron, the default configured behaviour will be followed. + -- + -- ## 10.1. Default **takeoff** behaviour. + -- + -- The default takeoff behaviour is set to **in the air**, which means that new spawned aircraft will be spawned directly in the air above the airbase by default. + -- + -- **The default takeoff method can be set for ALL squadrons that don't have an individual takeoff method configured.** + -- + -- * @{#AI_A2G_DISPATCHER.SetDefaultTakeoff}() is the generic configuration method to control takeoff by default from the air, hot, cold or from the runway. See the method for further details. + -- * @{#AI_A2G_DISPATCHER.SetDefaultTakeoffInAir}() will spawn by default new aircraft from the squadron directly in the air. + -- * @{#AI_A2G_DISPATCHER.SetDefaultTakeoffFromParkingCold}() will spawn by default new aircraft in without running engines at a parking spot at the airfield. + -- * @{#AI_A2G_DISPATCHER.SetDefaultTakeoffFromParkingHot}() will spawn by default new aircraft in with running engines at a parking spot at the airfield. + -- * @{#AI_A2G_DISPATCHER.SetDefaultTakeoffFromRunway}() will spawn by default new aircraft at the runway at the airfield. + -- + -- ## 10.2. Default landing behaviour. + -- + -- The default landing behaviour is set to **near the airbase**, which means that returning airplanes will be despawned directly in the air by default. + -- + -- The default landing method can be set for ALL squadrons that don't have an individual landing method configured. + -- + -- * @{#AI_A2G_DISPATCHER.SetDefaultLanding}() is the generic configuration method to control by default landing, namely despawn the aircraft near the airfield in the air, right after landing, or at engine shutdown. + -- * @{#AI_A2G_DISPATCHER.SetDefaultLandingNearAirbase}() will despawn by default the returning aircraft in the air when near the airfield. + -- * @{#AI_A2G_DISPATCHER.SetDefaultLandingAtRunway}() will despawn by default the returning aircraft directly after landing at the runway. + -- * @{#AI_A2G_DISPATCHER.SetDefaultLandingAtEngineShutdown}() will despawn by default the returning aircraft when the aircraft has returned to its parking spot and has turned off its engines. + -- + -- ## 10.3. Default **overhead**. + -- + -- The default overhead is set to **0.25**. That essentially means that for each 4 ground enemies there will be 1 aircraft dispatched. + -- + -- The default overhead value can be set for ALL squadrons that don't have an individual overhead value configured. + -- + -- Use the @{#AI_A2G_DISPATCHER.SetDefaultOverhead}() method can be used to set the default overhead or defense strength for ALL squadrons. + -- + -- ## 10.4. Default **grouping**. + -- + -- The default grouping is set to **one airplane**. That essentially means that there won't be any grouping applied by default. + -- + -- The default grouping value can be set for ALL squadrons that don't have an individual grouping value configured. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetDefaultGrouping}() to set the **default grouping** of spawned airplanes for all squadrons. + -- + -- ## 10.5. Default RTB fuel treshold. + -- + -- When an airplane gets **out of fuel** to a certain %-tage, which is **15% (0.15)**, it will go RTB, and will be replaced with a new airplane when applicable. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetDefaultFuelThreshold}() to set the **default fuel treshold** of spawned airplanes for all squadrons. + -- + -- ## 10.6. Default RTB damage treshold. + -- + -- When an airplane is **damaged** to a certain %-tage, which is **40% (0.40)**, it will go RTB, and will be replaced with a new airplane when applicable. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetDefaultDamageThreshold}() to set the **default damage treshold** of spawned airplanes for all squadrons. + -- + -- ## 10.7. Default settings for **patrol**. + -- + -- ### 10.7.1. Default **patrol time Interval**. + -- + -- Patrol dispatching is time event driven, and will evaluate in random time intervals if a new patrol needs to be dispatched. + -- + -- The default patrol time interval is between **180** and **600** seconds. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetDefaultPatrolTimeInterval}() to set the **default patrol time interval** of dispatched aircraft for ALL squadrons. + -- + -- Note that you can still change the patrol limit and patrol time intervals for each patrol individually using + -- the @{#AI_A2G_DISPATCHER.SetSquadronPatrolTimeInterval}() method. + -- + -- ### 10.7.2. Default **patrol limit**. + -- + -- Multiple patrol can be airborne at the same time for one squadron, which is controlled by the **patrol limit**. + -- The **default patrol limit** is 1 patrol per squadron to be airborne at the same time. + -- Note that the default patrol limit is used when a squadron patrol is defined, and cannot be changed afterwards. + -- So, ensure that you set the default patrol limit **before** you define or setup the squadron patrol. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetDefaultPatrolTimeInterval}() to set the **default patrol time interval** of dispatched aircraft patrols for all squadrons. + -- Note that you can still change the patrol limit and patrol time intervals for each patrol individually using + -- the @{#AI_A2G_DISPATCHER.SetSquadronPatrolTimeInterval}() method. + -- + -- ## 10.7.3. Default tanker for refuelling when executing CAP. + -- + -- Instead of sending CAP to RTB when out of fuel, you can let CAP refuel in mid air using a tanker. + -- This greatly increases the efficiency of your CAP operations. + -- + -- In the mission editor, setup a group with task Refuelling. A tanker unit of the correct coalition will be automatically selected. + -- Then, use the method @{#AI_A2G_DISPATCHER.SetDefaultTanker}() to set the tanker for the dispatcher. + -- Use the method @{#AI_A2G_DISPATCHER.SetDefaultFuelThreshold}() to set the %-tage left in the defender airplane tanks when a refuel action is needed. + -- + -- When the tanker specified is alive and in the air, the tanker will be used for refuelling. + -- + -- For example, the following setup will set the default refuel tanker to "Tanker": + -- + -- ![Banner Image](..\Presentations\AI_A2G_DISPATCHER\AI_A2G_DISPATCHER-ME_11.JPG) + -- + -- -- Define the CAP + -- A2ADispatcher:SetSquadron( "Sochi", AIRBASE.Caucasus.Sochi_Adler, { "SQ CCCP SU-34" }, 20 ) + -- A2ADispatcher:SetSquadronCap( "Sochi", ZONE:New( "PatrolZone" ), 4000, 8000, 600, 800, 1000, 1300 ) + -- A2ADispatcher:SetSquadronCapInterval("Sochi", 2, 30, 600, 1 ) + -- A2ADispatcher:SetSquadronGci( "Sochi", 900, 1200 ) + -- + -- -- Set the default tanker for refuelling to "Tanker", when the default fuel treshold has reached 90% fuel left. + -- A2ADispatcher:SetDefaultFuelThreshold( 0.9 ) + -- A2ADispatcher:SetDefaultTanker( "Tanker" ) + -- + -- ## 10.8. Default settings for GCI. + -- + -- ## 10.8.1. Optimal intercept point calculation. + -- + -- When intruders are detected, the intrusion path of the attackers can be monitored by the EWR. + -- Although defender planes might be on standby at the airbase, it can still take some time to get the defenses up in the air if there aren't any defenses airborne. + -- This time can easily take 2 to 3 minutes, and even then the defenders still need to fly towards the target, which takes also time. + -- + -- Therefore, an optimal **intercept point** is calculated which takes a couple of parameters: + -- + -- * The average bearing of the intruders for an amount of seconds. + -- * The average speed of the intruders for an amount of seconds. + -- * An assumed time it takes to get planes operational at the airbase. + -- + -- The **intercept point** will determine: + -- + -- * If there are any friendlies close to engage the target. These can be defenders performing CAP or defenders in RTB. + -- * The optimal airbase from where defenders will takeoff for GCI. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetIntercept}() to modify the assumed intercept delay time to calculate a valid interception. + -- + -- ## 10.8.2. Default Disengage Radius. + -- + -- The radius to **disengage any target** when the **distance** of the defender to the **home base** is larger than the specified meters. + -- The default Disengage Radius is **300km** (300000 meters). Note that the Disengage Radius is applicable to ALL squadrons! + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetDisengageRadius}() to modify the default Disengage Radius to another distance setting. + -- + -- ## 11. Airbase capture: + -- + -- Different squadrons can be located at one airbase. + -- If the airbase gets captured, that is, when there is an enemy unit near the airbase, and there aren't anymore friendlies at the airbase, the airbase will change coalition ownership. + -- As a result, the GCI and CAP will stop! + -- However, the squadron will still stay alive. Any airplane that is airborne will continue its operations until all airborne airplanes + -- of the squadron will be destroyed. This to keep consistency of air operations not to confuse the players. + -- + -- + -- + -- + -- @field #AI_A2G_DISPATCHER + AI_A2G_DISPATCHER = { + ClassName = "AI_A2G_DISPATCHER", + Detection = nil, + } + + --- Definition of a Squadron. + -- @type AI_A2G_DISPATCHER.Squadron + -- @field #string Name The Squadron name. + -- @field Wrapper.Airbase#AIRBASE Airbase The home airbase. + -- @field #string AirbaseName The name of the home airbase. + -- @field Core.Spawn#SPAWN Spawn The spawning object. + -- @field #number ResourceCount The number of resources available. + -- @field #list<#string> TemplatePrefixes The list of template prefixes. + -- @field #boolean Captured true if the squadron is captured. + -- @field #number Overhead The overhead for the squadron. + + + --- List of defense coordinates. + -- @type AI_A2G_DISPATCHER.DefenseCoordinates + -- @map <#string,Core.Point#COORDINATE> A list of all defense coordinates mapped per defense coordinate name. + + --- @field #AI_A2G_DISPATCHER.DefenseCoordinates DefenseCoordinates + AI_A2G_DISPATCHER.DefenseCoordinates = {} + + --- Enumerator for spawns at airbases + -- @type AI_A2G_DISPATCHER.Takeoff + -- @extends Wrapper.Group#GROUP.Takeoff + + --- @field #AI_A2G_DISPATCHER.Takeoff Takeoff + AI_A2G_DISPATCHER.Takeoff = GROUP.Takeoff + + --- Defnes Landing location. + -- @field #AI_A2G_DISPATCHER.Landing + AI_A2G_DISPATCHER.Landing = { + NearAirbase = 1, + AtRunway = 2, + AtEngineShutdown = 3, + } + + --- A defense queue item description + -- @type AI_A2G_DISPATCHER.DefenseQueueItem + -- @field Squadron + -- @field #AI_A2G_DISPATCHER.Squadron DefenderSquadron The squadron in the queue. + -- @field DefendersNeeded + -- @field Defense + -- @field DefenseTaskType + -- @field Functional.Detection#DETECTION_BASE AttackerDetection + -- @field DefenderGrouping + -- @field #string SquadronName The name of the squadron. + + --- Queue of planned defenses to be launched. + -- This queue exists because defenses must be launched on FARPS, or in the air, or on an airbase, or on carriers. + -- And some of these platforms have very limited amount of "launching" platforms. + -- Therefore, this queue concept is introduced that queues each defender request. + -- Depending on the location of the launching site, the queued defenders will be launched at varying time intervals. + -- This guarantees that launched defenders are also directly existing ... + -- @type AI_A2G_DISPATCHER.DefenseQueue + -- @list<#AI_A2G_DISPATCHER.DefenseQueueItem> DefenseQueueItem A list of all defenses being queued ... + + --- @field #AI_A2G_DISPATCHER.DefenseQueue DefenseQueue + AI_A2G_DISPATCHER.DefenseQueue = {} + + + --- Defense approach types + -- @type #AI_A2G_DISPATCHER.DefenseApproach + AI_A2G_DISPATCHER.DefenseApproach = { + Random = 1, + Distance = 2, + } + + --- AI_A2G_DISPATCHER constructor. + -- This is defining the A2G DISPATCHER for one coaliton. + -- The Dispatcher works with a @{Functional.Detection#DETECTION_BASE} object that is taking of the detection of targets using the EWR units. + -- The Detection object is polymorphic, depending on the type of detection object choosen, the detection will work differently. + -- @param #AI_A2G_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE Detection The DETECTION object that will detects targets using the the Early Warning Radar network. + -- @return #AI_A2G_DISPATCHER self + -- @usage + -- + -- -- Setup the Detection, using DETECTION_AREAS. + -- -- First define the SET of GROUPs that are defining the EWR network. + -- -- Here with prefixes DF CCCP AWACS, DF CCCP EWR. + -- DetectionSetGroup = SET_GROUP:New() + -- DetectionSetGroup:FilterPrefixes( { "DF CCCP AWACS", "DF CCCP EWR" } ) + -- DetectionSetGroup:FilterStart() + -- + -- -- Define the DETECTION_AREAS, using the DetectionSetGroup, with a 30km grouping radius. + -- Detection = DETECTION_AREAS:New( DetectionSetGroup, 30000 ) + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) -- + -- + function AI_A2G_DISPATCHER:New( Detection ) + + -- Inherits from DETECTION_MANAGER + local self = BASE:Inherit( self, DETECTION_MANAGER:New( nil, Detection ) ) -- #AI_A2G_DISPATCHER + + self.Detection = Detection -- Functional.Detection#DETECTION_AREAS + + self.Detection:FilterCategories( Unit.Category.GROUND_UNIT ) + + -- This table models the DefenderSquadron templates. + self.DefenderSquadrons = {} -- The Defender Squadrons. + self.DefenderSpawns = {} + self.DefenderTasks = {} -- The Defenders Tasks. + self.DefenderDefault = {} -- The Defender Default Settings over all Squadrons. + + -- TODO: Check detection through radar. +-- self.Detection:FilterCategories( { Unit.Category.GROUND } ) +-- self.Detection:InitDetectRadar( false ) +-- self.Detection:InitDetectVisual( true ) +-- self.Detection:SetRefreshTimeInterval( 30 ) + + self.SetSendPlayerMessages = false --flash messages to players + + self:SetDefenseRadius() + self:SetDefenseLimit( nil ) + self:SetDefenseApproach( AI_A2G_DISPATCHER.DefenseApproach.Random ) + self:SetIntercept( 300 ) -- A default intercept delay time of 300 seconds. + self:SetDisengageRadius( 300000 ) -- The default Disengage Radius is 300 km. + + self:SetDefaultTakeoff( AI_A2G_DISPATCHER.Takeoff.Air ) + self:SetDefaultTakeoffInAirAltitude( 500 ) -- Default takeoff is 500 meters above the ground. + self:SetDefaultLanding( AI_A2G_DISPATCHER.Landing.NearAirbase ) + self:SetDefaultOverhead( 1 ) + self:SetDefaultGrouping( 1 ) + self:SetDefaultFuelThreshold( 0.15, 0 ) -- 15% of fuel remaining in the tank will trigger the airplane to return to base or refuel. + self:SetDefaultDamageThreshold( 0.4 ) -- When 40% of damage, go RTB. + self:SetDefaultPatrolTimeInterval( 180, 600 ) -- Between 180 and 600 seconds. + self:SetDefaultPatrolLimit( 1 ) -- Maximum one Patrol per squadron. + + + self:AddTransition( "Started", "Assign", "Started" ) + + --- OnAfter Transition Handler for Event Assign. + -- @function [parent=#AI_A2G_DISPATCHER] OnAfterAssign + -- @param #AI_A2G_DISPATCHER self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @param Tasking.Task_A2G#AI_A2G Task + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param #string PlayerName + + self:AddTransition( "*", "Patrol", "*" ) + + --- Patrol Handler OnBefore for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] OnBeforePatrol + -- @param #AI_A2G_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Patrol Handler OnAfter for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] OnAfterPatrol + -- @param #AI_A2G_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Patrol Trigger for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] Patrol + -- @param #AI_A2G_DISPATCHER self + + --- Patrol Asynchronous Trigger for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] __Patrol + -- @param #AI_A2G_DISPATCHER self + -- @param #number Delay + + self:AddTransition( "*", "Defend", "*" ) + + --- Defend Handler OnBefore for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] OnBeforeDefend + -- @param #AI_A2G_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Defend Handler OnAfter for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] OnAfterDefend + -- @param #AI_A2G_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Defend Trigger for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] Defend + -- @param #AI_A2G_DISPATCHER self + + --- Defend Asynchronous Trigger for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] __Defend + -- @param #AI_A2G_DISPATCHER self + -- @param #number Delay + + self:AddTransition( "*", "Engage", "*" ) + + --- Engage Handler OnBefore for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] OnBeforeEngage + -- @param #AI_A2G_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Engage Handler OnAfter for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] OnAfterEngage + -- @param #AI_A2G_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Engage Trigger for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] Engage + -- @param #AI_A2G_DISPATCHER self + + --- Engage Asynchronous Trigger for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] __Engage + -- @param #AI_A2G_DISPATCHER self + -- @param #number Delay + + + -- Subscribe to the CRASH event so that when planes are shot + -- by a Unit from the dispatcher, they will be removed from the detection... + -- This will avoid the detection to still "know" the shot unit until the next detection. + -- Otherwise, a new defense or engage may happen for an already shot plane! + + + self:HandleEvent( EVENTS.Crash, self.OnEventCrashOrDead ) + self:HandleEvent( EVENTS.Dead, self.OnEventCrashOrDead ) + --self:HandleEvent( EVENTS.RemoveUnit, self.OnEventCrashOrDead ) + + + self:HandleEvent( EVENTS.Land ) + self:HandleEvent( EVENTS.EngineShutdown ) + + -- Handle the situation where the airbases are captured. + self:HandleEvent( EVENTS.BaseCaptured ) + + self:SetTacticalDisplay( false ) + + self.DefenderPatrolIndex = 0 + + self:SetDefenseReactivityMedium() + + self.TakeoffScheduleID = self:ScheduleRepeat( 10, 10, 0, nil, self.ResourceTakeoff, self ) + + self:__Start( 1 ) + + return self + end + + + --- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:onafterStart( From, Event, To ) + + self:GetParent( self ).onafterStart( self, From, Event, To ) + + -- Spawn the resources. + for SquadronName, DefenderSquadron in pairs( self.DefenderSquadrons ) do + DefenderSquadron.Resource = {} + for Resource = 1, DefenderSquadron.ResourceCount or 0 do + self:ResourcePark( DefenderSquadron ) + end + self:I( "Parked resources for squadron " .. DefenderSquadron.Name ) + end + + end + + --- Locks the DefenseItem from being defended. + -- @param #AI_A2G_DISPATCHER self + -- @param #string DetectedItemIndex The index of the detected item. + function AI_A2G_DISPATCHER:Lock( DetectedItemIndex ) + self:F( { DetectedItemIndex = DetectedItemIndex } ) + local DetectedItem = self.Detection:GetDetectedItemByIndex( DetectedItemIndex ) + if DetectedItem then + self:F( { Locked = DetectedItem } ) + self.Detection:LockDetectedItem( DetectedItem ) + end + end + + + --- Unlocks the DefenseItem from being defended. + -- @param #AI_A2G_DISPATCHER self + -- @param #string DetectedItemIndex The index of the detected item. + function AI_A2G_DISPATCHER:Unlock( DetectedItemIndex ) + self:F( { DetectedItemIndex = DetectedItemIndex } ) + self:F( { Index = self.Detection.DetectedItemsByIndex } ) + local DetectedItem = self.Detection:GetDetectedItemByIndex( DetectedItemIndex ) + if DetectedItem then + self:F( { Unlocked = DetectedItem } ) + self.Detection:UnlockDetectedItem( DetectedItem ) + end + end + + + --- Sets maximum zones to be engaged at one time by defenders. + -- @param #AI_A2G_DISPATCHER self + -- @param #number DefenseLimit The maximum amount of detected items to be engaged at the same time. + function AI_A2G_DISPATCHER:SetDefenseLimit( DefenseLimit ) + self:F( { DefenseLimit = DefenseLimit } ) + + self.DefenseLimit = DefenseLimit + end + + + --- Sets the method of the tactical approach of the defenses. + -- @param #AI_A2G_DISPATCHER self + -- @param #number DefenseApproach Use the structure AI_A2G_DISPATCHER.DefenseApproach to set the defense approach. + -- The default defense approach is AI_A2G_DISPATCHER.DefenseApproach.Random. + function AI_A2G_DISPATCHER:SetDefenseApproach( DefenseApproach ) + self:F( { DefenseApproach = DefenseApproach } ) + + self._DefenseApproach = DefenseApproach + end + + + --- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:ResourcePark( DefenderSquadron ) + local TemplateID = math.random( 1, #DefenderSquadron.Spawn ) + local Spawn = DefenderSquadron.Spawn[ TemplateID ] -- Core.Spawn#SPAWN + Spawn:InitGrouping( 1 ) + local SpawnGroup + if self:IsSquadronVisible( DefenderSquadron.Name ) then + SpawnGroup = Spawn:SpawnAtAirbase( DefenderSquadron.Airbase, SPAWN.Takeoff.Cold ) + local GroupName = SpawnGroup:GetName() + DefenderSquadron.Resources = DefenderSquadron.Resources or {} + DefenderSquadron.Resources[TemplateID] = DefenderSquadron.Resources[TemplateID] or {} + DefenderSquadron.Resources[TemplateID][GroupName] = {} + DefenderSquadron.Resources[TemplateID][GroupName] = SpawnGroup + end + end + + + --- @param #AI_A2G_DISPATCHER self + -- @param Core.Event#EVENTDATA EventData + function AI_A2G_DISPATCHER:OnEventBaseCaptured( EventData ) + + local AirbaseName = EventData.PlaceName -- The name of the airbase that was captured. + + self:I( "Captured " .. AirbaseName ) + + -- Now search for all squadrons located at the airbase, and sanatize them. + for SquadronName, Squadron in pairs( self.DefenderSquadrons ) do + if Squadron.AirbaseName == AirbaseName then + Squadron.ResourceCount = -999 -- The base has been captured, and the resources are eliminated. No more spawning. + Squadron.Captured = true + self:I( "Squadron " .. SquadronName .. " captured." ) + end + end + end + + --- @param #AI_A2G_DISPATCHER self + -- @param Core.Event#EVENTDATA EventData + function AI_A2G_DISPATCHER:OnEventCrashOrDead( EventData ) + self.Detection:ForgetDetectedUnit( EventData.IniUnitName ) + end + + --- @param #AI_A2G_DISPATCHER self + -- @param Core.Event#EVENTDATA EventData + function AI_A2G_DISPATCHER:OnEventLand( EventData ) + self:F( "Landed" ) + local DefenderUnit = EventData.IniUnit + local Defender = EventData.IniGroup + local Squadron = self:GetSquadronFromDefender( Defender ) + if Squadron then + self:F( { SquadronName = Squadron.Name } ) + local LandingMethod = self:GetSquadronLanding( Squadron.Name ) + + if LandingMethod == AI_A2G_DISPATCHER.Landing.AtRunway then + local DefenderSize = Defender:GetSize() + if DefenderSize == 1 then + self:RemoveDefenderFromSquadron( Squadron, Defender ) + end + DefenderUnit:Destroy() + self:ResourcePark( Squadron, Defender ) + return + end + if DefenderUnit:GetLife() ~= DefenderUnit:GetLife0() then + -- Damaged units cannot be repaired anymore. + DefenderUnit:Destroy() + return + end + end + end + + --- @param #AI_A2G_DISPATCHER self + -- @param Core.Event#EVENTDATA EventData + function AI_A2G_DISPATCHER:OnEventEngineShutdown( EventData ) + local DefenderUnit = EventData.IniUnit + local Defender = EventData.IniGroup + local Squadron = self:GetSquadronFromDefender( Defender ) + if Squadron then + self:F( { SquadronName = Squadron.Name } ) + local LandingMethod = self:GetSquadronLanding( Squadron.Name ) + if LandingMethod == AI_A2G_DISPATCHER.Landing.AtEngineShutdown and + not DefenderUnit:InAir() then + local DefenderSize = Defender:GetSize() + if DefenderSize == 1 then + self:RemoveDefenderFromSquadron( Squadron, Defender ) + end + DefenderUnit:Destroy() + self:ResourcePark( Squadron, Defender ) + end + end + end + + do -- Manage the defensive behaviour + + --- @param #AI_A2G_DISPATCHER self + -- @param #string DefenseCoordinateName The name of the coordinate to be defended by A2G defenses. + -- @param Core.Point#COORDINATE DefenseCoordinate The coordinate to be defended by A2G defenses. + function AI_A2G_DISPATCHER:AddDefenseCoordinate( DefenseCoordinateName, DefenseCoordinate ) + self.DefenseCoordinates[DefenseCoordinateName] = DefenseCoordinate + end + + --- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:SetDefenseReactivityLow() + self.DefenseReactivity = 0.05 + end + + --- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:SetDefenseReactivityMedium() + self.DefenseReactivity = 0.15 + end + + --- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:SetDefenseReactivityHigh() + self.DefenseReactivity = 0.5 + end + + end + + + --- Define the radius to disengage any target when the distance to the home base is larger than the specified meters. + -- @param #AI_A2G_DISPATCHER self + -- @param #number DisengageRadius (Optional, Default = 300000) The radius to disengage a target when too far from the home base. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Set 50km as the Disengage Radius. + -- A2GDispatcher:SetDisengageRadius( 50000 ) + -- + -- -- Set 100km as the Disengage Radius. + -- A2GDispatcher:SetDisngageRadius() -- 300000 is the default value. + -- + function AI_A2G_DISPATCHER:SetDisengageRadius( DisengageRadius ) + + self.DisengageRadius = DisengageRadius or 300000 + + return self + end + + + --- Define the defense radius to check if a target can be engaged by a squadron group for SEAD, CAS or BAI for defense. + -- When targets are detected that are still really far off, you don't want the AI_A2G_DISPATCHER to launch defenders, as they might need to travel too far. + -- You want it to wait until a certain defend radius is reached, which is calculated as: + -- 1. the **distance of the closest airbase to target**, being smaller than the **Defend Radius**. + -- 2. the **distance to any defense reference point**. + -- + -- The **default** defense radius is defined as **400000** or **40km**. Override the default defense radius when the era of the warfare is early, or, + -- when you don't want to let the AI_A2G_DISPATCHER react immediately when a certain border or area is not being crossed. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetDefendRadius}() to set a specific defend radius for all squadrons, + -- **the Defense Radius is defined for ALL squadrons which are operational.** + -- + -- @param #AI_A2G_DISPATCHER self + -- @param #number DefenseRadius (Optional, Default = 200000) The defense radius to engage detected targets from the nearest capable and available squadron airbase. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Set 100km as the radius to defend from detected targets from the nearest airbase. + -- A2GDispatcher:SetDefendRadius( 100000 ) + -- + -- -- Set 200km as the radius to defend. + -- A2GDispatcher:SetDefendRadius() -- 200000 is the default value. + -- + function AI_A2G_DISPATCHER:SetDefenseRadius( DefenseRadius ) + + self.DefenseRadius = DefenseRadius or 100000 + + self.Detection:SetAcceptRange( self.DefenseRadius ) + + return self + end + + + + --- Define a border area to simulate a **cold war** scenario. + -- A **cold war** is one where Patrol aircraft patrol their territory but will not attack enemy aircraft or launch GCI aircraft unless enemy aircraft enter their territory. In other words the EWR may detect an enemy aircraft but will only send aircraft to attack it if it crosses the border. + -- A **hot war** is one where Patrol aircraft will intercept any detected enemy aircraft and GCI aircraft will launch against detected enemy aircraft without regard for territory. In other words if the ground radar can detect the enemy aircraft then it will send Patrol and GCI aircraft to attack it. + -- If it's a cold war then the **borders of red and blue territory** need to be defined using a @{zone} object derived from @{Core.Zone#ZONE_BASE}. This method needs to be used for this. + -- If a hot war is chosen then **no borders** actually need to be defined using the helicopter units other than it makes it easier sometimes for the mission maker to envisage where the red and blue territories roughly are. In a hot war the borders are effectively defined by the ground based radar coverage of a coalition. Set the noborders parameter to 1 + -- @param #AI_A2G_DISPATCHER self + -- @param Core.Zone#ZONE_BASE BorderZone An object derived from ZONE_BASE, or a list of objects derived from ZONE_BASE. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Set one ZONE_POLYGON object as the border for the A2G dispatcher. + -- local BorderZone = ZONE_POLYGON( "CCCP Border", GROUP:FindByName( "CCCP Border" ) ) -- The GROUP object is a late activate helicopter unit. + -- A2GDispatcher:SetBorderZone( BorderZone ) + -- + -- or + -- + -- -- Set two ZONE_POLYGON objects as the border for the A2G dispatcher. + -- local BorderZone1 = ZONE_POLYGON( "CCCP Border1", GROUP:FindByName( "CCCP Border1" ) ) -- The GROUP object is a late activate helicopter unit. + -- local BorderZone2 = ZONE_POLYGON( "CCCP Border2", GROUP:FindByName( "CCCP Border2" ) ) -- The GROUP object is a late activate helicopter unit. + -- A2GDispatcher:SetBorderZone( { BorderZone1, BorderZone2 } ) + -- + -- + function AI_A2G_DISPATCHER:SetBorderZone( BorderZone ) + + self.Detection:SetAcceptZones( BorderZone ) + + return self + end + + --- Display a tactical report every 30 seconds about which aircraft are: + -- * Patrolling + -- * Engaging + -- * Returning + -- * Damaged + -- * Out of Fuel + -- * ... + -- @param #AI_A2G_DISPATCHER self + -- @param #boolean TacticalDisplay Provide a value of **true** to display every 30 seconds a tactical overview. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the Tactical Display for debug mode. + -- A2GDispatcher:SetTacticalDisplay( true ) + -- + function AI_A2G_DISPATCHER:SetTacticalDisplay( TacticalDisplay ) + + self.TacticalDisplay = TacticalDisplay + + return self + end + + + --- Set the default damage treshold when defenders will RTB. + -- The default damage treshold is by default set to 40%, which means that when the airplane is 40% damaged, it will go RTB. + -- @param #AI_A2G_DISPATCHER self + -- @param #number DamageThreshold A decimal number between 0 and 1, that expresses the %-tage of the damage treshold before going RTB. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default damage treshold. + -- A2GDispatcher:SetDefaultDamageThreshold( 0.90 ) -- Go RTB when the airplane 90% damaged. + -- + function AI_A2G_DISPATCHER:SetDefaultDamageThreshold( DamageThreshold ) + + self.DefenderDefault.DamageThreshold = DamageThreshold + + return self + end + + + --- Set the default Patrol time interval for squadrons, which will be used to determine a random Patrol timing. + -- The default Patrol time interval is between 180 and 600 seconds. + -- @param #AI_A2G_DISPATCHER self + -- @param #number PatrolMinSeconds The minimum amount of seconds for the random time interval. + -- @param #number PatrolMaxSeconds The maximum amount of seconds for the random time interval. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default Patrol time interval. + -- A2GDispatcher:SetDefaultPatrolTimeInterval( 300, 1200 ) -- Between 300 and 1200 seconds. + -- + function AI_A2G_DISPATCHER:SetDefaultPatrolTimeInterval( PatrolMinSeconds, PatrolMaxSeconds ) + + self.DefenderDefault.PatrolMinSeconds = PatrolMinSeconds + self.DefenderDefault.PatrolMaxSeconds = PatrolMaxSeconds + + return self + end + + + --- Set the default Patrol limit for squadrons, which will be used to determine how many Patrol can be airborne at the same time for the squadron. + -- The default Patrol limit is 1 Patrol, which means one Patrol group being spawned. + -- @param #AI_A2G_DISPATCHER self + -- @param #number PatrolLimit The maximum amount of Patrol that can be airborne at the same time for the squadron. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default Patrol limit. + -- A2GDispatcher:SetDefaultPatrolLimit( 2 ) -- Maximum 2 Patrol per squadron. + -- + function AI_A2G_DISPATCHER:SetDefaultPatrolLimit( PatrolLimit ) + + self.DefenderDefault.PatrolLimit = PatrolLimit + + return self + end + + + --- Set the default engage limit for squadrons, which will be used to determine how many air units will engage at the same time with the enemy. + -- The default eatrol limit is 1, which means one eatrol group maximum per squadron. + -- @param #AI_A2G_DISPATCHER self + -- @param #number EngageLimit The maximum engages that can be done at the same time per squadron. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default Patrol limit. + -- A2GDispatcher:SetDefaultEngageLimit( 2 ) -- Maximum 2 engagements with the enemy per squadron. + -- + function AI_A2G_DISPATCHER:SetDefaultEngageLimit( EngageLimit ) + + self.DefenderDefault.EngageLimit = EngageLimit + + return self + end + + + function AI_A2G_DISPATCHER:SetIntercept( InterceptDelay ) + + self.DefenderDefault.InterceptDelay = InterceptDelay + + local Detection = self.Detection -- Functional.Detection#DETECTION_AREAS + Detection:SetIntercept( true, InterceptDelay ) + + return self + end + + + --- Calculates which defender friendlies are nearby the area, to help protect the area. + -- @param #AI_A2G_DISPATCHER self + -- @param DetectedItem + -- @return #table A list of the defender friendlies nearby, sorted by distance. + function AI_A2G_DISPATCHER:GetDefenderFriendliesNearBy( DetectedItem ) + +-- local DefenderFriendliesNearBy = self.Detection:GetFriendliesDistance( DetectedItem ) + + local DefenderFriendliesNearBy = {} + + local DetectionCoordinate = self.Detection:GetDetectedItemCoordinate( DetectedItem ) + + local ScanZone = ZONE_RADIUS:New( "ScanZone", DetectionCoordinate:GetVec2(), self.DefenseRadius ) + + ScanZone:Scan( Object.Category.UNIT, { Unit.Category.AIRPLANE, Unit.Category.HELICOPTER } ) + + local DefenderUnits = ScanZone:GetScannedUnits() + + for DefenderUnitID, DefenderUnit in pairs( DefenderUnits ) do + local DefenderUnit = UNIT:FindByName( DefenderUnit:getName() ) + + DefenderFriendliesNearBy[#DefenderFriendliesNearBy+1] = DefenderUnit + end + + + return DefenderFriendliesNearBy + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:GetDefenderTasks() + return self.DefenderTasks or {} + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:GetDefenderTask( Defender ) + return self.DefenderTasks[Defender] + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:GetDefenderTaskFsm( Defender ) + return self:GetDefenderTask( Defender ).Fsm + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:GetDefenderTaskTarget( Defender ) + return self:GetDefenderTask( Defender ).Target + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:GetDefenderTaskSquadronName( Defender ) + return self:GetDefenderTask( Defender ).SquadronName + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:ClearDefenderTask( Defender ) + if Defender:IsAlive() and self.DefenderTasks[Defender] then + local Target = self.DefenderTasks[Defender].Target + local Message = "Clearing (" .. self.DefenderTasks[Defender].Type .. ") " + Message = Message .. Defender:GetName() + if Target then + Message = Message .. ( Target and ( " from " .. Target.Index .. " [" .. Target.Set:Count() .. "]" ) ) or "" + end + self:F( { Target = Message } ) + end + self.DefenderTasks[Defender] = nil + return self + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:ClearDefenderTaskTarget( Defender ) + + local DefenderTask = self:GetDefenderTask( Defender ) + + if Defender:IsAlive() and DefenderTask then + local Target = DefenderTask.Target + local Message = "Clearing (" .. DefenderTask.Type .. ") " + Message = Message .. Defender:GetName() + if Target then + Message = Message .. ( Target and ( " from " .. Target.Index .. " [" .. Target.Set:Count() .. "]" ) ) or "" + end + self:F( { Target = Message } ) + end + if Defender and DefenderTask and DefenderTask.Target then + DefenderTask.Target = nil + end +-- if Defender and DefenderTask then +-- if DefenderTask.Fsm:Is( "Fuel" ) +-- or DefenderTask.Fsm:Is( "LostControl") +-- or DefenderTask.Fsm:Is( "Damaged" ) then +-- self:ClearDefenderTask( Defender ) +-- end +-- end + return self + end + + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:SetDefenderTask( SquadronName, Defender, Type, Fsm, Target, Size ) + + self:F( { SquadronName = SquadronName, Defender = Defender:GetName() } ) + + self.DefenderTasks[Defender] = self.DefenderTasks[Defender] or {} + self.DefenderTasks[Defender].Type = Type + self.DefenderTasks[Defender].Fsm = Fsm + self.DefenderTasks[Defender].SquadronName = SquadronName + self.DefenderTasks[Defender].Size = Size + + if Target then + self:SetDefenderTaskTarget( Defender, Target ) + end + return self + end + + + --- + -- @param #AI_A2G_DISPATCHER self + -- @param Wrapper.Group#GROUP AIGroup + function AI_A2G_DISPATCHER:SetDefenderTaskTarget( Defender, AttackerDetection ) + + local Message = "(" .. self.DefenderTasks[Defender].Type .. ") " + Message = Message .. Defender:GetName() + Message = Message .. ( AttackerDetection and ( " target " .. AttackerDetection.Index .. " [" .. AttackerDetection.Set:Count() .. "]" ) ) or "" + self:F( { AttackerDetection = Message } ) + if AttackerDetection then + self.DefenderTasks[Defender].Target = AttackerDetection + end + return self + end + + + --- This is the main method to define Squadrons programmatically. + -- Squadrons: + -- + -- * Have a **name or key** that is the identifier or key of the squadron. + -- * Have **specific plane types** defined by **templates**. + -- * Are **located at one specific airbase**. Multiple squadrons can be located at one airbase through. + -- * Optionally have a limited set of **resources**. The default is that squadrons have unlimited resources. + -- + -- The name of the squadron given acts as the **squadron key** in the AI\_A2G\_DISPATCHER:Squadron...() methods. + -- + -- Additionally, squadrons have specific configuration options to: + -- + -- * Control how new aircraft are **taking off** from the airfield (in the air, cold, hot, at the runway). + -- * Control how returning aircraft are **landing** at the airfield (in the air near the airbase, after landing, after engine shutdown). + -- * Control the **grouping** of new aircraft spawned at the airfield. If there is more than one aircraft to be spawned, these may be grouped. + -- * Control the **overhead** or defensive strength of the squadron. Depending on the types of planes and amount of resources, the mission designer can choose to increase or reduce the amount of planes spawned. + -- + -- For performance and bug workaround reasons within DCS, squadrons have different methods to spawn new aircraft or land returning or damaged aircraft. + -- + -- @param #AI_A2G_DISPATCHER self + -- + -- @param #string SquadronName A string (text) that defines the squadron identifier or the key of the Squadron. + -- It can be any name, for example `"104th Squadron"` or `"SQ SQUADRON1"`, whatever. + -- As long as you remember that this name becomes the identifier of your squadron you have defined. + -- You need to use this name in other methods too! + -- + -- @param #string AirbaseName The airbase name where you want to have the squadron located. + -- You need to specify here EXACTLY the name of the airbase as you see it in the mission editor. + -- Examples are `"Batumi"` or `"Tbilisi-Lochini"`. + -- EXACTLY the airbase name, between quotes `""`. + -- To ease the airbase naming when using the LDT editor and IntelliSense, the @{Wrapper.Airbase#AIRBASE} class contains enumerations of the airbases of each map. + -- + -- * Caucasus: @{Wrapper.Airbase#AIRBASE.Caucaus} + -- * Nevada or NTTR: @{Wrapper.Airbase#AIRBASE.Nevada} + -- * Normandy: @{Wrapper.Airbase#AIRBASE.Normandy} + -- + -- @param #string TemplatePrefixes A string or an array of strings specifying the **prefix names of the templates** (not going to explain what is templates here again). + -- Examples are `{ "104th", "105th" }` or `"104th"` or `"Template 1"` or `"BLUE PLANES"`. + -- Just remember that your template (groups late activated) need to start with the prefix you have specified in your code. + -- If you have only one prefix name for a squadron, you don't need to use the `{ }`, otherwise you need to use the brackets. + -- + -- @param #number ResourceCount (optional) A number that specifies how many resources are in stock of the squadron. If not specified, the squadron will have infinite resources available. + -- + -- @usage + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- @usage + -- -- This will create squadron "Squadron1" at "Batumi" airbase, and will use plane types "SQ1" and has 40 planes in stock... + -- A2GDispatcher:SetSquadron( "Squadron1", "Batumi", "SQ1", 40 ) + -- + -- @usage + -- -- This will create squadron "Sq 1" at "Batumi" airbase, and will use plane types "Mig-29" and "Su-27" and has 20 planes in stock... + -- -- Note that in this implementation, the A2G dispatcher will select a random plane type when a new plane (group) needs to be spawned for defenses. + -- -- Note the usage of the {} for the airplane templates list. + -- A2GDispatcher:SetSquadron( "Sq 1", "Batumi", { "Mig-29", "Su-27" }, 40 ) + -- + -- @usage + -- -- This will create 2 squadrons "104th" and "23th" at "Batumi" airbase, and will use plane types "Mig-29" and "Su-27" respectively and each squadron has 10 planes in stock... + -- A2GDispatcher:SetSquadron( "104th", "Batumi", "Mig-29", 10 ) + -- A2GDispatcher:SetSquadron( "23th", "Batumi", "Su-27", 10 ) + -- + -- @usage + -- -- This is an example like the previous, but now with infinite resources. + -- -- The ResourceCount parameter is not given in the SetSquadron method. + -- A2GDispatcher:SetSquadron( "104th", "Batumi", "Mig-29" ) + -- A2GDispatcher:SetSquadron( "23th", "Batumi", "Su-27" ) + -- + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadron( SquadronName, AirbaseName, TemplatePrefixes, ResourceCount ) + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + + local DefenderSquadron = self.DefenderSquadrons[SquadronName] + + DefenderSquadron.Name = SquadronName + DefenderSquadron.Airbase = AIRBASE:FindByName( AirbaseName ) + DefenderSquadron.AirbaseName = DefenderSquadron.Airbase:GetName() + if not DefenderSquadron.Airbase then + error( "Cannot find airbase with name:" .. AirbaseName ) + end + + DefenderSquadron.Spawn = {} + if type( TemplatePrefixes ) == "string" then + local SpawnTemplate = TemplatePrefixes + self.DefenderSpawns[SpawnTemplate] = self.DefenderSpawns[SpawnTemplate] or SPAWN:New( SpawnTemplate ) -- :InitCleanUp( 180 ) + DefenderSquadron.Spawn[1] = self.DefenderSpawns[SpawnTemplate] + else + for TemplateID, SpawnTemplate in pairs( TemplatePrefixes ) do + self.DefenderSpawns[SpawnTemplate] = self.DefenderSpawns[SpawnTemplate] or SPAWN:New( SpawnTemplate ) -- :InitCleanUp( 180 ) + DefenderSquadron.Spawn[#DefenderSquadron.Spawn+1] = self.DefenderSpawns[SpawnTemplate] + end + end + DefenderSquadron.ResourceCount = ResourceCount + DefenderSquadron.TemplatePrefixes = TemplatePrefixes + DefenderSquadron.Captured = false -- Not captured. This flag will be set to true, when the airbase where the squadron is located, is captured. + + self:SetSquadronTakeoffInterval( SquadronName, 0 ) + + self:F( { Squadron = {SquadronName, AirbaseName, TemplatePrefixes, ResourceCount } } ) + + return self + end + + --- Get an item from the Squadron table. + -- @param #AI_A2G_DISPATCHER self + -- @return #table + function AI_A2G_DISPATCHER:GetSquadron( SquadronName ) + + local DefenderSquadron = self.DefenderSquadrons[SquadronName] + + if not DefenderSquadron then + error( "Unknown Squadron:" .. SquadronName ) + end + + return DefenderSquadron + end + + + --- Set the Squadron visible before startup of the dispatcher. + -- All planes will be spawned as uncontrolled on the parking spot. + -- They will lock the parking spot. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Set the Squadron visible before startup of dispatcher. + -- A2GDispatcher:SetSquadronVisible( "Mineralnye" ) + -- + -- TODO: disabling because of bug in queueing. +-- function AI_A2G_DISPATCHER:SetSquadronVisible( SquadronName ) +-- +-- self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} +-- +-- local DefenderSquadron = self:GetSquadron( SquadronName ) +-- +-- DefenderSquadron.Uncontrolled = true +-- self:SetSquadronTakeoffFromParkingCold( SquadronName ) +-- self:SetSquadronLandingAtEngineShutdown( SquadronName ) +-- +-- for SpawnTemplate, DefenderSpawn in pairs( self.DefenderSpawns ) do +-- DefenderSpawn:InitUnControlled() +-- end +-- +-- end + + --- Check if the Squadron is visible before startup of the dispatcher. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @return #bool true if visible. + -- @usage + -- + -- -- Set the Squadron visible before startup of dispatcher. + -- local IsVisible = A2GDispatcher:IsSquadronVisible( "Mineralnye" ) + -- + function AI_A2G_DISPATCHER:IsSquadronVisible( SquadronName ) + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + if DefenderSquadron then + return DefenderSquadron.Uncontrolled == true + end + + return nil + + end + + --- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number TakeoffInterval Only Takeoff new units each specified interval in seconds in 10 seconds steps. + -- @usage + -- + -- -- Set the Squadron Takeoff interval every 60 seconds for squadron "SQ50", which is good for a FARP cold start. + -- A2GDispatcher:SetSquadronTakeoffInterval( "SQ50", 60 ) + -- + function AI_A2G_DISPATCHER:SetSquadronTakeoffInterval( SquadronName, TakeoffInterval ) + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + if DefenderSquadron then + DefenderSquadron.TakeoffInterval = TakeoffInterval or 0 + DefenderSquadron.TakeoffTime = 0 + end + + end + + + + --- Set the squadron patrol parameters for a specific task type. + -- Mission designers should not use this method, instead use the below methods. This method is used by the below methods. + -- + -- - @{#AI_A2G_DISPATCHER:SetSquadronSeadPatrolInterval} for SEAD tasks. + -- - @{#AI_A2G_DISPATCHER:SetSquadronSeadPatrolInterval} for CAS tasks. + -- - @{#AI_A2G_DISPATCHER:SetSquadronSeadPatrolInterval} for BAI tasks. + -- + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number PatrolLimit (optional) The maximum amount of Patrol groups to be spawned. Note that a Patrol is a group, so can consist out of 1 to 4 airplanes. The default is 1 Patrol group. + -- @param #number LowInterval (optional) The minimum time boundary in seconds when a new Patrol will be spawned. The default is 180 seconds. + -- @param #number HighInterval (optional) The maximum time boundary in seconds when a new Patrol will be spawned. The default is 600 seconds. + -- @param #number Probability Is not in use, you can skip this parameter. + -- @param #string DefenseTaskType Should contain "SEAD", "CAS" or "BAI". + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronSeadPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- A2GDispatcher:SetSquadronPatrolInterval( "Mineralnye", 2, 30, 60, 1, "SEAD" ) + -- + function AI_A2G_DISPATCHER:SetSquadronPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability, DefenseTaskType ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + local Patrol = DefenderSquadron[DefenseTaskType] + if Patrol then + Patrol.LowInterval = LowInterval or 180 + Patrol.HighInterval = HighInterval or 600 + Patrol.Probability = Probability or 1 + Patrol.PatrolLimit = PatrolLimit or 1 + Patrol.Scheduler = Patrol.Scheduler or SCHEDULER:New( self ) + local Scheduler = Patrol.Scheduler -- Core.Scheduler#SCHEDULER + local ScheduleID = Patrol.ScheduleID + local Variance = ( Patrol.HighInterval - Patrol.LowInterval ) / 2 + local Repeat = Patrol.LowInterval + Variance + local Randomization = Variance / Repeat + local Start = math.random( 1, Patrol.HighInterval ) + + if ScheduleID then + Scheduler:Stop( ScheduleID ) + end + + Patrol.ScheduleID = Scheduler:Schedule( self, self.SchedulerPatrol, { SquadronName }, Start, Repeat, Randomization ) + else + error( "This squadron does not exist:" .. SquadronName ) + end + + end + + + + --- Set the squadron Patrol parameters for SEAD tasks. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number PatrolLimit (optional) The maximum amount of Patrol groups to be spawned. Note that a Patrol is a group, so can consist out of 1 to 4 airplanes. The default is 1 Patrol group. + -- @param #number LowInterval (optional) The minimum time boundary in seconds when a new Patrol will be spawned. The default is 180 seconds. + -- @param #number HighInterval (optional) The maximum time boundary in seconds when a new Patrol will be spawned. The default is 600 seconds. + -- @param #number Probability Is not in use, you can skip this parameter. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronSeadPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- A2GDispatcher:SetSquadronSeadPatrolInterval( "Mineralnye", 2, 30, 60, 1 ) + -- + function AI_A2G_DISPATCHER:SetSquadronSeadPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability ) + + self:SetSquadronPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability, "SEAD" ) + + end + + + --- Set the squadron Patrol parameters for CAS tasks. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number PatrolLimit (optional) The maximum amount of Patrol groups to be spawned. Note that a Patrol is a group, so can consist out of 1 to 4 airplanes. The default is 1 Patrol group. + -- @param #number LowInterval (optional) The minimum time boundary in seconds when a new Patrol will be spawned. The default is 180 seconds. + -- @param #number HighInterval (optional) The maximum time boundary in seconds when a new Patrol will be spawned. The default is 600 seconds. + -- @param #number Probability Is not in use, you can skip this parameter. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronCasPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- A2GDispatcher:SetSquadronCasPatrolInterval( "Mineralnye", 2, 30, 60, 1 ) + -- + function AI_A2G_DISPATCHER:SetSquadronCasPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability ) + + self:SetSquadronPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability, "CAS" ) + + end + + + --- Set the squadron Patrol parameters for BAI tasks. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number PatrolLimit (optional) The maximum amount of Patrol groups to be spawned. Note that a Patrol is a group, so can consist out of 1 to 4 airplanes. The default is 1 Patrol group. + -- @param #number LowInterval (optional) The minimum time boundary in seconds when a new Patrol will be spawned. The default is 180 seconds. + -- @param #number HighInterval (optional) The maximum time boundary in seconds when a new Patrol will be spawned. The default is 600 seconds. + -- @param #number Probability Is not in use, you can skip this parameter. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronBaiPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- A2GDispatcher:SetSquadronBaiPatrolInterval( "Mineralnye", 2, 30, 60, 1 ) + -- + function AI_A2G_DISPATCHER:SetSquadronBaiPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability ) + + self:SetSquadronPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability, "BAI" ) + + end + + + --- + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:GetPatrolDelay( SquadronName ) + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + self.DefenderSquadrons[SquadronName].Patrol = self.DefenderSquadrons[SquadronName].Patrol or {} + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + local Patrol = self.DefenderSquadrons[SquadronName].Patrol + if Patrol then + return math.random( Patrol.LowInterval, Patrol.HighInterval ) + else + error( "This squadron does not exist:" .. SquadronName ) + end + end + + --- + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @return #table DefenderSquadron + function AI_A2G_DISPATCHER:CanPatrol( SquadronName, DefenseTaskType ) + self:F({SquadronName = SquadronName}) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + if DefenderSquadron.Captured == false then -- We can only spawn new Patrol if the base has not been captured. + + if ( not DefenderSquadron.ResourceCount ) or ( DefenderSquadron.ResourceCount and DefenderSquadron.ResourceCount > 0 ) then -- And, if there are sufficient resources. + + local Patrol = DefenderSquadron[DefenseTaskType] + if Patrol and Patrol.Patrol == true then + local PatrolCount = self:CountPatrolAirborne( SquadronName, DefenseTaskType ) + self:F( { PatrolCount = PatrolCount, PatrolLimit = Patrol.PatrolLimit, PatrolProbability = Patrol.Probability } ) + if PatrolCount < Patrol.PatrolLimit then + local Probability = math.random() + if Probability <= Patrol.Probability then + return DefenderSquadron, Patrol + end + end + else + self:F( "No patrol for " .. SquadronName ) + end + end + end + return nil + end + + + --- + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @return #table DefenderSquadron + function AI_A2G_DISPATCHER:CanDefend( SquadronName, DefenseTaskType ) + self:F({SquadronName = SquadronName, DefenseTaskType}) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + if DefenderSquadron.Captured == false then -- We can only spawn new defense if the home airbase has not been captured. + + if ( not DefenderSquadron.ResourceCount ) or ( DefenderSquadron.ResourceCount and DefenderSquadron.ResourceCount > 0 ) then -- And, if there are sufficient resources. + if DefenderSquadron[DefenseTaskType] and ( DefenderSquadron[DefenseTaskType].Defend == true ) then + return DefenderSquadron, DefenderSquadron[DefenseTaskType] + end + end + end + return nil + end + + --- Set the squadron engage limit for a specific task type. + -- Mission designers should not use this method, instead use the below methods. This method is used by the below methods. + -- + -- - @{#AI_A2G_DISPATCHER:SetSquadronSeadEngageLimit} for SEAD tasks. + -- - @{#AI_A2G_DISPATCHER:SetSquadronSeadEngageLimit} for CAS tasks. + -- - @{#AI_A2G_DISPATCHER:SetSquadronSeadEngageLimit} for BAI tasks. + -- + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageLimit The maximum amount of groups to engage with the enemy for this squadron. + -- @param #string DefenseTaskType Should contain "SEAD", "CAS" or "BAI". + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronEngageLimit( "Mineralnye", 2, "SEAD" ) -- Engage maximum 2 groups with the enemy for SEAD defense. + -- + function AI_A2G_DISPATCHER:SetSquadronEngageLimit( SquadronName, EngageLimit, DefenseTaskType ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + local Defense = DefenderSquadron[DefenseTaskType] + if Defense then + Defense.EngageLimit = EngageLimit or 1 + else + error( "This squadron does not exist:" .. SquadronName ) + end + + end + + + + + --- Set a squadron to engage for suppression of air defenses, when a defense point is under attack. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the SEAD task can be executed. + -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the SEAD task can be executed. + -- @param DCS#Altitude EngageFloorAltitude (optional, default = 1000m ) The lowest altitude in meters where to execute the engagement. + -- @param DCS#Altitude EngageCeilingAltitude (optional, default = 1500m ) The highest altitude in meters where to execute the engagement. + -- @param #number EngageAltType The altitude type when engaging, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @usage + -- + -- -- SEAD Squadron execution. + -- A2GDispatcher:SetSquadronSead( "Mozdok", 900, 1200, 4000, 5000, "BARO" ) + -- A2GDispatcher:SetSquadronSead( "Novo", 900, 2100, 6000, 9000, "BARO" ) + -- A2GDispatcher:SetSquadronSead( "Maykop", 900, 1200, 30, 100, "RADIO" ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronSead2( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + DefenderSquadron.SEAD = DefenderSquadron.SEAD or {} + + local Sead = DefenderSquadron.SEAD + Sead.Name = SquadronName + Sead.EngageMinSpeed = EngageMinSpeed + Sead.EngageMaxSpeed = EngageMaxSpeed + Sead.EngageFloorAltitude = EngageFloorAltitude or 500 + Sead.EngageCeilingAltitude = EngageCeilingAltitude or 1000 + Sead.EngageAltType = EngageAltType + Sead.Defend = true + + self:I( { SEAD = { SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } ) + + return self + end + + --- Set a squadron to engage for suppression of air defenses, when a defense point is under attack. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the SEAD task can be executed. + -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the SEAD task can be executed. + -- @param DCS#Altitude EngageFloorAltitude (optional, default = 1000m ) The lowest altitude in meters where to execute the engagement. + -- @param DCS#Altitude EngageCeilingAltitude (optional, default = 1500m ) The highest altitude in meters where to execute the engagement. + -- @usage + -- + -- -- SEAD Squadron execution. + -- A2GDispatcher:SetSquadronSead( "Mozdok", 900, 1200, 4000, 5000 ) + -- A2GDispatcher:SetSquadronSead( "Novo", 900, 2100, 6000, 8000 ) + -- A2GDispatcher:SetSquadronSead( "Maykop", 900, 1200, 6000, 10000 ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronSead( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude ) + + return self:SetSquadronSead2( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, "RADIO" ) + end + + --- Set the squadron SEAD engage limit. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageLimit The maximum amount of groups to engage with the enemy for this squadron. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronSeadEngageLimit( "Mineralnye", 2 ) -- Engage maximum 2 groups with the enemy for SEAD defense. + -- + function AI_A2G_DISPATCHER:SetSquadronSeadEngageLimit( SquadronName, EngageLimit ) + + self:SetSquadronEngageLimit( SquadronName, EngageLimit, "SEAD" ) + + end + + + + + --- Set a Sead patrol for a Squadron. + -- The Sead patrol will start a patrol of the aircraft at a specified zone, and will engage when commanded. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed. + -- @param #number PatrolMinSpeed (optional, default = 50% of max speed) The minimum speed at which the cap can be executed. + -- @param #number PatrolMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the cap can be executed. + -- @param #number PatrolFloorAltitude (optional, default = 1000m ) The minimum altitude at which the cap can be executed. + -- @param #number PatrolCeilingAltitude (optional, default = 1500m ) The maximum altitude at which the cap can be executed. + -- @param #number PatrolAltType The altitude type when patrolling, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the engage can be executed. + -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the engage can be executed. + -- @param #number EngageFloorAltitude (optional, default = 1000m ) The minimum altitude at which the engage can be executed. + -- @param #number EngageCeilingAltitude (optional, default = 1500m ) The maximum altitude at which the engage can be executed. + -- @param #number EngageAltType The altitude type when engaging, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Sead Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronSeadPatrol2( "Mineralnye", PatrolZoneEast, 500, 600, 4000, 10000, "BARO", 800, 900, 2000, 3000, "RADIO", ) + -- + function AI_A2G_DISPATCHER:SetSquadronSeadPatrol2( SquadronName, Zone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + DefenderSquadron.SEAD = DefenderSquadron.SEAD or {} + + local SeadPatrol = DefenderSquadron.SEAD + SeadPatrol.Name = SquadronName + SeadPatrol.Zone = Zone + SeadPatrol.PatrolFloorAltitude = PatrolFloorAltitude + SeadPatrol.PatrolCeilingAltitude = PatrolCeilingAltitude + SeadPatrol.EngageFloorAltitude = EngageFloorAltitude + SeadPatrol.EngageCeilingAltitude = EngageCeilingAltitude + SeadPatrol.PatrolMinSpeed = PatrolMinSpeed + SeadPatrol.PatrolMaxSpeed = PatrolMaxSpeed + SeadPatrol.EngageMinSpeed = EngageMinSpeed + SeadPatrol.EngageMaxSpeed = EngageMaxSpeed + SeadPatrol.PatrolAltType = PatrolAltType + SeadPatrol.EngageAltType = EngageAltType + SeadPatrol.Patrol = true + + self:SetSquadronPatrolInterval( SquadronName, self.DefenderDefault.PatrolLimit, self.DefenderDefault.PatrolMinSeconds, self.DefenderDefault.PatrolMaxSeconds, 1, "SEAD" ) + + self:I( { SEAD = { Zone:GetName(), PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } ) + end + + + --- Set a Sead patrol for a Squadron. + -- The Sead patrol will start a patrol of the aircraft at a specified zone, and will engage when commanded. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed. + -- @param #number FloorAltitude (optional, default = 1000m ) The minimum altitude at which the cap can be executed. + -- @param #number CeilingAltitude (optional, default = 1500m ) The maximum altitude at which the cap can be executed. + -- @param #number PatrolMinSpeed (optional, default = 50% of max speed) The minimum speed at which the cap can be executed. + -- @param #number PatrolMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the cap can be executed. + -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the engage can be executed. + -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the engage can be executed. + -- @param #number AltType The altitude type, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Sead Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronSeadPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- + function AI_A2G_DISPATCHER:SetSquadronSeadPatrol( SquadronName, Zone, FloorAltitude, CeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, AltType ) + + self:SetSquadronSeadPatrol2( SquadronName, Zone, PatrolMinSpeed, PatrolMaxSpeed, FloorAltitude, CeilingAltitude, AltType, EngageMinSpeed, EngageMaxSpeed, FloorAltitude, CeilingAltitude, AltType ) + + end + + + --- Set a squadron to engage for close air support, when a defense point is under attack. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the CAS task can be executed. + -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the CAS task can be executed. + -- @param DCS#Altitude EngageFloorAltitude (optional, default = 1000m ) The lowest altitude in meters where to execute the engagement. + -- @param DCS#Altitude EngageCeilingAltitude (optional, default = 1500m ) The highest altitude in meters where to execute the engagement. + -- @param #number EngageAltType The altitude type when engaging, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @usage + -- + -- -- CAS Squadron execution. + -- A2GDispatcher:SetSquadronCas( "Mozdok", 900, 1200, 4000, 5000, "BARO" ) + -- A2GDispatcher:SetSquadronCas( "Novo", 900, 2100, 6000, 9000, "BARO" ) + -- A2GDispatcher:SetSquadronCas( "Maykop", 900, 1200, 30, 100, "RADIO" ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronCas2( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + DefenderSquadron.CAS = DefenderSquadron.CAS or {} + + local Cas = DefenderSquadron.CAS + Cas.Name = SquadronName + Cas.EngageMinSpeed = EngageMinSpeed + Cas.EngageMaxSpeed = EngageMaxSpeed + Cas.EngageFloorAltitude = EngageFloorAltitude or 500 + Cas.EngageCeilingAltitude = EngageCeilingAltitude or 1000 + Cas.EngageAltType = EngageAltType + Cas.Defend = true + + self:I( { CAS = { SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } ) + + return self + end + + --- Set a squadron to engage for close air support, when a defense point is under attack. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the CAS task can be executed. + -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the CAS task can be executed. + -- @param DCS#Altitude EngageFloorAltitude (optional, default = 1000m ) The lowest altitude in meters where to execute the engagement. + -- @param DCS#Altitude EngageCeilingAltitude (optional, default = 1500m ) The highest altitude in meters where to execute the engagement. + -- @usage + -- + -- -- CAS Squadron execution. + -- A2GDispatcher:SetSquadronCas( "Mozdok", 900, 1200, 4000, 5000 ) + -- A2GDispatcher:SetSquadronCas( "Novo", 900, 2100, 6000, 8000 ) + -- A2GDispatcher:SetSquadronCas( "Maykop", 900, 1200, 6000, 10000 ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronCas( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude ) + + return self:SetSquadronCas2( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, "RADIO" ) + end + + + --- Set the squadron CAS engage limit. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageLimit The maximum amount of groups to engage with the enemy for this squadron. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronCasEngageLimit( "Mineralnye", 2 ) -- Engage maximum 2 groups with the enemy for CAS defense. + -- + function AI_A2G_DISPATCHER:SetSquadronCasEngageLimit( SquadronName, EngageLimit ) + + self:SetSquadronEngageLimit( SquadronName, EngageLimit, "CAS" ) + + end + + + --- Set a Cas patrol for a Squadron. + -- The Cas patrol will start a patrol of the aircraft at a specified zone, and will engage when commanded. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed. + -- @param #number PatrolMinSpeed (optional, default = 50% of max speed) The minimum speed at which the cap can be executed. + -- @param #number PatrolMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the cap can be executed. + -- @param #number PatrolFloorAltitude (optional, default = 1000m ) The minimum altitude at which the cap can be executed. + -- @param #number PatrolCeilingAltitude (optional, default = 1500m ) The maximum altitude at which the cap can be executed. + -- @param #number PatrolAltType The altitude type when patrolling, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the engage can be executed. + -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the engage can be executed. + -- @param #number EngageFloorAltitude (optional, default = 1000m ) The minimum altitude at which the engage can be executed. + -- @param #number EngageCeilingAltitude (optional, default = 1500m ) The maximum altitude at which the engage can be executed. + -- @param #number EngageAltType The altitude type when engaging, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Cas Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronCasPatrol2( "Mineralnye", PatrolZoneEast, 500, 600, 4000, 10000, "BARO", 800, 900, 2000, 3000, "RADIO", ) + -- + function AI_A2G_DISPATCHER:SetSquadronCasPatrol2( SquadronName, Zone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + DefenderSquadron.CAS = DefenderSquadron.CAS or {} + + local CasPatrol = DefenderSquadron.CAS + CasPatrol.Name = SquadronName + CasPatrol.Zone = Zone + CasPatrol.PatrolFloorAltitude = PatrolFloorAltitude + CasPatrol.PatrolCeilingAltitude = PatrolCeilingAltitude + CasPatrol.EngageFloorAltitude = EngageFloorAltitude + CasPatrol.EngageCeilingAltitude = EngageCeilingAltitude + CasPatrol.PatrolMinSpeed = PatrolMinSpeed + CasPatrol.PatrolMaxSpeed = PatrolMaxSpeed + CasPatrol.EngageMinSpeed = EngageMinSpeed + CasPatrol.EngageMaxSpeed = EngageMaxSpeed + CasPatrol.PatrolAltType = PatrolAltType + CasPatrol.EngageAltType = EngageAltType + CasPatrol.Patrol = true + + self:SetSquadronPatrolInterval( SquadronName, self.DefenderDefault.PatrolLimit, self.DefenderDefault.PatrolMinSeconds, self.DefenderDefault.PatrolMaxSeconds, 1, "CAS" ) + + self:I( { CAS = { Zone:GetName(), PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } ) + end + + + --- Set a Cas patrol for a Squadron. + -- The Cas patrol will start a patrol of the aircraft at a specified zone, and will engage when commanded. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed. + -- @param #number FloorAltitude (optional, default = 1000m ) The minimum altitude at which the cap can be executed. + -- @param #number CeilingAltitude (optional, default = 1500m ) The maximum altitude at which the cap can be executed. + -- @param #number PatrolMinSpeed (optional, default = 50% of max speed) The minimum speed at which the cap can be executed. + -- @param #number PatrolMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the cap can be executed. + -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the engage can be executed. + -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the engage can be executed. + -- @param #number AltType The altitude type, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Cas Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronCasPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- + function AI_A2G_DISPATCHER:SetSquadronCasPatrol( SquadronName, Zone, FloorAltitude, CeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, AltType ) + + self:SetSquadronCasPatrol2( SquadronName, Zone, PatrolMinSpeed, PatrolMaxSpeed, FloorAltitude, CeilingAltitude, AltType, EngageMinSpeed, EngageMaxSpeed, FloorAltitude, CeilingAltitude, AltType ) + + end + + --- Set a squadron to engage for a battlefield area interdiction, when a defense point is under attack. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the BAI task can be executed. + -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the BAI task can be executed. + -- @param DCS#Altitude EngageFloorAltitude (optional, default = 1000m ) The lowest altitude in meters where to execute the engagement. + -- @param DCS#Altitude EngageCeilingAltitude (optional, default = 1500m ) The highest altitude in meters where to execute the engagement. + -- @param #number EngageAltType The altitude type when engaging, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @usage + -- + -- -- BAI Squadron execution. + -- A2GDispatcher:SetSquadronBai( "Mozdok", 900, 1200, 4000, 5000, "BARO" ) + -- A2GDispatcher:SetSquadronBai( "Novo", 900, 2100, 6000, 9000, "BARO" ) + -- A2GDispatcher:SetSquadronBai( "Maykop", 900, 1200, 30, 100, "RADIO" ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronBai2( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + DefenderSquadron.BAI = DefenderSquadron.BAI or {} + + local Bai = DefenderSquadron.BAI + Bai.Name = SquadronName + Bai.EngageMinSpeed = EngageMinSpeed + Bai.EngageMaxSpeed = EngageMaxSpeed + Bai.EngageFloorAltitude = EngageFloorAltitude or 500 + Bai.EngageCeilingAltitude = EngageCeilingAltitude or 1000 + Bai.EngageAltType = EngageAltType + Bai.Defend = true + + self:I( { BAI = { SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } ) + + return self + end + + --- Set a squadron to engage for a battlefield area interdiction, when a defense point is under attack. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the BAI task can be executed. + -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the BAI task can be executed. + -- @param DCS#Altitude EngageFloorAltitude (optional, default = 1000m ) The lowest altitude in meters where to execute the engagement. + -- @param DCS#Altitude EngageCeilingAltitude (optional, default = 1500m ) The highest altitude in meters where to execute the engagement. + -- @usage + -- + -- -- BAI Squadron execution. + -- A2GDispatcher:SetSquadronBai( "Mozdok", 900, 1200, 4000, 5000 ) + -- A2GDispatcher:SetSquadronBai( "Novo", 900, 2100, 6000, 8000 ) + -- A2GDispatcher:SetSquadronBai( "Maykop", 900, 1200, 6000, 10000 ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronBai( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude ) + + return self:SetSquadronBai2( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, "RADIO" ) + end + + + --- Set the squadron BAI engage limit. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageLimit The maximum amount of groups to engage with the enemy for this squadron. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronBaiEngageLimit( "Mineralnye", 2 ) -- Engage maximum 2 groups with the enemy for BAI defense. + -- + function AI_A2G_DISPATCHER:SetSquadronBaiEngageLimit( SquadronName, EngageLimit ) + + self:SetSquadronEngageLimit( SquadronName, EngageLimit, "BAI" ) + + end + + + --- Set a Bai patrol for a Squadron. + -- The Bai patrol will start a patrol of the aircraft at a specified zone, and will engage when commanded. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed. + -- @param #number PatrolMinSpeed (optional, default = 50% of max speed) The minimum speed at which the cap can be executed. + -- @param #number PatrolMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the cap can be executed. + -- @param #number PatrolFloorAltitude (optional, default = 1000m ) The minimum altitude at which the cap can be executed. + -- @param #number PatrolCeilingAltitude (optional, default = 1500m ) The maximum altitude at which the cap can be executed. + -- @param #number PatrolAltType The altitude type when patrolling, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the engage can be executed. + -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the engage can be executed. + -- @param #number EngageFloorAltitude (optional, default = 1000m ) The minimum altitude at which the engage can be executed. + -- @param #number EngageCeilingAltitude (optional, default = 1500m ) The maximum altitude at which the engage can be executed. + -- @param #number EngageAltType The altitude type when engaging, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Bai Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronBaiPatrol2( "Mineralnye", PatrolZoneEast, 500, 600, 4000, 10000, "BARO", 800, 900, 2000, 3000, "RADIO", ) + -- + function AI_A2G_DISPATCHER:SetSquadronBaiPatrol2( SquadronName, Zone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + DefenderSquadron.BAI = DefenderSquadron.BAI or {} + + local BaiPatrol = DefenderSquadron.BAI + BaiPatrol.Name = SquadronName + BaiPatrol.Zone = Zone + BaiPatrol.PatrolFloorAltitude = PatrolFloorAltitude + BaiPatrol.PatrolCeilingAltitude = PatrolCeilingAltitude + BaiPatrol.EngageFloorAltitude = EngageFloorAltitude + BaiPatrol.EngageCeilingAltitude = EngageCeilingAltitude + BaiPatrol.PatrolMinSpeed = PatrolMinSpeed + BaiPatrol.PatrolMaxSpeed = PatrolMaxSpeed + BaiPatrol.EngageMinSpeed = EngageMinSpeed + BaiPatrol.EngageMaxSpeed = EngageMaxSpeed + BaiPatrol.PatrolAltType = PatrolAltType + BaiPatrol.EngageAltType = EngageAltType + BaiPatrol.Patrol = true + + self:SetSquadronPatrolInterval( SquadronName, self.DefenderDefault.PatrolLimit, self.DefenderDefault.PatrolMinSeconds, self.DefenderDefault.PatrolMaxSeconds, 1, "BAI" ) + + self:I( { BAI = { Zone:GetName(), PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } ) + end + + --- Set a Bai patrol for a Squadron. + -- The Bai patrol will start a patrol of the aircraft at a specified zone, and will engage when commanded. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed. + -- @param #number FloorAltitude (optional, default = 1000m ) The minimum altitude at which the cap can be executed. + -- @param #number CeilingAltitude (optional, default = 1500m ) The maximum altitude at which the cap can be executed. + -- @param #number PatrolMinSpeed (optional, default = 50% of max speed) The minimum speed at which the cap can be executed. + -- @param #number PatrolMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the cap can be executed. + -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the engage can be executed. + -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the engage can be executed. + -- @param #number AltType The altitude type, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Bai Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronBaiPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- + function AI_A2G_DISPATCHER:SetSquadronBaiPatrol( SquadronName, Zone, FloorAltitude, CeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, AltType ) + + self:SetSquadronBaiPatrol2( SquadronName, Zone, PatrolMinSpeed, PatrolMaxSpeed, FloorAltitude, CeilingAltitude, AltType, EngageMinSpeed, EngageMaxSpeed, FloorAltitude, CeilingAltitude, AltType ) + + end + + + --- Defines the default amount of extra planes that will take-off as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #number Overhead The %-tage of Units that dispatching command will allocate to intercept in surplus of detected amount of units. + -- The default overhead is 1, so equal balance. The @{#AI_A2G_DISPATCHER.SetOverhead}() method can be used to tweak the defense strength, + -- taking into account the plane types of the squadron. For example, a MIG-31 with full long-distance A2G missiles payload, may still be less effective than a F-15C with short missiles... + -- So in this case, one may want to use the Overhead method to allocate more defending planes as the amount of detected attacking planes. + -- The overhead must be given as a decimal value with 1 as the neutral value, which means that Overhead values: + -- + -- * Higher than 1, will increase the defense unit amounts. + -- * Lower than 1, will decrease the defense unit amounts. + -- + -- The amount of defending units is calculated by multiplying the amount of detected attacking planes as part of the detected group + -- multiplied by the Overhead and rounded up to the smallest integer. + -- + -- The Overhead value set for a Squadron, can be programmatically adjusted (by using this SetOverhead method), to adjust the defense overhead during mission execution. + -- + -- See example below. + -- + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- An overhead of 1,5 with 1 planes detected, will allocate 2 planes ( 1 * 1,5 ) = 1,5 => rounded up gives 2. + -- -- An overhead of 1,5 with 2 planes detected, will allocate 3 planes ( 2 * 1,5 ) = 3 => rounded up gives 3. + -- -- An overhead of 1,5 with 3 planes detected, will allocate 5 planes ( 3 * 1,5 ) = 4,5 => rounded up gives 5 planes. + -- -- An overhead of 1,5 with 4 planes detected, will allocate 6 planes ( 4 * 1,5 ) = 6 => rounded up gives 6 planes. + -- + -- A2GDispatcher:SetDefaultOverhead( 1.5 ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetDefaultOverhead( Overhead ) + + self.DefenderDefault.Overhead = Overhead + + return self + end + + + --- Defines the amount of extra planes that will take-off as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number Overhead The %-tage of Units that dispatching command will allocate to intercept in surplus of detected amount of units. + -- The default overhead is 1, so equal balance. The @{#AI_A2G_DISPATCHER.SetOverhead}() method can be used to tweak the defense strength, + -- taking into account the plane types of the squadron. For example, a MIG-31 with full long-distance A2G missiles payload, may still be less effective than a F-15C with short missiles... + -- So in this case, one may want to use the Overhead method to allocate more defending planes as the amount of detected attacking planes. + -- The overhead must be given as a decimal value with 1 as the neutral value, which means that Overhead values: + -- + -- * Higher than 1, will increase the defense unit amounts. + -- * Lower than 1, will decrease the defense unit amounts. + -- + -- The amount of defending units is calculated by multiplying the amount of detected attacking planes as part of the detected group + -- multiplied by the Overhead and rounded up to the smallest integer. + -- + -- The Overhead value set for a Squadron, can be programmatically adjusted (by using this SetOverhead method), to adjust the defense overhead during mission execution. + -- + -- See example below. + -- + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- An overhead of 1,5 with 1 planes detected, will allocate 2 planes ( 1 * 1,5 ) = 1,5 => rounded up gives 2. + -- -- An overhead of 1,5 with 2 planes detected, will allocate 3 planes ( 2 * 1,5 ) = 3 => rounded up gives 3. + -- -- An overhead of 1,5 with 3 planes detected, will allocate 5 planes ( 3 * 1,5 ) = 4,5 => rounded up gives 5 planes. + -- -- An overhead of 1,5 with 4 planes detected, will allocate 6 planes ( 4 * 1,5 ) = 6 => rounded up gives 6 planes. + -- + -- A2GDispatcher:SetSquadronOverhead( "SquadronName", 1.5 ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronOverhead( SquadronName, Overhead ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.Overhead = Overhead + + return self + end + + + --- Gets the overhead of planes as part of the defense system, in comparison with the attackers. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @return #number The %-tage of Units that dispatching command will allocate to intercept in surplus of detected amount of units. + -- The default overhead is 1, so equal balance. The @{#AI_A2G_DISPATCHER.SetOverhead}() method can be used to tweak the defense strength, + -- taking into account the plane types of the squadron. For example, a MIG-31 with full long-distance A2G missiles payload, may still be less effective than a F-15C with short missiles... + -- So in this case, one may want to use the Overhead method to allocate more defending planes as the amount of detected attacking planes. + -- The overhead must be given as a decimal value with 1 as the neutral value, which means that Overhead values: + -- + -- * Higher than 1, will increase the defense unit amounts. + -- * Lower than 1, will decrease the defense unit amounts. + -- + -- The amount of defending units is calculated by multiplying the amount of detected attacking planes as part of the detected group + -- multiplied by the Overhead and rounded up to the smallest integer. + -- + -- The Overhead value set for a Squadron, can be programmatically adjusted (by using this SetOverhead method), to adjust the defense overhead during mission execution. + -- + -- See example below. + -- + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- An overhead of 1,5 with 1 planes detected, will allocate 2 planes ( 1 * 1,5 ) = 1,5 => rounded up gives 2. + -- -- An overhead of 1,5 with 2 planes detected, will allocate 3 planes ( 2 * 1,5 ) = 3 => rounded up gives 3. + -- -- An overhead of 1,5 with 3 planes detected, will allocate 5 planes ( 3 * 1,5 ) = 4,5 => rounded up gives 5 planes. + -- -- An overhead of 1,5 with 4 planes detected, will allocate 6 planes ( 4 * 1,5 ) = 6 => rounded up gives 6 planes. + -- + -- local SquadronOverhead = A2GDispatcher:GetSquadronOverhead( "SquadronName" ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:GetSquadronOverhead( SquadronName ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + return DefenderSquadron.Overhead or self.DefenderDefault.Overhead + end + + + --- Sets the default grouping of new airplanes spawned. + -- Grouping will trigger how new airplanes will be grouped if more than one airplane is spawned for defense. + -- @param #AI_A2G_DISPATCHER self + -- @param #number Grouping The level of grouping that will be applied of the Patrol or GCI defenders. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Set a grouping by default per 2 airplanes. + -- A2GDispatcher:SetDefaultGrouping( 2 ) + -- + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetDefaultGrouping( Grouping ) + + self.DefenderDefault.Grouping = Grouping + + return self + end + + + --- Sets the grouping of new airplanes spawned. + -- Grouping will trigger how new airplanes will be grouped if more than one airplane is spawned for defense. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number Grouping The level of grouping that will be applied of the Patrol or GCI defenders. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Set a grouping per 2 airplanes. + -- A2GDispatcher:SetSquadronGrouping( "SquadronName", 2 ) + -- + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronGrouping( SquadronName, Grouping ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.Grouping = Grouping + + return self + end + + + --- Sets the engage probability if the squadron will engage on a detected target. + -- This can be configured per squadron, to ensure that each squadron as a specific defensive probability setting. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number EngageProbability The probability when the squadron will consider to engage the detected target. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Set an defense probability for squadron SquadronName of 50%. + -- -- This will result that this squadron has 50% chance to engage on a detected target. + -- A2GDispatcher:SetSquadronEngageProbability( "SquadronName", 0.5 ) + -- + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronEngageProbability( SquadronName, EngageProbability ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.EngageProbability = EngageProbability + + return self + end + + + --- Defines the default method at which new flights will spawn and take-off as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default take-off in the air. + -- A2GDispatcher:SetDefaultTakeoff( AI_A2G_Dispatcher.Takeoff.Air ) + -- + -- -- Let new flights by default take-off from the runway. + -- A2GDispatcher:SetDefaultTakeoff( AI_A2G_Dispatcher.Takeoff.Runway ) + -- + -- -- Let new flights by default take-off from the airbase hot. + -- A2GDispatcher:SetDefaultTakeoff( AI_A2G_Dispatcher.Takeoff.Hot ) + -- + -- -- Let new flights by default take-off from the airbase cold. + -- A2GDispatcher:SetDefaultTakeoff( AI_A2G_Dispatcher.Takeoff.Cold ) + -- + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetDefaultTakeoff( Takeoff ) + + self.DefenderDefault.Takeoff = Takeoff + + return self + end + + --- Defines the method at which new flights will spawn and take-off as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off in the air. + -- A2GDispatcher:SetSquadronTakeoff( "SquadronName", AI_A2G_Dispatcher.Takeoff.Air ) + -- + -- -- Let new flights take-off from the runway. + -- A2GDispatcher:SetSquadronTakeoff( "SquadronName", AI_A2G_Dispatcher.Takeoff.Runway ) + -- + -- -- Let new flights take-off from the airbase hot. + -- A2GDispatcher:SetSquadronTakeoff( "SquadronName", AI_A2G_Dispatcher.Takeoff.Hot ) + -- + -- -- Let new flights take-off from the airbase cold. + -- A2GDispatcher:SetSquadronTakeoff( "SquadronName", AI_A2G_Dispatcher.Takeoff.Cold ) + -- + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetSquadronTakeoff( SquadronName, Takeoff ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.Takeoff = Takeoff + + return self + end + + + --- Gets the default method at which new flights will spawn and take-off as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @return #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default take-off in the air. + -- local TakeoffMethod = A2GDispatcher:GetDefaultTakeoff() + -- if TakeOffMethod == , AI_A2G_Dispatcher.Takeoff.InAir then + -- ... + -- end + -- + function AI_A2G_DISPATCHER:GetDefaultTakeoff( ) + + return self.DefenderDefault.Takeoff + end + + --- Gets the method at which new flights will spawn and take-off as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @return #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off in the air. + -- local TakeoffMethod = A2GDispatcher:GetSquadronTakeoff( "SquadronName" ) + -- if TakeOffMethod == , AI_A2G_Dispatcher.Takeoff.InAir then + -- ... + -- end + -- + function AI_A2G_DISPATCHER:GetSquadronTakeoff( SquadronName ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + return DefenderSquadron.Takeoff or self.DefenderDefault.Takeoff + end + + + --- Sets flights to default take-off in the air, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default take-off in the air. + -- A2GDispatcher:SetDefaultTakeoffInAir() + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetDefaultTakeoffInAir() + + self:SetDefaultTakeoff( AI_A2G_DISPATCHER.Takeoff.Air ) + + return self + end + + + --- Sets flights to take-off in the air, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number TakeoffAltitude (optional) The altitude in meters above the ground. If not given, the default takeoff altitude will be used. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off in the air. + -- A2GDispatcher:SetSquadronTakeoffInAir( "SquadronName" ) + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetSquadronTakeoffInAir( SquadronName, TakeoffAltitude ) + + self:SetSquadronTakeoff( SquadronName, AI_A2G_DISPATCHER.Takeoff.Air ) + + if TakeoffAltitude then + self:SetSquadronTakeoffInAirAltitude( SquadronName, TakeoffAltitude ) + end + + return self + end + + + --- Sets flights by default to take-off from the runway, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default take-off from the runway. + -- A2GDispatcher:SetDefaultTakeoffFromRunway() + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetDefaultTakeoffFromRunway() + + self:SetDefaultTakeoff( AI_A2G_DISPATCHER.Takeoff.Runway ) + + return self + end + + + --- Sets flights to take-off from the runway, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off from the runway. + -- A2GDispatcher:SetSquadronTakeoffFromRunway( "SquadronName" ) + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetSquadronTakeoffFromRunway( SquadronName ) + + self:SetSquadronTakeoff( SquadronName, AI_A2G_DISPATCHER.Takeoff.Runway ) + + return self + end + + + --- Sets flights by default to take-off from the airbase at a hot location, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default take-off at a hot parking spot. + -- A2GDispatcher:SetDefaultTakeoffFromParkingHot() + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetDefaultTakeoffFromParkingHot() + + self:SetDefaultTakeoff( AI_A2G_DISPATCHER.Takeoff.Hot ) + + return self + end + + --- Sets flights to take-off from the airbase at a hot location, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off in the air. + -- A2GDispatcher:SetSquadronTakeoffFromParkingHot( "SquadronName" ) + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetSquadronTakeoffFromParkingHot( SquadronName ) + + self:SetSquadronTakeoff( SquadronName, AI_A2G_DISPATCHER.Takeoff.Hot ) + + return self + end + + + --- Sets flights to by default take-off from the airbase at a cold location, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off from a cold parking spot. + -- A2GDispatcher:SetDefaultTakeoffFromParkingCold() + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetDefaultTakeoffFromParkingCold() + + self:SetDefaultTakeoff( AI_A2G_DISPATCHER.Takeoff.Cold ) + + return self + end + + + --- Sets flights to take-off from the airbase at a cold location, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off from a cold parking spot. + -- A2GDispatcher:SetSquadronTakeoffFromParkingCold( "SquadronName" ) + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetSquadronTakeoffFromParkingCold( SquadronName ) + + self:SetSquadronTakeoff( SquadronName, AI_A2G_DISPATCHER.Takeoff.Cold ) + + return self + end + + + --- Defines the default altitude where airplanes will spawn in the air and take-off as part of the defense system, when the take-off in the air method has been selected. + -- @param #AI_A2G_DISPATCHER self + -- @param #number TakeoffAltitude The altitude in meters above the ground. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Set the default takeoff altitude when taking off in the air. + -- A2GDispatcher:SetDefaultTakeoffInAirAltitude( 2000 ) -- This makes planes start at 2000 meters above the ground. + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetDefaultTakeoffInAirAltitude( TakeoffAltitude ) + + self.DefenderDefault.TakeoffAltitude = TakeoffAltitude + + return self + end + + --- Defines the default altitude where airplanes will spawn in the air and take-off as part of the defense system, when the take-off in the air method has been selected. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number TakeoffAltitude The altitude in meters above the ground. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Set the default takeoff altitude when taking off in the air. + -- A2GDispatcher:SetSquadronTakeoffInAirAltitude( "SquadronName", 2000 ) -- This makes planes start at 2000 meters above the ground. + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetSquadronTakeoffInAirAltitude( SquadronName, TakeoffAltitude ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.TakeoffAltitude = TakeoffAltitude + + return self + end + + + --- Defines the default method at which flights will land and despawn as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default despawn near the airbase when returning. + -- A2GDispatcher:SetDefaultLanding( AI_A2G_Dispatcher.Landing.NearAirbase ) + -- + -- -- Let new flights by default despawn after landing land at the runway. + -- A2GDispatcher:SetDefaultLanding( AI_A2G_Dispatcher.Landing.AtRunway ) + -- + -- -- Let new flights by default despawn after landing and parking, and after engine shutdown. + -- A2GDispatcher:SetDefaultLanding( AI_A2G_Dispatcher.Landing.AtEngineShutdown ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetDefaultLanding( Landing ) + + self.DefenderDefault.Landing = Landing + + return self + end + + + --- Defines the method at which flights will land and despawn as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights despawn near the airbase when returning. + -- A2GDispatcher:SetSquadronLanding( "SquadronName", AI_A2G_Dispatcher.Landing.NearAirbase ) + -- + -- -- Let new flights despawn after landing land at the runway. + -- A2GDispatcher:SetSquadronLanding( "SquadronName", AI_A2G_Dispatcher.Landing.AtRunway ) + -- + -- -- Let new flights despawn after landing and parking, and after engine shutdown. + -- A2GDispatcher:SetSquadronLanding( "SquadronName", AI_A2G_Dispatcher.Landing.AtEngineShutdown ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronLanding( SquadronName, Landing ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.Landing = Landing + + return self + end + + + --- Gets the default method at which flights will land and despawn as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @return #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default despawn near the airbase when returning. + -- local LandingMethod = A2GDispatcher:GetDefaultLanding( AI_A2G_Dispatcher.Landing.NearAirbase ) + -- if LandingMethod == AI_A2G_Dispatcher.Landing.NearAirbase then + -- ... + -- end + -- + function AI_A2G_DISPATCHER:GetDefaultLanding() + + return self.DefenderDefault.Landing + end + + + --- Gets the method at which flights will land and despawn as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @return #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights despawn near the airbase when returning. + -- local LandingMethod = A2GDispatcher:GetSquadronLanding( "SquadronName", AI_A2G_Dispatcher.Landing.NearAirbase ) + -- if LandingMethod == AI_A2G_Dispatcher.Landing.NearAirbase then + -- ... + -- end + -- + function AI_A2G_DISPATCHER:GetSquadronLanding( SquadronName ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + return DefenderSquadron.Landing or self.DefenderDefault.Landing + end + + + --- Sets flights by default to land and despawn near the airbase in the air, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let flights by default to land near the airbase and despawn. + -- A2GDispatcher:SetDefaultLandingNearAirbase() + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetDefaultLandingNearAirbase() + + self:SetDefaultLanding( AI_A2G_DISPATCHER.Landing.NearAirbase ) + + return self + end + + + --- Sets flights to land and despawn near the airbase in the air, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let flights to land near the airbase and despawn. + -- A2GDispatcher:SetSquadronLandingNearAirbase( "SquadronName" ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronLandingNearAirbase( SquadronName ) + + self:SetSquadronLanding( SquadronName, AI_A2G_DISPATCHER.Landing.NearAirbase ) + + return self + end + + + --- Sets flights by default to land and despawn at the runway, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let flights by default land at the runway and despawn. + -- A2GDispatcher:SetDefaultLandingAtRunway() + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetDefaultLandingAtRunway() + + self:SetDefaultLanding( AI_A2G_DISPATCHER.Landing.AtRunway ) + + return self + end + + + --- Sets flights to land and despawn at the runway, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let flights land at the runway and despawn. + -- A2GDispatcher:SetSquadronLandingAtRunway( "SquadronName" ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronLandingAtRunway( SquadronName ) + + self:SetSquadronLanding( SquadronName, AI_A2G_DISPATCHER.Landing.AtRunway ) + + return self + end + + + --- Sets flights by default to land and despawn at engine shutdown, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let flights by default land and despawn at engine shutdown. + -- A2GDispatcher:SetDefaultLandingAtEngineShutdown() + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetDefaultLandingAtEngineShutdown() + + self:SetDefaultLanding( AI_A2G_DISPATCHER.Landing.AtEngineShutdown ) + + return self + end + + + --- Sets flights to land and despawn at engine shutdown, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let flights land and despawn at engine shutdown. + -- A2GDispatcher:SetSquadronLandingAtEngineShutdown( "SquadronName" ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronLandingAtEngineShutdown( SquadronName ) + + self:SetSquadronLanding( SquadronName, AI_A2G_DISPATCHER.Landing.AtEngineShutdown ) + + return self + end + + --- Set the default fuel treshold when defenders will RTB or Refuel in the air. + -- The fuel treshold is by default set to 15%, which means that an airplane will stay in the air until 15% of its fuel has been consumed. + -- @param #AI_A2G_DISPATCHER self + -- @param #number FuelThreshold A decimal number between 0 and 1, that expresses the %-tage of the treshold of fuel remaining in the tank when the plane will go RTB or Refuel. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default fuel treshold. + -- A2GDispatcher:SetDefaultFuelThreshold( 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. + -- + function AI_A2G_DISPATCHER:SetDefaultFuelThreshold( FuelThreshold ) + + self.DefenderDefault.FuelThreshold = FuelThreshold + + return self + end + + + --- Set the fuel treshold for the squadron when defenders will RTB or Refuel in the air. + -- The fuel treshold is by default set to 15%, which means that an airplane will stay in the air until 15% of its fuel has been consumed. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number FuelThreshold A decimal number between 0 and 1, that expresses the %-tage of the treshold of fuel remaining in the tank when the plane will go RTB or Refuel. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default fuel treshold. + -- A2GDispatcher:SetSquadronRefuelThreshold( "SquadronName", 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. + -- + function AI_A2G_DISPATCHER:SetSquadronFuelThreshold( SquadronName, FuelThreshold ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.FuelThreshold = FuelThreshold + + return self + end + + --- Set the default tanker where defenders will Refuel in the air. + -- @param #AI_A2G_DISPATCHER self + -- @param #string TankerName A string defining the group name of the Tanker as defined within the Mission Editor. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default fuel treshold. + -- A2GDispatcher:SetDefaultFuelThreshold( 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. + -- + -- -- Now Setup the default tanker. + -- A2GDispatcher:SetDefaultTanker( "Tanker" ) -- The group name of the tanker is "Tanker" in the Mission Editor. + function AI_A2G_DISPATCHER:SetDefaultTanker( TankerName ) + + self.DefenderDefault.TankerName = TankerName + + return self + end + + + --- Set the squadron tanker where defenders will Refuel in the air. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #string TankerName A string defining the group name of the Tanker as defined within the Mission Editor. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the squadron fuel treshold. + -- A2GDispatcher:SetSquadronRefuelThreshold( "SquadronName", 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. + -- + -- -- Now Setup the squadron tanker. + -- A2GDispatcher:SetSquadronTanker( "SquadronName", "Tanker" ) -- The group name of the tanker is "Tanker" in the Mission Editor. + function AI_A2G_DISPATCHER:SetSquadronTanker( SquadronName, TankerName ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.TankerName = TankerName + + return self + end + + + --- Set the frequency of communication and the mode of communication for voice overs. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number RadioFrequency The frequency of communication. + -- @param #number RadioModulation The modulation of communication. + -- @param #number RadioPower The power in Watts of communication. + function AI_A2G_DISPATCHER:SetSquadronRadioFrequency( SquadronName, RadioFrequency, RadioModulation, RadioPower ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.RadioFrequency = RadioFrequency + DefenderSquadron.RadioModulation = RadioModulation or radio.modulation.AM + DefenderSquadron.RadioPower = RadioPower or 100 + + if DefenderSquadron.RadioQueue then + DefenderSquadron.RadioQueue:Stop() + end + + DefenderSquadron.RadioQueue = nil + + DefenderSquadron.RadioQueue = RADIOSPEECH:New( DefenderSquadron.RadioFrequency, DefenderSquadron.RadioModulation ) + DefenderSquadron.RadioQueue.power = DefenderSquadron.RadioPower + DefenderSquadron.RadioQueue:Start( 0.5 ) + + DefenderSquadron.RadioQueue:SetLanguage( DefenderSquadron.Language ) + end + + + --- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:AddDefenderToSquadron( Squadron, Defender, Size ) + self.Defenders = self.Defenders or {} + local DefenderName = Defender:GetName() + self.Defenders[ DefenderName ] = Squadron + if Squadron.ResourceCount then + Squadron.ResourceCount = Squadron.ResourceCount - Size + end + self:F( { DefenderName = DefenderName, SquadronResourceCount = Squadron.ResourceCount } ) + end + + --- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:RemoveDefenderFromSquadron( Squadron, Defender ) + self.Defenders = self.Defenders or {} + local DefenderName = Defender:GetName() + if Squadron.ResourceCount then + Squadron.ResourceCount = Squadron.ResourceCount + Defender:GetSize() + end + self.Defenders[ DefenderName ] = nil + self:F( { DefenderName = DefenderName, SquadronResourceCount = Squadron.ResourceCount } ) + end + + function AI_A2G_DISPATCHER:GetSquadronFromDefender( Defender ) + self.Defenders = self.Defenders or {} + local DefenderName = Defender:GetName() + self:F( { DefenderName = DefenderName } ) + return self.Defenders[ DefenderName ] + end + + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:CountPatrolAirborne( SquadronName, DefenseTaskType ) + + local PatrolCount = 0 + + local DefenderSquadron = self.DefenderSquadrons[SquadronName] + if DefenderSquadron then + for AIGroup, DefenderTask in pairs( self:GetDefenderTasks() ) do + if DefenderTask.SquadronName == SquadronName then + if DefenderTask.Type == DefenseTaskType then + if AIGroup:IsAlive() then + -- Check if the Patrol is patrolling or engaging. If not, this is not a valid Patrol, even if it is alive! + -- The Patrol could be damaged, lost control, or out of fuel! + if DefenderTask.Fsm:Is( "Patrolling" ) or DefenderTask.Fsm:Is( "Engaging" ) or DefenderTask.Fsm:Is( "Refuelling" ) + or DefenderTask.Fsm:Is( "Started" ) then + PatrolCount = PatrolCount + 1 + end + end + end + end + end + end + + return PatrolCount + end + + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:CountDefendersEngaged( AttackerDetection, AttackerCount ) + + -- First, count the active AIGroups Units, targetting the DetectedSet + local DefendersEngaged = 0 + local DefendersTotal = 0 + + local AttackerSet = AttackerDetection.Set + local DefendersMissing = AttackerCount + --DetectedSet:Flush() + + local DefenderTasks = self:GetDefenderTasks() + for DefenderGroup, DefenderTask in pairs( DefenderTasks ) do + local Defender = DefenderGroup -- Wrapper.Group#GROUP + local DefenderTaskTarget = DefenderTask.Target + local DefenderSquadronName = DefenderTask.SquadronName + local DefenderSize = DefenderTask.Size + + -- Count the total of defenders on the battlefield. + --local DefenderSize = Defender:GetInitialSize() + if DefenderTask.Target then + --if DefenderTask.Fsm:Is( "Engaging" ) then + self:F( "Defender Group Name: " .. Defender:GetName() .. ", Size: " .. DefenderSize ) + DefendersTotal = DefendersTotal + DefenderSize + if DefenderTaskTarget and DefenderTaskTarget.Index == AttackerDetection.Index then + + local SquadronOverhead = self:GetSquadronOverhead( DefenderSquadronName ) + self:F( { SquadronOverhead = SquadronOverhead } ) + if DefenderSize then + DefendersEngaged = DefendersEngaged + DefenderSize + DefendersMissing = DefendersMissing - DefenderSize / SquadronOverhead + self:F( "Defender Group Name: " .. Defender:GetName() .. ", Size: " .. DefenderSize ) + else + DefendersEngaged = 0 + end + end + --end + end + + + end + + for QueueID, QueueItem in pairs( self.DefenseQueue ) do + local QueueItem = QueueItem -- #AI_A2G_DISPATCHER.DefenseQueueItem + if QueueItem.AttackerDetection and QueueItem.AttackerDetection.ItemID == AttackerDetection.ItemID then + DefendersMissing = DefendersMissing - QueueItem.DefendersNeeded / QueueItem.DefenderSquadron.Overhead + --DefendersEngaged = DefendersEngaged + QueueItem.DefenderGrouping + self:F( { QueueItemName = QueueItem.Defense, QueueItem_ItemID = QueueItem.AttackerDetection.ItemID, DetectedItem = AttackerDetection.ItemID, DefendersMissing = DefendersMissing } ) + end + end + + self:F( { DefenderCount = DefendersEngaged } ) + + return DefendersTotal, DefendersEngaged, DefendersMissing + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:CountDefenders( AttackerDetection, DefenderCount, DefenderTaskType ) + + local Friendlies = nil + + local AttackerSet = AttackerDetection.Set + local AttackerCount = AttackerSet:Count() + + local DefenderFriendlies = self:GetDefenderFriendliesNearBy( AttackerDetection ) + + for FriendlyDistance, DefenderFriendlyUnit in UTILS.spairs( DefenderFriendlies or {} ) do + -- We only allow to engage targets as long as the units on both sides are balanced. + if AttackerCount > DefenderCount then + local FriendlyGroup = DefenderFriendlyUnit:GetGroup() -- Wrapper.Group#GROUP + if FriendlyGroup and FriendlyGroup:IsAlive() then + -- Ok, so we have a friendly near the potential target. + -- Now we need to check if the AIGroup has a Task. + local DefenderTask = self:GetDefenderTask( FriendlyGroup ) + if DefenderTask then + -- The Task should be of the same type. + if DefenderTaskType == DefenderTask.Type then + -- If there is no target, then add the AIGroup to the ResultAIGroups for Engagement to the AttackerSet + if DefenderTask.Target == nil then + if DefenderTask.Fsm:Is( "Returning" ) + or DefenderTask.Fsm:Is( "Patrolling" ) then + Friendlies = Friendlies or {} + Friendlies[FriendlyGroup] = FriendlyGroup + DefenderCount = DefenderCount + FriendlyGroup:GetSize() + self:F( { Friendly = FriendlyGroup:GetName(), FriendlyDistance = FriendlyDistance } ) + end + end + end + end + end + else + break + end + end + + return Friendlies + end + + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:ResourceActivate( DefenderSquadron, DefendersNeeded ) + + local SquadronName = DefenderSquadron.Name + DefendersNeeded = DefendersNeeded or 4 + local DefenderGrouping = DefenderSquadron.Grouping or self.DefenderDefault.Grouping + DefenderGrouping = ( DefenderGrouping < DefendersNeeded ) and DefenderGrouping or DefendersNeeded + + if self:IsSquadronVisible( SquadronName ) then + + -- Here we Patrol the new planes. + -- The Resources table is filled in advance. + local TemplateID = math.random( 1, #DefenderSquadron.Spawn ) -- Choose the template. + + -- We determine the grouping based on the parameters set. + self:F( { DefenderGrouping = DefenderGrouping } ) + + -- New we will form the group to spawn in. + -- We search for the first free resource matching the template. + local DefenderUnitIndex = 1 + local DefenderPatrolTemplate = nil + local DefenderName = nil + for GroupName, DefenderGroup in pairs( DefenderSquadron.Resources[TemplateID] or {} ) do + self:F( { GroupName = GroupName } ) + local DefenderTemplate = _DATABASE:GetGroupTemplate( GroupName ) + if DefenderUnitIndex == 1 then + DefenderPatrolTemplate = UTILS.DeepCopy( DefenderTemplate ) + self.DefenderPatrolIndex = self.DefenderPatrolIndex + 1 + --DefenderPatrolTemplate.name = SquadronName .. "#" .. self.DefenderPatrolIndex .. "#" .. GroupName + DefenderPatrolTemplate.name = GroupName + DefenderName = DefenderPatrolTemplate.name + else + -- Add the unit in the template to the DefenderPatrolTemplate. + local DefenderUnitTemplate = DefenderTemplate.units[1] + DefenderPatrolTemplate.units[DefenderUnitIndex] = DefenderUnitTemplate + end + DefenderPatrolTemplate.units[DefenderUnitIndex].name = string.format( DefenderPatrolTemplate.name .. '-%02d', DefenderUnitIndex ) + DefenderPatrolTemplate.units[DefenderUnitIndex].unitId = nil + DefenderUnitIndex = DefenderUnitIndex + 1 + DefenderSquadron.Resources[TemplateID][GroupName] = nil + if DefenderUnitIndex > DefenderGrouping then + break + end + + end + + if DefenderPatrolTemplate then + local TakeoffMethod = self:GetSquadronTakeoff( SquadronName ) + local SpawnGroup = GROUP:Register( DefenderName ) + DefenderPatrolTemplate.lateActivation = nil + DefenderPatrolTemplate.uncontrolled = nil + local Takeoff = self:GetSquadronTakeoff( SquadronName ) + DefenderPatrolTemplate.route.points[1].type = GROUPTEMPLATE.Takeoff[Takeoff][1] -- type + DefenderPatrolTemplate.route.points[1].action = GROUPTEMPLATE.Takeoff[Takeoff][2] -- action + local Defender = _DATABASE:Spawn( DefenderPatrolTemplate ) + self:AddDefenderToSquadron( DefenderSquadron, Defender, DefenderGrouping ) + Defender:Activate() + return Defender, DefenderGrouping + end + else + local Spawn = DefenderSquadron.Spawn[ math.random( 1, #DefenderSquadron.Spawn ) ] -- Core.Spawn#SPAWN + if DefenderGrouping then + Spawn:InitGrouping( DefenderGrouping ) + else + Spawn:InitGrouping() + end + + local TakeoffMethod = self:GetSquadronTakeoff( SquadronName ) + local Defender = Spawn:SpawnAtAirbase( DefenderSquadron.Airbase, TakeoffMethod, DefenderSquadron.TakeoffAltitude or self.DefenderDefault.TakeoffAltitude ) -- Wrapper.Group#GROUP + self:AddDefenderToSquadron( DefenderSquadron, Defender, DefenderGrouping ) + return Defender, DefenderGrouping + end + + return nil, nil + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:onafterPatrol( From, Event, To, SquadronName, DefenseTaskType ) + + local DefenderSquadron, Patrol = self:CanPatrol( SquadronName, DefenseTaskType ) + + -- Determine if there are sufficient resources to form a complete group for patrol. + if DefenderSquadron then + local DefendersNeeded + local DefendersGrouping = ( DefenderSquadron.Grouping or self.DefenderDefault.Grouping ) + if DefenderSquadron.ResourceCount == nil then + DefendersNeeded = DefendersGrouping + else + if DefenderSquadron.ResourceCount >= DefendersGrouping then + DefendersNeeded = DefendersGrouping + else + DefendersNeeded = DefenderSquadron.ResourceCount + end + end + + if Patrol then + self:ResourceQueue( true, DefenderSquadron, DefendersNeeded, Patrol, DefenseTaskType, nil, SquadronName ) + end + end + + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:ResourceQueue( Patrol, DefenderSquadron, DefendersNeeded, Defense, DefenseTaskType, AttackerDetection, SquadronName ) + + self:F( { DefenderSquadron, DefendersNeeded, Defense, DefenseTaskType, AttackerDetection, SquadronName } ) + + local DefenseQueueItem = {} -- #AI_A2G_DISPATCHER.DefenderQueueItem + + + DefenseQueueItem.Patrol = Patrol + DefenseQueueItem.DefenderSquadron = DefenderSquadron + DefenseQueueItem.DefendersNeeded = DefendersNeeded + DefenseQueueItem.Defense = Defense + DefenseQueueItem.DefenseTaskType = DefenseTaskType + DefenseQueueItem.AttackerDetection = AttackerDetection + DefenseQueueItem.SquadronName = SquadronName + + table.insert( self.DefenseQueue, DefenseQueueItem ) + self:F( { QueueItems = #self.DefenseQueue } ) + + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:ResourceTakeoff() + + for DefenseQueueID, DefenseQueueItem in pairs( self.DefenseQueue ) do + self:F( { DefenseQueueID } ) + end + + for SquadronName, Squadron in pairs( self.DefenderSquadrons ) do + + if #self.DefenseQueue > 0 then + + self:F( { SquadronName, Squadron.Name, Squadron.TakeoffTime, Squadron.TakeoffInterval, timer.getTime() } ) + + local DefenseQueueItem = self.DefenseQueue[1] + self:F( {DefenderSquadron=DefenseQueueItem.DefenderSquadron} ) + + if DefenseQueueItem.SquadronName == SquadronName then + + if Squadron.TakeoffTime + Squadron.TakeoffInterval < timer.getTime() then + Squadron.TakeoffTime = timer.getTime() + + if DefenseQueueItem.Patrol == true then + self:ResourcePatrol( DefenseQueueItem.DefenderSquadron, DefenseQueueItem.DefendersNeeded, DefenseQueueItem.Defense, DefenseQueueItem.DefenseTaskType, DefenseQueueItem.AttackerDetection, DefenseQueueItem.SquadronName ) + else + self:ResourceEngage( DefenseQueueItem.DefenderSquadron, DefenseQueueItem.DefendersNeeded, DefenseQueueItem.Defense, DefenseQueueItem.DefenseTaskType, DefenseQueueItem.AttackerDetection, DefenseQueueItem.SquadronName ) + end + table.remove( self.DefenseQueue, 1 ) + end + end + end + + end + + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:ResourcePatrol( DefenderSquadron, DefendersNeeded, Patrol, DefenseTaskType, AttackerDetection, SquadronName ) + + + self:F({DefenderSquadron=DefenderSquadron}) + self:F({DefendersNeeded=DefendersNeeded}) + self:F({Patrol=Patrol}) + self:F({DefenseTaskType=DefenseTaskType}) + self:F({AttackerDetection=AttackerDetection}) + self:F({SquadronName=SquadronName}) + + local DefenderGroup, DefenderGrouping = self:ResourceActivate( DefenderSquadron, DefendersNeeded ) + + if DefenderGroup then + + local AI_A2G_PATROL = { SEAD = AI_A2G_SEAD, BAI = AI_A2G_BAI, CAS = AI_A2G_CAS } + + local AI_A2G_Fsm = AI_A2G_PATROL[DefenseTaskType]:New2( DefenderGroup, Patrol.EngageMinSpeed, Patrol.EngageMaxSpeed, Patrol.EngageFloorAltitude, Patrol.EngageCeilingAltitude, Patrol.EngageAltType, Patrol.Zone, Patrol.PatrolFloorAltitude, Patrol.PatrolCeilingAltitude, Patrol.PatrolMinSpeed, Patrol.PatrolMaxSpeed, Patrol.PatrolAltType ) + AI_A2G_Fsm:SetDispatcher( self ) + AI_A2G_Fsm:SetHomeAirbase( DefenderSquadron.Airbase ) + AI_A2G_Fsm:SetFuelThreshold( DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold, 60 ) + AI_A2G_Fsm:SetDamageThreshold( self.DefenderDefault.DamageThreshold ) + AI_A2G_Fsm:SetDisengageRadius( self.DisengageRadius ) + AI_A2G_Fsm:SetTanker( DefenderSquadron.TankerName or self.DefenderDefault.TankerName ) + AI_A2G_Fsm:Start() + + self:SetDefenderTask( SquadronName, DefenderGroup, DefenseTaskType, AI_A2G_Fsm, nil, DefenderGrouping ) + + function AI_A2G_Fsm:onafterTakeoff( DefenderGroup, From, Event, To ) + self:F({"Takeoff", DefenderGroup:GetName()}) + --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) + + local DefenderName = DefenderGroup:GetCallsign() -- #string + local Dispatcher = AI_A2G_Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + + if Squadron then + if self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", wheels up.", DefenderGroup ) + end + AI_A2G_Fsm:Patrol() -- Engage on the TargetSetUnit + end + end + + function AI_A2G_Fsm:onafterPatrolRoute( DefenderGroup, From, Event, To ) + self:F({"PatrolRoute", DefenderGroup:GetName()}) + self:GetParent(self).onafterPatrolRoute( self, DefenderGroup, From, Event, To ) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + if Squadron and self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", patrolling.", DefenderGroup ) + end + + Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) + end + + function AI_A2G_Fsm:onafterEngageRoute( DefenderGroup, From, Event, To, AttackSetUnit ) + self:F({"Engage Route", DefenderGroup:GetName()}) + + self:GetParent(self).onafterEngageRoute( self, DefenderGroup, From, Event, To, AttackSetUnit ) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = AI_A2G_Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + + if Squadron and AttackSetUnit:Count() > 0 then + local FirstUnit = AttackSetUnit:GetFirst() + local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE + if self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", moving on to ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), DefenderGroup ) + end + end + end + + function AI_A2G_Fsm:OnAfterEngage( DefenderGroup, From, Event, To, AttackSetUnit ) + self:F({"Engage Route", DefenderGroup:GetName()}) + --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = AI_A2G_Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + local FirstUnit = AttackSetUnit:GetFirst() + if FirstUnit then + local Coordinate = FirstUnit:GetCoordinate() + if self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", engaging ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), DefenderGroup ) + end + end + end + + function AI_A2G_Fsm:onafterRTB( DefenderGroup, From, Event, To ) + self:F({"RTB", DefenderGroup:GetName()}) + self:GetParent(self).onafterRTB( self, DefenderGroup, From, Event, To ) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + if self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", returning to base.", DefenderGroup ) + end + Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) + end + + --- @param #AI_A2G_DISPATCHER self + function AI_A2G_Fsm:onafterLostControl( DefenderGroup, From, Event, To ) + self:F({"LostControl", DefenderGroup:GetName()}) + self:GetParent(self).onafterHome( self, DefenderGroup, From, Event, To ) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = AI_A2G_Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + if self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", lost control." ) + end + if DefenderGroup:IsAboveRunway() then + Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup ) + DefenderGroup:Destroy() + end + end + + --- @param #AI_A2G_DISPATCHER self + function AI_A2G_Fsm:onafterHome( DefenderGroup, From, Event, To, Action ) + self:F({"Home", DefenderGroup:GetName()}) + self:GetParent(self).onafterHome( self, DefenderGroup, From, Event, To ) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + if self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", landing at base.", DefenderGroup ) + end + if Action and Action == "Destroy" then + Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup ) + DefenderGroup:Destroy() + end + + if Dispatcher:GetSquadronLanding( Squadron.Name ) == AI_A2G_DISPATCHER.Landing.NearAirbase then + Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup ) + DefenderGroup:Destroy() + Dispatcher:ResourcePark( Squadron, DefenderGroup ) + end + end + end + + end + + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:ResourceEngage( DefenderSquadron, DefendersNeeded, Defense, DefenseTaskType, AttackerDetection, SquadronName ) + + self:F({DefenderSquadron=DefenderSquadron}) + self:F({DefendersNeeded=DefendersNeeded}) + self:F({Defense=Defense}) + self:F({DefenseTaskType=DefenseTaskType}) + self:F({AttackerDetection=AttackerDetection}) + self:F({SquadronName=SquadronName}) + + local DefenderGroup, DefenderGrouping = self:ResourceActivate( DefenderSquadron, DefendersNeeded ) + + if DefenderGroup then + + local AI_A2G_ENGAGE = { SEAD = AI_A2G_SEAD, BAI = AI_A2G_BAI, CAS = AI_A2G_CAS } + + local AI_A2G_Fsm = AI_A2G_ENGAGE[DefenseTaskType]:New( DefenderGroup, Defense.EngageMinSpeed, Defense.EngageMaxSpeed, Defense.EngageFloorAltitude, Defense.EngageCeilingAltitude, Defense.EngageAltType ) -- AI.AI_AIR_ENGAGE + AI_A2G_Fsm:SetDispatcher( self ) + AI_A2G_Fsm:SetHomeAirbase( DefenderSquadron.Airbase ) + AI_A2G_Fsm:SetFuelThreshold( DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold, 60 ) + AI_A2G_Fsm:SetDamageThreshold( self.DefenderDefault.DamageThreshold ) + AI_A2G_Fsm:SetDisengageRadius( self.DisengageRadius ) + AI_A2G_Fsm:Start() + + self:SetDefenderTask( SquadronName, DefenderGroup, DefenseTaskType, AI_A2G_Fsm, AttackerDetection, DefenderGrouping ) + + function AI_A2G_Fsm:onafterTakeoff( DefenderGroup, From, Event, To ) + self:F({"Defender Birth", DefenderGroup:GetName()}) + --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = AI_A2G_Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + local DefenderTarget = Dispatcher:GetDefenderTaskTarget( DefenderGroup ) + + self:F( { DefenderTarget = DefenderTarget } ) + + if DefenderTarget then + if self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", wheels up.", DefenderGroup ) + end + AI_A2G_Fsm:EngageRoute( DefenderTarget.Set ) -- Engage on the TargetSetUnit + end + end + + function AI_A2G_Fsm:onafterEngageRoute( DefenderGroup, From, Event, To, AttackSetUnit ) + self:F({"Engage Route", DefenderGroup:GetName()}) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = AI_A2G_Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + + if Squadron then + local FirstUnit = AttackSetUnit:GetFirst() + local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE + if self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", on route to ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), DefenderGroup ) + end + end + self:GetParent(self).onafterEngageRoute( self, DefenderGroup, From, Event, To, AttackSetUnit ) + end + + function AI_A2G_Fsm:OnAfterEngage( DefenderGroup, From, Event, To, AttackSetUnit ) + self:F({"Engage Route", DefenderGroup:GetName()}) + --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = AI_A2G_Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + local FirstUnit = AttackSetUnit:GetFirst() + if FirstUnit then + local Coordinate = FirstUnit:GetCoordinate() + if self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", engaging ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), DefenderGroup ) + end + end + end + + function AI_A2G_Fsm:onafterRTB( DefenderGroup, From, Event, To ) + self:F({"Defender RTB", DefenderGroup:GetName()}) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + if self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", returning to base.", DefenderGroup ) + end + self:GetParent(self).onafterRTB( self, DefenderGroup, From, Event, To ) + + Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) + end + + --- @param #AI_A2G_DISPATCHER self + function AI_A2G_Fsm:onafterLostControl( DefenderGroup, From, Event, To ) + self:F({"Defender LostControl", DefenderGroup:GetName()}) + self:GetParent(self).onafterHome( self, DefenderGroup, From, Event, To ) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = AI_A2G_Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + if self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " lost control." ) + end + if DefenderGroup:IsAboveRunway() then + Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup ) + DefenderGroup:Destroy() + end + end + + --- @param #AI_A2G_DISPATCHER self + function AI_A2G_Fsm:onafterHome( DefenderGroup, From, Event, To, Action ) + self:F({"Defender Home", DefenderGroup:GetName()}) + self:GetParent(self).onafterHome( self, DefenderGroup, From, Event, To ) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + if self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", landing at base.", DefenderGroup ) + end + if Action and Action == "Destroy" then + Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup ) + DefenderGroup:Destroy() + end + + if Dispatcher:GetSquadronLanding( Squadron.Name ) == AI_A2G_DISPATCHER.Landing.NearAirbase then + Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup ) + DefenderGroup:Destroy() + Dispatcher:ResourcePark( Squadron, DefenderGroup ) + end + end + end + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:onafterEngage( From, Event, To, AttackerDetection, Defenders ) + + if Defenders then + + for DefenderID, Defender in pairs( Defenders or {} ) do + + local Fsm = self:GetDefenderTaskFsm( Defender ) + Fsm:Engage( AttackerDetection.Set ) -- Engage on the TargetSetUnit + + self:SetDefenderTaskTarget( Defender, AttackerDetection ) + + end + end + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:HasDefenseLine( DefenseCoordinate, DetectedItem ) + + local AttackCoordinate = self.Detection:GetDetectedItemCoordinate( DetectedItem ) + local EvaluateDistance = AttackCoordinate:Get2DDistance( DefenseCoordinate ) + + -- Now check if this coordinate is not in a danger zone, meaning, that the attack line is not crossing other coordinates. + -- (y1 - y2)x + (x2 - x1)y + (x1y2 - x2y1) = 0 + + local c1 = DefenseCoordinate + local c2 = AttackCoordinate + + local a = c1.z - c2.z -- Calculate a + local b = c2.x - c1.x -- Calculate b + local c = c1.x * c2.z - c2.x * c1.z -- calculate c + + local ok = true + + -- Now we check if each coordinate radius of about 30km of each attack is crossing a defense line. If yes, then this is not a good attack! + for AttackItemID, CheckAttackItem in pairs( self.Detection:GetDetectedItems() ) do + + -- Only compare other detected coordinates. + if AttackItemID ~= DetectedItem.ID then + + local CheckAttackCoordinate = self.Detection:GetDetectedItemCoordinate( CheckAttackItem ) + + local x = CheckAttackCoordinate.x + local y = CheckAttackCoordinate.z + local r = 5000 + + -- now we check if the coordinate is intersecting with the defense line. + + local IntersectDistance = ( math.abs( a * x + b * y + c ) ) / math.sqrt( a * a + b * b ) + self:F( { IntersectDistance = IntersectDistance, x = x, y = y } ) + + local IntersectAttackDistance = CheckAttackCoordinate:Get2DDistance( DefenseCoordinate ) + + self:F( { IntersectAttackDistance=IntersectAttackDistance, EvaluateDistance=EvaluateDistance } ) + + -- If the distance of the attack coordinate is larger than the test radius; then the line intersects, and this is not a good coordinate. + if IntersectDistance < r and IntersectAttackDistance < EvaluateDistance then + ok = false + break + end + end + end + + return ok + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:onafterDefend( From, Event, To, DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, DefenderFriendlies, DefenseTaskType ) + + self:F( { From, Event, To, DetectedItem.Index, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing, DefenderFriendlies = DefenderFriendlies } ) + + DetectedItem.Type = DefenseTaskType -- This is set to report the task type in the status panel. + + local AttackerSet = DetectedItem.Set + local AttackerUnit = AttackerSet:GetFirst() + + if AttackerUnit and AttackerUnit:IsAlive() then + local AttackerCount = AttackerSet:Count() + local DefenderCount = 0 + + for DefenderID, DefenderGroup in pairs( DefenderFriendlies or {} ) do + + -- Here we check if the defenders have a defense line to the attackers. + -- If the attackers are behind enemy lines or too close to an other defense line; then don't engage. + local DefenseCoordinate = DefenderGroup:GetCoordinate() + local HasDefenseLine = self:HasDefenseLine( DefenseCoordinate, DetectedItem ) + + if HasDefenseLine == true then + local SquadronName = self:GetDefenderTask( DefenderGroup ).SquadronName + local SquadronOverhead = self:GetSquadronOverhead( SquadronName ) + + local Fsm = self:GetDefenderTaskFsm( DefenderGroup ) + Fsm:EngageRoute( AttackerSet ) -- Engage on the TargetSetUnit + + self:SetDefenderTaskTarget( DefenderGroup, DetectedItem ) + + local DefenderGroupSize = DefenderGroup:GetSize() + DefendersMissing = DefendersMissing - DefenderGroupSize / SquadronOverhead + DefendersTotal = DefendersTotal + DefenderGroupSize / SquadronOverhead + end + + if DefendersMissing <= 0 then + break + end + end + + self:F( { DefenderCount = DefenderCount, DefendersMissing = DefendersMissing } ) + DefenderCount = DefendersMissing + + local ClosestDistance = 0 + local EngageSquadronName = nil + + local BreakLoop = false + + while( DefenderCount > 0 and not BreakLoop ) do + + self:F( { DefenderSquadrons = self.DefenderSquadrons } ) + + for SquadronName, DefenderSquadron in UTILS.rpairs( self.DefenderSquadrons or {} ) do + + if DefenderSquadron[DefenseTaskType] then + + local AirbaseCoordinate = DefenderSquadron.Airbase:GetCoordinate() -- Core.Point#COORDINATE + local AttackerCoord = AttackerUnit:GetCoordinate() + local InterceptCoord = DetectedItem.InterceptCoord + self:F( { InterceptCoord = InterceptCoord } ) + if InterceptCoord then + local InterceptDistance = AirbaseCoordinate:Get2DDistance( InterceptCoord ) + local AirbaseDistance = AirbaseCoordinate:Get2DDistance( AttackerCoord ) + self:F( { InterceptDistance = InterceptDistance, AirbaseDistance = AirbaseDistance, InterceptCoord = InterceptCoord } ) + + -- Only intercept if the distance to target is smaller or equal to the GciRadius limit. + if AirbaseDistance <= self.DefenseRadius then + + -- Check if there is a defense line... + local HasDefenseLine = self:HasDefenseLine( AirbaseCoordinate, DetectedItem ) + if HasDefenseLine == true then + local EngageProbability = ( DefenderSquadron.EngageProbability or 1 ) + local Probability = math.random() + if Probability < EngageProbability then + EngageSquadronName = SquadronName + break + end + end + end + end + end + end + + if EngageSquadronName then + + local DefenderSquadron, Defense = self:CanDefend( EngageSquadronName, DefenseTaskType ) + + if Defense then + + local DefenderOverhead = DefenderSquadron.Overhead or self.DefenderDefault.Overhead + local DefenderGrouping = DefenderSquadron.Grouping or self.DefenderDefault.Grouping + local DefendersNeeded = math.ceil( DefenderCount * DefenderOverhead ) + + self:F( { Overhead = DefenderOverhead, SquadronOverhead = DefenderSquadron.Overhead , DefaultOverhead = self.DefenderDefault.Overhead } ) + self:F( { Grouping = DefenderGrouping, SquadronGrouping = DefenderSquadron.Grouping, DefaultGrouping = self.DefenderDefault.Grouping } ) + self:F( { DefendersCount = DefenderCount, DefendersNeeded = DefendersNeeded } ) + + -- Validate that the maximum limit of Defenders has been reached. + -- If yes, then cancel the engaging of more defenders. + local DefendersLimit = DefenderSquadron.EngageLimit or self.DefenderDefault.EngageLimit + if DefendersLimit then + if DefendersTotal >= DefendersLimit then + DefendersNeeded = 0 + BreakLoop = true + else + -- If the total of amount of defenders + the defenders needed, is larger than the limit of defenders, + -- then the defenders needed is the difference between defenders total - defenders limit. + if DefendersTotal + DefendersNeeded > DefendersLimit then + DefendersNeeded = DefendersLimit - DefendersTotal + end + end + end + + -- DefenderSquadron.ResourceCount can have the value nil, which expresses unlimited resources. + -- DefendersNeeded cannot exceed DefenderSquadron.ResourceCount! + if DefenderSquadron.ResourceCount and DefendersNeeded > DefenderSquadron.ResourceCount then + DefendersNeeded = DefenderSquadron.ResourceCount + BreakLoop = true + end + + while ( DefendersNeeded > 0 ) do + self:ResourceQueue( false, DefenderSquadron, DefendersNeeded, Defense, DefenseTaskType, DetectedItem, EngageSquadronName ) + DefendersNeeded = DefendersNeeded - DefenderGrouping + DefenderCount = DefenderCount - DefenderGrouping / DefenderOverhead + end -- while ( DefendersNeeded > 0 ) do + else + -- No more resources, try something else. + -- Subject for a later enhancement to try to depart from another squadron and disable this one. + BreakLoop = true + break + end + else + -- There isn't any closest airbase anymore, break the loop. + break + end + end -- if DefenderSquadron then + end -- if AttackerUnit + end + + + + --- Creates an SEAD task when the targets have radars. + -- @param #AI_A2G_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem The detected item. + -- @return Core.Set#SET_UNIT The set of units of the targets to be engaged. + -- @return #nil If there are no targets to be set. + function AI_A2G_DISPATCHER:Evaluate_SEAD( DetectedItem ) + self:F( { DetectedItem.ItemID } ) + + local AttackerSet = DetectedItem.Set -- Core.Set#SET_UNIT + local AttackerCount = AttackerSet:HasSEAD() -- Is the AttackerSet a SEAD group, then the amount of radar emitters will be returned; that need to be attacked. + + if ( AttackerCount > 0 ) then + + -- First, count the active defenders, engaging the DetectedItem. + local DefendersTotal, DefendersEngaged, DefendersMissing = self:CountDefendersEngaged( DetectedItem, AttackerCount ) + + self:F( { AttackerCount = AttackerCount, DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) + + local DefenderGroups = self:CountDefenders( DetectedItem, DefendersEngaged, "SEAD" ) + + + + if DetectedItem.IsDetected == true then + + return DefendersTotal, DefendersEngaged, DefendersMissing, DefenderGroups + end + end + + return 0, 0, 0 + end + + + --- Creates an CAS task. + -- @param #AI_A2G_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem The detected item. + -- @return Core.Set#SET_UNIT The set of units of the targets to be engaged. + -- @return #nil If there are no targets to be set. + function AI_A2G_DISPATCHER:Evaluate_CAS( DetectedItem ) + self:F( { DetectedItem.ItemID } ) + + local AttackerSet = DetectedItem.Set -- Core.Set#SET_UNIT + local AttackerCount = AttackerSet:Count() + local AttackerRadarCount = AttackerSet:HasSEAD() + local IsFriendliesNearBy = self.Detection:IsFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT ) + local IsCas = ( AttackerRadarCount == 0 ) and ( IsFriendliesNearBy == true ) -- Is the AttackerSet a CAS group? + + if IsCas == true then + + -- First, count the active defenders, engaging the DetectedItem. + local DefendersTotal, DefendersEngaged, DefendersMissing = self:CountDefendersEngaged( DetectedItem, AttackerCount ) + + self:F( { AttackerCount = AttackerCount, DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) + + local DefenderGroups = self:CountDefenders( DetectedItem, DefendersEngaged, "CAS" ) + + if DetectedItem.IsDetected == true then + + return DefendersTotal, DefendersEngaged, DefendersMissing, DefenderGroups + end + end + + return 0, 0, 0 + end + + + --- Evaluates an BAI task. + -- @param #AI_A2G_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem The detected item. + -- @return Core.Set#SET_UNIT The set of units of the targets to be engaged. + -- @return #nil If there are no targets to be set. + function AI_A2G_DISPATCHER:Evaluate_BAI( DetectedItem ) + self:F( { DetectedItem.ItemID } ) + + local AttackerSet = DetectedItem.Set -- Core.Set#SET_UNIT + local AttackerCount = AttackerSet:Count() + local AttackerRadarCount = AttackerSet:HasSEAD() + local IsFriendliesNearBy = self.Detection:IsFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT ) + local IsBai = ( AttackerRadarCount == 0 ) and ( IsFriendliesNearBy == false ) -- Is the AttackerSet a BAI group? + + if IsBai == true then + + -- First, count the active defenders, engaging the DetectedItem. + local DefendersTotal, DefendersEngaged, DefendersMissing = self:CountDefendersEngaged( DetectedItem, AttackerCount ) + + self:F( { AttackerCount = AttackerCount, DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) + + local DefenderGroups = self:CountDefenders( DetectedItem, DefendersEngaged, "BAI" ) + + if DetectedItem.IsDetected == true then + + return DefendersTotal, DefendersEngaged, DefendersMissing, DefenderGroups + end + end + + return 0, 0, 0 + end + + + --- Determine the distance as the keys of reference of the detected items. + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:Keys( DetectedItem ) + + self:F( { DetectedItem = DetectedItem } ) + + local AttackCoordinate = self.Detection:GetDetectedItemCoordinate( DetectedItem ) + + local ShortestDistance = 999999999 + + for DefenseCoordinateName, DefenseCoordinate in pairs( self.DefenseCoordinates ) do + local DefenseCoordinate = DefenseCoordinate -- Core.Point#COORDINATE + + local EvaluateDistance = AttackCoordinate:Get2DDistance( DefenseCoordinate ) + + if EvaluateDistance <= ShortestDistance then + ShortestDistance = EvaluateDistance + end + end + + return ShortestDistance + end + + --- Assigns A2G AI Tasks in relation to the detected items. + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:Order( DetectedItem ) + local AttackCoordinate = self.Detection:GetDetectedItemCoordinate( DetectedItem ) + + local ShortestDistance = 999999999 + + for DefenseCoordinateName, DefenseCoordinate in pairs( self.DefenseCoordinates ) do + local DefenseCoordinate = DefenseCoordinate -- Core.Point#COORDINATE + + local EvaluateDistance = AttackCoordinate:Get2DDistance( DefenseCoordinate ) + + if EvaluateDistance <= ShortestDistance then + ShortestDistance = EvaluateDistance + end + end + + return ShortestDistance + end + + --- Shows the tactical display. + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:ShowTacticalDisplay( Detection ) + + local AreaMsg = {} + local TaskMsg = {} + local ChangeMsg = {} + + local TaskReport = REPORT:New() + + local DefenseTotal = 0 + + local Report = REPORT:New( "\nTactical Overview" ) + + local DefenderGroupCount = 0 + local DefendersTotal = 0 + + -- Now that all obsolete tasks are removed, loop through the detected targets. + --for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do + for DetectedItemID, DetectedItem in UTILS.spairs( Detection:GetDetectedItems(), function( t, a, b ) return self:Order(t[a]) < self:Order(t[b]) end ) do + + if not self.Detection:IsDetectedItemLocked( DetectedItem ) == true then + local DetectedItem = DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem + local DetectedSet = DetectedItem.Set -- Core.Set#SET_UNIT + local DetectedCount = DetectedSet:Count() + local DetectedZone = DetectedItem.Zone + + self:F( { "Target ID", DetectedItem.ItemID } ) + + self:F( { DefenseLimit = self.DefenseLimit, DefenseTotal = DefenseTotal } ) + DetectedSet:Flush( self ) + + local DetectedID = DetectedItem.ID + local DetectionIndex = DetectedItem.Index + local DetectedItemChanged = DetectedItem.Changed + + -- Show tactical situation + local ThreatLevel = DetectedItem.Set:CalculateThreatLevelA2G() + Report:Add( string.format( " - %1s%s ( %04s ): ( #%02d - %-4s ) %s" , ( DetectedItem.IsDetected == true ) and "!" or " ", DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Type or " --- ", string.rep( "■", ThreatLevel ) ) ) + for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do + local Defender = Defender -- Wrapper.Group#GROUP + if DefenderTask.Target and DefenderTask.Target.Index == DetectedItem.Index then + if Defender:IsAlive() then + DefenderGroupCount = DefenderGroupCount + 1 + local Fuel = Defender:GetFuelMin() * 100 + local Damage = Defender:GetLife() / Defender:GetLife0() * 100 + Report:Add( string.format( " - %s ( %s - %s ): ( #%d ) F: %3d, D:%3d - %s", + Defender:GetName(), + DefenderTask.Type, + DefenderTask.Fsm:GetState(), + Defender:GetSize(), + Fuel, + Damage, + Defender:HasTask() == true and "Executing" or "Idle" ) ) + end + end + end + end + end + + Report:Add( "\n - No Targets:") + local TaskCount = 0 + for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do + TaskCount = TaskCount + 1 + local Defender = Defender -- Wrapper.Group#GROUP + if not DefenderTask.Target then + if Defender:IsAlive() then + local DefenderHasTask = Defender:HasTask() + local Fuel = Defender:GetFuelMin() * 100 + local Damage = Defender:GetLife() / Defender:GetLife0() * 100 + DefenderGroupCount = DefenderGroupCount + 1 + Report:Add( string.format( " - %s ( %s - %s ): ( #%d ) F: %3d, D:%3d - %s", + Defender:GetName(), + DefenderTask.Type, + DefenderTask.Fsm:GetState(), + Defender:GetSize(), + Fuel, + Damage, + Defender:HasTask() == true and "Executing" or "Idle" ) ) + end + end + end + Report:Add( string.format( "\n - %d Tasks - %d Defender Groups", TaskCount, DefenderGroupCount ) ) + + Report:Add( string.format( "\n - %d Queued Aircraft Launches", #self.DefenseQueue ) ) + for DefenseQueueID, DefenseQueueItem in pairs( self.DefenseQueue ) do + local DefenseQueueItem = DefenseQueueItem -- #AI_A2G_DISPATCHER.DefenseQueueItem + Report:Add( string.format( " - %s - %s", DefenseQueueItem.SquadronName, DefenseQueueItem.DefenderSquadron.TakeoffTime, DefenseQueueItem.DefenderSquadron.TakeoffInterval) ) + + end + + Report:Add( string.format( "\n - Squadron Resources: ", #self.DefenseQueue ) ) + for DefenderSquadronName, DefenderSquadron in pairs( self.DefenderSquadrons ) do + Report:Add( string.format( " - %s - %s", DefenderSquadronName, DefenderSquadron.ResourceCount and tostring(DefenderSquadron.ResourceCount) or "n/a" ) ) + end + + self:F( Report:Text( "\n" ) ) + trigger.action.outText( Report:Text( "\n" ), 25 ) + + end + + --- Assigns A2G AI Tasks in relation to the detected items. + -- @param #AI_A2G_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE Detection The detection created by the @{Functional.Detection#DETECTION_BASE} derived object. + -- @return #boolean Return true if you want the task assigning to continue... false will cancel the loop. + function AI_A2G_DISPATCHER:ProcessDetected( Detection ) + + local AreaMsg = {} + local TaskMsg = {} + local ChangeMsg = {} + + local TaskReport = REPORT:New() + + local DefenseTotal = 0 + + for DefenderGroup, DefenderTask in pairs( self:GetDefenderTasks() ) do + local DefenderGroup = DefenderGroup -- Wrapper.Group#GROUP + local DefenderTaskFsm = self:GetDefenderTaskFsm( DefenderGroup ) + --if DefenderTaskFsm:Is( "LostControl" ) then + -- self:ClearDefenderTask( DefenderGroup ) + --end + if not DefenderGroup:IsAlive() then + self:F( { Defender = DefenderGroup:GetName(), DefenderState = DefenderTaskFsm:GetState() } ) + if not DefenderTaskFsm:Is( "Started" ) then + self:ClearDefenderTask( DefenderGroup ) + end + else + -- TODO: prio 1, what is this index stuff again, simplify it. + if DefenderTask.Target then + self:F( { TargetIndex = DefenderTask.Target.Index } ) + local AttackerItem = Detection:GetDetectedItemByIndex( DefenderTask.Target.Index ) + if not AttackerItem then + self:F( { "Removing obsolete Target:", DefenderTask.Target.Index } ) + self:ClearDefenderTaskTarget( DefenderGroup ) + else + if DefenderTask.Target.Set then + local TargetCount = DefenderTask.Target.Set:Count() + if TargetCount == 0 then + self:F( { "All Targets destroyed in Target, removing:", DefenderTask.Target.Index } ) + self:ClearDefenderTask( DefenderGroup ) + end + end + end + end + end + end + +-- for DefenderGroup, DefenderTask in pairs( self:GetDefenderTasks() ) do +-- DefenseTotal = DefenseTotal + 1 +-- end + + local Report = REPORT:New( "\nTactical Overview" ) + + local DefenderGroupCount = 0 + + local DefendersTotal = 0 + + -- Now that all obsolete tasks are removed, loop through the detected targets. + --for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do + for DetectedDistance, DetectedItem in UTILS.kpairs( Detection:GetDetectedItems(), function( t ) return self:Keys( t ) end, function( t, a, b ) return self:Order(t[a]) < self:Order(t[b]) end ) do + + if not self.Detection:IsDetectedItemLocked( DetectedItem ) == true then + local DetectedItem = DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem + local DetectedSet = DetectedItem.Set -- Core.Set#SET_UNIT + local DetectedCount = DetectedSet:Count() + local DetectedZone = DetectedItem.Zone + + self:F( { "Target ID", DetectedItem.ItemID } ) + + self:F( { DefenseLimit = self.DefenseLimit, DefenseTotal = DefenseTotal } ) + DetectedSet:Flush( self ) + + local DetectedID = DetectedItem.ID + local DetectionIndex = DetectedItem.Index + local DetectedItemChanged = DetectedItem.Changed + + local AttackCoordinate = self.Detection:GetDetectedItemCoordinate( DetectedItem ) + + -- Calculate if for this DetectedItem if a defense needs to be initiated. + -- This calculation is based on the distance between the defense point and the attackers, and the defensiveness parameter. + -- The attackers closest to the defense coordinates will be handled first, or course! + + local EngageDefenses = nil + + self:F( { DetectedDistance = DetectedDistance, DefenseRadius = self.DefenseRadius } ) + if DetectedDistance <= self.DefenseRadius then + + self:F( { DetectedApproach = self._DefenseApproach } ) + if self._DefenseApproach == AI_A2G_DISPATCHER.DefenseApproach.Distance then + EngageDefenses = true + self:F( { EngageDefenses = EngageDefenses } ) + end + + if self._DefenseApproach == AI_A2G_DISPATCHER.DefenseApproach.Random then + local DistanceProbability = ( self.DefenseRadius / DetectedDistance * self.DefenseReactivity ) + local DefenseProbability = math.random() + + self:F( { DistanceProbability = DistanceProbability, DefenseProbability = DefenseProbability } ) + + if DefenseProbability <= DistanceProbability / ( 300 / 30 ) then + EngageDefenses = true + end + end + + + end + + self:F( { EngageDefenses = EngageDefenses, DefenseLimit = self.DefenseLimit, DefenseTotal = DefenseTotal } ) + + -- There needs to be an EngageCoordinate. + -- If self.DefenseLimit is set (thus limit the amount of defenses to one zone), then only start a new defense if the maximum has not been reached. + -- If self.DefenseLimit has not been set, there is an unlimited amount of zones to be defended. + if ( EngageDefenses and ( self.DefenseLimit and DefenseTotal < self.DefenseLimit ) or not self.DefenseLimit ) then + do + local DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies = self:Evaluate_SEAD( DetectedItem ) -- Returns a SET_UNIT with the SEAD targets to be engaged... + if DefendersMissing > 0 then + self:F( { DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) + self:Defend( DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies, "SEAD" ) + end + end + + do + local DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies = self:Evaluate_CAS( DetectedItem ) -- Returns a SET_UNIT with the CAS targets to be engaged... + if DefendersMissing > 0 then + self:F( { DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) + self:Defend( DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies, "CAS" ) + end + end + + do + local DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies = self:Evaluate_BAI( DetectedItem ) -- Returns a SET_UNIT with the CAS targets to be engaged... + if DefendersMissing > 0 then + self:F( { DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) + self:Defend( DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies, "BAI" ) + end + end + end + + for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do + local Defender = Defender -- Wrapper.Group#GROUP + if DefenderTask.Target and DefenderTask.Target.Index == DetectedItem.Index then + DefenseTotal = DefenseTotal + 1 + end + end + + for DefenseQueueID, DefenseQueueItem in pairs( self.DefenseQueue ) do + local DefenseQueueItem = DefenseQueueItem -- #AI_A2G_DISPATCHER.DefenseQueueItem + if DefenseQueueItem.AttackerDetection and DefenseQueueItem.AttackerDetection.Index and DefenseQueueItem.AttackerDetection.Index == DetectedItem.Index then + DefenseTotal = DefenseTotal + 1 + end + end + + if self.TacticalDisplay then + -- Show tactical situation + local ThreatLevel = DetectedItem.Set:CalculateThreatLevelA2G() + Report:Add( string.format( " - %1s%s ( %4s ): ( #%d - %4s ) %s" , ( DetectedItem.IsDetected == true ) and "!" or " ", DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Type or " --- ", string.rep( "■", ThreatLevel ) ) ) + for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do + local Defender = Defender -- Wrapper.Group#GROUP + if DefenderTask.Target and DefenderTask.Target.Index == DetectedItem.Index then + if Defender:IsAlive() then + DefenderGroupCount = DefenderGroupCount + 1 + local Fuel = Defender:GetFuelMin() * 100 + local Damage = Defender:GetLife() / Defender:GetLife0() * 100 + Report:Add( string.format( " - %s ( %s - %s ): ( #%d ) F: %3d, D:%3d - %s", + Defender:GetName(), + DefenderTask.Type, + DefenderTask.Fsm:GetState(), + Defender:GetSize(), + Fuel, + Damage, + Defender:HasTask() == true and "Executing" or "Idle" ) ) + end + end + end + end + end + end + + if self.TacticalDisplay then + Report:Add( "\n - No Targets:") + local TaskCount = 0 + for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do + TaskCount = TaskCount + 1 + local Defender = Defender -- Wrapper.Group#GROUP + if not DefenderTask.Target then + if Defender:IsAlive() then + local DefenderHasTask = Defender:HasTask() + local Fuel = Defender:GetFuelMin() * 100 + local Damage = Defender:GetLife() / Defender:GetLife0() * 100 + DefenderGroupCount = DefenderGroupCount + 1 + Report:Add( string.format( " - %s ( %s - %s ): ( #%d ) F: %3d, D:%3d - %s", + Defender:GetName(), + DefenderTask.Type, + DefenderTask.Fsm:GetState(), + Defender:GetSize(), + Fuel, + Damage, + Defender:HasTask() == true and "Executing" or "Idle" ) ) + end + end + end + Report:Add( string.format( "\n - %d Tasks - %d Defender Groups", TaskCount, DefenderGroupCount ) ) + + Report:Add( string.format( "\n - %d Queued Aircraft Launches", #self.DefenseQueue ) ) + for DefenseQueueID, DefenseQueueItem in pairs( self.DefenseQueue ) do + local DefenseQueueItem = DefenseQueueItem -- #AI_A2G_DISPATCHER.DefenseQueueItem + Report:Add( string.format( " - %s - %s", DefenseQueueItem.SquadronName, DefenseQueueItem.DefenderSquadron.TakeoffTime, DefenseQueueItem.DefenderSquadron.TakeoffInterval) ) + + end + + Report:Add( string.format( "\n - Squadron Resources: ", #self.DefenseQueue ) ) + for DefenderSquadronName, DefenderSquadron in pairs( self.DefenderSquadrons ) do + Report:Add( string.format( " - %s - %s", DefenderSquadronName, DefenderSquadron.ResourceCount and tostring(DefenderSquadron.ResourceCount) or "n/a" ) ) + end + + self:F( Report:Text( "\n" ) ) + trigger.action.outText( Report:Text( "\n" ), 25 ) + end + + return true + end + +end + +do + + --- Calculates which HUMAN friendlies are nearby the area. + -- @param #AI_A2G_DISPATCHER self + -- @param DetectedItem The detected item. + -- @return #number, Core.Report#REPORT The amount of friendlies and a text string explaining which friendlies of which type. + function AI_A2G_DISPATCHER:GetPlayerFriendliesNearBy( DetectedItem ) + + local DetectedSet = DetectedItem.Set + local PlayersNearBy = self.Detection:GetPlayersNearBy( DetectedItem ) + + local PlayerTypes = {} + local PlayersCount = 0 + + if PlayersNearBy then + local DetectedTreatLevel = DetectedSet:CalculateThreatLevelA2G() + for PlayerUnitName, PlayerUnitData in pairs( PlayersNearBy ) do + local PlayerUnit = PlayerUnitData -- Wrapper.Unit#UNIT + local PlayerName = PlayerUnit:GetPlayerName() + --self:F( { PlayerName = PlayerName, PlayerUnit = PlayerUnit } ) + if PlayerUnit:IsAirPlane() and PlayerName ~= nil then + local FriendlyUnitThreatLevel = PlayerUnit:GetThreatLevel() + PlayersCount = PlayersCount + 1 + local PlayerType = PlayerUnit:GetTypeName() + PlayerTypes[PlayerName] = PlayerType + if DetectedTreatLevel < FriendlyUnitThreatLevel + 2 then + end + end + end + + end + + --self:F( { PlayersCount = PlayersCount } ) + + local PlayerTypesReport = REPORT:New() + + if PlayersCount > 0 then + for PlayerName, PlayerType in pairs( PlayerTypes ) do + PlayerTypesReport:Add( string.format('"%s" in %s', PlayerName, PlayerType ) ) + end + else + PlayerTypesReport:Add( "-" ) + end + + + return PlayersCount, PlayerTypesReport + end + + --- Calculates which friendlies are nearby the area. + -- @param #AI_A2G_DISPATCHER self + -- @param DetectedItem The detected item. + -- @return #number, Core.Report#REPORT The amount of friendlies and a text string explaining which friendlies of which type. + function AI_A2G_DISPATCHER:GetFriendliesNearBy( DetectedItem ) + + local DetectedSet = DetectedItem.Set + local FriendlyUnitsNearBy = self.Detection:GetFriendliesNearBy( DetectedItem ) + + local FriendlyTypes = {} + local FriendliesCount = 0 + + if FriendlyUnitsNearBy then + local DetectedTreatLevel = DetectedSet:CalculateThreatLevelA2G() + for FriendlyUnitName, FriendlyUnitData in pairs( FriendlyUnitsNearBy ) do + local FriendlyUnit = FriendlyUnitData -- Wrapper.Unit#UNIT + if FriendlyUnit:IsAirPlane() then + local FriendlyUnitThreatLevel = FriendlyUnit:GetThreatLevel() + FriendliesCount = FriendliesCount + 1 + local FriendlyType = FriendlyUnit:GetTypeName() + FriendlyTypes[FriendlyType] = FriendlyTypes[FriendlyType] and ( FriendlyTypes[FriendlyType] + 1 ) or 1 + if DetectedTreatLevel < FriendlyUnitThreatLevel + 2 then + end + end + end + + end + + --self:F( { FriendliesCount = FriendliesCount } ) + + local FriendlyTypesReport = REPORT:New() + + if FriendliesCount > 0 then + for FriendlyType, FriendlyTypeCount in pairs( FriendlyTypes ) do + FriendlyTypesReport:Add( string.format("%d of %s", FriendlyTypeCount, FriendlyType ) ) + end + else + FriendlyTypesReport:Add( "-" ) + end + + + return FriendliesCount, FriendlyTypesReport + end + + --- Schedules a new Patrol for the given SquadronName. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + function AI_A2G_DISPATCHER:SchedulerPatrol( SquadronName ) + local PatrolTaskTypes = { "SEAD", "CAS", "BAI" } + local PatrolTaskType = PatrolTaskTypes[math.random(1,3)] + self:Patrol( SquadronName, PatrolTaskType ) + end + + --- Set flashing player messages on or off + -- @param #AI_A2G_DISPATCHER self + -- @param #boolean onoff Set messages on (true) or off (false) + function AI_A2G_DISPATCHER:SetSendMessages( onoff ) + self.SetSendPlayerMessages = onoff + end +end + +--- **AI** -- Perform Air Patrolling for airplanes. +-- +-- **Features:** +-- +-- * Patrol AI airplanes within a given zone. +-- * Trigger detected events when enemy airplanes are detected. +-- * Manage a fuel treshold to RTB on time. +-- +-- === +-- +-- AI PATROL classes makes AI Controllables execute an Patrol. +-- +-- There are the following types of PATROL classes defined: +-- +-- * @{#AI_PATROL_ZONE}: Perform a PATROL in a zone. +-- +-- === +-- +-- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master-release/PAT%20-%20Patrolling) +-- +-- === +-- +-- ### [YouTube Playlist](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl35HvYZKA6G22WMt7iI3zky) +-- +-- === +-- +-- ### Author: **FlightControl** +-- ### Contributions: +-- +-- * **[Dutch_Baron](https://forums.eagle.ru/member.php?u=112075)**: Working together with James has resulted in the creation of the AI_BALANCER class. James has shared his ideas on balancing AI with air units, and together we made a first design which you can use now :-) +-- * **[Pikey](https://forums.eagle.ru/member.php?u=62835)**: Testing and API concept review. +-- +-- === +-- +-- @module AI.AI_Patrol +-- @image AI_Air_Patrolling.JPG + +--- AI_PATROL_ZONE class +-- @type AI_PATROL_ZONE +-- @field Wrapper.Controllable#CONTROLLABLE AIControllable The @{Wrapper.Controllable} patrolling. +-- @field Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @field DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @field DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @field DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Controllable} in km/h. +-- @field DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Controllable} in km/h. +-- @field Core.Spawn#SPAWN CoordTest +-- @extends Core.Fsm#FSM_CONTROLLABLE + +--- Implements the core functions to patrol a @{Zone} by an AI @{Wrapper.Controllable} or @{Wrapper.Group}. +-- +-- ![Process](..\Presentations\AI_PATROL\Dia3.JPG) +-- +-- The AI_PATROL_ZONE is assigned a @{Wrapper.Group} and this must be done before the AI_PATROL_ZONE process can be started using the **Start** event. +-- +-- ![Process](..\Presentations\AI_PATROL\Dia4.JPG) +-- +-- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits. +-- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. +-- +-- ![Process](..\Presentations\AI_PATROL\Dia5.JPG) +-- +-- This cycle will continue. +-- +-- ![Process](..\Presentations\AI_PATROL\Dia6.JPG) +-- +-- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event. +-- +-- ![Process](..\Presentations\AI_PATROL\Dia9.JPG) +-- +---- Note that the enemy is not engaged! To model enemy engagement, either tailor the **Detected** event, or +-- use derived AI_ classes to model AI offensive or defensive behaviour. +-- +-- ![Process](..\Presentations\AI_PATROL\Dia10.JPG) +-- +-- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. +-- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. +-- +-- ![Process](..\Presentations\AI_PATROL\Dia11.JPG) +-- +-- ## 1. AI_PATROL_ZONE constructor +-- +-- * @{#AI_PATROL_ZONE.New}(): Creates a new AI_PATROL_ZONE object. +-- +-- ## 2. AI_PATROL_ZONE is a FSM +-- +-- ![Process](..\Presentations\AI_PATROL\Dia2.JPG) +-- +-- ### 2.1. AI_PATROL_ZONE States +-- +-- * **None** ( Group ): The process is not started yet. +-- * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone. +-- * **Returning** ( Group ): The AI is returning to Base. +-- * **Stopped** ( Group ): The process is stopped. +-- * **Crashed** ( Group ): The AI has crashed or is dead. +-- +-- ### 2.2. AI_PATROL_ZONE Events +-- +-- * **Start** ( Group ): Start the process. +-- * **Stop** ( Group ): Stop the process. +-- * **Route** ( Group ): Route the AI to a new random 3D point within the Patrol Zone. +-- * **RTB** ( Group ): Route the AI to the home base. +-- * **Detect** ( Group ): The AI is detecting targets. +-- * **Detected** ( Group ): The AI has detected new targets. +-- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. +-- +-- ## 3. Set or Get the AI controllable +-- +-- * @{#AI_PATROL_ZONE.SetControllable}(): Set the AIControllable. +-- * @{#AI_PATROL_ZONE.GetControllable}(): Get the AIControllable. +-- +-- ## 4. Set the Speed and Altitude boundaries of the AI controllable +-- +-- * @{#AI_PATROL_ZONE.SetSpeed}(): Set the patrol speed boundaries of the AI, for the next patrol. +-- * @{#AI_PATROL_ZONE.SetAltitude}(): Set altitude boundaries of the AI, for the next patrol. +-- +-- ## 5. Manage the detection process of the AI controllable +-- +-- The detection process of the AI controllable can be manipulated. +-- Detection requires an amount of CPU power, which has an impact on your mission performance. +-- Only put detection on when absolutely necessary, and the frequency of the detection can also be set. +-- +-- * @{#AI_PATROL_ZONE.SetDetectionOn}(): Set the detection on. The AI will detect for targets. +-- * @{#AI_PATROL_ZONE.SetDetectionOff}(): Set the detection off, the AI will not detect for targets. The existing target list will NOT be erased. +-- +-- The detection frequency can be set with @{#AI_PATROL_ZONE.SetRefreshTimeInterval}( seconds ), where the amount of seconds specify how much seconds will be waited before the next detection. +-- Use the method @{#AI_PATROL_ZONE.GetDetectedUnits}() to obtain a list of the @{Wrapper.Unit}s detected by the AI. +-- +-- The detection can be filtered to potential targets in a specific zone. +-- Use the method @{#AI_PATROL_ZONE.SetDetectionZone}() to set the zone where targets need to be detected. +-- Note that when the zone is too far away, or the AI is not heading towards the zone, or the AI is too high, no targets may be detected +-- according the weather conditions. +-- +-- ## 6. Manage the "out of fuel" in the AI_PATROL_ZONE +-- +-- When the AI is out of fuel, it is required that a new AI is started, before the old AI can return to the home base. +-- Therefore, with a parameter and a calculation of the distance to the home base, the fuel treshold is calculated. +-- When the fuel treshold is reached, the AI will continue for a given time its patrol task in orbit, +-- while a new AI is targetted to the AI_PATROL_ZONE. +-- Once the time is finished, the old AI will return to the base. +-- Use the method @{#AI_PATROL_ZONE.ManageFuel}() to have this proces in place. +-- +-- ## 7. Manage "damage" behaviour of the AI in the AI_PATROL_ZONE +-- +-- When the AI is damaged, it is required that a new AIControllable is started. However, damage cannon be foreseen early on. +-- Therefore, when the damage treshold is reached, the AI will return immediately to the home base (RTB). +-- Use the method @{#AI_PATROL_ZONE.ManageDamage}() to have this proces in place. +-- +-- === +-- +-- @field #AI_PATROL_ZONE +AI_PATROL_ZONE = { + ClassName = "AI_PATROL_ZONE", +} + +--- Creates a new AI_PATROL_ZONE object +-- @param #AI_PATROL_ZONE self +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Controllable} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Controllable} in km/h. +-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO +-- @return #AI_PATROL_ZONE self +-- @usage +-- -- Define a new AI_PATROL_ZONE Object. This PatrolArea will patrol an AIControllable within PatrolZone between 3000 and 6000 meters, with a variying speed between 600 and 900 km/h. +-- PatrolZone = ZONE:New( 'PatrolZone' ) +-- PatrolSpawn = SPAWN:New( 'Patrol Group' ) +-- PatrolArea = AI_PATROL_ZONE:New( PatrolZone, 3000, 6000, 600, 900 ) +function AI_PATROL_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) + + -- Inherits from BASE + local self = BASE:Inherit( self, FSM_CONTROLLABLE:New() ) -- #AI_PATROL_ZONE + + + self.PatrolZone = PatrolZone + self.PatrolFloorAltitude = PatrolFloorAltitude + self.PatrolCeilingAltitude = PatrolCeilingAltitude + self.PatrolMinSpeed = PatrolMinSpeed + self.PatrolMaxSpeed = PatrolMaxSpeed + + -- defafult PatrolAltType to "BARO" if not specified + self.PatrolAltType = PatrolAltType or "BARO" + + self:SetRefreshTimeInterval( 30 ) + + self.CheckStatus = true + + self:ManageFuel( .2, 60 ) + self:ManageDamage( 1 ) + + + self.DetectedUnits = {} -- This table contains the targets detected during patrol. + + self:SetStartState( "None" ) + + self:AddTransition( "*", "Stop", "Stopped" ) + +--- OnLeave Transition Handler for State Stopped. +-- @function [parent=#AI_PATROL_ZONE] OnLeaveStopped +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnEnter Transition Handler for State Stopped. +-- @function [parent=#AI_PATROL_ZONE] OnEnterStopped +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- OnBefore Transition Handler for Event Stop. +-- @function [parent=#AI_PATROL_ZONE] OnBeforeStop +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnAfter Transition Handler for Event Stop. +-- @function [parent=#AI_PATROL_ZONE] OnAfterStop +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- Synchronous Event Trigger for Event Stop. +-- @function [parent=#AI_PATROL_ZONE] Stop +-- @param #AI_PATROL_ZONE self + +--- Asynchronous Event Trigger for Event Stop. +-- @function [parent=#AI_PATROL_ZONE] __Stop +-- @param #AI_PATROL_ZONE self +-- @param #number Delay The delay in seconds. + + self:AddTransition( "None", "Start", "Patrolling" ) + +--- OnBefore Transition Handler for Event Start. +-- @function [parent=#AI_PATROL_ZONE] OnBeforeStart +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnAfter Transition Handler for Event Start. +-- @function [parent=#AI_PATROL_ZONE] OnAfterStart +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- Synchronous Event Trigger for Event Start. +-- @function [parent=#AI_PATROL_ZONE] Start +-- @param #AI_PATROL_ZONE self + +--- Asynchronous Event Trigger for Event Start. +-- @function [parent=#AI_PATROL_ZONE] __Start +-- @param #AI_PATROL_ZONE self +-- @param #number Delay The delay in seconds. + +--- OnLeave Transition Handler for State Patrolling. +-- @function [parent=#AI_PATROL_ZONE] OnLeavePatrolling +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnEnter Transition Handler for State Patrolling. +-- @function [parent=#AI_PATROL_ZONE] OnEnterPatrolling +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + + self:AddTransition( "Patrolling", "Route", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. + +--- OnBefore Transition Handler for Event Route. +-- @function [parent=#AI_PATROL_ZONE] OnBeforeRoute +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnAfter Transition Handler for Event Route. +-- @function [parent=#AI_PATROL_ZONE] OnAfterRoute +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- Synchronous Event Trigger for Event Route. +-- @function [parent=#AI_PATROL_ZONE] Route +-- @param #AI_PATROL_ZONE self + +--- Asynchronous Event Trigger for Event Route. +-- @function [parent=#AI_PATROL_ZONE] __Route +-- @param #AI_PATROL_ZONE self +-- @param #number Delay The delay in seconds. + + self:AddTransition( "*", "Status", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. + +--- OnBefore Transition Handler for Event Status. +-- @function [parent=#AI_PATROL_ZONE] OnBeforeStatus +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnAfter Transition Handler for Event Status. +-- @function [parent=#AI_PATROL_ZONE] OnAfterStatus +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- Synchronous Event Trigger for Event Status. +-- @function [parent=#AI_PATROL_ZONE] Status +-- @param #AI_PATROL_ZONE self + +--- Asynchronous Event Trigger for Event Status. +-- @function [parent=#AI_PATROL_ZONE] __Status +-- @param #AI_PATROL_ZONE self +-- @param #number Delay The delay in seconds. + + self:AddTransition( "*", "Detect", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. + +--- OnBefore Transition Handler for Event Detect. +-- @function [parent=#AI_PATROL_ZONE] OnBeforeDetect +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnAfter Transition Handler for Event Detect. +-- @function [parent=#AI_PATROL_ZONE] OnAfterDetect +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- Synchronous Event Trigger for Event Detect. +-- @function [parent=#AI_PATROL_ZONE] Detect +-- @param #AI_PATROL_ZONE self + +--- Asynchronous Event Trigger for Event Detect. +-- @function [parent=#AI_PATROL_ZONE] __Detect +-- @param #AI_PATROL_ZONE self +-- @param #number Delay The delay in seconds. + + self:AddTransition( "*", "Detected", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. + +--- OnBefore Transition Handler for Event Detected. +-- @function [parent=#AI_PATROL_ZONE] OnBeforeDetected +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnAfter Transition Handler for Event Detected. +-- @function [parent=#AI_PATROL_ZONE] OnAfterDetected +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- Synchronous Event Trigger for Event Detected. +-- @function [parent=#AI_PATROL_ZONE] Detected +-- @param #AI_PATROL_ZONE self + +--- Asynchronous Event Trigger for Event Detected. +-- @function [parent=#AI_PATROL_ZONE] __Detected +-- @param #AI_PATROL_ZONE self +-- @param #number Delay The delay in seconds. + + self:AddTransition( "*", "RTB", "Returning" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. + +--- OnBefore Transition Handler for Event RTB. +-- @function [parent=#AI_PATROL_ZONE] OnBeforeRTB +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnAfter Transition Handler for Event RTB. +-- @function [parent=#AI_PATROL_ZONE] OnAfterRTB +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- Synchronous Event Trigger for Event RTB. +-- @function [parent=#AI_PATROL_ZONE] RTB +-- @param #AI_PATROL_ZONE self + +--- Asynchronous Event Trigger for Event RTB. +-- @function [parent=#AI_PATROL_ZONE] __RTB +-- @param #AI_PATROL_ZONE self +-- @param #number Delay The delay in seconds. + +--- OnLeave Transition Handler for State Returning. +-- @function [parent=#AI_PATROL_ZONE] OnLeaveReturning +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnEnter Transition Handler for State Returning. +-- @function [parent=#AI_PATROL_ZONE] OnEnterReturning +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + + self:AddTransition( "*", "Reset", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. + + self:AddTransition( "*", "Eject", "*" ) + self:AddTransition( "*", "Crash", "Crashed" ) + self:AddTransition( "*", "PilotDead", "*" ) + + return self +end + + + + +--- Sets (modifies) the minimum and maximum speed of the patrol. +-- @param #AI_PATROL_ZONE self +-- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Controllable} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Controllable} in km/h. +-- @return #AI_PATROL_ZONE self +function AI_PATROL_ZONE:SetSpeed( PatrolMinSpeed, PatrolMaxSpeed ) + self:F2( { PatrolMinSpeed, PatrolMaxSpeed } ) + + self.PatrolMinSpeed = PatrolMinSpeed + self.PatrolMaxSpeed = PatrolMaxSpeed +end + + + +--- Sets the floor and ceiling altitude of the patrol. +-- @param #AI_PATROL_ZONE self +-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @return #AI_PATROL_ZONE self +function AI_PATROL_ZONE:SetAltitude( PatrolFloorAltitude, PatrolCeilingAltitude ) + self:F2( { PatrolFloorAltitude, PatrolCeilingAltitude } ) + + self.PatrolFloorAltitude = PatrolFloorAltitude + self.PatrolCeilingAltitude = PatrolCeilingAltitude +end + +-- * @{#AI_PATROL_ZONE.SetDetectionOn}(): Set the detection on. The AI will detect for targets. +-- * @{#AI_PATROL_ZONE.SetDetectionOff}(): Set the detection off, the AI will not detect for targets. The existing target list will NOT be erased. + +--- Set the detection on. The AI will detect for targets. +-- @param #AI_PATROL_ZONE self +-- @return #AI_PATROL_ZONE self +function AI_PATROL_ZONE:SetDetectionOn() + self:F2() + + self.DetectOn = true +end + +--- Set the detection off. The AI will NOT detect for targets. +-- However, the list of already detected targets will be kept and can be enquired! +-- @param #AI_PATROL_ZONE self +-- @return #AI_PATROL_ZONE self +function AI_PATROL_ZONE:SetDetectionOff() + self:F2() + + self.DetectOn = false +end + +--- Set the status checking off. +-- @param #AI_PATROL_ZONE self +-- @return #AI_PATROL_ZONE self +function AI_PATROL_ZONE:SetStatusOff() + self:F2() + + self.CheckStatus = false +end + +--- Activate the detection. The AI will detect for targets if the Detection is switched On. +-- @param #AI_PATROL_ZONE self +-- @return #AI_PATROL_ZONE self +function AI_PATROL_ZONE:SetDetectionActivated() + self:F2() + + self:ClearDetectedUnits() + self.DetectActivated = true + self:__Detect( -self.DetectInterval ) +end + +--- Deactivate the detection. The AI will NOT detect for targets. +-- @param #AI_PATROL_ZONE self +-- @return #AI_PATROL_ZONE self +function AI_PATROL_ZONE:SetDetectionDeactivated() + self:F2() + + self:ClearDetectedUnits() + self.DetectActivated = false +end + +--- Set the interval in seconds between each detection executed by the AI. +-- The list of already detected targets will be kept and updated. +-- Newly detected targets will be added, but already detected targets that were +-- not detected in this cycle, will NOT be removed! +-- The default interval is 30 seconds. +-- @param #AI_PATROL_ZONE self +-- @param #number Seconds The interval in seconds. +-- @return #AI_PATROL_ZONE self +function AI_PATROL_ZONE:SetRefreshTimeInterval( Seconds ) + self:F2() + + if Seconds then + self.DetectInterval = Seconds + else + self.DetectInterval = 30 + end +end + +--- Set the detection zone where the AI is detecting targets. +-- @param #AI_PATROL_ZONE self +-- @param Core.Zone#ZONE DetectionZone The zone where to detect targets. +-- @return #AI_PATROL_ZONE self +function AI_PATROL_ZONE:SetDetectionZone( DetectionZone ) + self:F2() + + if DetectionZone then + self.DetectZone = DetectionZone + else + self.DetectZone = nil + end +end + +--- Gets a list of @{Wrapper.Unit#UNIT}s that were detected by the AI. +-- No filtering is applied, so, ANY detected UNIT can be in this list. +-- It is up to the mission designer to use the @{Wrapper.Unit} class and methods to filter the targets. +-- @param #AI_PATROL_ZONE self +-- @return #table The list of @{Wrapper.Unit#UNIT}s +function AI_PATROL_ZONE:GetDetectedUnits() + self:F2() + + return self.DetectedUnits +end + +--- Clears the list of @{Wrapper.Unit#UNIT}s that were detected by the AI. +-- @param #AI_PATROL_ZONE self +function AI_PATROL_ZONE:ClearDetectedUnits() + self:F2() + self.DetectedUnits = {} +end + +--- When the AI is out of fuel, it is required that a new AI is started, before the old AI can return to the home base. +-- Therefore, with a parameter and a calculation of the distance to the home base, the fuel treshold is calculated. +-- When the fuel treshold is reached, the AI will continue for a given time its patrol task in orbit, while a new AIControllable is targetted to the AI_PATROL_ZONE. +-- Once the time is finished, the old AI will return to the base. +-- @param #AI_PATROL_ZONE self +-- @param #number PatrolFuelThresholdPercentage The treshold in percentage (between 0 and 1) when the AIControllable is considered to get out of fuel. +-- @param #number PatrolOutOfFuelOrbitTime The amount of seconds the out of fuel AIControllable will orbit before returning to the base. +-- @return #AI_PATROL_ZONE self +function AI_PATROL_ZONE:ManageFuel( PatrolFuelThresholdPercentage, PatrolOutOfFuelOrbitTime ) + + self.PatrolFuelThresholdPercentage = PatrolFuelThresholdPercentage + self.PatrolOutOfFuelOrbitTime = PatrolOutOfFuelOrbitTime + + return self +end + +--- When the AI is damaged beyond a certain treshold, it is required that the AI returns to the home base. +-- However, damage cannot be foreseen early on. +-- Therefore, when the damage treshold is reached, +-- the AI will return immediately to the home base (RTB). +-- Note that for groups, the average damage of the complete group will be calculated. +-- So, in a group of 4 airplanes, 2 lost and 2 with damage 0.2, the damage treshold will be 0.25. +-- @param #AI_PATROL_ZONE self +-- @param #number PatrolDamageThreshold The treshold in percentage (between 0 and 1) when the AI is considered to be damaged. +-- @return #AI_PATROL_ZONE self +function AI_PATROL_ZONE:ManageDamage( PatrolDamageThreshold ) + + self.PatrolManageDamage = true + self.PatrolDamageThreshold = PatrolDamageThreshold + + return self +end + +--- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. +-- @param #AI_PATROL_ZONE self +-- @return #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_PATROL_ZONE:onafterStart( Controllable, From, Event, To ) + self:F2() + + self:__Route( 1 ) -- Route to the patrol point. The asynchronous trigger is important, because a spawned group and units takes at least one second to come live. + self:__Status( 60 ) -- Check status status every 30 seconds. + self:SetDetectionActivated() + + self:HandleEvent( EVENTS.PilotDead, self.OnPilotDead ) + self:HandleEvent( EVENTS.Crash, self.OnCrash ) + self:HandleEvent( EVENTS.Ejection, self.OnEjection ) + + Controllable:OptionROEHoldFire() + Controllable:OptionROTVertical() + + self.Controllable:OnReSpawn( + function( PatrolGroup ) + self:T( "ReSpawn" ) + self:__Reset( 1 ) + self:__Route( 5 ) + end + ) + + self:SetDetectionOn() + +end + + +--- @param #AI_PATROL_ZONE self +--- @param Wrapper.Controllable#CONTROLLABLE Controllable +function AI_PATROL_ZONE:onbeforeDetect( Controllable, From, Event, To ) + + return self.DetectOn and self.DetectActivated +end + +--- @param #AI_PATROL_ZONE self +--- @param Wrapper.Controllable#CONTROLLABLE Controllable +function AI_PATROL_ZONE:onafterDetect( Controllable, From, Event, To ) + + local Detected = false + + local DetectedTargets = Controllable:GetDetectedTargets() + for TargetID, Target in pairs( DetectedTargets or {} ) do + local TargetObject = Target.object + + if TargetObject and TargetObject:isExist() and TargetObject.id_ < 50000000 then + + local TargetUnit = UNIT:Find( TargetObject ) + + -- Check that target is alive due to issue https://github.com/FlightControl-Master/MOOSE/issues/1234 + if TargetUnit and TargetUnit:IsAlive() then + + local TargetUnitName = TargetUnit:GetName() + + if self.DetectionZone then + if TargetUnit:IsInZone( self.DetectionZone ) then + self:T( {"Detected ", TargetUnit } ) + if self.DetectedUnits[TargetUnit] == nil then + self.DetectedUnits[TargetUnit] = true + end + Detected = true + end + else + if self.DetectedUnits[TargetUnit] == nil then + self.DetectedUnits[TargetUnit] = true + end + Detected = true + end + + end + end + end + + self:__Detect( -self.DetectInterval ) + + if Detected == true then + self:__Detected( 1.5 ) + end + +end + +--- @param Wrapper.Controllable#CONTROLLABLE AIControllable +-- This statis method is called from the route path within the last task at the last waaypoint of the Controllable. +-- Note that this method is required, as triggers the next route when patrolling for the Controllable. +function AI_PATROL_ZONE:_NewPatrolRoute( AIControllable ) + + local PatrolZone = AIControllable:GetState( AIControllable, "PatrolZone" ) -- PatrolCore.Zone#AI_PATROL_ZONE + PatrolZone:__Route( 1 ) +end + + +--- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_PATROL_ZONE:onafterRoute( Controllable, From, Event, To ) + + self:F2() + + -- When RTB, don't allow anymore the routing. + if From == "RTB" then + return + end + + + if self.Controllable:IsAlive() then + -- Determine if the AIControllable is within the PatrolZone. + -- If not, make a waypoint within the to that the AIControllable will fly at maximum speed to that point. + + local PatrolRoute = {} + + -- Calculate the current route point of the controllable as the start point of the route. + -- However, when the controllable is not in the air, + -- the controllable current waypoint is probably the airbase... + -- Thus, if we would take the current waypoint as the startpoint, upon take-off, the controllable flies + -- immediately back to the airbase, and this is not correct. + -- Therefore, when on a runway, get as the current route point a random point within the PatrolZone. + -- This will make the plane fly immediately to the patrol zone. + + if self.Controllable:InAir() == false then + self:T( "Not in the air, finding route path within PatrolZone" ) + local CurrentVec2 = self.Controllable:GetVec2() + --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). + local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() + local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) + local ToPatrolZoneSpeed = self.PatrolMaxSpeed + local CurrentRoutePoint = CurrentPointVec3:WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TakeOffParking, + POINT_VEC3.RoutePointAction.FromParkingArea, + ToPatrolZoneSpeed, + true + ) + PatrolRoute[#PatrolRoute+1] = CurrentRoutePoint + else + self:T( "In the air, finding route path within PatrolZone" ) + local CurrentVec2 = self.Controllable:GetVec2() + --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). + local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() + local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) + local ToPatrolZoneSpeed = self.PatrolMaxSpeed + local CurrentRoutePoint = CurrentPointVec3:WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToPatrolZoneSpeed, + true + ) + PatrolRoute[#PatrolRoute+1] = CurrentRoutePoint + end + + + --- Define a random point in the @{Zone}. The AI will fly to that point within the zone. + + --- Find a random 2D point in PatrolZone. + local ToTargetVec2 = self.PatrolZone:GetRandomVec2() + self:T2( ToTargetVec2 ) + + --- Define Speed and Altitude. + local ToTargetAltitude = math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ) + local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) + self:T2( { self.PatrolMinSpeed, self.PatrolMaxSpeed, ToTargetSpeed } ) + + --- Obtain a 3D @{Point} from the 2D point + altitude. + local ToTargetPointVec3 = POINT_VEC3:New( ToTargetVec2.x, ToTargetAltitude, ToTargetVec2.y ) + + --- Create a route point of type air. + local ToTargetRoutePoint = ToTargetPointVec3:WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToTargetSpeed, + true + ) + + --self.CoordTest:SpawnFromVec3( ToTargetPointVec3:GetVec3() ) + + --ToTargetPointVec3:SmokeRed() + + PatrolRoute[#PatrolRoute+1] = ToTargetRoutePoint + + --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... + self.Controllable:WayPointInitialize( PatrolRoute ) + + --- Do a trick, link the NewPatrolRoute function of the PATROLGROUP object to the AIControllable in a temporary variable ... + self.Controllable:SetState( self.Controllable, "PatrolZone", self ) + self.Controllable:WayPointFunction( #PatrolRoute, 1, "AI_PATROL_ZONE:_NewPatrolRoute" ) + + --- NOW ROUTE THE GROUP! + self.Controllable:WayPointExecute( 1, 2 ) + end + +end + +--- @param #AI_PATROL_ZONE self +function AI_PATROL_ZONE:onbeforeStatus() + + return self.CheckStatus +end + +--- @param #AI_PATROL_ZONE self +function AI_PATROL_ZONE:onafterStatus() + self:F2() + + if self.Controllable and self.Controllable:IsAlive() then + + local RTB = false + + local Fuel = self.Controllable:GetFuelMin() + if Fuel < self.PatrolFuelThresholdPercentage then + self:I( self.Controllable:GetName() .. " is out of fuel:" .. Fuel .. ", RTB!" ) + local OldAIControllable = self.Controllable + + local OrbitTask = OldAIControllable:TaskOrbitCircle( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ), self.PatrolMinSpeed ) + local TimedOrbitTask = OldAIControllable:TaskControlled( OrbitTask, OldAIControllable:TaskCondition(nil,nil,nil,nil,self.PatrolOutOfFuelOrbitTime,nil ) ) + OldAIControllable:SetTask( TimedOrbitTask, 10 ) + + RTB = true + else + end + + -- TODO: Check GROUP damage function. + local Damage = self.Controllable:GetLife() + if Damage <= self.PatrolDamageThreshold then + self:I( self.Controllable:GetName() .. " is damaged:" .. Damage .. ", RTB!" ) + RTB = true + end + + if RTB == true then + self:RTB() + else + self:__Status( 60 ) -- Execute the Patrol event after 30 seconds. + end + end +end + +--- @param #AI_PATROL_ZONE self +function AI_PATROL_ZONE:onafterRTB() + self:F2() + + if self.Controllable and self.Controllable:IsAlive() then + + self:SetDetectionOff() + self.CheckStatus = false + + local PatrolRoute = {} + + --- Calculate the current route point. + local CurrentVec2 = self.Controllable:GetVec2() + + --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). + local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() + local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) + local ToPatrolZoneSpeed = self.PatrolMaxSpeed + local CurrentRoutePoint = CurrentPointVec3:WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToPatrolZoneSpeed, + true + ) + + PatrolRoute[#PatrolRoute+1] = CurrentRoutePoint + + --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... + self.Controllable:WayPointInitialize( PatrolRoute ) + + --- NOW ROUTE THE GROUP! + self.Controllable:WayPointExecute( 1, 1 ) + + end + +end + +--- @param #AI_PATROL_ZONE self +function AI_PATROL_ZONE:onafterDead() + self:SetDetectionOff() + self:SetStatusOff() +end + +--- @param #AI_PATROL_ZONE self +-- @param Core.Event#EVENTDATA EventData +function AI_PATROL_ZONE:OnCrash( EventData ) + + if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then + if #self.Controllable:GetUnits() == 1 then + self:__Crash( 1, EventData ) + end + end +end + +--- @param #AI_PATROL_ZONE self +-- @param Core.Event#EVENTDATA EventData +function AI_PATROL_ZONE:OnEjection( EventData ) + + if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then + self:__Eject( 1, EventData ) + end +end + +--- @param #AI_PATROL_ZONE self +-- @param Core.Event#EVENTDATA EventData +function AI_PATROL_ZONE:OnPilotDead( EventData ) + + if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then + self:__PilotDead( 1, EventData ) + end +end +--- **AI** -- Perform Combat Air Patrolling (CAP) for airplanes. +-- +-- **Features:** +-- +-- * Patrol AI airplanes within a given zone. +-- * Trigger detected events when enemy airplanes are detected. +-- * Manage a fuel treshold to RTB on time. +-- * Engage the enemy when detected. +-- +-- +-- === +-- +-- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master-release/CAP%20-%20Combat%20Air%20Patrol) +-- +-- === +-- +-- ### [YouTube Playlist](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl1YCyPxJgoZn-CfhwyeW65L) +-- +-- === +-- +-- ### Author: **FlightControl** +-- ### Contributions: +-- +-- * **[Quax](https://forums.eagle.ru/member.php?u=90530)**: Concept, Advice & Testing. +-- * **[Pikey](https://forums.eagle.ru/member.php?u=62835)**: Concept, Advice & Testing. +-- * **[Gunterlund](http://forums.eagle.ru:8080/member.php?u=75036)**: Test case revision. +-- * **[Whisper](http://forums.eagle.ru/member.php?u=3829): Testing. +-- * **[Delta99](https://forums.eagle.ru/member.php?u=125166): Testing. +-- +-- === +-- +-- @module AI.AI_Cap +-- @image AI_Combat_Air_Patrol.JPG + + +--- @type AI_CAP_ZONE +-- @field Wrapper.Controllable#CONTROLLABLE AIControllable The @{Wrapper.Controllable} patrolling. +-- @field Core.Zone#ZONE_BASE TargetZone The @{Zone} where the patrol needs to be executed. +-- @extends AI.AI_Patrol#AI_PATROL_ZONE + + +--- Implements the core functions to patrol a @{Zone} by an AI @{Wrapper.Controllable} or @{Wrapper.Group} +-- and automatically engage any airborne enemies that are within a certain range or within a certain zone. +-- +-- ![Process](..\Presentations\AI_CAP\Dia3.JPG) +-- +-- The AI_CAP_ZONE is assigned a @{Wrapper.Group} and this must be done before the AI_CAP_ZONE process can be started using the **Start** event. +-- +-- ![Process](..\Presentations\AI_CAP\Dia4.JPG) +-- +-- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits. +-- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. +-- +-- ![Process](..\Presentations\AI_CAP\Dia5.JPG) +-- +-- This cycle will continue. +-- +-- ![Process](..\Presentations\AI_CAP\Dia6.JPG) +-- +-- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event. +-- +-- ![Process](..\Presentations\AI_CAP\Dia9.JPG) +-- +-- When enemies are detected, the AI will automatically engage the enemy. +-- +-- ![Process](..\Presentations\AI_CAP\Dia10.JPG) +-- +-- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. +-- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. +-- +-- ![Process](..\Presentations\AI_CAP\Dia13.JPG) +-- +-- ## 1. AI_CAP_ZONE constructor +-- +-- * @{#AI_CAP_ZONE.New}(): Creates a new AI_CAP_ZONE object. +-- +-- ## 2. AI_CAP_ZONE is a FSM +-- +-- ![Process](..\Presentations\AI_CAP\Dia2.JPG) +-- +-- ### 2.1 AI_CAP_ZONE States +-- +-- * **None** ( Group ): The process is not started yet. +-- * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone. +-- * **Engaging** ( Group ): The AI is engaging the bogeys. +-- * **Returning** ( Group ): The AI is returning to Base.. +-- +-- ### 2.2 AI_CAP_ZONE Events +-- +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Start}**: Start the process. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Route}**: Route the AI to a new random 3D point within the Patrol Zone. +-- * **@{#AI_CAP_ZONE.Engage}**: Let the AI engage the bogeys. +-- * **@{#AI_CAP_ZONE.Abort}**: Aborts the engagement and return patrolling in the patrol zone. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.RTB}**: Route the AI to the home base. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detect}**: The AI is detecting targets. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets. +-- * **@{#AI_CAP_ZONE.Destroy}**: The AI has destroyed a bogey @{Wrapper.Unit}. +-- * **@{#AI_CAP_ZONE.Destroyed}**: The AI has destroyed all bogeys @{Wrapper.Unit}s assigned in the CAS task. +-- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. +-- +-- ## 3. Set the Range of Engagement +-- +-- ![Range](..\Presentations\AI_CAP\Dia11.JPG) +-- +-- An optional range can be set in meters, +-- that will define when the AI will engage with the detected airborne enemy targets. +-- The range can be beyond or smaller than the range of the Patrol Zone. +-- The range is applied at the position of the AI. +-- Use the method @{AI.AI_CAP#AI_CAP_ZONE.SetEngageRange}() to define that range. +-- +-- ## 4. Set the Zone of Engagement +-- +-- ![Zone](..\Presentations\AI_CAP\Dia12.JPG) +-- +-- An optional @{Zone} can be set, +-- that will define when the AI will engage with the detected airborne enemy targets. +-- Use the method @{AI.AI_Cap#AI_CAP_ZONE.SetEngageZone}() to define that Zone. +-- +-- === +-- +-- @field #AI_CAP_ZONE +AI_CAP_ZONE = { + ClassName = "AI_CAP_ZONE", +} + + + +--- Creates a new AI_CAP_ZONE object +-- @param #AI_CAP_ZONE self +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Controllable} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Controllable} in km/h. +-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO +-- @return #AI_CAP_ZONE self +function AI_CAP_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) + + -- Inherits from BASE + local self = BASE:Inherit( self, AI_PATROL_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_CAP_ZONE + + self.Accomplished = false + self.Engaging = false + + self:AddTransition( { "Patrolling", "Engaging" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAP_ZONE. + + --- OnBefore Transition Handler for Event Engage. + -- @function [parent=#AI_CAP_ZONE] OnBeforeEngage + -- @param #AI_CAP_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Engage. + -- @function [parent=#AI_CAP_ZONE] OnAfterEngage + -- @param #AI_CAP_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Engage. + -- @function [parent=#AI_CAP_ZONE] Engage + -- @param #AI_CAP_ZONE self + + --- Asynchronous Event Trigger for Event Engage. + -- @function [parent=#AI_CAP_ZONE] __Engage + -- @param #AI_CAP_ZONE self + -- @param #number Delay The delay in seconds. + +--- OnLeave Transition Handler for State Engaging. +-- @function [parent=#AI_CAP_ZONE] OnLeaveEngaging +-- @param #AI_CAP_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnEnter Transition Handler for State Engaging. +-- @function [parent=#AI_CAP_ZONE] OnEnterEngaging +-- @param #AI_CAP_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + + self:AddTransition( "Engaging", "Fired", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAP_ZONE. + + --- OnBefore Transition Handler for Event Fired. + -- @function [parent=#AI_CAP_ZONE] OnBeforeFired + -- @param #AI_CAP_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Fired. + -- @function [parent=#AI_CAP_ZONE] OnAfterFired + -- @param #AI_CAP_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Fired. + -- @function [parent=#AI_CAP_ZONE] Fired + -- @param #AI_CAP_ZONE self + + --- Asynchronous Event Trigger for Event Fired. + -- @function [parent=#AI_CAP_ZONE] __Fired + -- @param #AI_CAP_ZONE self + -- @param #number Delay The delay in seconds. + + self:AddTransition( "*", "Destroy", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_CAP_ZONE. + + --- OnBefore Transition Handler for Event Destroy. + -- @function [parent=#AI_CAP_ZONE] OnBeforeDestroy + -- @param #AI_CAP_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Destroy. + -- @function [parent=#AI_CAP_ZONE] OnAfterDestroy + -- @param #AI_CAP_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Destroy. + -- @function [parent=#AI_CAP_ZONE] Destroy + -- @param #AI_CAP_ZONE self + + --- Asynchronous Event Trigger for Event Destroy. + -- @function [parent=#AI_CAP_ZONE] __Destroy + -- @param #AI_CAP_ZONE self + -- @param #number Delay The delay in seconds. + + + self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_CAP_ZONE. + + --- OnBefore Transition Handler for Event Abort. + -- @function [parent=#AI_CAP_ZONE] OnBeforeAbort + -- @param #AI_CAP_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Abort. + -- @function [parent=#AI_CAP_ZONE] OnAfterAbort + -- @param #AI_CAP_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Abort. + -- @function [parent=#AI_CAP_ZONE] Abort + -- @param #AI_CAP_ZONE self + + --- Asynchronous Event Trigger for Event Abort. + -- @function [parent=#AI_CAP_ZONE] __Abort + -- @param #AI_CAP_ZONE self + -- @param #number Delay The delay in seconds. + + self:AddTransition( "Engaging", "Accomplish", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_CAP_ZONE. + + --- OnBefore Transition Handler for Event Accomplish. + -- @function [parent=#AI_CAP_ZONE] OnBeforeAccomplish + -- @param #AI_CAP_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Accomplish. + -- @function [parent=#AI_CAP_ZONE] OnAfterAccomplish + -- @param #AI_CAP_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Accomplish. + -- @function [parent=#AI_CAP_ZONE] Accomplish + -- @param #AI_CAP_ZONE self + + --- Asynchronous Event Trigger for Event Accomplish. + -- @function [parent=#AI_CAP_ZONE] __Accomplish + -- @param #AI_CAP_ZONE self + -- @param #number Delay The delay in seconds. + + return self +end + + +--- Set the Engage Zone which defines where the AI will engage bogies. +-- @param #AI_CAP_ZONE self +-- @param Core.Zone#ZONE EngageZone The zone where the AI is performing CAP. +-- @return #AI_CAP_ZONE self +function AI_CAP_ZONE:SetEngageZone( EngageZone ) + self:F2() + + if EngageZone then + self.EngageZone = EngageZone + else + self.EngageZone = nil + end +end + +--- Set the Engage Range when the AI will engage with airborne enemies. +-- @param #AI_CAP_ZONE self +-- @param #number EngageRange The Engage Range. +-- @return #AI_CAP_ZONE self +function AI_CAP_ZONE:SetEngageRange( EngageRange ) + self:F2() + + if EngageRange then + self.EngageRange = EngageRange + else + self.EngageRange = nil + end +end + +--- onafter State Transition for Event Start. +-- @param #AI_CAP_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_CAP_ZONE:onafterStart( Controllable, From, Event, To ) + + -- Call the parent Start event handler + self:GetParent(self).onafterStart( self, Controllable, From, Event, To ) + self:HandleEvent( EVENTS.Dead ) + +end + + +--- @param AI.AI_CAP#AI_CAP_ZONE +-- @param Wrapper.Group#GROUP EngageGroup +function AI_CAP_ZONE.EngageRoute( EngageGroup, Fsm ) + + EngageGroup:F( { "AI_CAP_ZONE.EngageRoute:", EngageGroup:GetName() } ) + + if EngageGroup:IsAlive() then + Fsm:__Engage( 1 ) + end +end + + + +--- @param #AI_CAP_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_CAP_ZONE:onbeforeEngage( Controllable, From, Event, To ) + + if self.Accomplished == true then + return false + end +end + +--- @param #AI_CAP_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_CAP_ZONE:onafterDetected( Controllable, From, Event, To ) + + if From ~= "Engaging" then + + local Engage = false + + for DetectedUnit, Detected in pairs( self.DetectedUnits ) do + + local DetectedUnit = DetectedUnit -- Wrapper.Unit#UNIT + self:T( DetectedUnit ) + if DetectedUnit:IsAlive() and DetectedUnit:IsAir() then + Engage = true + break + end + end + + if Engage == true then + self:F( 'Detected -> Engaging' ) + self:__Engage( 1 ) + end + end +end + + +--- @param #AI_CAP_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_CAP_ZONE:onafterAbort( Controllable, From, Event, To ) + Controllable:ClearTasks() + self:__Route( 1 ) +end + + + + +--- @param #AI_CAP_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_CAP_ZONE:onafterEngage( Controllable, From, Event, To ) + + if Controllable and Controllable:IsAlive() then + + local EngageRoute = {} + + --- Calculate the current route point. + local CurrentVec2 = self.Controllable:GetVec2() + + --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). + local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() + local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) + local ToEngageZoneSpeed = self.PatrolMaxSpeed + local CurrentRoutePoint = CurrentPointVec3:WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToEngageZoneSpeed, + true + ) + + EngageRoute[#EngageRoute+1] = CurrentRoutePoint + + + --- Find a random 2D point in PatrolZone. + local ToTargetVec2 = self.PatrolZone:GetRandomVec2() + self:T2( ToTargetVec2 ) + + --- Define Speed and Altitude. + local ToTargetAltitude = math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude ) + local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) + self:T2( { self.PatrolMinSpeed, self.PatrolMaxSpeed, ToTargetSpeed } ) + + --- Obtain a 3D @{Point} from the 2D point + altitude. + local ToTargetPointVec3 = POINT_VEC3:New( ToTargetVec2.x, ToTargetAltitude, ToTargetVec2.y ) + + --- Create a route point of type air. + local ToPatrolRoutePoint = ToTargetPointVec3:WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToTargetSpeed, + true + ) + + EngageRoute[#EngageRoute+1] = ToPatrolRoutePoint + + Controllable:OptionROEOpenFire() + Controllable:OptionROTEvadeFire() + + local AttackTasks = {} + + for DetectedUnit, Detected in pairs( self.DetectedUnits ) do + local DetectedUnit = DetectedUnit -- Wrapper.Unit#UNIT + self:T( { DetectedUnit, DetectedUnit:IsAlive(), DetectedUnit:IsAir() } ) + if DetectedUnit:IsAlive() and DetectedUnit:IsAir() then + if self.EngageZone then + if DetectedUnit:IsInZone( self.EngageZone ) then + self:F( {"Within Zone and Engaging ", DetectedUnit } ) + AttackTasks[#AttackTasks+1] = Controllable:TaskAttackUnit( DetectedUnit ) + end + else + if self.EngageRange then + if DetectedUnit:GetPointVec3():Get2DDistance(Controllable:GetPointVec3() ) <= self.EngageRange then + self:F( {"Within Range and Engaging", DetectedUnit } ) + AttackTasks[#AttackTasks+1] = Controllable:TaskAttackUnit( DetectedUnit ) + end + else + AttackTasks[#AttackTasks+1] = Controllable:TaskAttackUnit( DetectedUnit ) + end + end + else + self.DetectedUnits[DetectedUnit] = nil + end + end + + if #AttackTasks == 0 then + self:F("No targets found -> Going back to Patrolling") + self:__Abort( 1 ) + self:__Route( 1 ) + self:SetDetectionActivated() + else + + AttackTasks[#AttackTasks+1] = Controllable:TaskFunction( "AI_CAP_ZONE.EngageRoute", self ) + EngageRoute[1].task = Controllable:TaskCombo( AttackTasks ) + + self:SetDetectionDeactivated() + end + + Controllable:Route( EngageRoute, 0.5 ) + + end +end + +--- @param #AI_CAP_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_CAP_ZONE:onafterAccomplish( Controllable, From, Event, To ) + self.Accomplished = true + self:SetDetectionOff() +end + +--- @param #AI_CAP_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @param Core.Event#EVENTDATA EventData +function AI_CAP_ZONE:onafterDestroy( Controllable, From, Event, To, EventData ) + + if EventData.IniUnit then + self.DetectedUnits[EventData.IniUnit] = nil + end +end + +--- @param #AI_CAP_ZONE self +-- @param Core.Event#EVENTDATA EventData +function AI_CAP_ZONE:OnEventDead( EventData ) + self:F( { "EventDead", EventData } ) + + if EventData.IniDCSUnit then + if self.DetectedUnits and self.DetectedUnits[EventData.IniUnit] then + self:__Destroy( 1, EventData ) + end + end +end +--- **AI** -- Perform Close Air Support (CAS) near friendlies. +-- +-- **Features:** +-- +-- * Hold and standby within a patrol zone. +-- * Engage upon command the enemies within an engagement zone. +-- * Loop the zone until all enemies are eliminated. +-- * Trigger different events upon the results achieved. +-- * After combat, return to the patrol zone and hold. +-- * RTB when commanded or after fuel. +-- +-- === +-- +-- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master-release/CAS%20-%20Close%20Air%20Support) +-- +-- === +-- +-- ### [YouTube Playlist](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl3JBO1WDqqpyYRRmIkR2ir2) +-- +-- === +-- +-- ### Author: **FlightControl** +-- ### Contributions: +-- +-- * **[Quax](https://forums.eagle.ru/member.php?u=90530)**: Concept, Advice & Testing. +-- * **[Pikey](https://forums.eagle.ru/member.php?u=62835)**: Concept, Advice & Testing. +-- * **[Gunterlund](http://forums.eagle.ru:8080/member.php?u=75036)**: Test case revision. +-- +-- === +-- +-- @module AI.AI_Cas +-- @image AI_Close_Air_Support.JPG + +--- AI_CAS_ZONE class +-- @type AI_CAS_ZONE +-- @field Wrapper.Controllable#CONTROLLABLE AIControllable The @{Wrapper.Controllable} patrolling. +-- @field Core.Zone#ZONE_BASE TargetZone The @{Zone} where the patrol needs to be executed. +-- @extends AI.AI_Patrol#AI_PATROL_ZONE + +--- Implements the core functions to provide Close Air Support in an Engage @{Zone} by an AIR @{Wrapper.Controllable} or @{Wrapper.Group}. +-- The AI_CAS_ZONE runs a process. It holds an AI in a Patrol Zone and when the AI is commanded to engage, it will fly to an Engage Zone. +-- +-- ![HoldAndEngage](..\Presentations\AI_CAS\Dia3.JPG) +-- +-- The AI_CAS_ZONE is assigned a @{Wrapper.Group} and this must be done before the AI_CAS_ZONE process can be started through the **Start** event. +-- +-- ![Start Event](..\Presentations\AI_CAS\Dia4.JPG) +-- +-- Upon started, The AI will **Route** itself towards the random 3D point within a patrol zone, +-- using a random speed within the given altitude and speed limits. +-- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. +-- This cycle will continue until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. +-- +-- ![Route Event](..\Presentations\AI_CAS\Dia5.JPG) +-- +-- When the AI is commanded to provide Close Air Support (through the event **Engage**), the AI will fly towards the Engage Zone. +-- Any target that is detected in the Engage Zone will be reported and will be destroyed by the AI. +-- +-- ![Engage Event](..\Presentations\AI_CAS\Dia6.JPG) +-- +-- The AI will detect the targets and will only destroy the targets within the Engage Zone. +-- +-- ![Engage Event](..\Presentations\AI_CAS\Dia7.JPG) +-- +-- Every target that is destroyed, is reported< by the AI. +-- +-- ![Engage Event](..\Presentations\AI_CAS\Dia8.JPG) +-- +-- Note that the AI does not know when the Engage Zone is cleared, and therefore will keep circling in the zone. +-- +-- ![Engage Event](..\Presentations\AI_CAS\Dia9.JPG) +-- +-- Until it is notified through the event **Accomplish**, which is to be triggered by an observing party: +-- +-- * a FAC +-- * a timed event +-- * a menu option selected by a human +-- * a condition +-- * others ... +-- +-- ![Engage Event](..\Presentations\AI_CAS\Dia10.JPG) +-- +-- When the AI has accomplished the CAS, it will fly back to the Patrol Zone. +-- +-- ![Engage Event](..\Presentations\AI_CAS\Dia11.JPG) +-- +-- It will keep patrolling there, until it is notified to RTB or move to another CAS Zone. +-- It can be notified to go RTB through the **RTB** event. +-- +-- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. +-- +-- ![Engage Event](..\Presentations\AI_CAS\Dia12.JPG) +-- +-- ## AI_CAS_ZONE constructor +-- +-- * @{#AI_CAS_ZONE.New}(): Creates a new AI_CAS_ZONE object. +-- +-- ## AI_CAS_ZONE is a FSM +-- +-- ![Process](..\Presentations\AI_CAS\Dia2.JPG) +-- +-- ### 2.1. AI_CAS_ZONE States +-- +-- * **None** ( Group ): The process is not started yet. +-- * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone. +-- * **Engaging** ( Group ): The AI is engaging the targets in the Engage Zone, executing CAS. +-- * **Returning** ( Group ): The AI is returning to Base.. +-- +-- ### 2.2. AI_CAS_ZONE Events +-- +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Start}**: Start the process. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Route}**: Route the AI to a new random 3D point within the Patrol Zone. +-- * **@{#AI_CAS_ZONE.Engage}**: Engage the AI to provide CAS in the Engage Zone, destroying any target it finds. +-- * **@{#AI_CAS_ZONE.Abort}**: Aborts the engagement and return patrolling in the patrol zone. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.RTB}**: Route the AI to the home base. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detect}**: The AI is detecting targets. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets. +-- * **@{#AI_CAS_ZONE.Destroy}**: The AI has destroyed a target @{Wrapper.Unit}. +-- * **@{#AI_CAS_ZONE.Destroyed}**: The AI has destroyed all target @{Wrapper.Unit}s assigned in the CAS task. +-- * **Status**: The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. +-- +-- === +-- +-- @field #AI_CAS_ZONE +AI_CAS_ZONE = { + ClassName = "AI_CAS_ZONE", +} + + + +--- Creates a new AI_CAS_ZONE object +-- @param #AI_CAS_ZONE self +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Controllable} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Controllable} in km/h. +-- @param Core.Zone#ZONE_BASE EngageZone The zone where the engage will happen. +-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO +-- @return #AI_CAS_ZONE self +function AI_CAS_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageZone, PatrolAltType ) + + -- Inherits from BASE + local self = BASE:Inherit( self, AI_PATROL_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_CAS_ZONE + + self.EngageZone = EngageZone + self.Accomplished = false + + self:SetDetectionZone( self.EngageZone ) + + self:AddTransition( { "Patrolling", "Engaging" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. + + --- OnBefore Transition Handler for Event Engage. + -- @function [parent=#AI_CAS_ZONE] OnBeforeEngage + -- @param #AI_CAS_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Engage. + -- @function [parent=#AI_CAS_ZONE] OnAfterEngage + -- @param #AI_CAS_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Engage. + -- @function [parent=#AI_CAS_ZONE] Engage + -- @param #AI_CAS_ZONE self + -- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone. + -- @param DCS#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. + -- @param DCS#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. + -- If parameter is not defined the unit / controllable will choose expend on its own discretion. + -- Use the structure @{DCS#AI.Task.WeaponExpend} to define the amount of weapons to be release at each attack. + -- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. + -- @param DCS#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. + + --- Asynchronous Event Trigger for Event Engage. + -- @function [parent=#AI_CAS_ZONE] __Engage + -- @param #AI_CAS_ZONE self + -- @param #number Delay The delay in seconds. + -- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone. + -- @param DCS#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. + -- @param DCS#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. + -- If parameter is not defined the unit / controllable will choose expend on its own discretion. + -- Use the structure @{DCS#AI.Task.WeaponExpend} to define the amount of weapons to be release at each attack. + -- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. + -- @param DCS#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. + +--- OnLeave Transition Handler for State Engaging. +-- @function [parent=#AI_CAS_ZONE] OnLeaveEngaging +-- @param #AI_CAS_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnEnter Transition Handler for State Engaging. +-- @function [parent=#AI_CAS_ZONE] OnEnterEngaging +-- @param #AI_CAS_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + + self:AddTransition( "Engaging", "Target", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. + + self:AddTransition( "Engaging", "Fired", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. + + --- OnBefore Transition Handler for Event Fired. + -- @function [parent=#AI_CAS_ZONE] OnBeforeFired + -- @param #AI_CAS_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Fired. + -- @function [parent=#AI_CAS_ZONE] OnAfterFired + -- @param #AI_CAS_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Fired. + -- @function [parent=#AI_CAS_ZONE] Fired + -- @param #AI_CAS_ZONE self + + --- Asynchronous Event Trigger for Event Fired. + -- @function [parent=#AI_CAS_ZONE] __Fired + -- @param #AI_CAS_ZONE self + -- @param #number Delay The delay in seconds. + + self:AddTransition( "*", "Destroy", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. + + --- OnBefore Transition Handler for Event Destroy. + -- @function [parent=#AI_CAS_ZONE] OnBeforeDestroy + -- @param #AI_CAS_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Destroy. + -- @function [parent=#AI_CAS_ZONE] OnAfterDestroy + -- @param #AI_CAS_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Destroy. + -- @function [parent=#AI_CAS_ZONE] Destroy + -- @param #AI_CAS_ZONE self + + --- Asynchronous Event Trigger for Event Destroy. + -- @function [parent=#AI_CAS_ZONE] __Destroy + -- @param #AI_CAS_ZONE self + -- @param #number Delay The delay in seconds. + + + self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. + + --- OnBefore Transition Handler for Event Abort. + -- @function [parent=#AI_CAS_ZONE] OnBeforeAbort + -- @param #AI_CAS_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Abort. + -- @function [parent=#AI_CAS_ZONE] OnAfterAbort + -- @param #AI_CAS_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Abort. + -- @function [parent=#AI_CAS_ZONE] Abort + -- @param #AI_CAS_ZONE self + + --- Asynchronous Event Trigger for Event Abort. + -- @function [parent=#AI_CAS_ZONE] __Abort + -- @param #AI_CAS_ZONE self + -- @param #number Delay The delay in seconds. + + self:AddTransition( "Engaging", "Accomplish", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. + + --- OnBefore Transition Handler for Event Accomplish. + -- @function [parent=#AI_CAS_ZONE] OnBeforeAccomplish + -- @param #AI_CAS_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Accomplish. + -- @function [parent=#AI_CAS_ZONE] OnAfterAccomplish + -- @param #AI_CAS_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Accomplish. + -- @function [parent=#AI_CAS_ZONE] Accomplish + -- @param #AI_CAS_ZONE self + + --- Asynchronous Event Trigger for Event Accomplish. + -- @function [parent=#AI_CAS_ZONE] __Accomplish + -- @param #AI_CAS_ZONE self + -- @param #number Delay The delay in seconds. + + return self +end + + +--- Set the Engage Zone where the AI is performing CAS. Note that if the EngageZone is changed, the AI needs to re-detect targets. +-- @param #AI_CAS_ZONE self +-- @param Core.Zone#ZONE EngageZone The zone where the AI is performing CAS. +-- @return #AI_CAS_ZONE self +function AI_CAS_ZONE:SetEngageZone( EngageZone ) + self:F2() + + if EngageZone then + self.EngageZone = EngageZone + else + self.EngageZone = nil + end +end + + + +--- onafter State Transition for Event Start. +-- @param #AI_CAS_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_CAS_ZONE:onafterStart( Controllable, From, Event, To ) + + -- Call the parent Start event handler + self:GetParent(self).onafterStart( self, Controllable, From, Event, To ) + self:HandleEvent( EVENTS.Dead ) + + self:SetDetectionDeactivated() -- When not engaging, set the detection off. +end + +--- @param AI.AI_CAS#AI_CAS_ZONE +-- @param Wrapper.Group#GROUP EngageGroup +function AI_CAS_ZONE.EngageRoute( EngageGroup, Fsm ) + + EngageGroup:F( { "AI_CAS_ZONE.EngageRoute:", EngageGroup:GetName() } ) + + if EngageGroup:IsAlive() then + Fsm:__Engage( 1, Fsm.EngageSpeed, Fsm.EngageAltitude, Fsm.EngageWeaponExpend, Fsm.EngageAttackQty, Fsm.EngageDirection ) + end +end + + +--- @param #AI_CAS_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_CAS_ZONE:onbeforeEngage( Controllable, From, Event, To ) + + if self.Accomplished == true then + return false + end +end + +--- @param #AI_CAS_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_CAS_ZONE:onafterTarget( Controllable, From, Event, To ) + + if Controllable:IsAlive() then + + local AttackTasks = {} + + for DetectedUnit, Detected in pairs( self.DetectedUnits ) do + local DetectedUnit = DetectedUnit -- Wrapper.Unit#UNIT + if DetectedUnit:IsAlive() then + if DetectedUnit:IsInZone( self.EngageZone ) then + if Detected == true then + self:F( {"Target: ", DetectedUnit } ) + self.DetectedUnits[DetectedUnit] = false + local AttackTask = Controllable:TaskAttackUnit( DetectedUnit, false, self.EngageWeaponExpend, self.EngageAttackQty, self.EngageDirection, self.EngageAltitude, nil ) + self.Controllable:PushTask( AttackTask, 1 ) + end + end + else + self.DetectedUnits[DetectedUnit] = nil + end + end + + self:__Target( -10 ) + + end +end + + +--- @param #AI_CAS_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_CAS_ZONE:onafterAbort( Controllable, From, Event, To ) + Controllable:ClearTasks() + self:__Route( 1 ) +end + +--- @param #AI_CAS_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone. +-- @param DCS#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. +-- @param DCS#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. +-- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. +-- @param DCS#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. +function AI_CAS_ZONE:onafterEngage( Controllable, From, Event, To, + EngageSpeed, + EngageAltitude, + EngageWeaponExpend, + EngageAttackQty, + EngageDirection ) + self:F("onafterEngage") + + self.EngageSpeed = EngageSpeed or 400 + self.EngageAltitude = EngageAltitude or 2000 + self.EngageWeaponExpend = EngageWeaponExpend + self.EngageAttackQty = EngageAttackQty + self.EngageDirection = EngageDirection + + if Controllable:IsAlive() then + + Controllable:OptionROEOpenFire() + Controllable:OptionROTVertical() + + local EngageRoute = {} + + --- Calculate the current route point. + local CurrentVec2 = self.Controllable:GetVec2() + + --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). + local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() + local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) + local ToEngageZoneSpeed = self.PatrolMaxSpeed + local CurrentRoutePoint = CurrentPointVec3:WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + self.EngageSpeed, + true + ) + + EngageRoute[#EngageRoute+1] = CurrentRoutePoint + + local AttackTasks = {} + + for DetectedUnit, Detected in pairs( self.DetectedUnits ) do + local DetectedUnit = DetectedUnit -- Wrapper.Unit#UNIT + self:T( DetectedUnit ) + if DetectedUnit:IsAlive() then + if DetectedUnit:IsInZone( self.EngageZone ) then + self:F( {"Engaging ", DetectedUnit } ) + AttackTasks[#AttackTasks+1] = Controllable:TaskAttackUnit( DetectedUnit, + true, + EngageWeaponExpend, + EngageAttackQty, + EngageDirection + ) + end + else + self.DetectedUnits[DetectedUnit] = nil + end + end + + AttackTasks[#AttackTasks+1] = Controllable:TaskFunction( "AI_CAS_ZONE.EngageRoute", self ) + EngageRoute[#EngageRoute].task = Controllable:TaskCombo( AttackTasks ) + + --- Define a random point in the @{Zone}. The AI will fly to that point within the zone. + + --- Find a random 2D point in EngageZone. + local ToTargetVec2 = self.EngageZone:GetRandomVec2() + self:T2( ToTargetVec2 ) + + --- Obtain a 3D @{Point} from the 2D point + altitude. + local ToTargetPointVec3 = POINT_VEC3:New( ToTargetVec2.x, self.EngageAltitude, ToTargetVec2.y ) + + --- Create a route point of type air. + local ToTargetRoutePoint = ToTargetPointVec3:WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + self.EngageSpeed, + true + ) + + EngageRoute[#EngageRoute+1] = ToTargetRoutePoint + + Controllable:Route( EngageRoute, 0.5 ) + + self:SetRefreshTimeInterval( 2 ) + self:SetDetectionActivated() + self:__Target( -2 ) -- Start Targetting + end +end + + +--- @param #AI_CAS_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_CAS_ZONE:onafterAccomplish( Controllable, From, Event, To ) + self.Accomplished = true + self:SetDetectionDeactivated() +end + + +--- @param #AI_CAS_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @param Core.Event#EVENTDATA EventData +function AI_CAS_ZONE:onafterDestroy( Controllable, From, Event, To, EventData ) + + if EventData.IniUnit then + self.DetectedUnits[EventData.IniUnit] = nil + end +end + + +--- @param #AI_CAS_ZONE self +-- @param Core.Event#EVENTDATA EventData +function AI_CAS_ZONE:OnEventDead( EventData ) + self:F( { "EventDead", EventData } ) + + if EventData.IniDCSUnit then + if self.DetectedUnits and self.DetectedUnits[EventData.IniUnit] then + self:__Destroy( 1, EventData ) + end + end +end + + +--- **AI** -- Peform Battlefield Area Interdiction (BAI) within an engagement zone. +-- +-- **Features:** +-- +-- * Hold and standby within a patrol zone. +-- * Engage upon command the assigned targets within an engagement zone. +-- * Loop the zone until all targets are eliminated. +-- * Trigger different events upon the results achieved. +-- * After combat, return to the patrol zone and hold. +-- * RTB when commanded or after out of fuel. +-- +-- === +-- +-- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/BAI%20-%20Battlefield%20Air%20Interdiction) +-- +-- === +-- +-- ### [YouTube Playlist]() +-- +-- === +-- +-- ### Author: **FlightControl** +-- ### Contributions: +-- +-- * **[Gunterlund](http://forums.eagle.ru:8080/member.php?u=75036)**: Test case revision. +-- +-- === +-- +-- @module AI.AI_Bai +-- @image AI_Battlefield_Air_Interdiction.JPG + + +--- AI_BAI_ZONE class +-- @type AI_BAI_ZONE +-- @field Wrapper.Controllable#CONTROLLABLE AIControllable The @{Wrapper.Controllable} patrolling. +-- @field Core.Zone#ZONE_BASE TargetZone The @{Zone} where the patrol needs to be executed. +-- @extends AI.AI_Patrol#AI_PATROL_ZONE + +--- Implements the core functions to provide BattleGround Air Interdiction in an Engage @{Zone} by an AIR @{Wrapper.Controllable} or @{Wrapper.Group}. +-- +-- The AI_BAI_ZONE runs a process. It holds an AI in a Patrol Zone and when the AI is commanded to engage, it will fly to an Engage Zone. +-- +-- ![HoldAndEngage](..\Presentations\AI_BAI\Dia3.JPG) +-- +-- The AI_BAI_ZONE is assigned a @{Wrapper.Group} and this must be done before the AI_BAI_ZONE process can be started through the **Start** event. +-- +-- ![Start Event](..\Presentations\AI_BAI\Dia4.JPG) +-- +-- Upon started, The AI will **Route** itself towards the random 3D point within a patrol zone, +-- using a random speed within the given altitude and speed limits. +-- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. +-- This cycle will continue until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. +-- +-- ![Route Event](..\Presentations\AI_BAI\Dia5.JPG) +-- +-- When the AI is commanded to provide BattleGround Air Interdiction (through the event **Engage**), the AI will fly towards the Engage Zone. +-- Any target that is detected in the Engage Zone will be reported and will be destroyed by the AI. +-- +-- ![Engage Event](..\Presentations\AI_BAI\Dia6.JPG) +-- +-- The AI will detect the targets and will only destroy the targets within the Engage Zone. +-- +-- ![Engage Event](..\Presentations\AI_BAI\Dia7.JPG) +-- +-- Every target that is destroyed, is reported< by the AI. +-- +-- ![Engage Event](..\Presentations\AI_BAI\Dia8.JPG) +-- +-- Note that the AI does not know when the Engage Zone is cleared, and therefore will keep circling in the zone. +-- +-- ![Engage Event](..\Presentations\AI_BAI\Dia9.JPG) +-- +-- Until it is notified through the event **Accomplish**, which is to be triggered by an observing party: +-- +-- * a FAC +-- * a timed event +-- * a menu option selected by a human +-- * a condition +-- * others ... +-- +-- ![Engage Event](..\Presentations\AI_BAI\Dia10.JPG) +-- +-- When the AI has accomplished the Bombing, it will fly back to the Patrol Zone. +-- +-- ![Engage Event](..\Presentations\AI_BAI\Dia11.JPG) +-- +-- It will keep patrolling there, until it is notified to RTB or move to another BOMB Zone. +-- It can be notified to go RTB through the **RTB** event. +-- +-- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. +-- +-- ![Engage Event](..\Presentations\AI_BAI\Dia12.JPG) +-- +-- # 1. AI_BAI_ZONE constructor +-- +-- * @{#AI_BAI_ZONE.New}(): Creates a new AI_BAI_ZONE object. +-- +-- ## 2. AI_BAI_ZONE is a FSM +-- +-- ![Process](..\Presentations\AI_BAI\Dia2.JPG) +-- +-- ### 2.1. AI_BAI_ZONE States +-- +-- * **None** ( Group ): The process is not started yet. +-- * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone. +-- * **Engaging** ( Group ): The AI is engaging the targets in the Engage Zone, executing BOMB. +-- * **Returning** ( Group ): The AI is returning to Base.. +-- +-- ### 2.2. AI_BAI_ZONE Events +-- +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Start}**: Start the process. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Route}**: Route the AI to a new random 3D point within the Patrol Zone. +-- * **@{#AI_BAI_ZONE.Engage}**: Engage the AI to provide BOMB in the Engage Zone, destroying any target it finds. +-- * **@{#AI_BAI_ZONE.Abort}**: Aborts the engagement and return patrolling in the patrol zone. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.RTB}**: Route the AI to the home base. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detect}**: The AI is detecting targets. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets. +-- * **@{#AI_BAI_ZONE.Destroy}**: The AI has destroyed a target @{Wrapper.Unit}. +-- * **@{#AI_BAI_ZONE.Destroyed}**: The AI has destroyed all target @{Wrapper.Unit}s assigned in the BOMB task. +-- * **Status**: The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. +-- +-- ## 3. Modify the Engage Zone behaviour to pinpoint a **map object** or **scenery object** +-- +-- Use the method @{#AI_BAI_ZONE.SearchOff}() to specify that the EngageZone is not to be searched for potential targets (UNITs), but that the center of the zone +-- is the point where a map object is to be destroyed (like a bridge). +-- +-- Example: +-- +-- -- Tell the BAI not to search for potential targets in the BAIEngagementZone, but rather use the center of the BAIEngagementZone as the bombing location. +-- AIBAIZone:SearchOff() +-- +-- Searching can be switched back on with the method @{#AI_BAI_ZONE.SearchOn}(). Use the method @{#AI_BAI_ZONE.SearchOnOff}() to flexibily switch searching on or off. +-- +-- === +-- +-- @field #AI_BAI_ZONE +AI_BAI_ZONE = { + ClassName = "AI_BAI_ZONE", +} + + + +--- Creates a new AI_BAI_ZONE object +-- @param #AI_BAI_ZONE self +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Controllable} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Controllable} in km/h. +-- @param Core.Zone#ZONE_BASE EngageZone The zone where the engage will happen. +-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO +-- @return #AI_BAI_ZONE self +function AI_BAI_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageZone, PatrolAltType ) + + -- Inherits from BASE + local self = BASE:Inherit( self, AI_PATROL_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_BAI_ZONE + + self.EngageZone = EngageZone + self.Accomplished = false + + self:SetDetectionZone( self.EngageZone ) + self:SearchOn() + + self:AddTransition( { "Patrolling", "Engaging" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_BAI_ZONE. + + --- OnBefore Transition Handler for Event Engage. + -- @function [parent=#AI_BAI_ZONE] OnBeforeEngage + -- @param #AI_BAI_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Engage. + -- @function [parent=#AI_BAI_ZONE] OnAfterEngage + -- @param #AI_BAI_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Engage. + -- @function [parent=#AI_BAI_ZONE] Engage + -- @param #AI_BAI_ZONE self + -- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone. + -- @param DCS#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. + -- @param DCS#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. + -- If parameter is not defined the unit / controllable will choose expend on its own discretion. + -- Use the structure @{DCS#AI.Task.WeaponExpend} to define the amount of weapons to be release at each attack. + -- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. + -- @param DCS#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. + + --- Asynchronous Event Trigger for Event Engage. + -- @function [parent=#AI_BAI_ZONE] __Engage + -- @param #AI_BAI_ZONE self + -- @param #number Delay The delay in seconds. + -- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone. + -- @param DCS#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. + -- @param DCS#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. + -- If parameter is not defined the unit / controllable will choose expend on its own discretion. + -- Use the structure @{DCS#AI.Task.WeaponExpend} to define the amount of weapons to be release at each attack. + -- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. + -- @param DCS#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. + +--- OnLeave Transition Handler for State Engaging. +-- @function [parent=#AI_BAI_ZONE] OnLeaveEngaging +-- @param #AI_BAI_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnEnter Transition Handler for State Engaging. +-- @function [parent=#AI_BAI_ZONE] OnEnterEngaging +-- @param #AI_BAI_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + + self:AddTransition( "Engaging", "Target", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_BAI_ZONE. + + self:AddTransition( "Engaging", "Fired", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_BAI_ZONE. + + --- OnBefore Transition Handler for Event Fired. + -- @function [parent=#AI_BAI_ZONE] OnBeforeFired + -- @param #AI_BAI_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Fired. + -- @function [parent=#AI_BAI_ZONE] OnAfterFired + -- @param #AI_BAI_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Fired. + -- @function [parent=#AI_BAI_ZONE] Fired + -- @param #AI_BAI_ZONE self + + --- Asynchronous Event Trigger for Event Fired. + -- @function [parent=#AI_BAI_ZONE] __Fired + -- @param #AI_BAI_ZONE self + -- @param #number Delay The delay in seconds. + + self:AddTransition( "*", "Destroy", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_BAI_ZONE. + + --- OnBefore Transition Handler for Event Destroy. + -- @function [parent=#AI_BAI_ZONE] OnBeforeDestroy + -- @param #AI_BAI_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Destroy. + -- @function [parent=#AI_BAI_ZONE] OnAfterDestroy + -- @param #AI_BAI_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Destroy. + -- @function [parent=#AI_BAI_ZONE] Destroy + -- @param #AI_BAI_ZONE self + + --- Asynchronous Event Trigger for Event Destroy. + -- @function [parent=#AI_BAI_ZONE] __Destroy + -- @param #AI_BAI_ZONE self + -- @param #number Delay The delay in seconds. + + + self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_BAI_ZONE. + + --- OnBefore Transition Handler for Event Abort. + -- @function [parent=#AI_BAI_ZONE] OnBeforeAbort + -- @param #AI_BAI_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Abort. + -- @function [parent=#AI_BAI_ZONE] OnAfterAbort + -- @param #AI_BAI_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Abort. + -- @function [parent=#AI_BAI_ZONE] Abort + -- @param #AI_BAI_ZONE self + + --- Asynchronous Event Trigger for Event Abort. + -- @function [parent=#AI_BAI_ZONE] __Abort + -- @param #AI_BAI_ZONE self + -- @param #number Delay The delay in seconds. + + self:AddTransition( "Engaging", "Accomplish", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_BAI_ZONE. + + --- OnBefore Transition Handler for Event Accomplish. + -- @function [parent=#AI_BAI_ZONE] OnBeforeAccomplish + -- @param #AI_BAI_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Accomplish. + -- @function [parent=#AI_BAI_ZONE] OnAfterAccomplish + -- @param #AI_BAI_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Accomplish. + -- @function [parent=#AI_BAI_ZONE] Accomplish + -- @param #AI_BAI_ZONE self + + --- Asynchronous Event Trigger for Event Accomplish. + -- @function [parent=#AI_BAI_ZONE] __Accomplish + -- @param #AI_BAI_ZONE self + -- @param #number Delay The delay in seconds. + + return self +end + + +--- Set the Engage Zone where the AI is performing BOMB. Note that if the EngageZone is changed, the AI needs to re-detect targets. +-- @param #AI_BAI_ZONE self +-- @param Core.Zone#ZONE EngageZone The zone where the AI is performing BOMB. +-- @return #AI_BAI_ZONE self +function AI_BAI_ZONE:SetEngageZone( EngageZone ) + self:F2() + + if EngageZone then + self.EngageZone = EngageZone + else + self.EngageZone = nil + end +end + + +--- Specifies whether to search for potential targets in the zone, or let the center of the zone be the bombing coordinate. +-- AI_BAI_ZONE will search for potential targets by default. +-- @param #AI_BAI_ZONE self +-- @return #AI_BAI_ZONE +function AI_BAI_ZONE:SearchOnOff( Search ) + + self.Search = Search + + return self +end + +--- If Search is Off, the current zone coordinate will be the center of the bombing. +-- @param #AI_BAI_ZONE self +-- @return #AI_BAI_ZONE +function AI_BAI_ZONE:SearchOff() + + self:SearchOnOff( false ) + + return self +end + + +--- If Search is On, BAI will search for potential targets in the zone. +-- @param #AI_BAI_ZONE self +-- @return #AI_BAI_ZONE +function AI_BAI_ZONE:SearchOn() + + self:SearchOnOff( true ) + + return self +end + + +--- onafter State Transition for Event Start. +-- @param #AI_BAI_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_BAI_ZONE:onafterStart( Controllable, From, Event, To ) + + -- Call the parent Start event handler + self:GetParent(self).onafterStart( self, Controllable, From, Event, To ) + self:HandleEvent( EVENTS.Dead ) + + self:SetDetectionDeactivated() -- When not engaging, set the detection off. +end + +--- @param Wrapper.Controllable#CONTROLLABLE AIControllable +function _NewEngageRoute( AIControllable ) + + AIControllable:T( "NewEngageRoute" ) + local EngageZone = AIControllable:GetState( AIControllable, "EngageZone" ) -- AI.AI_BAI#AI_BAI_ZONE + EngageZone:__Engage( 1, EngageZone.EngageSpeed, EngageZone.EngageAltitude, EngageZone.EngageWeaponExpend, EngageZone.EngageAttackQty, EngageZone.EngageDirection ) +end + + +--- @param #AI_BAI_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_BAI_ZONE:onbeforeEngage( Controllable, From, Event, To ) + + if self.Accomplished == true then + return false + end +end + +--- @param #AI_BAI_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_BAI_ZONE:onafterTarget( Controllable, From, Event, To ) + self:F({"onafterTarget",self.Search,Controllable:IsAlive()}) + + + + if Controllable:IsAlive() then + + local AttackTasks = {} + + if self.Search == true then + for DetectedUnit, Detected in pairs( self.DetectedUnits ) do + local DetectedUnit = DetectedUnit -- Wrapper.Unit#UNIT + if DetectedUnit:IsAlive() then + if DetectedUnit:IsInZone( self.EngageZone ) then + if Detected == true then + self:F( {"Target: ", DetectedUnit } ) + self.DetectedUnits[DetectedUnit] = false + local AttackTask = Controllable:TaskAttackUnit( DetectedUnit, false, self.EngageWeaponExpend, self.EngageAttackQty, self.EngageDirection, self.EngageAltitude, nil ) + self.Controllable:PushTask( AttackTask, 1 ) + end + end + else + self.DetectedUnits[DetectedUnit] = nil + end + end + else + self:F("Attack zone") + local AttackTask = Controllable:TaskAttackMapObject( + self.EngageZone:GetPointVec2():GetVec2(), + true, + self.EngageWeaponExpend, + self.EngageAttackQty, + self.EngageDirection, + self.EngageAltitude + ) + self.Controllable:PushTask( AttackTask, 1 ) + end + + self:__Target( -10 ) + + end +end + + +--- @param #AI_BAI_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_BAI_ZONE:onafterAbort( Controllable, From, Event, To ) + Controllable:ClearTasks() + self:__Route( 1 ) +end + +--- @param #AI_BAI_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone. +-- @param DCS#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. +-- @param DCS#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. +-- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. +-- @param DCS#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. +function AI_BAI_ZONE:onafterEngage( Controllable, From, Event, To, + EngageSpeed, + EngageAltitude, + EngageWeaponExpend, + EngageAttackQty, + EngageDirection ) + + self:F("onafterEngage") + + self.EngageSpeed = EngageSpeed or 400 + self.EngageAltitude = EngageAltitude or 2000 + self.EngageWeaponExpend = EngageWeaponExpend + self.EngageAttackQty = EngageAttackQty + self.EngageDirection = EngageDirection + + if Controllable:IsAlive() then + + local EngageRoute = {} + + --- Calculate the current route point. + local CurrentVec2 = self.Controllable:GetVec2() + + --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). + local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() + local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) + local ToEngageZoneSpeed = self.PatrolMaxSpeed + local CurrentRoutePoint = CurrentPointVec3:WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + self.EngageSpeed, + true + ) + + EngageRoute[#EngageRoute+1] = CurrentRoutePoint + + local AttackTasks = {} + + if self.Search == true then + + for DetectedUnitID, DetectedUnitData in pairs( self.DetectedUnits ) do + local DetectedUnit = DetectedUnitData -- Wrapper.Unit#UNIT + self:T( DetectedUnit ) + if DetectedUnit:IsAlive() then + if DetectedUnit:IsInZone( self.EngageZone ) then + self:F( {"Engaging ", DetectedUnit } ) + AttackTasks[#AttackTasks+1] = Controllable:TaskBombing( + DetectedUnit:GetPointVec2():GetVec2(), + true, + EngageWeaponExpend, + EngageAttackQty, + EngageDirection, + EngageAltitude + ) + end + else + self.DetectedUnits[DetectedUnit] = nil + end + end + else + self:F("Attack zone") + AttackTasks[#AttackTasks+1] = Controllable:TaskAttackMapObject( + self.EngageZone:GetPointVec2():GetVec2(), + true, + EngageWeaponExpend, + EngageAttackQty, + EngageDirection, + EngageAltitude + ) + end + + EngageRoute[#EngageRoute].task = Controllable:TaskCombo( AttackTasks ) + + --- Define a random point in the @{Zone}. The AI will fly to that point within the zone. + + --- Find a random 2D point in EngageZone. + local ToTargetVec2 = self.EngageZone:GetRandomVec2() + self:T2( ToTargetVec2 ) + + --- Obtain a 3D @{Point} from the 2D point + altitude. + local ToTargetPointVec3 = POINT_VEC3:New( ToTargetVec2.x, self.EngageAltitude, ToTargetVec2.y ) + + --- Create a route point of type air. + local ToTargetRoutePoint = ToTargetPointVec3:WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + self.EngageSpeed, + true + ) + + EngageRoute[#EngageRoute+1] = ToTargetRoutePoint + + Controllable:OptionROEOpenFire() + Controllable:OptionROTVertical() + + --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... + Controllable:WayPointInitialize( EngageRoute ) + + --- Do a trick, link the NewEngageRoute function of the object to the AIControllable in a temporary variable ... + Controllable:SetState( Controllable, "EngageZone", self ) + + Controllable:WayPointFunction( #EngageRoute, 1, "_NewEngageRoute" ) + + --- NOW ROUTE THE GROUP! + Controllable:WayPointExecute( 1 ) + + self:SetRefreshTimeInterval( 2 ) + self:SetDetectionActivated() + self:__Target( -2 ) -- Start Targetting + end +end + + +--- @param #AI_BAI_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_BAI_ZONE:onafterAccomplish( Controllable, From, Event, To ) + self.Accomplished = true + self:SetDetectionDeactivated() +end + + +--- @param #AI_BAI_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @param Core.Event#EVENTDATA EventData +function AI_BAI_ZONE:onafterDestroy( Controllable, From, Event, To, EventData ) + + if EventData.IniUnit then + self.DetectedUnits[EventData.IniUnit] = nil + end +end + + +--- @param #AI_BAI_ZONE self +-- @param Core.Event#EVENTDATA EventData +function AI_BAI_ZONE:OnEventDead( EventData ) + self:F( { "EventDead", EventData } ) + + if EventData.IniDCSUnit then + if self.DetectedUnits and self.DetectedUnits[EventData.IniUnit] then + self:__Destroy( 1, EventData ) + end + end +end + + +--- **AI** -- Build large airborne formations of aircraft. +-- +-- **Features:** +-- +-- * Build in-air formations consisting of more than 40 aircraft as one group. +-- * Build different formation types. +-- * Assign a group leader that will guide the large formation path. +-- +-- === +-- +-- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/FOR%20-%20Formation) +-- +-- === +-- +-- ### [YouTube Playlist](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl0bFIJ9jIdYM22uaWmIN4oz) +-- +-- === +-- +-- ### Author: **FlightControl** +-- ### Contributions: +-- +-- === +-- +-- @module AI.AI_Formation +-- @image AI_Large_Formations.JPG + +--- AI_FORMATION class +-- @type AI_FORMATION +-- @extends Core.Fsm#FSM_SET +-- @field Wrapper.Unit#UNIT FollowUnit +-- @field Core.Set#SET_GROUP FollowGroupSet +-- @field #string FollowName +-- @field #AI_FORMATION.MODE FollowMode The mode the escort is in. +-- @field Scheduler#SCHEDULER FollowScheduler The instance of the SCHEDULER class. +-- @field #number FollowDistance The current follow distance. +-- @field #boolean ReportTargets If true, nearby targets are reported. +-- @Field DCSTypes#AI.Option.Air.val.ROE OptionROE Which ROE is set to the FollowGroup. +-- @field DCSTypes#AI.Option.Air.val.REACTION_ON_THREAT OptionReactionOnThreat Which REACTION_ON_THREAT is set to the FollowGroup. +-- @field #number dtFollow Time step between position updates. + + +--- Build large formations, make AI follow a @{Wrapper.Client#CLIENT} (player) leader or a @{Wrapper.Unit#UNIT} (AI) leader. +-- +-- AI_FORMATION makes AI @{GROUP}s fly in formation of various compositions. +-- The AI_FORMATION class models formations in a different manner than the internal DCS formation logic!!! +-- The purpose of the class is to: +-- +-- * Make formation building a process that can be managed while in flight, rather than a task. +-- * Human players can guide formations, consisting of larget planes. +-- * Build large formations (like a large bomber field). +-- * Form formations that DCS does not support off the shelve. +-- +-- A few remarks: +-- +-- * Depending on the type of plane, the change in direction by the leader may result in the formation getting disentangled while in flight and needs to be rebuild. +-- * Formations are vulnerable to collissions, but is depending on the type of plane, the distance between the planes and the speed and angle executed by the leader. +-- * Formations may take a while to build up. +-- +-- As a result, the AI_FORMATION is not perfect, but is very useful to: +-- +-- * Model large formations when flying straight line. You can build close formations when doing this. +-- * Make humans guide a large formation, when the planes are wide from each other. +-- +-- ## AI_FORMATION construction +-- +-- Create a new SPAWN object with the @{#AI_FORMATION.New} method: +-- +-- * @{#AI_FORMATION.New}(): Creates a new AI_FORMATION object from a @{Wrapper.Group#GROUP} for a @{Wrapper.Client#CLIENT} or a @{Wrapper.Unit#UNIT}, with an optional briefing text. +-- +-- ## Formation methods +-- +-- The following methods can be used to set or change the formation: +-- +-- * @{#AI_FORMATION.FormationLine}(): Form a line formation (core formation function). +-- * @{#AI_FORMATION.FormationTrail}(): Form a trail formation. +-- * @{#AI_FORMATION.FormationLeftLine}(): Form a left line formation. +-- * @{#AI_FORMATION.FormationRightLine}(): Form a right line formation. +-- * @{#AI_FORMATION.FormationRightWing}(): Form a right wing formation. +-- * @{#AI_FORMATION.FormationLeftWing}(): Form a left wing formation. +-- * @{#AI_FORMATION.FormationCenterWing}(): Form a center wing formation. +-- * @{#AI_FORMATION.FormationCenterVic}(): Form a Vic formation (same as CenterWing. +-- * @{#AI_FORMATION.FormationCenterBoxed}(): Form a center boxed formation. +-- +-- ## Randomization +-- +-- Use the method @{AI.AI_Formation#AI_FORMATION.SetFlightRandomization}() to simulate the formation flying errors that pilots make while in formation. Is a range set in meters. +-- +-- @usage +-- local FollowGroupSet = SET_GROUP:New():FilterCategories("plane"):FilterCoalitions("blue"):FilterPrefixes("Follow"):FilterStart() +-- FollowGroupSet:Flush() +-- local LeaderUnit = UNIT:FindByName( "Leader" ) +-- local LargeFormation = AI_FORMATION:New( LeaderUnit, FollowGroupSet, "Center Wing Formation", "Briefing" ) +-- LargeFormation:FormationCenterWing( 500, 50, 0, 250, 250 ) +-- LargeFormation:__Start( 1 ) +-- +-- @field #AI_FORMATION +AI_FORMATION = { + ClassName = "AI_FORMATION", + FollowName = nil, -- The Follow Name + FollowUnit = nil, + FollowGroupSet = nil, + FollowMode = 1, + MODE = { + FOLLOW = 1, + MISSION = 2, + }, + FollowScheduler = nil, + OptionROE = AI.Option.Air.val.ROE.OPEN_FIRE, + OptionReactionOnThreat = AI.Option.Air.val.REACTION_ON_THREAT.ALLOW_ABORT_MISSION, + dtFollow = 0.5, +} + +AI_FORMATION.__Enum = {} + +--- @type AI_FORMATION.__Enum.Formation +-- @field #number None +-- @field #number Line +-- @field #number Trail +-- @field #number Stack +-- @field #number LeftLine +-- @field #number RightLine +-- @field #number LeftWing +-- @field #number RightWing +-- @field #number Vic +-- @field #number Box +AI_FORMATION.__Enum.Formation = { + None = 0, + Mission = 1, + Line = 2, + Trail = 3, + Stack = 4, + LeftLine = 5, + RightLine = 6, + LeftWing = 7, + RightWing = 8, + Vic = 9, + Box = 10, +} + +--- @type AI_FORMATION.__Enum.Mode +-- @field #number Mission +-- @field #number Formation +AI_FORMATION.__Enum.Mode = { + Mission = "M", + Formation = "F", + Attack = "A", + Reconnaissance = "R", +} + +--- @type AI_FORMATION.__Enum.ReportType +-- @field #number All +-- @field #number Airborne +-- @field #number GroundRadar +-- @field #number Ground +AI_FORMATION.__Enum.ReportType = { + Airborne = "*", + Airborne = "A", + GroundRadar = "R", + Ground = "G", +} + + + +--- MENUPARAM type +-- @type MENUPARAM +-- @field #AI_FORMATION ParamSelf +-- @field #number ParamDistance +-- @field #function ParamFunction +-- @field #string ParamMessage + +--- AI_FORMATION class constructor for an AI group +-- @param #AI_FORMATION self +-- @param Wrapper.Unit#UNIT FollowUnit The UNIT leading the FolllowGroupSet. +-- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. +-- @param #string FollowName Name of the escort. +-- @param #string FollowBriefing Briefing. +-- @return #AI_FORMATION self +function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefing ) --R2.1 + local self = BASE:Inherit( self, FSM_SET:New( FollowGroupSet ) ) + self:F( { FollowUnit, FollowGroupSet, FollowName } ) + + self.FollowUnit = FollowUnit -- Wrapper.Unit#UNIT + self.FollowGroupSet = FollowGroupSet -- Core.Set#SET_GROUP + + self.FollowGroupSet:ForEachGroup( + function( FollowGroup ) + --self:E("Following") + FollowGroup:SetState( self, "Mode", self.__Enum.Mode.Formation ) + end + ) + + self:SetFlightModeFormation() + + self:SetFlightRandomization( 2 ) + + self:SetStartState( "None" ) + + self:AddTransition( "*", "Stop", "Stopped" ) + + self:AddTransition( {"None", "Stopped"}, "Start", "Following" ) + + self:AddTransition( "*", "FormationLine", "*" ) + --- FormationLine Handler OnBefore for AI_FORMATION + -- @function [parent=#AI_FORMATION] OnBeforeFormationLine + -- @param #AI_FORMATION self + -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param #number XStart The start position on the X-axis in meters for the first group. + -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. + -- @param #number YStart The start position on the Y-axis in meters for the first group. + -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. + -- @param #number ZStart The start position on the Z-axis in meters for the first group. + -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. + -- @return #boolean + + --- FormationLine Handler OnAfter for AI_FORMATION + -- @function [parent=#AI_FORMATION] OnAfterFormationLine + -- @param #AI_FORMATION self + -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param #number XStart The start position on the X-axis in meters for the first group. + -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. + -- @param #number YStart The start position on the Y-axis in meters for the first group. + -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. + -- @param #number ZStart The start position on the Z-axis in meters for the first group. + -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. + + --- FormationLine Trigger for AI_FORMATION + -- @function [parent=#AI_FORMATION] FormationLine + -- @param #AI_FORMATION self + -- @param #number XStart The start position on the X-axis in meters for the first group. + -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. + -- @param #number YStart The start position on the Y-axis in meters for the first group. + -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. + -- @param #number ZStart The start position on the Z-axis in meters for the first group. + -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. + + --- FormationLine Asynchronous Trigger for AI_FORMATION + -- @function [parent=#AI_FORMATION] __FormationLine + -- @param #AI_FORMATION self + -- @param #number Delay + -- @param #number XStart The start position on the X-axis in meters for the first group. + -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. + -- @param #number YStart The start position on the Y-axis in meters for the first group. + -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. + -- @param #number ZStart The start position on the Z-axis in meters for the first group. + -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. + + self:AddTransition( "*", "FormationTrail", "*" ) + --- FormationTrail Handler OnBefore for AI_FORMATION + -- @function [parent=#AI_FORMATION] OnBeforeFormationTrail + -- @param #AI_FORMATION self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param #number XStart The start position on the X-axis in meters for the first group. + -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. + -- @param #number YStart The start position on the Y-axis in meters for the first group. + -- @return #boolean + + --- FormationTrail Handler OnAfter for AI_FORMATION + -- @function [parent=#AI_FORMATION] OnAfterFormationTrail + -- @param #AI_FORMATION self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param #number XStart The start position on the X-axis in meters for the first group. + -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. + -- @param #number YStart The start position on the Y-axis in meters for the first group. + + --- FormationTrail Trigger for AI_FORMATION + -- @function [parent=#AI_FORMATION] FormationTrail + -- @param #AI_FORMATION self + -- @param #number XStart The start position on the X-axis in meters for the first group. + -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. + -- @param #number YStart The start position on the Y-axis in meters for the first group. + + --- FormationTrail Asynchronous Trigger for AI_FORMATION + -- @function [parent=#AI_FORMATION] __FormationTrail + -- @param #AI_FORMATION self + -- @param #number Delay + -- @param #number XStart The start position on the X-axis in meters for the first group. + -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. + -- @param #number YStart The start position on the Y-axis in meters for the first group. + + self:AddTransition( "*", "FormationStack", "*" ) + --- FormationStack Handler OnBefore for AI_FORMATION + -- @function [parent=#AI_FORMATION] OnBeforeFormationStack + -- @param #AI_FORMATION self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param #number XStart The start position on the X-axis in meters for the first group. + -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. + -- @param #number YStart The start position on the Y-axis in meters for the first group. + -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. + -- @return #boolean + + --- FormationStack Handler OnAfter for AI_FORMATION + -- @function [parent=#AI_FORMATION] OnAfterFormationStack + -- @param #AI_FORMATION self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param #number XStart The start position on the X-axis in meters for the first group. + -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. + -- @param #number YStart The start position on the Y-axis in meters for the first group. + -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. + + --- FormationStack Trigger for AI_FORMATION + -- @function [parent=#AI_FORMATION] FormationStack + -- @param #AI_FORMATION self + -- @param #number XStart The start position on the X-axis in meters for the first group. + -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. + -- @param #number YStart The start position on the Y-axis in meters for the first group. + -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. + + --- FormationStack Asynchronous Trigger for AI_FORMATION + -- @function [parent=#AI_FORMATION] __FormationStack + -- @param #AI_FORMATION self + -- @param #number Delay + -- @param #number XStart The start position on the X-axis in meters for the first group. + -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. + -- @param #number YStart The start position on the Y-axis in meters for the first group. + -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. + + self:AddTransition( "*", "FormationLeftLine", "*" ) + --- FormationLeftLine Handler OnBefore for AI_FORMATION + -- @function [parent=#AI_FORMATION] OnBeforeFormationLeftLine + -- @param #AI_FORMATION self + -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param #number XStart The start position on the X-axis in meters for the first group. + -- @param #number YStart The start position on the Y-axis in meters for the first group. + -- @param #number ZStart The start position on the Z-axis in meters for the first group. + -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. + -- @return #boolean + + --- FormationLeftLine Handler OnAfter for AI_FORMATION + -- @function [parent=#AI_FORMATION] OnAfterFormationLeftLine + -- @param #AI_FORMATION self + -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param #number XStart The start position on the X-axis in meters for the first group. + -- @param #number YStart The start position on the Y-axis in meters for the first group. + -- @param #number ZStart The start position on the Z-axis in meters for the first group. + -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. + + --- FormationLeftLine Trigger for AI_FORMATION + -- @function [parent=#AI_FORMATION] FormationLeftLine + -- @param #AI_FORMATION self + -- @param #number XStart The start position on the X-axis in meters for the first group. + -- @param #number YStart The start position on the Y-axis in meters for the first group. + -- @param #number ZStart The start position on the Z-axis in meters for the first group. + -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. + + --- FormationLeftLine Asynchronous Trigger for AI_FORMATION + -- @function [parent=#AI_FORMATION] __FormationLeftLine + -- @param #AI_FORMATION self + -- @param #number Delay + -- @param #number XStart The start position on the X-axis in meters for the first group. + -- @param #number YStart The start position on the Y-axis in meters for the first group. + -- @param #number ZStart The start position on the Z-axis in meters for the first group. + -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. + + self:AddTransition( "*", "FormationRightLine", "*" ) + --- FormationRightLine Handler OnBefore for AI_FORMATION + -- @function [parent=#AI_FORMATION] OnBeforeFormationRightLine + -- @param #AI_FORMATION self + -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param #number XStart The start position on the X-axis in meters for the first group. + -- @param #number YStart The start position on the Y-axis in meters for the first group. + -- @param #number ZStart The start position on the Z-axis in meters for the first group. + -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. + -- @return #boolean + + --- FormationRightLine Handler OnAfter for AI_FORMATION + -- @function [parent=#AI_FORMATION] OnAfterFormationRightLine + -- @param #AI_FORMATION self + -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param #number XStart The start position on the X-axis in meters for the first group. + -- @param #number YStart The start position on the Y-axis in meters for the first group. + -- @param #number ZStart The start position on the Z-axis in meters for the first group. + -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. + + --- FormationRightLine Trigger for AI_FORMATION + -- @function [parent=#AI_FORMATION] FormationRightLine + -- @param #AI_FORMATION self + -- @param #number XStart The start position on the X-axis in meters for the first group. + -- @param #number YStart The start position on the Y-axis in meters for the first group. + -- @param #number ZStart The start position on the Z-axis in meters for the first group. + -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. + + --- FormationRightLine Asynchronous Trigger for AI_FORMATION + -- @function [parent=#AI_FORMATION] __FormationRightLine + -- @param #AI_FORMATION self + -- @param #number Delay + -- @param #number XStart The start position on the X-axis in meters for the first group. + -- @param #number YStart The start position on the Y-axis in meters for the first group. + -- @param #number ZStart The start position on the Z-axis in meters for the first group. + -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. + + self:AddTransition( "*", "FormationLeftWing", "*" ) + --- FormationLeftWing Handler OnBefore for AI_FORMATION + -- @function [parent=#AI_FORMATION] OnBeforeFormationLeftWing + -- @param #AI_FORMATION self + -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param #number XStart The start position on the X-axis in meters for the first group. + -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. + -- @param #number YStart The start position on the Y-axis in meters for the first group. + -- @param #number ZStart The start position on the Z-axis in meters for the first group. + -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. + -- @return #boolean + + --- FormationLeftWing Handler OnAfter for AI_FORMATION + -- @function [parent=#AI_FORMATION] OnAfterFormationLeftWing + -- @param #AI_FORMATION self + -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param #number XStart The start position on the X-axis in meters for the first group. + -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. + -- @param #number YStart The start position on the Y-axis in meters for the first group. + -- @param #number ZStart The start position on the Z-axis in meters for the first group. + -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. + + --- FormationLeftWing Trigger for AI_FORMATION + -- @function [parent=#AI_FORMATION] FormationLeftWing + -- @param #AI_FORMATION self + -- @param #number XStart The start position on the X-axis in meters for the first group. + -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. + -- @param #number YStart The start position on the Y-axis in meters for the first group. + -- @param #number ZStart The start position on the Z-axis in meters for the first group. + -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. + + --- FormationLeftWing Asynchronous Trigger for AI_FORMATION + -- @function [parent=#AI_FORMATION] __FormationLeftWing + -- @param #AI_FORMATION self + -- @param #number Delay + -- @param #number XStart The start position on the X-axis in meters for the first group. + -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. + -- @param #number YStart The start position on the Y-axis in meters for the first group. + -- @param #number ZStart The start position on the Z-axis in meters for the first group. + -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. + + self:AddTransition( "*", "FormationRightWing", "*" ) + --- FormationRightWing Handler OnBefore for AI_FORMATION + -- @function [parent=#AI_FORMATION] OnBeforeFormationRightWing + -- @param #AI_FORMATION self + -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param #number XStart The start position on the X-axis in meters for the first group. + -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. + -- @param #number YStart The start position on the Y-axis in meters for the first group. + -- @param #number ZStart The start position on the Z-axis in meters for the first group. + -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. + -- @return #boolean + + --- FormationRightWing Handler OnAfter for AI_FORMATION + -- @function [parent=#AI_FORMATION] OnAfterFormationRightWing + -- @param #AI_FORMATION self + -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param #number XStart The start position on the X-axis in meters for the first group. + -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. + -- @param #number YStart The start position on the Y-axis in meters for the first group. + -- @param #number ZStart The start position on the Z-axis in meters for the first group. + -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. + + --- FormationRightWing Trigger for AI_FORMATION + -- @function [parent=#AI_FORMATION] FormationRightWing + -- @param #AI_FORMATION self + -- @param #number XStart The start position on the X-axis in meters for the first group. + -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. + -- @param #number YStart The start position on the Y-axis in meters for the first group. + -- @param #number ZStart The start position on the Z-axis in meters for the first group. + -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. + + --- FormationRightWing Asynchronous Trigger for AI_FORMATION + -- @function [parent=#AI_FORMATION] __FormationRightWing + -- @param #AI_FORMATION self + -- @param #number Delay + -- @param #number XStart The start position on the X-axis in meters for the first group. + -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. + -- @param #number YStart The start position on the Y-axis in meters for the first group. + -- @param #number ZStart The start position on the Z-axis in meters for the first group. + -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. + + self:AddTransition( "*", "FormationCenterWing", "*" ) + --- FormationCenterWing Handler OnBefore for AI_FORMATION + -- @function [parent=#AI_FORMATION] OnBeforeFormationCenterWing + -- @param #AI_FORMATION self + -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param #number XStart The start position on the X-axis in meters for the first group. + -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. + -- @param #number YStart The start position on the Y-axis in meters for the first group. + -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. + -- @param #number ZStart The start position on the Z-axis in meters for the first group. + -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. + -- @return #boolean + + --- FormationCenterWing Handler OnAfter for AI_FORMATION + -- @function [parent=#AI_FORMATION] OnAfterFormationCenterWing + -- @param #AI_FORMATION self + -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param #number XStart The start position on the X-axis in meters for the first group. + -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. + -- @param #number YStart The start position on the Y-axis in meters for the first group. + -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. + -- @param #number ZStart The start position on the Z-axis in meters for the first group. + -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. + + --- FormationCenterWing Trigger for AI_FORMATION + -- @function [parent=#AI_FORMATION] FormationCenterWing + -- @param #AI_FORMATION self + -- @param #number XStart The start position on the X-axis in meters for the first group. + -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. + -- @param #number YStart The start position on the Y-axis in meters for the first group. + -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. + -- @param #number ZStart The start position on the Z-axis in meters for the first group. + -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. + + --- FormationCenterWing Asynchronous Trigger for AI_FORMATION + -- @function [parent=#AI_FORMATION] __FormationCenterWing + -- @param #AI_FORMATION self + -- @param #number Delay + -- @param #number XStart The start position on the X-axis in meters for the first group. + -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. + -- @param #number YStart The start position on the Y-axis in meters for the first group. + -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. + -- @param #number ZStart The start position on the Z-axis in meters for the first group. + -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. + + self:AddTransition( "*", "FormationVic", "*" ) + --- FormationVic Handler OnBefore for AI_FORMATION + -- @function [parent=#AI_FORMATION] OnBeforeFormationVic + -- @param #AI_FORMATION self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param #number XStart The start position on the X-axis in meters for the first group. + -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. + -- @param #number YStart The start position on the Y-axis in meters for the first group. + -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. + -- @param #number ZStart The start position on the Z-axis in meters for the first group. + -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. + -- @return #boolean + + --- FormationVic Handler OnAfter for AI_FORMATION + -- @function [parent=#AI_FORMATION] OnAfterFormationVic + -- @param #AI_FORMATION self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param #number XStart The start position on the X-axis in meters for the first group. + -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. + -- @param #number YStart The start position on the Y-axis in meters for the first group. + -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. + -- @param #number ZStart The start position on the Z-axis in meters for the first group. + -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. + + --- FormationVic Trigger for AI_FORMATION + -- @function [parent=#AI_FORMATION] FormationVic + -- @param #AI_FORMATION self + -- @param #number XStart The start position on the X-axis in meters for the first group. + -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. + -- @param #number YStart The start position on the Y-axis in meters for the first group. + -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. + -- @param #number ZStart The start position on the Z-axis in meters for the first group. + -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. + + --- FormationVic Asynchronous Trigger for AI_FORMATION + -- @function [parent=#AI_FORMATION] __FormationVic + -- @param #AI_FORMATION self + -- @param #number Delay + -- @param #number XStart The start position on the X-axis in meters for the first group. + -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. + -- @param #number YStart The start position on the Y-axis in meters for the first group. + -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. + -- @param #number ZStart The start position on the Z-axis in meters for the first group. + -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. + + self:AddTransition( "*", "FormationBox", "*" ) + --- FormationBox Handler OnBefore for AI_FORMATION + -- @function [parent=#AI_FORMATION] OnBeforeFormationBox + -- @param #AI_FORMATION self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param #number XStart The start position on the X-axis in meters for the first group. + -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. + -- @param #number YStart The start position on the Y-axis in meters for the first group. + -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. + -- @param #number ZStart The start position on the Z-axis in meters for the first group. + -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. + -- @param #number ZLevels The amount of levels on the Z-axis. + -- @return #boolean + + --- FormationBox Handler OnAfter for AI_FORMATION + -- @function [parent=#AI_FORMATION] OnAfterFormationBox + -- @param #AI_FORMATION self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param #number XStart The start position on the X-axis in meters for the first group. + -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. + -- @param #number YStart The start position on the Y-axis in meters for the first group. + -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. + -- @param #number ZStart The start position on the Z-axis in meters for the first group. + -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. + -- @param #number ZLevels The amount of levels on the Z-axis. + + --- FormationBox Trigger for AI_FORMATION + -- @function [parent=#AI_FORMATION] FormationBox + -- @param #AI_FORMATION self + -- @param #number XStart The start position on the X-axis in meters for the first group. + -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. + -- @param #number YStart The start position on the Y-axis in meters for the first group. + -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. + -- @param #number ZStart The start position on the Z-axis in meters for the first group. + -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. + -- @param #number ZLevels The amount of levels on the Z-axis. + + --- FormationBox Asynchronous Trigger for AI_FORMATION + -- @function [parent=#AI_FORMATION] __FormationBox + -- @param #AI_FORMATION self + -- @param #number Delay + -- @param #number XStart The start position on the X-axis in meters for the first group. + -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. + -- @param #number YStart The start position on the Y-axis in meters for the first group. + -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. + -- @param #number ZStart The start position on the Z-axis in meters for the first group. + -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. + -- @param #number ZLevels The amount of levels on the Z-axis. + + + self:AddTransition( "*", "Follow", "Following" ) + + self:FormationLeftLine( 500, 0, 250, 250 ) + + self.FollowName = FollowName + self.FollowBriefing = FollowBriefing + + + self.CT1 = 0 + self.GT1 = 0 + + self.FollowMode = AI_FORMATION.MODE.MISSION + + return self +end + + +--- Set time interval between updates of the formation. +-- @param #AI_FORMATION self +-- @param #number dt Time step in seconds between formation updates. Default is every 0.5 seconds. +-- @return #AI_FORMATION +function AI_FORMATION:SetFollowTimeInterval(dt) --R2.1 + self.dtFollow=dt or 0.5 + return self +end + +--- This function is for test, it will put on the frequency of the FollowScheduler a red smoke at the direction vector calculated for the escort to fly to. +-- This allows to visualize where the escort is flying to. +-- @param #AI_FORMATION self +-- @param #boolean SmokeDirection If true, then the direction vector will be smoked. +-- @return #AI_FORMATION +function AI_FORMATION:TestSmokeDirectionVector( SmokeDirection ) --R2.1 + self.SmokeDirectionVector = ( SmokeDirection == true ) and true or false + return self +end + +--- FormationLine Handler OnAfter for AI_FORMATION +-- @param #AI_FORMATION self +-- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param #number XStart The start position on the X-axis in meters for the first group. +-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. +-- @param #number YStart The start position on the Y-axis in meters for the first group. +-- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. +-- @param #number ZStart The start position on the Z-axis in meters for the first group. +-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. +-- @return #AI_FORMATION +function AI_FORMATION:onafterFormationLine( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, YSpace, ZStart, ZSpace, Formation ) --R2.1 + self:F( { FollowGroupSet, From , Event ,To, XStart, XSpace, YStart, YSpace, ZStart, ZSpace, Formation } ) + + XStart = XStart or self.XStart + XSpace = XSpace or self.XSpace + YStart = YStart or self.YStart + YSpace = YSpace or self.YSpace + ZStart = ZStart or self.ZStart + ZSpace = ZSpace or self.ZSpace + + FollowGroupSet:Flush( self ) + + local FollowSet = FollowGroupSet:GetSet() + + local i = 1 --FF i=0 caused first unit to have no XSpace! Probably needs further adjustments. This is just a quick work around. + + for FollowID, FollowGroup in pairs( FollowSet ) do + + local PointVec3 = POINT_VEC3:New() + PointVec3:SetX( XStart + i * XSpace ) + PointVec3:SetY( YStart + i * YSpace ) + PointVec3:SetZ( ZStart + i * ZSpace ) + + local Vec3 = PointVec3:GetVec3() + FollowGroup:SetState( self, "FormationVec3", Vec3 ) + i = i + 1 + + FollowGroup:SetState( FollowGroup, "Formation", Formation ) + end + + return self + +end + +--- FormationTrail Handler OnAfter for AI_FORMATION +-- @param #AI_FORMATION self +-- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param #number XStart The start position on the X-axis in meters for the first group. +-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. +-- @param #number YStart The start position on the Y-axis in meters for the first group. +-- @return #AI_FORMATION +function AI_FORMATION:onafterFormationTrail( FollowGroupSet, From , Event , To, XStart, XSpace, YStart ) --R2.1 + + self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,0,0,0, self.__Enum.Formation.Trail ) + + return self +end + + +--- FormationStack Handler OnAfter for AI_FORMATION +-- @param #AI_FORMATION self +-- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param #number XStart The start position on the X-axis in meters for the first group. +-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. +-- @param #number YStart The start position on the Y-axis in meters for the first group. +-- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. +-- @return #AI_FORMATION +function AI_FORMATION:onafterFormationStack( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, YSpace ) --R2.1 + + self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,YSpace,0,0, self.__Enum.Formation.Stack ) + + return self +end + + + + +--- FormationLeftLine Handler OnAfter for AI_FORMATION +-- @param #AI_FORMATION self +-- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param #number XStart The start position on the X-axis in meters for the first group. +-- @param #number YStart The start position on the Y-axis in meters for the first group. +-- @param #number ZStart The start position on the Z-axis in meters for the first group. +-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. +-- @return #AI_FORMATION +function AI_FORMATION:onafterFormationLeftLine( FollowGroupSet, From , Event , To, XStart, YStart, ZStart, ZSpace ) --R2.1 + + self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,0,YStart,0,-ZStart,-ZSpace, self.__Enum.Formation.LeftLine ) + + return self +end + + +--- FormationRightLine Handler OnAfter for AI_FORMATION +-- @param #AI_FORMATION self +-- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param #number XStart The start position on the X-axis in meters for the first group. +-- @param #number YStart The start position on the Y-axis in meters for the first group. +-- @param #number ZStart The start position on the Z-axis in meters for the first group. +-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. +-- @return #AI_FORMATION +function AI_FORMATION:onafterFormationRightLine( FollowGroupSet, From , Event , To, XStart, YStart, ZStart, ZSpace ) --R2.1 + + self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,0,YStart,0,ZStart,ZSpace,self.__Enum.Formation.RightLine) + + return self +end + + +--- FormationLeftWing Handler OnAfter for AI_FORMATION +-- @param #AI_FORMATION self +-- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param #number XStart The start position on the X-axis in meters for the first group. +-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. +-- @param #number YStart The start position on the Y-axis in meters for the first group. +-- @param #number ZStart The start position on the Z-axis in meters for the first group. +-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. +function AI_FORMATION:onafterFormationLeftWing( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, ZStart, ZSpace ) --R2.1 + + self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,0,-ZStart,-ZSpace,self.__Enum.Formation.LeftWing) + + return self +end + + +--- FormationRightWing Handler OnAfter for AI_FORMATION +-- @function [parent=#AI_FORMATION] OnAfterFormationRightWing +-- @param #AI_FORMATION self +-- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param #number XStart The start position on the X-axis in meters for the first group. +-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. +-- @param #number YStart The start position on the Y-axis in meters for the first group. +-- @param #number ZStart The start position on the Z-axis in meters for the first group. +-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. +function AI_FORMATION:onafterFormationRightWing( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, ZStart, ZSpace ) --R2.1 + + self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,0,ZStart,ZSpace,self.__Enum.Formation.RightWing) + + return self +end + + +--- FormationCenterWing Handler OnAfter for AI_FORMATION +-- @param #AI_FORMATION self +-- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param #number XStart The start position on the X-axis in meters for the first group. +-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. +-- @param #number YStart The start position on the Y-axis in meters for the first group. +-- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. +-- @param #number ZStart The start position on the Z-axis in meters for the first group. +-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. +function AI_FORMATION:onafterFormationCenterWing( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, YSpace, ZStart, ZSpace ) --R2.1 + + local FollowSet = FollowGroupSet:GetSet() + + local i = 0 + + for FollowID, FollowGroup in pairs( FollowSet ) do + + local PointVec3 = POINT_VEC3:New() + + local Side = ( i % 2 == 0 ) and 1 or -1 + local Row = i / 2 + 1 + + PointVec3:SetX( XStart + Row * XSpace ) + PointVec3:SetY( YStart ) + PointVec3:SetZ( Side * ( ZStart + i * ZSpace ) ) + + local Vec3 = PointVec3:GetVec3() + FollowGroup:SetState( self, "FormationVec3", Vec3 ) + i = i + 1 + FollowGroup:SetState( FollowGroup, "Formation", self.__Enum.Formation.Vic ) + end + + return self +end + + +--- FormationVic Handle for AI_FORMATION +-- @param #AI_FORMATION self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param #number XStart The start position on the X-axis in meters for the first group. +-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. +-- @param #number YStart The start position on the Y-axis in meters for the first group. +-- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. +-- @param #number ZStart The start position on the Z-axis in meters for the first group. +-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. +-- @return #AI_FORMATION +function AI_FORMATION:onafterFormationVic( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, YSpace, ZStart, ZSpace ) --R2.1 + + self:onafterFormationCenterWing(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,YSpace,ZStart,ZSpace) + + return self +end + +--- FormationBox Handler OnAfter for AI_FORMATION +-- @param #AI_FORMATION self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param #number XStart The start position on the X-axis in meters for the first group. +-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. +-- @param #number YStart The start position on the Y-axis in meters for the first group. +-- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. +-- @param #number ZStart The start position on the Z-axis in meters for the first group. +-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. +-- @param #number ZLevels The amount of levels on the Z-axis. +-- @return #AI_FORMATION +function AI_FORMATION:onafterFormationBox( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, YSpace, ZStart, ZSpace, ZLevels ) --R2.1 + + local FollowSet = FollowGroupSet:GetSet() + + local i = 0 + + for FollowID, FollowGroup in pairs( FollowSet ) do + + local PointVec3 = POINT_VEC3:New() + + local ZIndex = i % ZLevels + local XIndex = math.floor( i / ZLevels ) + local YIndex = math.floor( i / ZLevels ) + + PointVec3:SetX( XStart + XIndex * XSpace ) + PointVec3:SetY( YStart + YIndex * YSpace ) + PointVec3:SetZ( -ZStart - (ZSpace * ZLevels / 2 ) + ZSpace * ZIndex ) + + local Vec3 = PointVec3:GetVec3() + FollowGroup:SetState( self, "FormationVec3", Vec3 ) + i = i + 1 + FollowGroup:SetState( FollowGroup, "Formation", self.__Enum.Formation.Box ) + end + + return self +end + + +--- Use the method @{AI.AI_Formation#AI_FORMATION.SetFlightRandomization}() to make the air units in your formation randomize their flight a bit while in formation. +-- @param #AI_FORMATION self +-- @param #number FlightRandomization The formation flying errors that pilots can make while in formation. Is a range set in meters. +-- @return #AI_FORMATION +function AI_FORMATION:SetFlightRandomization( FlightRandomization ) --R2.1 + + self.FlightRandomization = FlightRandomization + + return self +end + + +--- Gets your escorts to flight mode. +-- @param #AI_FORMATION self +-- @param Wrapper.Group#GROUP FollowGroup FollowGroup. +-- @return #AI_FORMATION +function AI_FORMATION:GetFlightMode( FollowGroup ) + + if FollowGroup then + FollowGroup:SetState( FollowGroup, "PreviousMode", FollowGroup:GetState( FollowGroup, "Mode" ) ) + FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Mission ) + end + + + return FollowGroup:GetState( FollowGroup, "Mode" ) +end + + + +--- This sets your escorts to fly a mission. +-- @param #AI_FORMATION self +-- @param Wrapper.Group#GROUP FollowGroup FollowGroup. +-- @return #AI_FORMATION +function AI_FORMATION:SetFlightModeMission( FollowGroup ) + + if FollowGroup then + FollowGroup:SetState( FollowGroup, "PreviousMode", FollowGroup:GetState( FollowGroup, "Mode" ) ) + FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Mission ) + else + self.FollowGroupSet:ForSomeGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( FollowGroup ) + FollowGroup:SetState( FollowGroup, "PreviousMode", FollowGroup:GetState( FollowGroup, "Mode" ) ) + FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Mission ) + end + ) + end + + + return self +end + + +--- This sets your escorts to execute an attack. +-- @param #AI_FORMATION self +-- @param Wrapper.Group#GROUP FollowGroup FollowGroup. +-- @return #AI_FORMATION +function AI_FORMATION:SetFlightModeAttack( FollowGroup ) + + if FollowGroup then + FollowGroup:SetState( FollowGroup, "PreviousMode", FollowGroup:GetState( FollowGroup, "Mode" ) ) + FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Attack ) + else + self.FollowGroupSet:ForSomeGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( FollowGroup ) + FollowGroup:SetState( FollowGroup, "PreviousMode", FollowGroup:GetState( FollowGroup, "Mode" ) ) + FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Attack ) + end + ) + end + + + return self +end + + +--- This sets your escorts to fly in a formation. +-- @param #AI_FORMATION self +-- @param Wrapper.Group#GROUP FollowGroup FollowGroup. +-- @return #AI_FORMATION +function AI_FORMATION:SetFlightModeFormation( FollowGroup ) + + if FollowGroup then + FollowGroup:SetState( FollowGroup, "PreviousMode", FollowGroup:GetState( FollowGroup, "Mode" ) ) + FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Formation ) + else + self.FollowGroupSet:ForSomeGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( FollowGroup ) + FollowGroup:SetState( FollowGroup, "PreviousMode", FollowGroup:GetState( FollowGroup, "Mode" ) ) + FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Formation ) + end + ) + end + + return self +end + + + + +--- Stop function. Formation will not be updated any more. +-- @param #AI_FORMATION self +-- @param Core.Set#SET_GROUP FollowGroupSet The following set of groups. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To The to state. +function AI_FORMATION:onafterStop(FollowGroupSet, From, Event, To) --R2.1 + self:E("Stopping formation.") +end + +--- Follow event fuction. Check if coming from state "stopped". If so the transition is rejected. +-- @param #AI_FORMATION self +-- @param Core.Set#SET_GROUP FollowGroupSet The following set of groups. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To The to state. +function AI_FORMATION:onbeforeFollow( FollowGroupSet, From, Event, To ) --R2.1 + if From=="Stopped" then + return false -- Deny transition. + end + return true +end + +--- Enter following state. +-- @param #AI_FORMATION self +-- @param Core.Set#SET_GROUP FollowGroupSet The following set of groups. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To The to state. +function AI_FORMATION:onenterFollowing( FollowGroupSet ) --R2.1 + + if self.FollowUnit:IsAlive() then + + local ClientUnit = self.FollowUnit + + local CT1, CT2, CV1, CV2 + CT1 = ClientUnit:GetState( self, "CT1" ) + + local CuVec3=ClientUnit:GetVec3() + + if CT1 == nil or CT1 == 0 then + ClientUnit:SetState( self, "CV1", CuVec3) + ClientUnit:SetState( self, "CT1", timer.getTime() ) + else + CT1 = ClientUnit:GetState( self, "CT1" ) + CT2 = timer.getTime() + CV1 = ClientUnit:GetState( self, "CV1" ) + CV2 = CuVec3 + + ClientUnit:SetState( self, "CT1", CT2 ) + ClientUnit:SetState( self, "CV1", CV2 ) + end + + --FollowGroupSet:ForEachGroupAlive( bla, self, ClientUnit, CT1, CV1, CT2, CV2) + + for _,_group in pairs(FollowGroupSet:GetSet()) do + local group=_group --Wrapper.Group#GROUP + if group and group:IsAlive() then + self:FollowMe(group, ClientUnit, CT1, CV1, CT2, CV2) + end + end + + self:__Follow( -self.dtFollow ) + end + +end + + +--- Follow me. +-- @param #AI_FORMATION self +-- @param Wrapper.Group#GROUP FollowGroup Follow group. +-- @param Wrapper.Unit#UNIT ClientUnit Client Unit. +-- @param DCS#Time CT1 Time +-- @param DCS#Vec3 CV1 Vec3 +-- @param DCS#Time CT2 Time +-- @param DCS#Vec3 CV2 Vec3 +function AI_FORMATION:FollowMe(FollowGroup, ClientUnit, CT1, CV1, CT2, CV2) + + if FollowGroup:GetState( FollowGroup, "Mode" ) == self.__Enum.Mode.Formation and not self:Is("Stopped") then + + self:T({Mode=FollowGroup:GetState( FollowGroup, "Mode" )}) + + FollowGroup:OptionROTEvadeFire() + FollowGroup:OptionROEReturnFire() + + local GroupUnit = FollowGroup:GetUnit( 1 ) + + local GuVec3=GroupUnit:GetVec3() + + local FollowFormation = FollowGroup:GetState( self, "FormationVec3" ) + + if FollowFormation then + local FollowDistance = FollowFormation.x + + local GT1 = GroupUnit:GetState( self, "GT1" ) + + if CT1 == nil or CT1 == 0 or GT1 == nil or GT1 == 0 then + GroupUnit:SetState( self, "GV1", GuVec3) + GroupUnit:SetState( self, "GT1", timer.getTime() ) + else + local CD = ( ( CV2.x - CV1.x )^2 + ( CV2.y - CV1.y )^2 + ( CV2.z - CV1.z )^2 ) ^ 0.5 + local CT = CT2 - CT1 + + local CS = ( 3600 / CT ) * ( CD / 1000 ) / 3.6 + + local CDv = { x = CV2.x - CV1.x, y = CV2.y - CV1.y, z = CV2.z - CV1.z } + local Ca = math.atan2( CDv.x, CDv.z ) + + local GT1 = GroupUnit:GetState( self, "GT1" ) + local GT2 = timer.getTime() + + local GV1 = GroupUnit:GetState( self, "GV1" ) + local GV2 = GuVec3 + + --[[ + GV2:AddX( math.random( -Formation.FlightRandomization / 2, Formation.FlightRandomization / 2 ) ) + GV2:AddY( math.random( -Formation.FlightRandomization / 2, Formation.FlightRandomization / 2 ) ) + GV2:AddZ( math.random( -Formation.FlightRandomization / 2, Formation.FlightRandomization / 2 ) ) + ]] + + GV2.x=GV2.x+math.random( -self.FlightRandomization / 2, self.FlightRandomization / 2 ) + GV2.y=GV2.y+math.random( -self.FlightRandomization / 2, self.FlightRandomization / 2 ) + GV2.z=GV2.z+math.random( -self.FlightRandomization / 2, self.FlightRandomization / 2 ) + + + GroupUnit:SetState( self, "GT1", GT2 ) + GroupUnit:SetState( self, "GV1", GV2 ) + + + local GD = ( ( GV2.x - GV1.x )^2 + ( GV2.y - GV1.y )^2 + ( GV2.z - GV1.z )^2 ) ^ 0.5 + local GT = GT2 - GT1 + + + -- Calculate the distance + local GDv = { x = GV2.x - CV1.x, y = GV2.y - CV1.y, z = GV2.z - CV1.z } + local Alpha_T = math.atan2( GDv.x, GDv.z ) - math.atan2( CDv.x, CDv.z ) + local Alpha_R = ( Alpha_T < 0 ) and Alpha_T + 2 * math.pi or Alpha_T + local Position = math.cos( Alpha_R ) + local GD = ( ( GDv.x )^2 + ( GDv.z )^2 ) ^ 0.5 + local Distance = GD * Position + - CS * 0.5 + + -- Calculate the group direction vector + local GV = { x = GV2.x - CV2.x, y = GV2.y - CV2.y, z = GV2.z - CV2.z } + + -- Calculate GH2, GH2 with the same height as CV2. + local GH2 = { x = GV2.x, y = CV2.y + FollowFormation.y, z = GV2.z } + + -- Calculate the angle of GV to the orthonormal plane + local alpha = math.atan2( GV.x, GV.z ) + + local GVx = FollowFormation.z * math.cos( Ca ) + FollowFormation.x * math.sin( Ca ) + local GVz = FollowFormation.x * math.cos( Ca ) - FollowFormation.z * math.sin( Ca ) + + + -- Now we calculate the intersecting vector between the circle around CV2 with radius FollowDistance and GH2. + -- From the GeoGebra model: CVI = (x(CV2) + FollowDistance cos(alpha), y(GH2) + FollowDistance sin(alpha), z(CV2)) + local Inclination = ( Distance + FollowFormation.x ) / 10 + if Inclination < -30 then + Inclination = - 30 + end + + local CVI = { + x = CV2.x + CS * 10 * math.sin(Ca), + y = GH2.y + Inclination, -- + FollowFormation.y, + y = GH2.y, + z = CV2.z + CS * 10 * math.cos(Ca), + } + + -- Calculate the direction vector DV of the escort group. We use CVI as the base and CV2 as the direction. + local DV = { x = CV2.x - CVI.x, y = CV2.y - CVI.y, z = CV2.z - CVI.z } + + -- We now calculate the unary direction vector DVu, so that we can multiply DVu with the speed, which is expressed in meters / s. + -- We need to calculate this vector to predict the point the escort group needs to fly to according its speed. + -- The distance of the destination point should be far enough not to have the aircraft starting to swipe left to right... + local DVu = { x = DV.x / FollowDistance, y = DV.y, z = DV.z / FollowDistance } + + -- Now we can calculate the group destination vector GDV. + local GDV = { x = CVI.x, y = CVI.y, z = CVI.z } + + local ADDx = FollowFormation.x * math.cos(alpha) - FollowFormation.z * math.sin(alpha) + local ADDz = FollowFormation.z * math.cos(alpha) + FollowFormation.x * math.sin(alpha) + + local GDV_Formation = { + x = GDV.x - GVx, + y = GDV.y, + z = GDV.z - GVz + } + + -- Debug smoke. + if self.SmokeDirectionVector == true then + trigger.action.smoke( GDV, trigger.smokeColor.Green ) + trigger.action.smoke( GDV_Formation, trigger.smokeColor.White ) + end + + + + local Time = 120 + + local Speed = - ( Distance + FollowFormation.x ) / Time + + if Distance > -10000 then + Speed = - ( Distance + FollowFormation.x ) / 60 + end + + if Distance > -2500 then + Speed = - ( Distance + FollowFormation.x ) / 20 + end + + local GS = Speed + CS + + --self:F( { Distance = Distance, Speed = Speed, CS = CS, GS = GS } ) + + -- Now route the escort to the desired point with the desired speed. + FollowGroup:RouteToVec3( GDV_Formation, GS ) -- DCS models speed in Mps (Miles per second) + + end + end + end +end +--- **Functional** -- Taking the lead of AI escorting your flight or of other AI. +-- +-- === +-- +-- ## Features: +-- +-- * Escort navigation commands. +-- * Escort hold at position commands. +-- * Escorts reporting detected targets. +-- * Escorts scanning targets in advance. +-- * Escorts attacking specific targets. +-- * Request assistance from other groups for attack. +-- * Manage rule of engagement of escorts. +-- * Manage the allowed evasion techniques of escorts. +-- * Make escort to execute a defined mission or path. +-- * Escort tactical situation reporting. +-- +-- === +-- +-- ## Missions: +-- +-- [ESC - Escorting](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/ESC%20-%20Escorting) +-- +-- === +-- +-- Allows you to interact with escorting AI on your flight and take the lead. +-- +-- Each escorting group can be commanded with a complete set of radio commands (radio menu in your flight, and then F10). +-- +-- The radio commands will vary according the category of the group. The richest set of commands are with helicopters and airPlanes. +-- Ships and Ground troops will have a more limited set, but they can provide support through the bombing of targets designated by the other escorts. +-- +-- Escorts detect targets using a built-in detection mechanism. The detected targets are reported at a specified time interval. +-- Once targets are reported, each escort has these targets as menu options to command the attack of these targets. +-- Targets are by default grouped per area of 5000 meters, but the kind of detection and the grouping range can be altered. +-- +-- Different formations can be selected in the Flight menu: Trail, Stack, Left Line, Right Line, Left Wing, Right Wing, Central Wing and Boxed formations are available. +-- The Flight menu also allows for a mass attack, where all of the escorts are commanded to attack a target. +-- +-- Escorts can emit flares to reports their location. They can be commanded to hold at a location, which can be their current or the leader location. +-- In this way, you can spread out the escorts over the battle field before a coordinated attack. +-- +-- But basically, the escort class provides 4 modes of operation, and depending on the mode, you are either leading the flight, or following the flight. +-- +-- ## Leading the flight +-- +-- When leading the flight, you are expected to guide the escorts towards the target areas, +-- and carefully coordinate the attack based on the threat levels reported, and the available weapons +-- carried by the escorts. Ground ships or ground troops can execute A-assisted attacks, when they have long-range ground precision weapons for attack. +-- +-- ## Following the flight +-- +-- Escorts can be commanded to execute a specific mission path. In this mode, the escorts are in the lead. +-- You as a player, are following the escorts, and are commanding them to progress the mission while +-- ensuring that the escorts survive. You are joining the escorts in the battlefield. They will detect and report targets +-- and you will ensure that the attacks are well coordinated, assigning the correct escort type for the detected target +-- type. Once the attack is finished, the escort will resume the mission it was assigned. +-- In other words, you can use the escorts for reconnaissance, and for guiding the attack. +-- Imagine you as a mi-8 pilot, assigned to pickup cargo. Two ka-50s are guiding the way, and you are +-- following. You are in control. The ka-50s detect targets, report them, and you command how the attack +-- will commence and from where. You can control where the escorts are holding position and which targets +-- are attacked first. You are in control how the ka-50s will follow their mission path. +-- +-- Escorts can act as part of a AI A2G dispatcher offensive. In this way, You was a player are in control. +-- The mission is defined by the A2G dispatcher, and you are responsible to join the flight and ensure that the +-- attack is well coordinated. +-- +-- It is with great proud that I present you this class, and I hope you will enjoy the functionality and the dynamism +-- it brings in your DCS world simulations. +-- +-- # RADIO MENUs that can be created: +-- +-- Find a summary below of the current available commands: +-- +-- ## Navigation ...: +-- +-- Escort group navigation functions: +-- +-- * **"Join-Up":** The escort group fill follow you in the assigned formation. +-- * **"Flare":** Provides menu commands to let the escort group shoot a flare in the air in a color. +-- * **"Smoke":** Provides menu commands to let the escort group smoke the air in a color. Note that smoking is only available for ground and naval troops. +-- +-- ## Hold position ...: +-- +-- Escort group navigation functions: +-- +-- * **"At current location":** The escort group will hover above the ground at the position they were. The altitude can be specified as a parameter. +-- * **"At my location":** The escort group will hover or orbit at the position where you are. The escort will fly to your location and hold position. The altitude can be specified as a parameter. +-- +-- ## Report targets ...: +-- +-- Report targets will make the escort group to report any target that it identifies within detection range. Any detected target can be attacked using the "Attack Targets" menu function. (see below). +-- +-- * **"Report now":** Will report the current detected targets. +-- * **"Report targets on":** Will make the escorts to report the detected targets and will fill the "Attack Targets" menu list. +-- * **"Report targets off":** Will stop detecting targets. +-- +-- ## Attack targets ...: +-- +-- This menu item will list all detected targets within a 15km range. Depending on the level of detection (known/unknown) and visuality, the targets type will also be listed. +-- This menu will be available in Flight menu or in each Escort menu. +-- +-- ## Scan targets ...: +-- +-- Menu items to pop-up the escort group for target scanning. After scanning, the escort group will resume with the mission or rejoin formation. +-- +-- * **"Scan targets 30 seconds":** Scan 30 seconds for targets. +-- * **"Scan targets 60 seconds":** Scan 60 seconds for targets. +-- +-- ## Request assistance from ...: +-- +-- This menu item will list all detected targets within a 15km range, similar as with the menu item **Attack Targets**. +-- This menu item allows to request attack support from other ground based escorts supporting the current escort. +-- eg. the function allows a player to request support from the Ship escort to attack a target identified by the Plane escort with its Tomahawk missiles. +-- eg. the function allows a player to request support from other Planes escorting to bomb the unit with illumination missiles or bombs, so that the main plane escort can attack the area. +-- +-- ## ROE ...: +-- +-- Sets the Rules of Engagement (ROE) of the escort group when in flight. +-- +-- * **"Hold Fire":** The escort group will hold fire. +-- * **"Return Fire":** The escort group will return fire. +-- * **"Open Fire":** The escort group will open fire on designated targets. +-- * **"Weapon Free":** The escort group will engage with any target. +-- +-- ## Evasion ...: +-- +-- Will define the evasion techniques that the escort group will perform during flight or combat. +-- +-- * **"Fight until death":** The escort group will have no reaction to threats. +-- * **"Use flares, chaff and jammers":** The escort group will use passive defense using flares and jammers. No evasive manoeuvres are executed. +-- * **"Evade enemy fire":** The rescort group will evade enemy fire before firing. +-- * **"Go below radar and evade fire":** The escort group will perform evasive vertical manoeuvres. +-- +-- ## Resume Mission ...: +-- +-- Escort groups can have their own mission. This menu item will allow the escort group to resume their Mission from a given waypoint. +-- Note that this is really fantastic, as you now have the dynamic of taking control of the escort groups, and allowing them to resume their path or mission. +-- +-- === +-- +-- ### Authors: **FlightControl** +-- +-- === +-- +-- @module AI.AI_Escort +-- @image Escorting.JPG + + + +--- @type AI_ESCORT +-- @extends AI.AI_Formation#AI_FORMATION + + +-- TODO: Add the menus when the class Start method is activated. +-- TODO: Remove the menus when the class Stop method is called. + +--- AI_ESCORT class +-- +-- # AI_ESCORT construction methods. +-- +-- Create a new AI_ESCORT object with the @{#AI_ESCORT.New} method: +-- +-- * @{#AI_ESCORT.New}: Creates a new AI_ESCORT object from a @{Wrapper.Group#GROUP} for a @{Wrapper.Client#CLIENT}, with an optional briefing text. +-- +-- @usage +-- -- Declare a new EscortPlanes object as follows: +-- +-- -- First find the GROUP object and the CLIENT object. +-- local EscortUnit = CLIENT:FindByName( "Unit Name" ) -- The Unit Name is the name of the unit flagged with the skill Client in the mission editor. +-- local EscortGroup = GROUP:FindByName( "Group Name" ) -- The Group Name is the name of the group that will escort the Escort Client. +-- +-- -- Now use these 2 objects to construct the new EscortPlanes object. +-- EscortPlanes = AI_ESCORT:New( EscortUnit, EscortGroup, "Desert", "Welcome to the mission. You are escorted by a plane with code name 'Desert', which can be instructed through the F10 radio menu." ) +-- +-- @field #AI_ESCORT +AI_ESCORT = { + ClassName = "AI_ESCORT", + EscortName = nil, -- The Escort Name + EscortUnit = nil, + EscortGroup = nil, + EscortMode = 1, + Targets = {}, -- The identified targets + FollowScheduler = nil, + ReportTargets = true, + OptionROE = AI.Option.Air.val.ROE.OPEN_FIRE, + OptionReactionOnThreat = AI.Option.Air.val.REACTION_ON_THREAT.ALLOW_ABORT_MISSION, + SmokeDirectionVector = false, + TaskPoints = {} +} + +--- @field Functional.Detection#DETECTION_AREAS +AI_ESCORT.Detection = nil + +--- MENUPARAM type +-- @type MENUPARAM +-- @field #AI_ESCORT ParamSelf +-- @field #Distance ParamDistance +-- @field #function ParamFunction +-- @field #string ParamMessage + +--- AI_ESCORT class constructor for an AI group +-- @param #AI_ESCORT self +-- @param Wrapper.Client#CLIENT EscortUnit The client escorted by the EscortGroup. +-- @param Core.Set#SET_GROUP EscortGroupSet The set of group AI escorting the EscortUnit. +-- @param #string EscortName Name of the escort. +-- @param #string EscortBriefing A text showing the AI_ESCORT briefing to the player. Note that if no EscortBriefing is provided, the default briefing will be shown. +-- @return #AI_ESCORT self +-- @usage +-- -- Declare a new EscortPlanes object as follows: +-- +-- -- First find the GROUP object and the CLIENT object. +-- local EscortUnit = CLIENT:FindByName( "Unit Name" ) -- The Unit Name is the name of the unit flagged with the skill Client in the mission editor. +-- local EscortGroup = GROUP:FindByName( "Group Name" ) -- The Group Name is the name of the group that will escort the Escort Client. +-- +-- -- Now use these 2 objects to construct the new EscortPlanes object. +-- EscortPlanes = AI_ESCORT:New( EscortUnit, EscortGroup, "Desert", "Welcome to the mission. You are escorted by a plane with code name 'Desert', which can be instructed through the F10 radio menu." ) +function AI_ESCORT:New( EscortUnit, EscortGroupSet, EscortName, EscortBriefing ) + + local self = BASE:Inherit( self, AI_FORMATION:New( EscortUnit, EscortGroupSet, EscortName, EscortBriefing ) ) -- #AI_ESCORT + self:F( { EscortUnit, EscortGroupSet } ) + + self.PlayerUnit = self.FollowUnit -- Wrapper.Unit#UNIT + self.PlayerGroup = self.FollowUnit:GetGroup() -- Wrapper.Group#GROUP + + self.EscortName = EscortName + self.EscortGroupSet = EscortGroupSet + + self.EscortGroupSet:SetSomeIteratorLimit( 8 ) + + self.EscortBriefing = EscortBriefing + + self.Menu = {} + +-- if not EscortBriefing then +-- EscortGroup:MessageToClient( EscortGroup:GetCategoryName() .. " '" .. EscortName .. "' (" .. EscortGroup:GetCallsign() .. ") reporting! " .. +-- "We're escorting your flight. " .. +-- "Use the Radio Menu and F10 and use the options under + " .. EscortName .. "\n", +-- 60, EscortUnit +-- ) +-- else +-- EscortGroup:MessageToClient( EscortGroup:GetCategoryName() .. " '" .. EscortName .. "' (" .. EscortGroup:GetCallsign() .. ") " .. EscortBriefing, +-- 60, EscortUnit +-- ) +-- end + + self.FollowDistance = 100 + self.CT1 = 0 + self.GT1 = 0 + + + EscortGroupSet:ForEachGroup( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + -- Set EscortGroup known at EscortUnit. + if not self.PlayerUnit._EscortGroups then + self.PlayerUnit._EscortGroups = {} + end + + if not self.PlayerUnit._EscortGroups[EscortGroup:GetName()] then + self.PlayerUnit._EscortGroups[EscortGroup:GetName()] = {} + self.PlayerUnit._EscortGroups[EscortGroup:GetName()].EscortGroup = EscortGroup + self.PlayerUnit._EscortGroups[EscortGroup:GetName()].EscortName = self.EscortName + self.PlayerUnit._EscortGroups[EscortGroup:GetName()].Detection = self.Detection + end + end + ) + + self:SetFlightReportType( self.__Enum.ReportType.All ) + + return self +end + + +function AI_ESCORT:_InitFlightMenus() + + self:SetFlightMenuJoinUp() + self:SetFlightMenuFormation( "Trail" ) + self:SetFlightMenuFormation( "Stack" ) + self:SetFlightMenuFormation( "LeftLine" ) + self:SetFlightMenuFormation( "RightLine" ) + self:SetFlightMenuFormation( "LeftWing" ) + self:SetFlightMenuFormation( "RightWing" ) + self:SetFlightMenuFormation( "Vic" ) + self:SetFlightMenuFormation( "Box" ) + + self:SetFlightMenuHoldAtEscortPosition() + self:SetFlightMenuHoldAtLeaderPosition() + + self:SetFlightMenuFlare() + self:SetFlightMenuSmoke() + + self:SetFlightMenuROE() + self:SetFlightMenuROT() + + self:SetFlightMenuTargets() + self:SetFlightMenuReportType() + +end + +function AI_ESCORT:_InitEscortMenus( EscortGroup ) + + EscortGroup.EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroup:GetCallsign(), self.MainMenu ) + + self:SetEscortMenuJoinUp( EscortGroup ) + self:SetEscortMenuResumeMission( EscortGroup ) + + self:SetEscortMenuHoldAtEscortPosition( EscortGroup ) + self:SetEscortMenuHoldAtLeaderPosition( EscortGroup ) + + self:SetEscortMenuFlare( EscortGroup ) + self:SetEscortMenuSmoke( EscortGroup ) + + self:SetEscortMenuROE( EscortGroup ) + self:SetEscortMenuROT( EscortGroup ) + + self:SetEscortMenuTargets( EscortGroup ) + +end + +function AI_ESCORT:_InitEscortRoute( EscortGroup ) + + EscortGroup.MissionRoute = EscortGroup:GetTaskRoute() + +end + + +--- @param #AI_ESCORT self +-- @param Core.Set#SET_GROUP EscortGroupSet +function AI_ESCORT:onafterStart( EscortGroupSet ) + + self:F() + + EscortGroupSet:ForEachGroup( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + EscortGroup:WayPointInitialize() + + EscortGroup:OptionROTVertical() + EscortGroup:OptionROEOpenFire() + end + ) + + -- TODO:Revise this... + local LeaderEscort = EscortGroupSet:GetFirst() -- Wrapper.Group#GROUP + if LeaderEscort then + local Report = REPORT:New( "Escort reporting:" ) + Report:Add( "Joining Up " .. EscortGroupSet:GetUnitTypeNames():Text( ", " ) .. " from " .. LeaderEscort:GetCoordinate():ToString( self.PlayerUnit ) ) + LeaderEscort:MessageTypeToGroup( Report:Text(), MESSAGE.Type.Information, self.PlayerUnit ) + end + + self.Detection = DETECTION_AREAS:New( EscortGroupSet, 5000 ) + + -- This only makes the escort report detections made by the escort, not through DLINK. + -- These must be enquired using other facilities. + -- In this way, the escort will report the target areas that are relevant for the mission. + self.Detection:InitDetectVisual( true ) + self.Detection:InitDetectIRST( true ) + self.Detection:InitDetectOptical( true ) + self.Detection:InitDetectRadar( true ) + self.Detection:InitDetectRWR( true ) + + self.Detection:SetAcceptRange( 100000 ) + + self.Detection:__Start( 30 ) + + self.MainMenu = MENU_GROUP:New( self.PlayerGroup, self.EscortName ) + self.FlightMenu = MENU_GROUP:New( self.PlayerGroup, "Flight", self.MainMenu ) + + self:_InitFlightMenus() + + self.EscortGroupSet:ForSomeGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + + self:_InitEscortMenus( EscortGroup ) + self:_InitEscortRoute( EscortGroup ) + + self:SetFlightModeFormation( EscortGroup ) + + --- @param #AI_ESCORT self + -- @param Core.Event#EVENTDATA EventData + function EscortGroup:OnEventDeadOrCrash( EventData ) + self:F( { "EventDead", EventData } ) + self.EscortMenu:Remove() + end + + EscortGroup:HandleEvent( EVENTS.Dead, EscortGroup.OnEventDeadOrCrash ) + EscortGroup:HandleEvent( EVENTS.Crash, EscortGroup.OnEventDeadOrCrash ) + + end + ) + + +end + +--- @param #AI_ESCORT self +-- @param Core.Set#SET_GROUP EscortGroupSet +function AI_ESCORT:onafterStop( EscortGroupSet ) + + self:F() + + EscortGroupSet:ForEachGroup( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + EscortGroup:WayPointInitialize() + + EscortGroup:OptionROTVertical() + EscortGroup:OptionROEOpenFire() + end + ) + + self.Detection:Stop() + + self.MainMenu:Remove() + +end + +--- Set a Detection method for the EscortUnit to be reported upon. +-- Detection methods are based on the derived classes from DETECTION_BASE. +-- @param #AI_ESCORT self +-- @param Functional.Detection#DETECTION_AREAS Detection +function AI_ESCORT:SetDetection( Detection ) + + self.Detection = Detection + self.EscortGroup.Detection = self.Detection + self.PlayerUnit._EscortGroups[self.EscortGroup:GetName()].Detection = self.EscortGroup.Detection + + Detection:__Start( 1 ) + +end + +--- This function is for test, it will put on the frequency of the FollowScheduler a red smoke at the direction vector calculated for the escort to fly to. +-- This allows to visualize where the escort is flying to. +-- @param #AI_ESCORT self +-- @param #boolean SmokeDirection If true, then the direction vector will be smoked. +function AI_ESCORT:TestSmokeDirectionVector( SmokeDirection ) + self.SmokeDirectionVector = ( SmokeDirection == true ) and true or false +end + + +--- Defines the default menus for helicopters. +-- @param #AI_ESCORT self +-- @param #number XStart The start position on the X-axis in meters for the first group. +-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. +-- @param #nubmer YStart The start position on the Y-axis in meters for the first group. +-- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. +-- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. +-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. +-- @param #number ZLevels The amount of levels on the Z-axis. +-- @return #AI_ESCORT +function AI_ESCORT:MenusHelicopters( XStart, XSpace, YStart, YSpace, ZStart, ZSpace, ZLevels ) + self:F() + +-- self:MenuScanForTargets( 100, 60 ) + + self.XStart = XStart or 50 + self.XSpace = XSpace or 50 + self.YStart = YStart or 50 + self.YSpace = YSpace or 50 + self.ZStart = ZStart or 50 + self.ZSpace = ZSpace or 50 + self.ZLevels = ZLevels or 10 + + self:MenuJoinUp() + self:MenuFormationTrail(self.XStart,self.XSpace,self.YStart) + self:MenuFormationStack(self.XStart,self.XSpace,self.YStart,self.YSpace) + self:MenuFormationLeftLine(self.XStart,self.YStart,self.ZStart,self.ZSpace) + self:MenuFormationRightLine(self.XStart,self.YStart,self.ZStart,self.ZSpace) + self:MenuFormationLeftWing(self.XStart,self.XSpace,self.YStart,self.ZStart,self.ZSpace) + self:MenuFormationRightWing(self.XStart,self.XSpace,self.YStart,self.ZStart,self.ZSpace) + self:MenuFormationVic(self.XStart,self.XSpace,self.YStart,self.YSpace,self.ZStart,self.ZSpace) + self:MenuFormationBox(self.XStart,self.XSpace,self.YStart,self.YSpace,self.ZStart,self.ZSpace,self.ZLevels) + + self:MenuHoldAtEscortPosition( 30 ) + self:MenuHoldAtEscortPosition( 100 ) + self:MenuHoldAtEscortPosition( 500 ) + self:MenuHoldAtLeaderPosition( 30, 500 ) + + self:MenuFlare() + self:MenuSmoke() + + self:MenuTargets( 60 ) + self:MenuAssistedAttack() + self:MenuROE() + self:MenuROT() + + return self +end + + +--- Defines the default menus for airplanes. +-- @param #AI_ESCORT self +-- @param #number XStart The start position on the X-axis in meters for the first group. +-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. +-- @param #nubmer YStart The start position on the Y-axis in meters for the first group. +-- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. +-- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. +-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. +-- @param #number ZLevels The amount of levels on the Z-axis. +-- @return #AI_ESCORT +function AI_ESCORT:MenusAirplanes( XStart, XSpace, YStart, YSpace, ZStart, ZSpace, ZLevels ) + self:F() + +-- self:MenuScanForTargets( 100, 60 ) + + self.XStart = XStart or 50 + self.XSpace = XSpace or 50 + self.YStart = YStart or 50 + self.YSpace = YSpace or 50 + self.ZStart = ZStart or 50 + self.ZSpace = ZSpace or 50 + self.ZLevels = ZLevels or 10 + + self:MenuJoinUp() + self:MenuFormationTrail(self.XStart,self.XSpace,self.YStart) + self:MenuFormationStack(self.XStart,self.XSpace,self.YStart,self.YSpace) + self:MenuFormationLeftLine(self.XStart,self.YStart,self.ZStart,self.ZSpace) + self:MenuFormationRightLine(self.XStart,self.YStart,self.ZStart,self.ZSpace) + self:MenuFormationLeftWing(self.XStart,self.XSpace,self.YStart,self.ZStart,self.ZSpace) + self:MenuFormationRightWing(self.XStart,self.XSpace,self.YStart,self.ZStart,self.ZSpace) + self:MenuFormationVic(self.XStart,self.XSpace,self.YStart,self.YSpace,self.ZStart,self.ZSpace) + self:MenuFormationBox(self.XStart,self.XSpace,self.YStart,self.YSpace,self.ZStart,self.ZSpace,self.ZLevels) + + self:MenuHoldAtEscortPosition( 1000, 500 ) + self:MenuHoldAtLeaderPosition( 1000, 500 ) + + self:MenuFlare() + self:MenuSmoke() + + self:MenuTargets( 60 ) + self:MenuAssistedAttack() + self:MenuROE() + self:MenuROT() + + return self +end + + +function AI_ESCORT:SetFlightMenuFormation( Formation ) + + local FormationID = "Formation" .. Formation + + local MenuFormation = self.Menu[FormationID] + + if MenuFormation then + local Arguments = MenuFormation.Arguments + --self:I({Arguments=unpack(Arguments)}) + local FlightMenuFormation = MENU_GROUP:New( self.PlayerGroup, "Formation", self.MainMenu ) + local MenuFlightFormationID = MENU_GROUP_COMMAND:New( self.PlayerGroup, Formation, FlightMenuFormation, + function ( self, Formation, ... ) + self.EscortGroupSet:ForSomeGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup, self, Formation, Arguments ) + if EscortGroup:IsAir() then + self:E({FormationID=FormationID}) + self[FormationID]( self, unpack(Arguments) ) + end + end, self, Formation, Arguments + ) + end, self, Formation, Arguments + ) + end + + return self +end + + +function AI_ESCORT:MenuFormation( Formation, ... ) + + local FormationID = "Formation"..Formation + self.Menu[FormationID] = self.Menu[FormationID] or {} + self.Menu[FormationID].Arguments = arg + +end + + +--- Defines a menu slot to let the escort to join in a trail formation. +-- This menu will appear under **Formation**. +-- @param #AI_ESCORT self +-- @param #number XStart The start position on the X-axis in meters for the first group. +-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. +-- @param #nubmer YStart The start position on the Y-axis in meters for the first group. +-- @return #AI_ESCORT +function AI_ESCORT:MenuFormationTrail( XStart, XSpace, YStart ) + + self:MenuFormation( "Trail", XStart, XSpace, YStart ) + + return self +end + +--- Defines a menu slot to let the escort to join in a stacked formation. +-- This menu will appear under **Formation**. +-- @param #AI_ESCORT self +-- @param #number XStart The start position on the X-axis in meters for the first group. +-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. +-- @param #nubmer YStart The start position on the Y-axis in meters for the first group. +-- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. +-- @return #AI_ESCORT +function AI_ESCORT:MenuFormationStack( XStart, XSpace, YStart, YSpace ) + + self:MenuFormation( "Stack", XStart, XSpace, YStart, YSpace ) + + return self +end + + +--- Defines a menu slot to let the escort to join in a leFt wing formation. +-- This menu will appear under **Formation**. +-- @param #AI_ESCORT self +-- @param #number XStart The start position on the X-axis in meters for the first group. +-- @param #nubmer YStart The start position on the Y-axis in meters for the first group. +-- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. +-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. +-- @return #AI_ESCORT +function AI_ESCORT:MenuFormationLeftLine( XStart, YStart, ZStart, ZSpace ) + + self:MenuFormation( "LeftLine", XStart, YStart, ZStart, ZSpace ) + + return self +end + + +--- Defines a menu slot to let the escort to join in a right line formation. +-- This menu will appear under **Formation**. +-- @param #AI_ESCORT self +-- @param #number XStart The start position on the X-axis in meters for the first group. +-- @param #nubmer YStart The start position on the Y-axis in meters for the first group. +-- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. +-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. +-- @return #AI_ESCORT +function AI_ESCORT:MenuFormationRightLine( XStart, YStart, ZStart, ZSpace ) + + self:MenuFormation( "RightLine", XStart, YStart, ZStart, ZSpace ) + + return self +end + + +--- Defines a menu slot to let the escort to join in a left wing formation. +-- This menu will appear under **Formation**. +-- @param #AI_ESCORT self +-- @param #number XStart The start position on the X-axis in meters for the first group. +-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. +-- @param #nubmer YStart The start position on the Y-axis in meters for the first group. +-- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. +-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. +-- @return #AI_ESCORT +function AI_ESCORT:MenuFormationLeftWing( XStart, XSpace, YStart, ZStart, ZSpace ) + + self:MenuFormation( "LeftWing", XStart, XSpace, YStart, ZStart, ZSpace ) + + return self +end + + +--- Defines a menu slot to let the escort to join in a right wing formation. +-- This menu will appear under **Formation**. +-- @param #AI_ESCORT self +-- @param #number XStart The start position on the X-axis in meters for the first group. +-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. +-- @param #nubmer YStart The start position on the Y-axis in meters for the first group. +-- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. +-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. +-- @return #AI_ESCORT +function AI_ESCORT:MenuFormationRightWing( XStart, XSpace, YStart, ZStart, ZSpace ) + + self:MenuFormation( "RightWing", XStart, XSpace, YStart, ZStart, ZSpace ) + + return self +end + + +--- Defines a menu slot to let the escort to join in a center wing formation. +-- This menu will appear under **Formation**. +-- @param #AI_ESCORT self +-- @param #number XStart The start position on the X-axis in meters for the first group. +-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. +-- @param #nubmer YStart The start position on the Y-axis in meters for the first group. +-- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. +-- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. +-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. +-- @return #AI_ESCORT +function AI_ESCORT:MenuFormationCenterWing( XStart, XSpace, YStart, YSpace, ZStart, ZSpace ) + + self:MenuFormation( "CenterWing", XStart, XSpace, YStart, YSpace, ZStart, ZSpace ) + + return self +end + + +--- Defines a menu slot to let the escort to join in a vic formation. +-- This menu will appear under **Formation**. +-- @param #AI_ESCORT self +-- @param #number XStart The start position on the X-axis in meters for the first group. +-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. +-- @param #nubmer YStart The start position on the Y-axis in meters for the first group. +-- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. +-- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. +-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. +-- @return #AI_ESCORT +function AI_ESCORT:MenuFormationVic( XStart, XSpace, YStart, YSpace, ZStart, ZSpace ) + + self:MenuFormation( "Vic", XStart, XSpace, YStart, YSpace, ZStart, ZSpace ) + + return self +end + + +--- Defines a menu slot to let the escort to join in a box formation. +-- This menu will appear under **Formation**. +-- @param #AI_ESCORT self +-- @param #number XStart The start position on the X-axis in meters for the first group. +-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. +-- @param #nubmer YStart The start position on the Y-axis in meters for the first group. +-- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. +-- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. +-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. +-- @param #number ZLevels The amount of levels on the Z-axis. +-- @return #AI_ESCORT +function AI_ESCORT:MenuFormationBox( XStart, XSpace, YStart, YSpace, ZStart, ZSpace, ZLevels ) + + self:MenuFormation( "Box", XStart, XSpace, YStart, YSpace, ZStart, ZSpace, ZLevels ) + + return self +end + +function AI_ESCORT:SetFlightMenuJoinUp() + + if self.Menu.JoinUp == true then + local FlightMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", self.FlightMenu ) + local FlightMenuJoinUp = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Join Up", FlightMenuReportNavigation, AI_ESCORT._FlightJoinUp, self ) + end + +end + + +--- Sets a menu slot to join formation for an escort. +-- @param #AI_ESCORT self +-- @return #AI_ESCORT +function AI_ESCORT:SetEscortMenuJoinUp( EscortGroup ) + + if self.Menu.JoinUp == true then + if EscortGroup:IsAir() then + local EscortGroupName = EscortGroup:GetName() + local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortGroup.EscortMenu ) + local EscortMenuJoinUp = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Join Up", EscortMenuReportNavigation, AI_ESCORT._JoinUp, self, EscortGroup ) + end + end +end + + + +--- Defines --- Defines a menu slot to let the escort to join formation. +-- @param #AI_ESCORT self +-- @return #AI_ESCORT +function AI_ESCORT:MenuJoinUp() + + self.Menu.JoinUp = true + + return self +end + + +function AI_ESCORT:SetFlightMenuHoldAtEscortPosition() + + for _, MenuHoldAtEscortPosition in pairs( self.Menu.HoldAtEscortPosition ) do + local FlightMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", self.FlightMenu ) + + local FlightMenuHoldPosition = MENU_GROUP_COMMAND + :New( + self.PlayerGroup, + MenuHoldAtEscortPosition.MenuText, + FlightMenuReportNavigation, + AI_ESCORT._FlightHoldPosition, + self, + nil, + MenuHoldAtEscortPosition.Height, + MenuHoldAtEscortPosition.Speed + ) + + end + return self +end + +function AI_ESCORT:SetEscortMenuHoldAtEscortPosition( EscortGroup ) + + for _, HoldAtEscortPosition in pairs( self.Menu.HoldAtEscortPosition ) do + if EscortGroup:IsAir() then + local EscortGroupName = EscortGroup:GetName() + local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortGroup.EscortMenu ) + local EscortMenuHoldPosition = MENU_GROUP_COMMAND + :New( + self.PlayerGroup, + HoldAtEscortPosition.MenuText, + EscortMenuReportNavigation, + AI_ESCORT._HoldPosition, + self, + EscortGroup, + EscortGroup, + HoldAtEscortPosition.Height, + HoldAtEscortPosition.Speed + ) + end + end + + return self +end + + +--- Defines a menu slot to let the escort hold at their current position and stay low with a specified height during a specified time in seconds. +-- This menu will appear under **Hold position**. +-- @param #AI_ESCORT self +-- @param DCS#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. +-- @param DCS#Time Speed Optional parameter that lets the escort orbit with a specified speed. The default value is a speed that is average for the type of airplane or helicopter. +-- @param #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed. +-- @return #AI_ESCORT +function AI_ESCORT:MenuHoldAtEscortPosition( Height, Speed, MenuTextFormat ) + self:F( { Height, Speed, MenuTextFormat } ) + + if not Height then + Height = 30 + end + + if not Speed then + Speed = 0 + end + + local MenuText = "" + if not MenuTextFormat then + if Speed == 0 then + MenuText = string.format( "Hold at %d meter", Height ) + else + MenuText = string.format( "Hold at %d meter at %d", Height, Speed ) + end + else + if Speed == 0 then + MenuText = string.format( MenuTextFormat, Height ) + else + MenuText = string.format( MenuTextFormat, Height, Speed ) + end + end + + self.Menu.HoldAtEscortPosition = self.Menu.HoldAtEscortPosition or {} + self.Menu.HoldAtEscortPosition[#self.Menu.HoldAtEscortPosition+1] = {} + self.Menu.HoldAtEscortPosition[#self.Menu.HoldAtEscortPosition].Height = Height + self.Menu.HoldAtEscortPosition[#self.Menu.HoldAtEscortPosition].Speed = Speed + self.Menu.HoldAtEscortPosition[#self.Menu.HoldAtEscortPosition].MenuText = MenuText + + return self +end + + +function AI_ESCORT:SetFlightMenuHoldAtLeaderPosition() + + for _, MenuHoldAtLeaderPosition in pairs( self.Menu.HoldAtLeaderPosition ) do + local FlightMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", self.FlightMenu ) + + local FlightMenuHoldAtLeaderPosition = MENU_GROUP_COMMAND + :New( + self.PlayerGroup, + MenuHoldAtLeaderPosition.MenuText, + FlightMenuReportNavigation, + AI_ESCORT._FlightHoldPosition, + self, + self.PlayerGroup, + MenuHoldAtLeaderPosition.Height, + MenuHoldAtLeaderPosition.Speed + ) + end + + return self +end + +function AI_ESCORT:SetEscortMenuHoldAtLeaderPosition( EscortGroup ) + + for _, HoldAtLeaderPosition in pairs( self.Menu.HoldAtLeaderPosition ) do + if EscortGroup:IsAir() then + + local EscortGroupName = EscortGroup:GetName() + local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortGroup.EscortMenu ) + + local EscortMenuHoldAtLeaderPosition = MENU_GROUP_COMMAND + :New( + self.PlayerGroup, + HoldAtLeaderPosition.MenuText, + EscortMenuReportNavigation, + AI_ESCORT._HoldPosition, + self, + self.PlayerGroup, + EscortGroup, + HoldAtLeaderPosition.Height, + HoldAtLeaderPosition.Speed + ) + end + end + + return self +end + +--- Defines a menu slot to let the escort hold at the client position and stay low with a specified height during a specified time in seconds. +-- This menu will appear under **Navigation**. +-- @param #AI_ESCORT self +-- @param DCS#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. +-- @param DCS#Time Speed Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. +-- @param #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain one or two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed. +-- @return #AI_ESCORT +function AI_ESCORT:MenuHoldAtLeaderPosition( Height, Speed, MenuTextFormat ) + self:F( { Height, Speed, MenuTextFormat } ) + + if not Height then + Height = 30 + end + + if not Speed then + Speed = 0 + end + + local MenuText = "" + if not MenuTextFormat then + if Speed == 0 then + MenuText = string.format( "Rejoin and hold at %d meter", Height ) + else + MenuText = string.format( "Rejoin and hold at %d meter at %d", Height, Speed ) + end + else + if Speed == 0 then + MenuText = string.format( MenuTextFormat, Height ) + else + MenuText = string.format( MenuTextFormat, Height, Speed ) + end + end + + self.Menu.HoldAtLeaderPosition = self.Menu.HoldAtLeaderPosition or {} + self.Menu.HoldAtLeaderPosition[#self.Menu.HoldAtLeaderPosition+1] = {} + self.Menu.HoldAtLeaderPosition[#self.Menu.HoldAtLeaderPosition].Height = Height + self.Menu.HoldAtLeaderPosition[#self.Menu.HoldAtLeaderPosition].Speed = Speed + self.Menu.HoldAtLeaderPosition[#self.Menu.HoldAtLeaderPosition].MenuText = MenuText + + return self +end + +--- Defines a menu slot to let the escort scan for targets at a certain height for a certain time in seconds. +-- This menu will appear under **Scan targets**. +-- @param #AI_ESCORT self +-- @param DCS#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. +-- @param DCS#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. +-- @param #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain one or two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed. +-- @return #AI_ESCORT +function AI_ESCORT:MenuScanForTargets( Height, Seconds, MenuTextFormat ) + self:F( { Height, Seconds, MenuTextFormat } ) + + if self.EscortGroup:IsAir() then + if not self.EscortMenuScan then + self.EscortMenuScan = MENU_GROUP:New( self.PlayerGroup, "Scan for targets", self.EscortMenu ) + end + + if not Height then + Height = 100 + end + + if not Seconds then + Seconds = 30 + end + + local MenuText = "" + if not MenuTextFormat then + if Seconds == 0 then + MenuText = string.format( "At %d meter", Height ) + else + MenuText = string.format( "At %d meter for %d seconds", Height, Seconds ) + end + else + if Seconds == 0 then + MenuText = string.format( MenuTextFormat, Height ) + else + MenuText = string.format( MenuTextFormat, Height, Seconds ) + end + end + + if not self.EscortMenuScanForTargets then + self.EscortMenuScanForTargets = {} + end + + self.EscortMenuScanForTargets[#self.EscortMenuScanForTargets+1] = MENU_GROUP_COMMAND + :New( + self.PlayerGroup, + MenuText, + self.EscortMenuScan, + AI_ESCORT._ScanTargets, + self, + 30 + ) + end + + return self +end + + +function AI_ESCORT:SetFlightMenuFlare() + + for _, MenuFlare in pairs( self.Menu.Flare) do + local FlightMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", self.FlightMenu ) + local FlightMenuFlare = MENU_GROUP:New( self.PlayerGroup, MenuFlare.MenuText, FlightMenuReportNavigation ) + + local FlightMenuFlareGreenFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release green flare", FlightMenuFlare, AI_ESCORT._FlightFlare, self, FLARECOLOR.Green, "Released a green flare!" ) + local FlightMenuFlareRedFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release red flare", FlightMenuFlare, AI_ESCORT._FlightFlare, self, FLARECOLOR.Red, "Released a red flare!" ) + local FlightMenuFlareWhiteFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release white flare", FlightMenuFlare, AI_ESCORT._FlightFlare, self, FLARECOLOR.White, "Released a white flare!" ) + local FlightMenuFlareYellowFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release yellow flare", FlightMenuFlare, AI_ESCORT._FlightFlare, self, FLARECOLOR.Yellow, "Released a yellow flare!" ) + end + + return self +end + +function AI_ESCORT:SetEscortMenuFlare( EscortGroup ) + + for _, MenuFlare in pairs( self.Menu.Flare) do + if EscortGroup:IsAir() then + + local EscortGroupName = EscortGroup:GetName() + local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortGroup.EscortMenu ) + local EscortMenuFlare = MENU_GROUP:New( self.PlayerGroup, MenuFlare.MenuText, EscortMenuReportNavigation ) + + local EscortMenuFlareGreen = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release green flare", EscortMenuFlare, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.Green, "Released a green flare!" ) + local EscortMenuFlareRed = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release red flare", EscortMenuFlare, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.Red, "Released a red flare!" ) + local EscortMenuFlareWhite = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release white flare", EscortMenuFlare, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.White, "Released a white flare!" ) + local EscortMenuFlareYellow = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release yellow flare", EscortMenuFlare, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.Yellow, "Released a yellow flare!" ) + end + end + + return self +end + + + +--- Defines a menu slot to let the escort disperse a flare in a certain color. +-- This menu will appear under **Navigation**. +-- The flare will be fired from the first unit in the group. +-- @param #AI_ESCORT self +-- @param #string MenuTextFormat Optional parameter that shows the menu option text. If no text is given, the default text will be displayed. +-- @return #AI_ESCORT +function AI_ESCORT:MenuFlare( MenuTextFormat ) + self:F() + + local MenuText = "" + if not MenuTextFormat then + MenuText = "Flare" + else + MenuText = MenuTextFormat + end + + self.Menu.Flare = self.Menu.Flare or {} + self.Menu.Flare[#self.Menu.Flare+1] = {} + self.Menu.Flare[#self.Menu.Flare].MenuText = MenuText + + return self +end + + +function AI_ESCORT:SetFlightMenuSmoke() + + for _, MenuSmoke in pairs( self.Menu.Smoke) do + local FlightMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", self.FlightMenu ) + local FlightMenuSmoke = MENU_GROUP:New( self.PlayerGroup, MenuSmoke.MenuText, FlightMenuReportNavigation ) + + local FlightMenuSmokeGreenFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release green smoke", FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Green, "Releasing green smoke!" ) + local FlightMenuSmokeRedFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release red smoke", FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Red, "Releasing red smoke!" ) + local FlightMenuSmokeWhiteFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release white smoke", FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.White, "Releasing white smoke!" ) + local FlightMenuSmokeOrangeFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release orange smoke", FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Orange, "Releasing orange smoke!" ) + local FlightMenuSmokeBlueFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release blue smoke", FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Blue, "Releasing blue smoke!" ) + end + + return self +end + + +function AI_ESCORT:SetEscortMenuSmoke( EscortGroup ) + + for _, MenuSmoke in pairs( self.Menu.Smoke) do + if EscortGroup:IsAir() then + + local EscortGroupName = EscortGroup:GetName() + local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortGroup.EscortMenu ) + local EscortMenuSmoke = MENU_GROUP:New( self.PlayerGroup, MenuSmoke.MenuText, EscortMenuReportNavigation ) + + local EscortMenuSmokeGreen = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release green smoke", EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Green, "Releasing green smoke!" ) + local EscortMenuSmokeRed = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release red smoke", EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Red, "Releasing red smoke!" ) + local EscortMenuSmokeWhite = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release white smoke", EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.White, "Releasing white smoke!" ) + local EscortMenuSmokeOrange = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release orange smoke", EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Orange, "Releasing orange smoke!" ) + local EscortMenuSmokeBlue = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release blue smoke", EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Blue, "Releasing blue smoke!" ) + end + end + + return self +end + + +--- Defines a menu slot to let the escort disperse a smoke in a certain color. +-- This menu will appear under **Navigation**. +-- Note that smoke menu options will only be displayed for ships and ground units. Not for air units. +-- The smoke will be fired from the first unit in the group. +-- @param #AI_ESCORT self +-- @param #string MenuTextFormat Optional parameter that shows the menu option text. If no text is given, the default text will be displayed. +-- @return #AI_ESCORT +function AI_ESCORT:MenuSmoke( MenuTextFormat ) + self:F() + + local MenuText = "" + if not MenuTextFormat then + MenuText = "Smoke" + else + MenuText = MenuTextFormat + end + + self.Menu.Smoke = self.Menu.Smoke or {} + self.Menu.Smoke[#self.Menu.Smoke+1] = {} + self.Menu.Smoke[#self.Menu.Smoke].MenuText = MenuText + + return self +end + +function AI_ESCORT:SetFlightMenuReportType() + + local FlightMenuReportTargets = MENU_GROUP:New( self.PlayerGroup, "Report targets", self.FlightMenu ) + local MenuStamp = FlightMenuReportTargets:GetStamp() + + local FlightReportType = self:GetFlightReportType() + + if FlightReportType ~= self.__Enum.ReportType.All then + local FlightMenuReportTargetsAll = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report all targets", FlightMenuReportTargets, AI_ESCORT._FlightSwitchReportTypeAll, self ) + :SetTag( "ReportType" ) + :SetStamp( MenuStamp ) + end + + if FlightReportType == self.__Enum.ReportType.All or FlightReportType ~= self.__Enum.ReportType.Airborne then + local FlightMenuReportTargetsAirborne = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report airborne targets", FlightMenuReportTargets, AI_ESCORT._FlightSwitchReportTypeAirborne, self ) + :SetTag( "ReportType" ) + :SetStamp( MenuStamp ) + end + + if FlightReportType == self.__Enum.ReportType.All or FlightReportType ~= self.__Enum.ReportType.GroundRadar then + local FlightMenuReportTargetsGroundRadar = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report gound radar targets", FlightMenuReportTargets, AI_ESCORT._FlightSwitchReportTypeGroundRadar, self ) + :SetTag( "ReportType" ) + :SetStamp( MenuStamp ) + end + if FlightReportType == self.__Enum.ReportType.All or FlightReportType ~= self.__Enum.ReportType.Ground then + local FlightMenuReportTargetsGround = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report ground targets", FlightMenuReportTargets, AI_ESCORT._FlightSwitchReportTypeGround, self ) + :SetTag( "ReportType" ) + :SetStamp( MenuStamp ) + end + + FlightMenuReportTargets:RemoveSubMenus( MenuStamp, "ReportType" ) + +end + + +function AI_ESCORT:SetFlightMenuTargets() + + local FlightMenuReportTargets = MENU_GROUP:New( self.PlayerGroup, "Report targets", self.FlightMenu ) + + -- Report Targets + local FlightMenuReportTargetsNow = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets now!", FlightMenuReportTargets, AI_ESCORT._FlightReportNearbyTargetsNow, self ) + local FlightMenuReportTargetsOn = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets on", FlightMenuReportTargets, AI_ESCORT._FlightSwitchReportNearbyTargets, self, true ) + local FlightMenuReportTargetsOff = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets off", FlightMenuReportTargets, AI_ESCORT._FlightSwitchReportNearbyTargets, self, false ) + + -- Attack Targets + self.FlightMenuAttack = MENU_GROUP:New( self.PlayerGroup, "Attack targets", self.FlightMenu ) + local FlightMenuAttackNearby = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Attack nearest targets", self.FlightMenuAttack, AI_ESCORT._FlightAttackNearestTarget, self ):SetTag( "Attack" ) + local FlightMenuAttackNearbyAir = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Attack nearest airborne targets", self.FlightMenuAttack, AI_ESCORT._FlightAttackNearestTarget, self, self.__Enum.ReportType.Air ):SetTag( "Attack" ) + local FlightMenuAttackNearbyGround = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Attack nearest ground targets", self.FlightMenuAttack, AI_ESCORT._FlightAttackNearestTarget, self, self.__Enum.ReportType.Ground ):SetTag( "Attack" ) + + for _, MenuTargets in pairs( self.Menu.Targets) do + MenuTargets.FlightReportTargetsScheduler = SCHEDULER:New( self, self._FlightReportTargetsScheduler, {}, MenuTargets.Interval, MenuTargets.Interval ) + end + + return self +end + + +function AI_ESCORT:SetEscortMenuTargets( EscortGroup ) + + for _, MenuTargets in pairs( self.Menu.Targets) do + if EscortGroup:IsAir() then + local EscortGroupName = EscortGroup:GetName() + --local EscortMenuReportTargets = MENU_GROUP:New( self.PlayerGroup, "Report targets", EscortGroup.EscortMenu ) + + -- Report Targets + EscortGroup.EscortMenuReportNearbyTargetsNow = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets", EscortGroup.EscortMenu, AI_ESCORT._ReportNearbyTargetsNow, self, EscortGroup, true ) + --EscortGroup.EscortMenuReportNearbyTargetsOn = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets on", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._SwitchReportNearbyTargets, self, EscortGroup, true ) + --EscortGroup.EscortMenuReportNearbyTargetsOff = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets off", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._SwitchReportNearbyTargets, self, EscortGroup, false ) + + -- Attack Targets + --local EscortMenuAttackTargets = MENU_GROUP:New( self.PlayerGroup, "Attack targets", EscortGroup.EscortMenu ) + + EscortGroup.ReportTargetsScheduler = SCHEDULER:New( self, self._ReportTargetsScheduler, { EscortGroup }, 1, MenuTargets.Interval ) + EscortGroup.ResumeScheduler = SCHEDULER:New( self, self._ResumeScheduler, { EscortGroup }, 1, 60 ) + end + end + + return self +end + + + +--- Defines a menu slot to let the escort report their current detected targets with a specified time interval in seconds. +-- This menu will appear under **Report targets**. +-- Note that if a report targets menu is not specified, no targets will be detected by the escort, and the attack and assisted attack menus will not be displayed. +-- @param #AI_ESCORT self +-- @param DCS#Time Seconds Optional parameter that lets the escort report their current detected targets after specified time interval in seconds. The default time is 30 seconds. +-- @return #AI_ESCORT +function AI_ESCORT:MenuTargets( Seconds ) + self:F( { Seconds } ) + + if not Seconds then + Seconds = 30 + end + + self.Menu.Targets = self.Menu.Targets or {} + self.Menu.Targets[#self.Menu.Targets+1] = {} + self.Menu.Targets[#self.Menu.Targets].Interval = Seconds + + return self +end + +--- Defines a menu slot to let the escort attack its detected targets using assisted attack from another escort joined also with the client. +-- This menu will appear under **Request assistance from**. +-- Note that this method needs to be preceded with the method MenuTargets. +-- @param #AI_ESCORT self +-- @return #AI_ESCORT +function AI_ESCORT:MenuAssistedAttack() + self:F() + + self.EscortGroupSet:ForSomeGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + if not EscortGroup:IsAir() then + -- Request assistance from other escorts. + -- This is very useful to let f.e. an escorting ship attack a target detected by an escorting plane... + self.EscortMenuTargetAssistance = MENU_GROUP:New( self.PlayerGroup, "Request assistance from", EscortGroup.EscortMenu ) + end + end + ) + + return self +end + +function AI_ESCORT:SetFlightMenuROE() + + for _, MenuROE in pairs( self.Menu.ROE) do + local FlightMenuROE = MENU_GROUP:New( self.PlayerGroup, "Rule Of Engagement", self.FlightMenu ) + + local FlightMenuROEHoldFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Hold fire", FlightMenuROE, AI_ESCORT._FlightROEHoldFire, self, "Holding weapons!" ) + local FlightMenuROEReturnFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Return fire", FlightMenuROE, AI_ESCORT._FlightROEReturnFire, self, "Returning fire!" ) + local FlightMenuROEOpenFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Open Fire", FlightMenuROE, AI_ESCORT._FlightROEOpenFire, self, "Open fire at designated targets!" ) + local FlightMenuROEWeaponFree = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Engage all targets", FlightMenuROE, AI_ESCORT._FlightROEWeaponFree, self, "Engaging all targets!" ) + end + + return self +end + + +function AI_ESCORT:SetEscortMenuROE( EscortGroup ) + + for _, MenuROE in pairs( self.Menu.ROE) do + if EscortGroup:IsAir() then + + local EscortGroupName = EscortGroup:GetName() + local EscortMenuROE = MENU_GROUP:New( self.PlayerGroup, "Rule Of Engagement", EscortGroup.EscortMenu ) + + if EscortGroup:OptionROEHoldFirePossible() then + local EscortMenuROEHoldFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Hold fire", EscortMenuROE, AI_ESCORT._ROE, self, EscortGroup, EscortGroup.OptionROEHoldFire, "Holding weapons!" ) + end + if EscortGroup:OptionROEReturnFirePossible() then + local EscortMenuROEReturnFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Return fire", EscortMenuROE, AI_ESCORT._ROE, self, EscortGroup, EscortGroup.OptionROEReturnFire, "Returning fire!" ) + end + if EscortGroup:OptionROEOpenFirePossible() then + EscortGroup.EscortMenuROEOpenFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Open Fire", EscortMenuROE, AI_ESCORT._ROE, self, EscortGroup, EscortGroup.OptionROEOpenFire, "Opening fire on designated targets!!" ) + end + if EscortGroup:OptionROEWeaponFreePossible() then + EscortGroup.EscortMenuROEWeaponFree = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Engage all targets", EscortMenuROE, AI_ESCORT._ROE, self, EscortGroup, EscortGroup.OptionROEWeaponFree, "Opening fire on targets of opportunity!" ) + end + end + end + + return self +end + + +--- Defines a menu to let the escort set its rules of engagement. +-- All rules of engagement will appear under the menu **ROE**. +-- @param #AI_ESCORT self +-- @return #AI_ESCORT +function AI_ESCORT:MenuROE() + self:F() + + self.Menu.ROE = self.Menu.ROE or {} + self.Menu.ROE[#self.Menu.ROE+1] = {} + + return self +end + + +function AI_ESCORT:SetFlightMenuROT() + + for _, MenuROT in pairs( self.Menu.ROT) do + local FlightMenuROT = MENU_GROUP:New( self.PlayerGroup, "Reaction On Threat", self.FlightMenu ) + + local FlightMenuROTNoReaction = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Fight until death", FlightMenuROT, AI_ESCORT._FlightROTNoReaction, self, "Fighting until death!" ) + local FlightMenuROTPassiveDefense = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Use flares, chaff and jammers", FlightMenuROT, AI_ESCORT._FlightROTPassiveDefense, self, "Defending using jammers, chaff and flares!" ) + local FlightMenuROTEvadeFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Open fire", FlightMenuROT, AI_ESCORT._FlightROTEvadeFire, self, "Evading on enemy fire!" ) + local FlightMenuROTVertical = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Avoid radar and evade fire", FlightMenuROT, AI_ESCORT._FlightROTVertical, self, "Evading on enemy fire with vertical manoeuvres!" ) + end + + return self +end + + +function AI_ESCORT:SetEscortMenuROT( EscortGroup ) + + for _, MenuROT in pairs( self.Menu.ROT) do + if EscortGroup:IsAir() then + + local EscortGroupName = EscortGroup:GetName() + local EscortMenuROT = MENU_GROUP:New( self.PlayerGroup, "Reaction On Threat", EscortGroup.EscortMenu ) + + if not EscortGroup.EscortMenuEvasion then + -- Reaction to Threats + if EscortGroup:OptionROTNoReactionPossible() then + local EscortMenuEvasionNoReaction = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Fight until death", EscortMenuROT, AI_ESCORT._ROT, self, EscortGroup, EscortGroup.OptionROTNoReaction, "Fighting until death!" ) + end + if EscortGroup:OptionROTPassiveDefensePossible() then + local EscortMenuEvasionPassiveDefense = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Use flares, chaff and jammers", EscortMenuROT, AI_ESCORT._ROT, self, EscortGroup, EscortGroup.OptionROTPassiveDefense, "Defending using jammers, chaff and flares!" ) + end + if EscortGroup:OptionROTEvadeFirePossible() then + local EscortMenuEvasionEvadeFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Open fire", EscortMenuROT, AI_ESCORT._ROT, self, EscortGroup, EscortGroup.OptionROTEvadeFire, "Evading on enemy fire!" ) + end + if EscortGroup:OptionROTVerticalPossible() then + local EscortMenuOptionEvasionVertical = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Avoid radar and evade fire", EscortMenuROT, AI_ESCORT._ROT, self, EscortGroup, EscortGroup.OptionROTVertical, "Evading on enemy fire with vertical manoeuvres!" ) + end + end + end + end + + return self +end + + + +--- Defines a menu to let the escort set its evasion when under threat. +-- All rules of engagement will appear under the menu **Evasion**. +-- @param #AI_ESCORT self +-- @return #AI_ESCORT +function AI_ESCORT:MenuROT( MenuTextFormat ) + self:F( MenuTextFormat ) + + self.Menu.ROT = self.Menu.ROT or {} + self.Menu.ROT[#self.Menu.ROT+1] = {} + + return self +end + +--- Defines a menu to let the escort resume its mission from a waypoint on its route. +-- All rules of engagement will appear under the menu **Resume mission from**. +-- @param #AI_ESCORT self +-- @return #AI_ESCORT +function AI_ESCORT:SetEscortMenuResumeMission( EscortGroup ) + self:F() + + if EscortGroup:IsAir() then + local EscortGroupName = EscortGroup:GetName() + EscortGroup.EscortMenuResumeMission = MENU_GROUP:New( self.PlayerGroup, "Resume from", EscortGroup.EscortMenu ) + end + + return self +end + + +--- @param #AI_ESCORT self +-- @param Wrapper.Group#GROUP OrbitGroup +-- @param Wrapper.Group#GROUP EscortGroup +-- @param #number OrbitHeight +-- @param #number OrbitSeconds +function AI_ESCORT:_HoldPosition( OrbitGroup, EscortGroup, OrbitHeight, OrbitSeconds ) + + local EscortUnit = self.PlayerUnit + + local OrbitUnit = OrbitGroup:GetUnit(1) -- Wrapper.Unit#UNIT + + self:SetFlightModeMission( EscortGroup ) + + local PointFrom = {} + local GroupVec3 = EscortGroup:GetUnit(1):GetVec3() + PointFrom = {} + PointFrom.x = GroupVec3.x + PointFrom.y = GroupVec3.z + PointFrom.speed = 250 + PointFrom.type = AI.Task.WaypointType.TURNING_POINT + PointFrom.alt = GroupVec3.y + PointFrom.alt_type = AI.Task.AltitudeType.BARO + + local OrbitPoint = OrbitUnit:GetVec2() + local PointTo = {} + PointTo.x = OrbitPoint.x + PointTo.y = OrbitPoint.y + PointTo.speed = 250 + PointTo.type = AI.Task.WaypointType.TURNING_POINT + PointTo.alt = OrbitHeight + PointTo.alt_type = AI.Task.AltitudeType.BARO + PointTo.task = EscortGroup:TaskOrbitCircleAtVec2( OrbitPoint, OrbitHeight, 0 ) + + local Points = { PointFrom, PointTo } + + EscortGroup:OptionROEHoldFire() + EscortGroup:OptionROTPassiveDefense() + + EscortGroup:SetTask( EscortGroup:TaskRoute( Points ), 1 ) + EscortGroup:MessageTypeToGroup( "Orbiting at current location.", MESSAGE.Type.Information, EscortUnit:GetGroup() ) + +end + + +--- @param #AI_ESCORT self +-- @param Wrapper.Group#GROUP OrbitGroup +-- @param #number OrbitHeight +-- @param #number OrbitSeconds +function AI_ESCORT:_FlightHoldPosition( OrbitGroup, OrbitHeight, OrbitSeconds ) + + local EscortUnit = self.PlayerUnit + + self.EscortGroupSet:ForEachGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup, OrbitGroup ) + if EscortGroup:IsAir() then + if OrbitGroup == nil then + OrbitGroup = EscortGroup + end + self:_HoldPosition( OrbitGroup, EscortGroup, OrbitHeight, OrbitSeconds ) + end + end, OrbitGroup + ) + +end + + + +function AI_ESCORT:_JoinUp( EscortGroup ) + + local EscortUnit = self.PlayerUnit + + self:SetFlightModeFormation( EscortGroup ) + + EscortGroup:MessageTypeToGroup( "Joining up!", MESSAGE.Type.Information, EscortUnit:GetGroup() ) +end + + +function AI_ESCORT:_FlightJoinUp() + + self.EscortGroupSet:ForEachGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + if EscortGroup:IsAir() then + self:_JoinUp( EscortGroup ) + end + end + ) + +end + + +--- Lets the escort to join in a trail formation. +-- @param #AI_ESCORT self +-- @param #number XStart The start position on the X-axis in meters for the first group. +-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. +-- @param #nubmer YStart The start position on the Y-axis in meters for the first group. +-- @return #AI_ESCORT +function AI_ESCORT:_EscortFormationTrail( EscortGroup, XStart, XSpace, YStart ) + + self:FormationTrail( XStart, XSpace, YStart ) + +end + + +function AI_ESCORT:_FlightFormationTrail( XStart, XSpace, YStart ) + + self.EscortGroupSet:ForEachGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + if EscortGroup:IsAir() then + self:_EscortFormationTrail( EscortGroup, XStart, XSpace, YStart ) + end + end + ) + +end + +--- Lets the escort to join in a stacked formation. +-- @param #AI_ESCORT self +-- @param #number XStart The start position on the X-axis in meters for the first group. +-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. +-- @param #number YStart The start position on the Y-axis in meters for the first group. +-- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. +-- @return #AI_ESCORT +function AI_ESCORT:_EscortFormationStack( EscortGroup, XStart, XSpace, YStart, YSpace ) + + self:FormationStack( XStart, XSpace, YStart, YSpace ) + +end + + +function AI_ESCORT:_FlightFormationStack( XStart, XSpace, YStart, YSpace ) + + self.EscortGroupSet:ForEachGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + if EscortGroup:IsAir() then + self:_EscortFormationStack( EscortGroup, XStart, XSpace, YStart, YSpace ) + end + end + ) + +end + + +function AI_ESCORT:_Flare( EscortGroup, Color, Message ) + + local EscortUnit = self.PlayerUnit + + EscortGroup:GetUnit(1):Flare( Color ) + EscortGroup:MessageTypeToGroup( Message, MESSAGE.Type.Information, EscortUnit:GetGroup() ) +end + + +function AI_ESCORT:_FlightFlare( Color, Message ) + + self.EscortGroupSet:ForEachGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + if EscortGroup:IsAir() then + self:_Flare( EscortGroup, Color, Message ) + end + end + ) + +end + + + +function AI_ESCORT:_Smoke( EscortGroup, Color, Message ) + + local EscortUnit = self.PlayerUnit + + EscortGroup:GetUnit(1):Smoke( Color ) + EscortGroup:MessageTypeToGroup( Message, MESSAGE.Type.Information, EscortUnit:GetGroup() ) +end + +function AI_ESCORT:_FlightSmoke( Color, Message ) + + self.EscortGroupSet:ForEachGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + if EscortGroup:IsAir() then + self:_Smoke( EscortGroup, Color, Message ) + end + end + ) + +end + + +function AI_ESCORT:_ReportNearbyTargetsNow( EscortGroup ) + + local EscortUnit = self.PlayerUnit + + self:_ReportTargetsScheduler( EscortGroup ) + +end + + +function AI_ESCORT:_FlightReportNearbyTargetsNow() + + self:_FlightReportTargetsScheduler() + +end + + + +function AI_ESCORT:_FlightSwitchReportNearbyTargets( ReportTargets ) + + self.EscortGroupSet:ForEachGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + if EscortGroup:IsAir() then + self:_EscortSwitchReportNearbyTargets( EscortGroup, ReportTargets ) + end + end + ) + +end + +function AI_ESCORT:SetFlightReportType( ReportType ) + + self.FlightReportType = ReportType + +end + +function AI_ESCORT:GetFlightReportType() + + return self.FlightReportType + +end + +function AI_ESCORT:_FlightSwitchReportTypeAll() + + self:SetFlightReportType( self.__Enum.ReportType.All ) + self:SetFlightMenuReportType() + + local EscortGroup = self.EscortGroupSet:GetFirst() + EscortGroup:MessageTypeToGroup( "Reporting all targets.", MESSAGE.Type.Information, self.PlayerGroup ) + +end + +function AI_ESCORT:_FlightSwitchReportTypeAirborne() + + self:SetFlightReportType( self.__Enum.ReportType.Airborne ) + self:SetFlightMenuReportType() + + local EscortGroup = self.EscortGroupSet:GetFirst() + EscortGroup:MessageTypeToGroup( "Reporting airborne targets.", MESSAGE.Type.Information, self.PlayerGroup ) + +end + +function AI_ESCORT:_FlightSwitchReportTypeGroundRadar() + + self:SetFlightReportType( self.__Enum.ReportType.Ground ) + self:SetFlightMenuReportType() + + local EscortGroup = self.EscortGroupSet:GetFirst() + EscortGroup:MessageTypeToGroup( "Reporting ground radar targets.", MESSAGE.Type.Information, self.PlayerGroup ) + +end + +function AI_ESCORT:_FlightSwitchReportTypeGround() + + self:SetFlightReportType( self.__Enum.ReportType.Ground ) + self:SetFlightMenuReportType() + + local EscortGroup = self.EscortGroupSet:GetFirst() + EscortGroup:MessageTypeToGroup( "Reporting ground targets.", MESSAGE.Type.Information, self.PlayerGroup ) + +end + + +function AI_ESCORT:_ScanTargets( ScanDuration ) + + local EscortGroup = self.EscortGroup -- Wrapper.Group#GROUP + local EscortUnit = self.PlayerUnit + + self.FollowScheduler:Stop( self.FollowSchedule ) + + if EscortGroup:IsHelicopter() then + EscortGroup:PushTask( + EscortGroup:TaskControlled( + EscortGroup:TaskOrbitCircle( 200, 20 ), + EscortGroup:TaskCondition( nil, nil, nil, nil, ScanDuration, nil ) + ), 1 ) + elseif EscortGroup:IsAirPlane() then + EscortGroup:PushTask( + EscortGroup:TaskControlled( + EscortGroup:TaskOrbitCircle( 1000, 500 ), + EscortGroup:TaskCondition( nil, nil, nil, nil, ScanDuration, nil ) + ), 1 ) + end + + EscortGroup:MessageToClient( "Scanning targets for " .. ScanDuration .. " seconds.", ScanDuration, EscortUnit ) + + if self.EscortMode == AI_ESCORT.MODE.FOLLOW then + self.FollowScheduler:Start( self.FollowSchedule ) + end + +end + +--- @param Wrapper.Group#GROUP EscortGroup +-- @param #AI_ESCORT self +function AI_ESCORT.___Resume( EscortGroup, self ) + + self:F( { self=self } ) + + local PlayerGroup = self.PlayerGroup + + EscortGroup:OptionROEHoldFire() + EscortGroup:OptionROTVertical() + + EscortGroup:SetState( EscortGroup, "Mode", EscortGroup:GetState( EscortGroup, "PreviousMode" ) ) + + if EscortGroup:GetState( EscortGroup, "Mode" ) == self.__Enum.Mode.Mission then + EscortGroup:MessageTypeToGroup( "Resuming route.", MESSAGE.Type.Information, PlayerGroup ) + else + EscortGroup:MessageTypeToGroup( "Rejoining formation.", MESSAGE.Type.Information, PlayerGroup ) + end + +end + + +--- @param #AI_ESCORT self +-- @param Wrapper.Group#GROUP EscortGroup +-- @param #number WayPoint +function AI_ESCORT:_ResumeMission( EscortGroup, WayPoint ) + + --self.FollowScheduler:Stop( self.FollowSchedule ) + + self:SetFlightModeMission( EscortGroup ) + + local WayPoints = EscortGroup.MissionRoute + self:T( WayPoint, WayPoints ) + + for WayPointIgnore = 1, WayPoint do + table.remove( WayPoints, 1 ) + end + + EscortGroup:SetTask( EscortGroup:TaskRoute( WayPoints ), 1 ) + + EscortGroup:MessageTypeToGroup( "Resuming mission from waypoint ", MESSAGE.Type.Information, self.PlayerGroup ) +end + + +--- @param #AI_ESCORT self +-- @param Wrapper.Group#GROUP EscortGroup The escort group that will attack the detected item. +-- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem +function AI_ESCORT:_AttackTarget( EscortGroup, DetectedItem ) + + self:F( EscortGroup ) + + self:SetFlightModeAttack( EscortGroup ) + + if EscortGroup:IsAir() then + EscortGroup:OptionROEOpenFire() + EscortGroup:OptionROTVertical() + EscortGroup:SetState( EscortGroup, "Escort", self ) + + local DetectedSet = self.Detection:GetDetectedItemSet( DetectedItem ) + + local Tasks = {} + local AttackUnitTasks = {} + + DetectedSet:ForEachUnit( + --- @param Wrapper.Unit#UNIT DetectedUnit + function( DetectedUnit, Tasks ) + if DetectedUnit:IsAlive() then + AttackUnitTasks[#AttackUnitTasks+1] = EscortGroup:TaskAttackUnit( DetectedUnit ) + end + end, Tasks + ) + + Tasks[#Tasks+1] = EscortGroup:TaskCombo( AttackUnitTasks ) + Tasks[#Tasks+1] = EscortGroup:TaskFunction( "AI_ESCORT.___Resume", self ) + + EscortGroup:PushTask( + EscortGroup:TaskCombo( + Tasks + ), 1 + ) + + else + + local DetectedSet = self.Detection:GetDetectedItemSet( DetectedItem ) + + local Tasks = {} + + DetectedSet:ForEachUnit( + --- @param Wrapper.Unit#UNIT DetectedUnit + function( DetectedUnit, Tasks ) + if DetectedUnit:IsAlive() then + Tasks[#Tasks+1] = EscortGroup:TaskFireAtPoint( DetectedUnit:GetVec2(), 50 ) + end + end, Tasks + ) + + EscortGroup:PushTask( + EscortGroup:TaskCombo( + Tasks + ), 1 + ) + + end + + local DetectedTargetsReport = REPORT:New( "Engaging target:\n" ) + local DetectedItemReportSummary = self.Detection:DetectedItemReportSummary( DetectedItem, self.PlayerGroup, _DATABASE:GetPlayerSettings( self.PlayerUnit:GetPlayerName() ) ) + local ReportSummary = DetectedItemReportSummary:Text(", ") + DetectedTargetsReport:AddIndent( ReportSummary, "-" ) + + EscortGroup:MessageTypeToGroup( DetectedTargetsReport:Text(), MESSAGE.Type.Information, self.PlayerGroup ) +end + + +function AI_ESCORT:_FlightAttackTarget( DetectedItem ) + + self.EscortGroupSet:ForEachGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup, DetectedItem ) + if EscortGroup:IsAir() then + self:_AttackTarget( EscortGroup, DetectedItem ) + end + end, DetectedItem + ) + +end + + +function AI_ESCORT:_FlightAttackNearestTarget( TargetType ) + + self.Detection:Detect() + self:_FlightReportTargetsScheduler() + + local EscortGroup = self.EscortGroupSet:GetFirst() + local AttackDetectedItem = nil + local DetectedItems = self.Detection:GetDetectedItems() + + for DetectedItemIndex, DetectedItem in UTILS.spairs( DetectedItems, function( t, a, b ) return self:Distance( self.PlayerUnit, t[a] ) < self:Distance( self.PlayerUnit, t[b] ) end ) do + + local DetectedItemSet = self.Detection:GetDetectedItemSet( DetectedItem ) + + local HasGround = DetectedItemSet:HasGroundUnits() > 0 + local HasAir = DetectedItemSet:HasAirUnits() > 0 + + local FlightReportType = self:GetFlightReportType() + + if ( TargetType and TargetType == self.__Enum.ReportType.Ground and HasGround ) or + ( TargetType and TargetType == self.__Enum.ReportType.Air and HasAir ) or + ( TargetType == nil ) then + AttackDetectedItem = DetectedItem + break + end + end + + if AttackDetectedItem then + self:_FlightAttackTarget( AttackDetectedItem ) + else + EscortGroup:MessageTypeToGroup( "Nothing to attack!", MESSAGE.Type.Information, self.PlayerGroup ) + end + +end + + +--- +--- @param #AI_ESCORT self +-- @param Wrapper.Group#GROUP EscortGroup The escort group that will attack the detected item. +-- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem +function AI_ESCORT:_AssistTarget( EscortGroup, DetectedItem ) + + local EscortUnit = self.PlayerUnit + + local DetectedSet = self.Detection:GetDetectedItemSet( DetectedItem ) + + local Tasks = {} + + DetectedSet:ForEachUnit( + --- @param Wrapper.Unit#UNIT DetectedUnit + function( DetectedUnit, Tasks ) + if DetectedUnit:IsAlive() then + Tasks[#Tasks+1] = EscortGroup:TaskFireAtPoint( DetectedUnit:GetVec2(), 50 ) + end + end, Tasks + ) + + EscortGroup:SetTask( + EscortGroup:TaskCombo( + Tasks + ), 1 + ) + + + EscortGroup:MessageTypeToGroup( "Assisting attack!", MESSAGE.Type.Information, EscortUnit:GetGroup() ) + +end + +function AI_ESCORT:_ROE( EscortGroup, EscortROEFunction, EscortROEMessage ) + pcall( function() EscortROEFunction( EscortGroup ) end ) + EscortGroup:MessageTypeToGroup( EscortROEMessage, MESSAGE.Type.Information, self.PlayerGroup ) +end + + +function AI_ESCORT:_FlightROEHoldFire( EscortROEMessage ) + self.EscortGroupSet:ForEachGroupAlive( + --- @param Wrapper.Group#GROUP EscortGroup + function( EscortGroup ) + self:_ROE( EscortGroup, EscortGroup.OptionROEHoldFire, EscortROEMessage ) + end + ) +end + +function AI_ESCORT:_FlightROEOpenFire( EscortROEMessage ) + self.EscortGroupSet:ForEachGroupAlive( + --- @param Wrapper.Group#GROUP EscortGroup + function( EscortGroup ) + self:_ROE( EscortGroup, EscortGroup.OptionROEOpenFire, EscortROEMessage ) + end + ) +end + +function AI_ESCORT:_FlightROEReturnFire( EscortROEMessage ) + self.EscortGroupSet:ForEachGroupAlive( + --- @param Wrapper.Group#GROUP EscortGroup + function( EscortGroup ) + self:_ROE( EscortGroup, EscortGroup.OptionROEReturnFire, EscortROEMessage ) + end + ) +end + +function AI_ESCORT:_FlightROEWeaponFree( EscortROEMessage ) + self.EscortGroupSet:ForEachGroupAlive( + --- @param Wrapper.Group#GROUP EscortGroup + function( EscortGroup ) + self:_ROE( EscortGroup, EscortGroup.OptionROEWeaponFree, EscortROEMessage ) + end + ) +end + + +function AI_ESCORT:_ROT( EscortGroup, EscortROTFunction, EscortROTMessage ) + pcall( function() EscortROTFunction( EscortGroup ) end ) + EscortGroup:MessageTypeToGroup( EscortROTMessage, MESSAGE.Type.Information, self.PlayerGroup ) +end + + +function AI_ESCORT:_FlightROTNoReaction( EscortROTMessage ) + self.EscortGroupSet:ForEachGroupAlive( + --- @param Wrapper.Group#GROUP EscortGroup + function( EscortGroup ) + self:_ROT( EscortGroup, EscortGroup.OptionROTNoReaction, EscortROTMessage ) + end + ) +end + +function AI_ESCORT:_FlightROTPassiveDefense( EscortROTMessage ) + self.EscortGroupSet:ForEachGroupAlive( + --- @param Wrapper.Group#GROUP EscortGroup + function( EscortGroup ) + self:_ROT( EscortGroup, EscortGroup.OptionROTPassiveDefense, EscortROTMessage ) + end + ) +end + +function AI_ESCORT:_FlightROTEvadeFire( EscortROTMessage ) + self.EscortGroupSet:ForEachGroupAlive( + --- @param Wrapper.Group#GROUP EscortGroup + function( EscortGroup ) + self:_ROT( EscortGroup, EscortGroup.OptionROTEvadeFire, EscortROTMessage ) + end + ) +end + +function AI_ESCORT:_FlightROTVertical( EscortROTMessage ) + self.EscortGroupSet:ForEachGroupAlive( + --- @param Wrapper.Group#GROUP EscortGroup + function( EscortGroup ) + self:_ROT( EscortGroup, EscortGroup.OptionROTVertical, EscortROTMessage ) + end + ) +end + +--- Registers the waypoints +-- @param #AI_ESCORT self +-- @return #table +function AI_ESCORT:RegisterRoute() + self:F() + + local EscortGroup = self.EscortGroup -- Wrapper.Group#GROUP + + local TaskPoints = EscortGroup:GetTaskRoute() + + self:T( TaskPoints ) + + return TaskPoints +end + +--- Resume Scheduler. +-- @param #AI_ESCORT self +-- @param Wrapper.Group#GROUP EscortGroup +function AI_ESCORT:_ResumeScheduler( EscortGroup ) + self:F( EscortGroup:GetName() ) + + if EscortGroup:IsAlive() and self.PlayerUnit:IsAlive() then + + + local EscortGroupName = EscortGroup:GetCallsign() + + if EscortGroup.EscortMenuResumeMission then + EscortGroup.EscortMenuResumeMission:RemoveSubMenus() + + local TaskPoints = EscortGroup.MissionRoute + + for WayPointID, WayPoint in pairs( TaskPoints ) do + local EscortVec3 = EscortGroup:GetVec3() + local Distance = ( ( WayPoint.x - EscortVec3.x )^2 + + ( WayPoint.y - EscortVec3.z )^2 + ) ^ 0.5 / 1000 + MENU_GROUP_COMMAND:New( self.PlayerGroup, "Waypoint " .. WayPointID .. " at " .. string.format( "%.2f", Distance ).. "km", EscortGroup.EscortMenuResumeMission, AI_ESCORT._ResumeMission, self, EscortGroup, WayPointID ) + end + end + end +end + + +--- Measure distance between coordinate player and coordinate detected item. +-- @param #AI_ESCORT self +function AI_ESCORT:Distance( PlayerUnit, DetectedItem ) + + local DetectedCoordinate = self.Detection:GetDetectedItemCoordinate( DetectedItem ) + local PlayerCoordinate = PlayerUnit:GetCoordinate() + + return DetectedCoordinate:Get3DDistance( PlayerCoordinate ) + +end + +--- Report Targets Scheduler. +-- @param #AI_ESCORT self +-- @param Wrapper.Group#GROUP EscortGroup +function AI_ESCORT:_ReportTargetsScheduler( EscortGroup, Report ) + self:F( EscortGroup:GetName() ) + + if EscortGroup:IsAlive() and self.PlayerUnit:IsAlive() then + + local EscortGroupName = EscortGroup:GetCallsign() + + local DetectedTargetsReport = REPORT:New( "Reporting targets:\n" ) -- A new report to display the detected targets as a message to the player. + + + if EscortGroup.EscortMenuTargetAssistance then + EscortGroup.EscortMenuTargetAssistance:RemoveSubMenus() + end + + local DetectedItems = self.Detection:GetDetectedItems() + + local ClientEscortTargets = self.Detection + + local TimeUpdate = timer.getTime() + + local EscortMenuAttackTargets = MENU_GROUP:New( self.PlayerGroup, "Attack targets", EscortGroup.EscortMenu ) + + local DetectedTargets = false + + for DetectedItemIndex, DetectedItem in UTILS.spairs( DetectedItems, function( t, a, b ) return self:Distance( self.PlayerUnit, t[a] ) < self:Distance( self.PlayerUnit, t[b] ) end ) do + --for DetectedItemIndex, DetectedItem in pairs( DetectedItems ) do + + local DetectedItemSet = self.Detection:GetDetectedItemSet( DetectedItem ) + + local HasGround = DetectedItemSet:HasGroundUnits() > 0 + local HasGroundRadar = HasGround and DetectedItemSet:HasRadar() > 0 + local HasAir = DetectedItemSet:HasAirUnits() > 0 + + local FlightReportType = self:GetFlightReportType() + + + if ( FlightReportType == self.__Enum.ReportType.All ) or + ( FlightReportType == self.__Enum.ReportType.Airborne and HasAir ) or + ( FlightReportType == self.__Enum.ReportType.Ground and HasGround ) or + ( FlightReportType == self.__Enum.ReportType.GroundRadar and HasGroundRadar ) then + + DetectedTargets = true + + local DetectedMenu = self.Detection:DetectedItemReportMenu( DetectedItem, EscortGroup, _DATABASE:GetPlayerSettings( self.PlayerUnit:GetPlayerName() ) ):Text("\n") + + local DetectedItemReportSummary = self.Detection:DetectedItemReportSummary( DetectedItem, EscortGroup, _DATABASE:GetPlayerSettings( self.PlayerUnit:GetPlayerName() ) ) + local ReportSummary = DetectedItemReportSummary:Text(", ") + DetectedTargetsReport:AddIndent( ReportSummary, "-" ) + + if EscortGroup:IsAir() then + + MENU_GROUP_COMMAND:New( self.PlayerGroup, + DetectedMenu, + EscortMenuAttackTargets, + AI_ESCORT._AttackTarget, + self, + EscortGroup, + DetectedItem + ):SetTag( "Escort" ):SetTime( TimeUpdate ) + else + if self.EscortMenuTargetAssistance then + local MenuTargetAssistance = MENU_GROUP:New( self.PlayerGroup, EscortGroupName, EscortGroup.EscortMenuTargetAssistance ) + MENU_GROUP_COMMAND:New( self.PlayerGroup, + DetectedMenu, + MenuTargetAssistance, + AI_ESCORT._AssistTarget, + self, + EscortGroup, + DetectedItem + ) + end + end + end + end + + EscortMenuAttackTargets:RemoveSubMenus( TimeUpdate, "Escort" ) + + if Report then + if DetectedTargets then + EscortGroup:MessageTypeToGroup( DetectedTargetsReport:Text( "\n" ), MESSAGE.Type.Information, self.PlayerGroup ) + else + EscortGroup:MessageTypeToGroup( "No targets detected.", MESSAGE.Type.Information, self.PlayerGroup ) + end + end + + return true + end + + return false +end + +--- Report Targets Scheduler for the flight. The report is generated from the perspective of the player plane, and is reported by the first plane in the formation set. +-- @param #AI_ESCORT self +-- @param Wrapper.Group#GROUP EscortGroup +function AI_ESCORT:_FlightReportTargetsScheduler() + + self:F("FlightReportTargetScheduler") + + local EscortGroup = self.EscortGroupSet:GetFirst() -- Wrapper.Group#GROUP + + local DetectedTargetsReport = REPORT:New( "Reporting your targets:\n" ) -- A new report to display the detected targets as a message to the player. + + if EscortGroup and ( self.PlayerUnit:IsAlive() and EscortGroup:IsAlive() ) then + + local TimeUpdate = timer.getTime() + + local DetectedItems = self.Detection:GetDetectedItems() + + local DetectedTargets = false + + local ClientEscortTargets = self.Detection + + for DetectedItemIndex, DetectedItem in UTILS.spairs( DetectedItems, function( t, a, b ) return self:Distance( self.PlayerUnit, t[a] ) < self:Distance( self.PlayerUnit, t[b] ) end ) do + + self:F("FlightReportTargetScheduler Targets") + + local DetectedItemSet = self.Detection:GetDetectedItemSet( DetectedItem ) + + local HasGround = DetectedItemSet:HasGroundUnits() > 0 + local HasGroundRadar = HasGround and DetectedItemSet:HasRadar() > 0 + local HasAir = DetectedItemSet:HasAirUnits() > 0 + + local FlightReportType = self:GetFlightReportType() + + + if ( FlightReportType == self.__Enum.ReportType.All ) or + ( FlightReportType == self.__Enum.ReportType.Airborne and HasAir ) or + ( FlightReportType == self.__Enum.ReportType.Ground and HasGround ) or + ( FlightReportType == self.__Enum.ReportType.GroundRadar and HasGroundRadar ) then + + + DetectedTargets = true -- There are detected targets, when the content of the for loop is executed. We use it to display a message. + + local DetectedItemReportMenu = self.Detection:DetectedItemReportMenu( DetectedItem, self.PlayerGroup, _DATABASE:GetPlayerSettings( self.PlayerUnit:GetPlayerName() ) ) + local ReportMenuText = DetectedItemReportMenu:Text(", ") + + MENU_GROUP_COMMAND:New( self.PlayerGroup, + ReportMenuText, + self.FlightMenuAttack, + AI_ESCORT._FlightAttackTarget, + self, + DetectedItem + ):SetTag( "Flight" ):SetTime( TimeUpdate ) + + local DetectedItemReportSummary = self.Detection:DetectedItemReportSummary( DetectedItem, self.PlayerGroup, _DATABASE:GetPlayerSettings( self.PlayerUnit:GetPlayerName() ) ) + local ReportSummary = DetectedItemReportSummary:Text(", ") + DetectedTargetsReport:AddIndent( ReportSummary, "-" ) + end + end + + self.FlightMenuAttack:RemoveSubMenus( TimeUpdate, "Flight" ) + + if DetectedTargets then + EscortGroup:MessageTypeToGroup( DetectedTargetsReport:Text( "\n" ), MESSAGE.Type.Information, self.PlayerGroup ) +-- else +-- EscortGroup:MessageTypeToGroup( "No targets detected.", MESSAGE.Type.Information, self.PlayerGroup ) + end + + return true + end + + return false +end + + +--- **Functional** -- Taking the lead of AI escorting your flight or of other AI, upon request using the menu. +-- +-- === +-- +-- ## Features: +-- +-- * Escort navigation commands. +-- * Escort hold at position commands. +-- * Escorts reporting detected targets. +-- * Escorts scanning targets in advance. +-- * Escorts attacking specific targets. +-- * Request assistance from other groups for attack. +-- * Manage rule of engagement of escorts. +-- * Manage the allowed evasion techniques of escorts. +-- * Make escort to execute a defined mission or path. +-- * Escort tactical situation reporting. +-- +-- === +-- +-- ## Missions: +-- +-- [ESC - Escorting](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/ESC%20-%20Escorting) +-- +-- === +-- +-- Allows you to interact with escorting AI on your flight and take the lead. +-- +-- Each escorting group can be commanded with a complete set of radio commands (radio menu in your flight, and then F10). +-- +-- The radio commands will vary according the category of the group. The richest set of commands are with helicopters and airPlanes. +-- Ships and Ground troops will have a more limited set, but they can provide support through the bombing of targets designated by the other escorts. +-- +-- Escorts detect targets using a built-in detection mechanism. The detected targets are reported at a specified time interval. +-- Once targets are reported, each escort has these targets as menu options to command the attack of these targets. +-- Targets are by default grouped per area of 5000 meters, but the kind of detection and the grouping range can be altered. +-- +-- Different formations can be selected in the Flight menu: Trail, Stack, Left Line, Right Line, Left Wing, Right Wing, Central Wing and Boxed formations are available. +-- The Flight menu also allows for a mass attack, where all of the escorts are commanded to attack a target. +-- +-- Escorts can emit flares to reports their location. They can be commanded to hold at a location, which can be their current or the leader location. +-- In this way, you can spread out the escorts over the battle field before a coordinated attack. +-- +-- But basically, the escort class provides 4 modes of operation, and depending on the mode, you are either leading the flight, or following the flight. +-- +-- ## Leading the flight +-- +-- When leading the flight, you are expected to guide the escorts towards the target areas, +-- and carefully coordinate the attack based on the threat levels reported, and the available weapons +-- carried by the escorts. Ground ships or ground troops can execute A-assisted attacks, when they have long-range ground precision weapons for attack. +-- +-- ## Following the flight +-- +-- Escorts can be commanded to execute a specific mission path. In this mode, the escorts are in the lead. +-- You as a player, are following the escorts, and are commanding them to progress the mission while +-- ensuring that the escorts survive. You are joining the escorts in the battlefield. They will detect and report targets +-- and you will ensure that the attacks are well coordinated, assigning the correct escort type for the detected target +-- type. Once the attack is finished, the escort will resume the mission it was assigned. +-- In other words, you can use the escorts for reconnaissance, and for guiding the attack. +-- Imagine you as a mi-8 pilot, assigned to pickup cargo. Two ka-50s are guiding the way, and you are +-- following. You are in control. The ka-50s detect targets, report them, and you command how the attack +-- will commence and from where. You can control where the escorts are holding position and which targets +-- are attacked first. You are in control how the ka-50s will follow their mission path. +-- +-- Escorts can act as part of a AI A2G dispatcher offensive. In this way, You was a player are in control. +-- The mission is defined by the A2G dispatcher, and you are responsible to join the flight and ensure that the +-- attack is well coordinated. +-- +-- It is with great proud that I present you this class, and I hope you will enjoy the functionality and the dynamism +-- it brings in your DCS world simulations. +-- +-- # RADIO MENUs that can be created: +-- +-- Find a summary below of the current available commands: +-- +-- ## Navigation ...: +-- +-- Escort group navigation functions: +-- +-- * **"Join-Up":** The escort group fill follow you in the assigned formation. +-- * **"Flare":** Provides menu commands to let the escort group shoot a flare in the air in a color. +-- * **"Smoke":** Provides menu commands to let the escort group smoke the air in a color. Note that smoking is only available for ground and naval troops. +-- +-- ## Hold position ...: +-- +-- Escort group navigation functions: +-- +-- * **"At current location":** The escort group will hover above the ground at the position they were. The altitude can be specified as a parameter. +-- * **"At my location":** The escort group will hover or orbit at the position where you are. The escort will fly to your location and hold position. The altitude can be specified as a parameter. +-- +-- ## Report targets ...: +-- +-- Report targets will make the escort group to report any target that it identifies within detection range. Any detected target can be attacked using the "Attack Targets" menu function. (see below). +-- +-- * **"Report now":** Will report the current detected targets. +-- * **"Report targets on":** Will make the escorts to report the detected targets and will fill the "Attack Targets" menu list. +-- * **"Report targets off":** Will stop detecting targets. +-- +-- ## Attack targets ...: +-- +-- This menu item will list all detected targets within a 15km range. Depending on the level of detection (known/unknown) and visuality, the targets type will also be listed. +-- This menu will be available in Flight menu or in each Escort menu. +-- +-- ## Scan targets ...: +-- +-- Menu items to pop-up the escort group for target scanning. After scanning, the escort group will resume with the mission or rejoin formation. +-- +-- * **"Scan targets 30 seconds":** Scan 30 seconds for targets. +-- * **"Scan targets 60 seconds":** Scan 60 seconds for targets. +-- +-- ## Request assistance from ...: +-- +-- This menu item will list all detected targets within a 15km range, similar as with the menu item **Attack Targets**. +-- This menu item allows to request attack support from other ground based escorts supporting the current escort. +-- eg. the function allows a player to request support from the Ship escort to attack a target identified by the Plane escort with its Tomahawk missiles. +-- eg. the function allows a player to request support from other Planes escorting to bomb the unit with illumination missiles or bombs, so that the main plane escort can attack the area. +-- +-- ## ROE ...: +-- +-- Sets the Rules of Engagement (ROE) of the escort group when in flight. +-- +-- * **"Hold Fire":** The escort group will hold fire. +-- * **"Return Fire":** The escort group will return fire. +-- * **"Open Fire":** The escort group will open fire on designated targets. +-- * **"Weapon Free":** The escort group will engage with any target. +-- +-- ## Evasion ...: +-- +-- Will define the evasion techniques that the escort group will perform during flight or combat. +-- +-- * **"Fight until death":** The escort group will have no reaction to threats. +-- * **"Use flares, chaff and jammers":** The escort group will use passive defense using flares and jammers. No evasive manoeuvres are executed. +-- * **"Evade enemy fire":** The rescort group will evade enemy fire before firing. +-- * **"Go below radar and evade fire":** The escort group will perform evasive vertical manoeuvres. +-- +-- ## Resume Mission ...: +-- +-- Escort groups can have their own mission. This menu item will allow the escort group to resume their Mission from a given waypoint. +-- Note that this is really fantastic, as you now have the dynamic of taking control of the escort groups, and allowing them to resume their path or mission. +-- +-- === +-- +-- ### Authors: **FlightControl** +-- +-- === +-- +-- @module AI.AI_Escort +-- @image Escorting.JPG + + + +--- @type AI_ESCORT_REQUEST +-- @extends AI.AI_Escort#AI_ESCORT + +--- AI_ESCORT_REQUEST class +-- +-- # AI_ESCORT_REQUEST construction methods. +-- +-- Create a new AI_ESCORT_REQUEST object with the @{#AI_ESCORT_REQUEST.New} method: +-- +-- * @{#AI_ESCORT_REQUEST.New}: Creates a new AI_ESCORT_REQUEST object from a @{Wrapper.Group#GROUP} for a @{Wrapper.Client#CLIENT}, with an optional briefing text. +-- +-- @usage +-- -- Declare a new EscortPlanes object as follows: +-- +-- -- First find the GROUP object and the CLIENT object. +-- local EscortUnit = CLIENT:FindByName( "Unit Name" ) -- The Unit Name is the name of the unit flagged with the skill Client in the mission editor. +-- local EscortGroup = GROUP:FindByName( "Group Name" ) -- The Group Name is the name of the group that will escort the Escort Client. +-- +-- -- Now use these 2 objects to construct the new EscortPlanes object. +-- EscortPlanes = AI_ESCORT_REQUEST:New( EscortUnit, EscortGroup, "Desert", "Welcome to the mission. You are escorted by a plane with code name 'Desert', which can be instructed through the F10 radio menu." ) +-- +-- @field #AI_ESCORT_REQUEST +AI_ESCORT_REQUEST = { + ClassName = "AI_ESCORT_REQUEST", +} + +--- AI_ESCORT_REQUEST.Mode class +-- @type AI_ESCORT_REQUEST.MODE +-- @field #number FOLLOW +-- @field #number MISSION + +--- MENUPARAM type +-- @type MENUPARAM +-- @field #AI_ESCORT_REQUEST ParamSelf +-- @field #Distance ParamDistance +-- @field #function ParamFunction +-- @field #string ParamMessage + +--- AI_ESCORT_REQUEST class constructor for an AI group +-- @param #AI_ESCORT_REQUEST self +-- @param Wrapper.Client#CLIENT EscortUnit The client escorted by the EscortGroup. +-- @param Core.Spawn#SPAWN EscortSpawn The spawn object of AI, escorting the EscortUnit. +-- @param Wrapper.Airbase#AIRBASE EscortAirbase The airbase where escorts will be spawned once requested. +-- @param #string EscortName Name of the escort. +-- @param #string EscortBriefing A text showing the AI_ESCORT_REQUEST briefing to the player. Note that if no EscortBriefing is provided, the default briefing will be shown. +-- @return #AI_ESCORT_REQUEST +-- @usage +-- EscortSpawn = SPAWN:NewWithAlias( "Red A2G Escort Template", "Red A2G Escort AI" ):InitLimit( 10, 10 ) +-- EscortSpawn:ParkAtAirbase( AIRBASE:FindByName( AIRBASE.Caucasus.Sochi_Adler ), AIRBASE.TerminalType.OpenBig ) +-- +-- local EscortUnit = UNIT:FindByName( "Red A2G Pilot" ) +-- +-- Escort = AI_ESCORT_REQUEST:New( EscortUnit, EscortSpawn, AIRBASE:FindByName(AIRBASE.Caucasus.Sochi_Adler), "A2G", "Briefing" ) +-- Escort:FormationTrail( 50, 100, 100 ) +-- Escort:Menus() +-- Escort:__Start( 5 ) +function AI_ESCORT_REQUEST:New( EscortUnit, EscortSpawn, EscortAirbase, EscortName, EscortBriefing ) + + local EscortGroupSet = SET_GROUP:New():FilterDeads():FilterCrashes() + local self = BASE:Inherit( self, AI_ESCORT:New( EscortUnit, EscortGroupSet, EscortName, EscortBriefing ) ) -- #AI_ESCORT_REQUEST + + self.EscortGroupSet = EscortGroupSet + self.EscortSpawn = EscortSpawn + self.EscortAirbase = EscortAirbase + + self.LeaderGroup = self.PlayerUnit:GetGroup() + + self.Detection = DETECTION_AREAS:New( self.EscortGroupSet, 5000 ) + self.Detection:__Start( 30 ) + + self.SpawnMode = self.__Enum.Mode.Mission + + return self +end + +--- @param #AI_ESCORT_REQUEST self +function AI_ESCORT_REQUEST:SpawnEscort() + + local EscortGroup = self.EscortSpawn:SpawnAtAirbase( self.EscortAirbase, SPAWN.Takeoff.Hot ) + + self:ScheduleOnce( 0.1, + function( EscortGroup ) + + EscortGroup:OptionROTVertical() + EscortGroup:OptionROEHoldFire() + + self.EscortGroupSet:AddGroup( EscortGroup ) + + local LeaderEscort = self.EscortGroupSet:GetFirst() -- Wrapper.Group#GROUP + local Report = REPORT:New() + Report:Add( "Joining Up " .. self.EscortGroupSet:GetUnitTypeNames():Text( ", " ) .. " from " .. LeaderEscort:GetCoordinate():ToString( self.EscortUnit ) ) + LeaderEscort:MessageTypeToGroup( Report:Text(), MESSAGE.Type.Information, self.PlayerUnit ) + + self:SetFlightModeFormation( EscortGroup ) + self:FormationTrail() + + self:_InitFlightMenus() + self:_InitEscortMenus( EscortGroup ) + self:_InitEscortRoute( EscortGroup ) + + --- @param #AI_ESCORT self + -- @param Core.Event#EVENTDATA EventData + function EscortGroup:OnEventDeadOrCrash( EventData ) + self:F( { "EventDead", EventData } ) + self.EscortMenu:Remove() + end + + EscortGroup:HandleEvent( EVENTS.Dead, EscortGroup.OnEventDeadOrCrash ) + EscortGroup:HandleEvent( EVENTS.Crash, EscortGroup.OnEventDeadOrCrash ) + + end, EscortGroup + ) + +end + +--- @param #AI_ESCORT_REQUEST self +-- @param Core.Set#SET_GROUP EscortGroupSet +function AI_ESCORT_REQUEST:onafterStart( EscortGroupSet ) + + self:F() + + if not self.MenuRequestEscort then + self.MainMenu = MENU_GROUP:New( self.PlayerGroup, self.EscortName ) + self.MenuRequestEscort = MENU_GROUP_COMMAND:New( self.LeaderGroup, "Request new escort ", self.MainMenu, + function() + self:SpawnEscort() + end + ) + end + + self:GetParent( self ).onafterStart( self, EscortGroupSet ) + + self:HandleEvent( EVENTS.Dead, self.OnEventDeadOrCrash ) + self:HandleEvent( EVENTS.Crash, self.OnEventDeadOrCrash ) + +end + +--- @param #AI_ESCORT_REQUEST self +-- @param Core.Set#SET_GROUP EscortGroupSet +function AI_ESCORT_REQUEST:onafterStop( EscortGroupSet ) + + self:F() + + EscortGroupSet:ForEachGroup( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + EscortGroup:WayPointInitialize() + + EscortGroup:OptionROTVertical() + EscortGroup:OptionROEOpenFire() + end + ) + + self.Detection:Stop() + + self.MainMenu:Remove() + +end + +--- Set the spawn mode to be mission execution. +-- @param #AI_ESCORT_REQUEST self +function AI_ESCORT_REQUEST:SetEscortSpawnMission() + + self.SpawnMode = self.__Enum.Mode.Mission + +end +--- **AI** - Models the automatic assignment of AI escorts to player flights. +-- +-- ## Features: +-- -- +-- * Provides the facilities to trigger escorts when players join flight slots. +-- * +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_Escort_Dispatcher +-- @image MOOSE.JPG + + +--- @type AI_ESCORT_DISPATCHER +-- @extends Core.Fsm#FSM + + +--- Models the automatic assignment of AI escorts to player flights. +-- +-- === +-- +-- @field #AI_ESCORT_DISPATCHER +AI_ESCORT_DISPATCHER = { + ClassName = "AI_ESCORT_DISPATCHER", +} + +--- @field #list +AI_ESCORT_DISPATCHER.AI_Escorts = {} + + +--- Creates a new AI_ESCORT_DISPATCHER object. +-- @param #AI_ESCORT_DISPATCHER self +-- @param Core.Set#SET_GROUP CarrierSet The set of @{Wrapper.Group#GROUP} objects of carriers for which escorts are spawned in. +-- @param Core.Spawn#SPAWN EscortSpawn The spawn object that will spawn in the Escorts. +-- @param Wrapper.Airbase#AIRBASE EscortAirbase The airbase where the escorts are spawned. +-- @param #string EscortName Name of the escort, which will also be the name of the escort menu. +-- @param #string EscortBriefing A text showing the briefing to the player. Note that if no EscortBriefing is provided, the default briefing will be shown. +-- @return #AI_ESCORT_DISPATCHER +-- @usage +-- +-- -- Create a new escort when a player joins an SU-25T plane. +-- Create a carrier set, which contains the player slots that can be joined by the players, for which escorts will be defined. +-- local Red_SU25T_CarrierSet = SET_GROUP:New():FilterPrefixes( "Red A2G Player Su-25T" ):FilterStart() +-- +-- -- Create a spawn object that will spawn in the escorts, once the player has joined the player slot. +-- local Red_SU25T_EscortSpawn = SPAWN:NewWithAlias( "Red A2G Su-25 Escort", "Red AI A2G SU-25 Escort" ):InitLimit( 10, 10 ) +-- +-- -- Create an airbase object, where the escorts will be spawned. +-- local Red_SU25T_Airbase = AIRBASE:FindByName( AIRBASE.Caucasus.Maykop_Khanskaya ) +-- +-- -- Park the airplanes at the airbase, visible before start. +-- Red_SU25T_EscortSpawn:ParkAtAirbase( Red_SU25T_Airbase, AIRBASE.TerminalType.OpenMedOrBig ) +-- +-- -- New create the escort dispatcher, using the carrier set, the escort spawn object at the escort airbase. +-- -- Provide a name of the escort, which will be also the name appearing on the radio menu for the group. +-- -- And a briefing to appear when the player joins the player slot. +-- Red_SU25T_EscortDispatcher = AI_ESCORT_DISPATCHER:New( Red_SU25T_CarrierSet, Red_SU25T_EscortSpawn, Red_SU25T_Airbase, "Escort Su-25", "You Su-25T is escorted by one Su-25. Use the radio menu to control the escorts." ) +-- +-- -- The dispatcher needs to be started using the :Start() method. +-- Red_SU25T_EscortDispatcher:Start() +function AI_ESCORT_DISPATCHER:New( CarrierSet, EscortSpawn, EscortAirbase, EscortName, EscortBriefing ) + + local self = BASE:Inherit( self, FSM:New() ) -- #AI_ESCORT_DISPATCHER + + self.CarrierSet = CarrierSet + self.EscortSpawn = EscortSpawn + self.EscortAirbase = EscortAirbase + self.EscortName = EscortName + self.EscortBriefing = EscortBriefing + + self:SetStartState( "Idle" ) + + self:AddTransition( "Monitoring", "Monitor", "Monitoring" ) + + self:AddTransition( "Idle", "Start", "Monitoring" ) + self:AddTransition( "Monitoring", "Stop", "Idle" ) + + -- Put a Dead event handler on CarrierSet, to ensure that when a carrier is destroyed, that all internal parameters are reset. + function self.CarrierSet.OnAfterRemoved( CarrierSet, From, Event, To, CarrierName, Carrier ) + self:F( { Carrier = Carrier:GetName() } ) + end + + return self +end + +function AI_ESCORT_DISPATCHER:onafterStart( From, Event, To ) + + self:HandleEvent( EVENTS.Birth ) + + self:HandleEvent( EVENTS.PlayerLeaveUnit, self.OnEventExit ) + self:HandleEvent( EVENTS.Crash, self.OnEventExit ) + self:HandleEvent( EVENTS.Dead, self.OnEventExit ) + +end + +--- @param #AI_ESCORT_DISPATCHER self +-- @param Core.Event#EVENTDATA EventData +function AI_ESCORT_DISPATCHER:OnEventExit( EventData ) + + local PlayerGroupName = EventData.IniGroupName + local PlayerGroup = EventData.IniGroup + local PlayerUnit = EventData.IniUnit + + self:I({EscortAirbase= self.EscortAirbase } ) + self:I({PlayerGroupName = PlayerGroupName } ) + self:I({PlayerGroup = PlayerGroup}) + self:I({FirstGroup = self.CarrierSet:GetFirst()}) + self:I({FindGroup = self.CarrierSet:FindGroup( PlayerGroupName )}) + + if self.CarrierSet:FindGroup( PlayerGroupName ) then + if self.AI_Escorts[PlayerGroupName] then + self.AI_Escorts[PlayerGroupName]:Stop() + self.AI_Escorts[PlayerGroupName] = nil + end + end + +end + +--- @param #AI_ESCORT_DISPATCHER self +-- @param Core.Event#EVENTDATA EventData +function AI_ESCORT_DISPATCHER:OnEventBirth( EventData ) + + local PlayerGroupName = EventData.IniGroupName + local PlayerGroup = EventData.IniGroup + local PlayerUnit = EventData.IniUnit + + self:I({EscortAirbase= self.EscortAirbase } ) + self:I({PlayerGroupName = PlayerGroupName } ) + self:I({PlayerGroup = PlayerGroup}) + self:I({FirstGroup = self.CarrierSet:GetFirst()}) + self:I({FindGroup = self.CarrierSet:FindGroup( PlayerGroupName )}) + + if self.CarrierSet:FindGroup( PlayerGroupName ) then + if not self.AI_Escorts[PlayerGroupName] then + local LeaderUnit = PlayerUnit + local EscortGroup = self.EscortSpawn:SpawnAtAirbase( self.EscortAirbase, SPAWN.Takeoff.Hot ) + self:I({EscortGroup = EscortGroup}) + + self:ScheduleOnce( 1, + function( EscortGroup ) + local EscortSet = SET_GROUP:New() + EscortSet:AddGroup( EscortGroup ) + self.AI_Escorts[PlayerGroupName] = AI_ESCORT:New( LeaderUnit, EscortSet, self.EscortName, self.EscortBriefing ) + self.AI_Escorts[PlayerGroupName]:FormationTrail( 0, 100, 0 ) + if EscortGroup:IsHelicopter() then + self.AI_Escorts[PlayerGroupName]:MenusHelicopters() + else + self.AI_Escorts[PlayerGroupName]:MenusAirplanes() + end + self.AI_Escorts[PlayerGroupName]:__Start( 0.1 ) + end, EscortGroup + ) + end + end + +end + + +--- Start Trigger for AI_ESCORT_DISPATCHER +-- @function [parent=#AI_ESCORT_DISPATCHER] Start +-- @param #AI_ESCORT_DISPATCHER self + +--- Start Asynchronous Trigger for AI_ESCORT_DISPATCHER +-- @function [parent=#AI_ESCORT_DISPATCHER] __Start +-- @param #AI_ESCORT_DISPATCHER self +-- @param #number Delay + +--- Stop Trigger for AI_ESCORT_DISPATCHER +-- @function [parent=#AI_ESCORT_DISPATCHER] Stop +-- @param #AI_ESCORT_DISPATCHER self + +--- Stop Asynchronous Trigger for AI_ESCORT_DISPATCHER +-- @function [parent=#AI_ESCORT_DISPATCHER] __Stop +-- @param #AI_ESCORT_DISPATCHER self +-- @param #number Delay + + + + + + +--- **AI** - Models the assignment of AI escorts to player flights upon request using the radio menu. +-- +-- ## Features: +-- +-- * Provides the facilities to trigger escorts when players join flight units. +-- * Provide a menu for which escorts can be requested. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_ESCORT_DISPATCHER_REQUEST +-- @image MOOSE.JPG + + +--- @type AI_ESCORT_DISPATCHER_REQUEST +-- @extends Core.Fsm#FSM + + +--- Models the assignment of AI escorts to player flights upon request using the radio menu. +-- +-- === +-- +-- @field #AI_ESCORT_DISPATCHER_REQUEST +AI_ESCORT_DISPATCHER_REQUEST = { + ClassName = "AI_ESCORT_DISPATCHER_REQUEST", +} + +--- @field #list +AI_ESCORT_DISPATCHER_REQUEST.AI_Escorts = {} + + +--- Creates a new AI_ESCORT_DISPATCHER_REQUEST object. +-- @param #AI_ESCORT_DISPATCHER_REQUEST self +-- @param Core.Set#SET_GROUP CarrierSet The set of @{Wrapper.Group#GROUP} objects of carriers for which escorts are requested. +-- @param Core.Spawn#SPAWN EscortSpawn The spawn object that will spawn in the Escorts. +-- @param Wrapper.Airbase#AIRBASE EscortAirbase The airbase where the escorts are spawned. +-- @param #string EscortName Name of the escort, which will also be the name of the escort menu. +-- @param #string EscortBriefing A text showing the briefing to the player. Note that if no EscortBriefing is provided, the default briefing will be shown. +-- @return #AI_ESCORT_DISPATCHER_REQUEST +function AI_ESCORT_DISPATCHER_REQUEST:New( CarrierSet, EscortSpawn, EscortAirbase, EscortName, EscortBriefing ) + + local self = BASE:Inherit( self, FSM:New() ) -- #AI_ESCORT_DISPATCHER_REQUEST + + self.CarrierSet = CarrierSet + self.EscortSpawn = EscortSpawn + self.EscortAirbase = EscortAirbase + self.EscortName = EscortName + self.EscortBriefing = EscortBriefing + + self:SetStartState( "Idle" ) + + self:AddTransition( "Monitoring", "Monitor", "Monitoring" ) + + self:AddTransition( "Idle", "Start", "Monitoring" ) + self:AddTransition( "Monitoring", "Stop", "Idle" ) + + -- Put a Dead event handler on CarrierSet, to ensure that when a carrier is destroyed, that all internal parameters are reset. + function self.CarrierSet.OnAfterRemoved( CarrierSet, From, Event, To, CarrierName, Carrier ) + self:F( { Carrier = Carrier:GetName() } ) + end + + return self +end + +function AI_ESCORT_DISPATCHER_REQUEST:onafterStart( From, Event, To ) + + self:HandleEvent( EVENTS.Birth ) + + self:HandleEvent( EVENTS.PlayerLeaveUnit, self.OnEventExit ) + self:HandleEvent( EVENTS.Crash, self.OnEventExit ) + self:HandleEvent( EVENTS.Dead, self.OnEventExit ) + +end + +--- @param #AI_ESCORT_DISPATCHER_REQUEST self +-- @param Core.Event#EVENTDATA EventData +function AI_ESCORT_DISPATCHER_REQUEST:OnEventExit( EventData ) + + local PlayerGroupName = EventData.IniGroupName + local PlayerGroup = EventData.IniGroup + local PlayerUnit = EventData.IniUnit + + if self.CarrierSet:FindGroup( PlayerGroupName ) then + if self.AI_Escorts[PlayerGroupName] then + self.AI_Escorts[PlayerGroupName]:Stop() + self.AI_Escorts[PlayerGroupName] = nil + end + end + +end + +--- @param #AI_ESCORT_DISPATCHER_REQUEST self +-- @param Core.Event#EVENTDATA EventData +function AI_ESCORT_DISPATCHER_REQUEST:OnEventBirth( EventData ) + + local PlayerGroupName = EventData.IniGroupName + local PlayerGroup = EventData.IniGroup + local PlayerUnit = EventData.IniUnit + + if self.CarrierSet:FindGroup( PlayerGroupName ) then + if not self.AI_Escorts[PlayerGroupName] then + local LeaderUnit = PlayerUnit + self:ScheduleOnce( 0.1, + function() + self.AI_Escorts[PlayerGroupName] = AI_ESCORT_REQUEST:New( LeaderUnit, self.EscortSpawn, self.EscortAirbase, self.EscortName, self.EscortBriefing ) + self.AI_Escorts[PlayerGroupName]:FormationTrail( 0, 100, 0 ) + if PlayerGroup:IsHelicopter() then + self.AI_Escorts[PlayerGroupName]:MenusHelicopters() + else + self.AI_Escorts[PlayerGroupName]:MenusAirplanes() + end + self.AI_Escorts[PlayerGroupName]:__Start( 0.1 ) + end + ) + end + end + +end + + +--- Start Trigger for AI_ESCORT_DISPATCHER_REQUEST +-- @function [parent=#AI_ESCORT_DISPATCHER_REQUEST] Start +-- @param #AI_ESCORT_DISPATCHER_REQUEST self + +--- Start Asynchronous Trigger for AI_ESCORT_DISPATCHER_REQUEST +-- @function [parent=#AI_ESCORT_DISPATCHER_REQUEST] __Start +-- @param #AI_ESCORT_DISPATCHER_REQUEST self +-- @param #number Delay + +--- Stop Trigger for AI_ESCORT_DISPATCHER_REQUEST +-- @function [parent=#AI_ESCORT_DISPATCHER_REQUEST] Stop +-- @param #AI_ESCORT_DISPATCHER_REQUEST self + +--- Stop Asynchronous Trigger for AI_ESCORT_DISPATCHER_REQUEST +-- @function [parent=#AI_ESCORT_DISPATCHER_REQUEST] __Stop +-- @param #AI_ESCORT_DISPATCHER_REQUEST self +-- @param #number Delay + + + + + + +--- **AI** - Models the intelligent transportation of infantry and other cargo. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_Cargo +-- @image Cargo.JPG + +--- @type AI_CARGO +-- @extends Core.Fsm#FSM_CONTROLLABLE + + +--- Base class for the dynamic cargo handling capability for AI groups. +-- +-- Carriers can be mobilized to intelligently transport infantry and other cargo within the simulation. +-- The AI_CARGO module uses the @{Cargo.Cargo} capabilities within the MOOSE framework. +-- CARGO derived objects must be declared within the mission to make the AI_CARGO object recognize the cargo. +-- Please consult the @{Cargo.Cargo} module for more information. +-- +-- The derived classes from this module are: +-- +-- * @{AI.AI_Cargo_APC} - Cargo transportation using APCs and other vehicles between zones. +-- * @{AI.AI_Cargo_Helicopter} - Cargo transportation using helicopters between zones. +-- * @{AI.AI_Cargo_Airplane} - Cargo transportation using airplanes to and from airbases. +-- +-- @field #AI_CARGO +AI_CARGO = { + ClassName = "AI_CARGO", + Coordinate = nil, -- Core.Point#COORDINATE, + Carrier_Cargo = {}, +} + +--- Creates a new AI_CARGO object. +-- @param #AI_CARGO self +-- @param Wrapper.Group#GROUP Carrier Cargo carrier group. +-- @param Core.Set#SET_CARGO CargoSet Set of cargo(s) to transport. +-- @return #AI_CARGO self +function AI_CARGO:New( Carrier, CargoSet ) + + local self = BASE:Inherit( self, FSM_CONTROLLABLE:New( Carrier ) ) -- #AI_CARGO + + self.CargoSet = CargoSet -- Core.Set#SET_CARGO + self.CargoCarrier = Carrier -- Wrapper.Group#GROUP + + self:SetStartState( "Unloaded" ) + + -- Board + self:AddTransition( "Unloaded", "Pickup", "Unloaded" ) + self:AddTransition( "*", "Load", "*" ) + self:AddTransition( "*", "Reload", "*" ) + self:AddTransition( "*", "Board", "*" ) + self:AddTransition( "*", "Loaded", "Loaded" ) + self:AddTransition( "Loaded", "PickedUp", "Loaded" ) + + -- Unload + self:AddTransition( "Loaded", "Deploy", "*" ) + self:AddTransition( "*", "Unload", "*" ) + self:AddTransition( "*", "Unboard", "*" ) + self:AddTransition( "*", "Unloaded", "Unloaded" ) + self:AddTransition( "Unloaded", "Deployed", "Unloaded" ) + + + --- Pickup Handler OnBefore for AI_CARGO + -- @function [parent=#AI_CARGO] OnBeforePickup + -- @param #AI_CARGO self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Core.Point#COORDINATE Coordinate + -- @param #number Speed Speed in km/h. Default is 50% of max possible speed the group can do. + -- @return #boolean + + --- Pickup Handler OnAfter for AI_CARGO + -- @function [parent=#AI_CARGO] OnAfterPickup + -- @param #AI_CARGO self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Core.Point#COORDINATE Coordinate + -- @param #number Speed Speed in km/h. Default is 50% of max possible speed the group can do. + + --- Pickup Trigger for AI_CARGO + -- @function [parent=#AI_CARGO] Pickup + -- @param #AI_CARGO self + -- @param Core.Point#COORDINATE Coordinate + -- @param #number Speed Speed in km/h. Default is 50% of max possible speed the group can do. + + --- Pickup Asynchronous Trigger for AI_CARGO + -- @function [parent=#AI_CARGO] __Pickup + -- @param #AI_CARGO self + -- @param #number Delay + -- @param Core.Point#COORDINATE Coordinate Pickup place. If not given, loading starts at the current location. + -- @param #number Speed Speed in km/h. Default is 50% of max possible speed the group can do. + + --- Deploy Handler OnBefore for AI_CARGO + -- @function [parent=#AI_CARGO] OnBeforeDeploy + -- @param #AI_CARGO self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Core.Point#COORDINATE Coordinate + -- @param #number Speed Speed in km/h. Default is 50% of max possible speed the group can do. + -- @return #boolean + + --- Deploy Handler OnAfter for AI_CARGO + -- @function [parent=#AI_CARGO] OnAfterDeploy + -- @param #AI_CARGO self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Core.Point#COORDINATE Coordinate + -- @param #number Speed Speed in km/h. Default is 50% of max possible speed the group can do. + + --- Deploy Trigger for AI_CARGO + -- @function [parent=#AI_CARGO] Deploy + -- @param #AI_CARGO self + -- @param Core.Point#COORDINATE Coordinate + -- @param #number Speed Speed in km/h. Default is 50% of max possible speed the group can do. + + --- Deploy Asynchronous Trigger for AI_CARGO + -- @function [parent=#AI_CARGO] __Deploy + -- @param #AI_CARGO self + -- @param #number Delay + -- @param Core.Point#COORDINATE Coordinate + -- @param #number Speed Speed in km/h. Default is 50% of max possible speed the group can do. + + + --- Loaded Handler OnAfter for AI_CARGO + -- @function [parent=#AI_CARGO] OnAfterLoaded + -- @param #AI_CARGO self + -- @param Wrapper.Group#GROUP Carrier + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Unloaded Handler OnAfter for AI_CARGO + -- @function [parent=#AI_CARGO] OnAfterUnloaded + -- @param #AI_CARGO self + -- @param Wrapper.Group#GROUP Carrier + -- @param #string From + -- @param #string Event + -- @param #string To + + --- On after Deployed event. + -- @function [parent=#AI_CARGO] OnAfterDeployed + -- @param #AI_CARGO self + -- @param Wrapper.Group#GROUP Carrier + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. + -- @param #boolean Defend Defend for APCs. + + + for _, CarrierUnit in pairs( Carrier:GetUnits() ) do + local CarrierUnit = CarrierUnit -- Wrapper.Unit#UNIT + CarrierUnit:SetCargoBayWeightLimit() + end + + self.Transporting = false + self.Relocating = false + + return self +end + + + +function AI_CARGO:IsTransporting() + + return self.Transporting == true +end + +function AI_CARGO:IsRelocating() + + return self.Relocating == true +end + + +--- On after Pickup event. +-- @param #AI_CARGO self +-- @param Wrapper.Group#GROUP APC +-- @param From +-- @param Event +-- @param To +-- @param Core.Point#COORDINATE Coordinate of the pickup point. +-- @param #number Speed Speed in km/h to drive to the pickup coordinate. Default is 50% of max possible speed the unit can go. +-- @param #number Height Height in meters to move to the home coordinate. +-- @param Core.Zone#ZONE PickupZone (optional) The zone where the cargo will be picked up. The PickupZone can be nil, if there wasn't any PickupZoneSet provided. +function AI_CARGO:onafterPickup( APC, From, Event, To, Coordinate, Speed, Height, PickupZone ) + + self.Transporting = false + self.Relocating = true + +end + + +--- On after Deploy event. +-- @param #AI_CARGO self +-- @param Wrapper.Group#GROUP APC +-- @param From +-- @param Event +-- @param To +-- @param Core.Point#COORDINATE Coordinate Deploy place. +-- @param #number Speed Speed in km/h to drive to the depoly coordinate. Default is 50% of max possible speed the unit can go. +-- @param #number Height Height in meters to move to the deploy coordinate. +-- @param Core.Zone#ZONE DeployZone The zone where the cargo will be deployed. +function AI_CARGO:onafterDeploy( APC, From, Event, To, Coordinate, Speed, Height, DeployZone ) + + self.Relocating = false + self.Transporting = true + +end + +--- On before Load event. +-- @param #AI_CARGO self +-- @param Wrapper.Group#GROUP Carrier +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Zone#ZONE PickupZone (optional) The zone where the cargo will be picked up. The PickupZone can be nil, if there wasn't any PickupZoneSet provided. +function AI_CARGO:onbeforeLoad( Carrier, From, Event, To, PickupZone ) + self:F( { Carrier, From, Event, To } ) + + local Boarding = false + + local LoadInterval = 2 + local LoadDelay = 1 + local Carrier_List = {} + local Carrier_Weight = {} + + if Carrier and Carrier:IsAlive() then + self.Carrier_Cargo = {} + for _, CarrierUnit in pairs( Carrier:GetUnits() ) do + local CarrierUnit = CarrierUnit -- Wrapper.Unit#UNIT + + local CargoBayFreeWeight = CarrierUnit:GetCargoBayFreeWeight() + self:F({CargoBayFreeWeight=CargoBayFreeWeight}) + + Carrier_List[#Carrier_List+1] = CarrierUnit + Carrier_Weight[CarrierUnit] = CargoBayFreeWeight + end + + local Carrier_Count = #Carrier_List + local Carrier_Index = 1 + + local Loaded = false + + for _, Cargo in UTILS.spairs( self.CargoSet:GetSet(), function( t, a, b ) return t[a]:GetWeight() > t[b]:GetWeight() end ) do + local Cargo = Cargo -- Cargo.Cargo#CARGO + + self:F( { IsUnLoaded = Cargo:IsUnLoaded(), IsDeployed = Cargo:IsDeployed(), Cargo:GetName(), Carrier:GetName() } ) + + -- Try all Carriers, but start from the one according the Carrier_Index + for Carrier_Loop = 1, #Carrier_List do + + local CarrierUnit = Carrier_List[Carrier_Index] -- Wrapper.Unit#UNIT + + -- This counters loop through the available Carriers. + Carrier_Index = Carrier_Index + 1 + if Carrier_Index > Carrier_Count then + Carrier_Index = 1 + end + + if Cargo:IsUnLoaded() and not Cargo:IsDeployed() then + if Cargo:IsInLoadRadius( CarrierUnit:GetCoordinate() ) then + self:F( { "In radius", CarrierUnit:GetName() } ) + + local CargoWeight = Cargo:GetWeight() + local CarrierSpace=Carrier_Weight[CarrierUnit] + + -- Only when there is space within the bay to load the next cargo item! + if CarrierSpace > CargoWeight then + Carrier:RouteStop() + --Cargo:Ungroup() + Cargo:__Board( -LoadDelay, CarrierUnit ) + self:__Board( LoadDelay, Cargo, CarrierUnit, PickupZone ) + + LoadDelay = LoadDelay + Cargo:GetCount() * LoadInterval + + -- So now this CarrierUnit has Cargo that is being loaded. + -- This will be used further in the logic to follow and to check cargo status. + self.Carrier_Cargo[Cargo] = CarrierUnit + Boarding = true + Carrier_Weight[CarrierUnit] = Carrier_Weight[CarrierUnit] - CargoWeight + Loaded = true + + -- Ok, we loaded a cargo, now we can stop the loop. + break + else + self:T(string.format("WARNING: Cargo too heavy for carrier %s. Cargo=%.1f > %.1f free space", tostring(CarrierUnit:GetName()), CargoWeight, CarrierSpace)) + end + end + end + + end + + end + + if not Loaded == true then + -- No loading happened, so we need to pickup something else. + self.Relocating = false + end + end + + return Boarding + +end + + +--- On before Reload event. +-- @param #AI_CARGO self +-- @param Wrapper.Group#GROUP Carrier +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Zone#ZONE PickupZone (optional) The zone where the cargo will be picked up. The PickupZone can be nil, if there wasn't any PickupZoneSet provided. +function AI_CARGO:onbeforeReload( Carrier, From, Event, To ) + self:F( { Carrier, From, Event, To } ) + + local Boarding = false + + local LoadInterval = 2 + local LoadDelay = 1 + local Carrier_List = {} + local Carrier_Weight = {} + + if Carrier and Carrier:IsAlive() then + for _, CarrierUnit in pairs( Carrier:GetUnits() ) do + local CarrierUnit = CarrierUnit -- Wrapper.Unit#UNIT + + Carrier_List[#Carrier_List+1] = CarrierUnit + end + + local Carrier_Count = #Carrier_List + local Carrier_Index = 1 + + local Loaded = false + + for Cargo, CarrierUnit in pairs( self.Carrier_Cargo ) do + local Cargo = Cargo -- Cargo.Cargo#CARGO + + self:F( { IsUnLoaded = Cargo:IsUnLoaded(), IsDeployed = Cargo:IsDeployed(), Cargo:GetName(), Carrier:GetName() } ) + + -- Try all Carriers, but start from the one according the Carrier_Index + for Carrier_Loop = 1, #Carrier_List do + + local CarrierUnit = Carrier_List[Carrier_Index] -- Wrapper.Unit#UNIT + + -- This counters loop through the available Carriers. + Carrier_Index = Carrier_Index + 1 + if Carrier_Index > Carrier_Count then + Carrier_Index = 1 + end + + if Cargo:IsUnLoaded() and not Cargo:IsDeployed() then + Carrier:RouteStop() + Cargo:__Board( -LoadDelay, CarrierUnit ) + self:__Board( LoadDelay, Cargo, CarrierUnit ) + + LoadDelay = LoadDelay + Cargo:GetCount() * LoadInterval + + -- So now this CarrierUnit has Cargo that is being loaded. + -- This will be used further in the logic to follow and to check cargo status. + self.Carrier_Cargo[Cargo] = CarrierUnit + Boarding = true + Loaded = true + end + + end + + end + + if not Loaded == true then + -- No loading happened, so we need to pickup something else. + self.Relocating = false + end + end + + return Boarding + +end + +--- On after Board event. +-- @param #AI_CARGO self +-- @param Wrapper.Group#GROUP Carrier +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Cargo.Cargo#CARGO Cargo Cargo object. +-- @param Wrapper.Unit#UNIT CarrierUnit +-- @param Core.Zone#ZONE PickupZone (optional) The zone where the cargo will be picked up. The PickupZone can be nil, if there wasn't any PickupZoneSet provided. +function AI_CARGO:onafterBoard( Carrier, From, Event, To, Cargo, CarrierUnit, PickupZone ) + self:F( { Carrier, From, Event, To, Cargo, CarrierUnit:GetName() } ) + + if Carrier and Carrier:IsAlive() then + self:F({ IsLoaded = Cargo:IsLoaded(), Cargo:GetName(), Carrier:GetName() } ) + if not Cargo:IsLoaded() and not Cargo:IsDestroyed() then + self:__Board( -10, Cargo, CarrierUnit, PickupZone ) + return + end + end + + self:__Loaded( 0.1, Cargo, CarrierUnit, PickupZone ) + +end + +--- On after Loaded event. +-- @param #AI_CARGO self +-- @param Wrapper.Group#GROUP Carrier +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @return #boolean Cargo loaded. +-- @param Core.Zone#ZONE PickupZone (optional) The zone where the cargo will be picked up. The PickupZone can be nil, if there wasn't any PickupZoneSet provided. +function AI_CARGO:onafterLoaded( Carrier, From, Event, To, Cargo, PickupZone ) + self:F( { Carrier, From, Event, To } ) + + local Loaded = true + + if Carrier and Carrier:IsAlive() then + for Cargo, CarrierUnit in pairs( self.Carrier_Cargo ) do + local Cargo = Cargo -- Cargo.Cargo#CARGO + self:F( { IsLoaded = Cargo:IsLoaded(), IsDestroyed = Cargo:IsDestroyed(), Cargo:GetName(), Carrier:GetName() } ) + if not Cargo:IsLoaded() and not Cargo:IsDestroyed() then + Loaded = false + end + end + end + + if Loaded then + self:__PickedUp( 0.1, PickupZone ) + end + +end + +--- On after PickedUp event. +-- @param #AI_CARGO self +-- @param Wrapper.Group#GROUP Carrier +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Zone#ZONE PickupZone (optional) The zone where the cargo will be picked up. The PickupZone can be nil, if there wasn't any PickupZoneSet provided. +function AI_CARGO:onafterPickedUp( Carrier, From, Event, To, PickupZone ) + self:F( { Carrier, From, Event, To } ) + + Carrier:RouteResume() + + local HasCargo = false + if Carrier and Carrier:IsAlive() then + for Cargo, CarrierUnit in pairs( self.Carrier_Cargo ) do + HasCargo = true + break + end + end + + self.Relocating = false + if HasCargo then + self:F( "Transporting" ) + self.Transporting = true + end + +end + + + + +--- On after Unload event. +-- @param #AI_CARGO self +-- @param Wrapper.Group#GROUP Carrier +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. +function AI_CARGO:onafterUnload( Carrier, From, Event, To, DeployZone, Defend ) + self:F( { Carrier, From, Event, To, DeployZone, Defend = Defend } ) + + local UnboardInterval = 5 + local UnboardDelay = 5 + + if Carrier and Carrier:IsAlive() then + for _, CarrierUnit in pairs( Carrier:GetUnits() ) do + local CarrierUnit = CarrierUnit -- Wrapper.Unit#UNIT + Carrier:RouteStop() + for _, Cargo in pairs( CarrierUnit:GetCargo() ) do + self:F( { Cargo = Cargo:GetName(), Isloaded = Cargo:IsLoaded() } ) + if Cargo:IsLoaded() then + Cargo:__UnBoard( UnboardDelay ) + UnboardDelay = UnboardDelay + Cargo:GetCount() * UnboardInterval + self:__Unboard( UnboardDelay, Cargo, CarrierUnit, DeployZone, Defend ) + if not Defend == true then + Cargo:SetDeployed( true ) + end + end + end + end + end + +end + +--- On after Unboard event. +-- @param #AI_CARGO self +-- @param Wrapper.Group#GROUP Carrier +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string Cargo.Cargo#CARGO Cargo Cargo object. +-- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. +function AI_CARGO:onafterUnboard( Carrier, From, Event, To, Cargo, CarrierUnit, DeployZone, Defend ) + self:F( { Carrier, From, Event, To, Cargo:GetName(), DeployZone = DeployZone, Defend = Defend } ) + + if Carrier and Carrier:IsAlive() then + if not Cargo:IsUnLoaded() then + self:__Unboard( 10, Cargo, CarrierUnit, DeployZone, Defend ) + return + end + end + + self:Unloaded( Cargo, CarrierUnit, DeployZone, Defend ) + +end + +--- On after Unloaded event. +-- @param #AI_CARGO self +-- @param Wrapper.Group#GROUP Carrier +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string Cargo.Cargo#CARGO Cargo Cargo object. +-- @param #boolean Deployed Cargo is deployed. +-- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. +function AI_CARGO:onafterUnloaded( Carrier, From, Event, To, Cargo, CarrierUnit, DeployZone, Defend ) + self:F( { Carrier, From, Event, To, Cargo:GetName(), DeployZone = DeployZone, Defend = Defend } ) + + local AllUnloaded = true + + --Cargo:Regroup() + + if Carrier and Carrier:IsAlive() then + for _, CarrierUnit in pairs( Carrier:GetUnits() ) do + local CarrierUnit = CarrierUnit -- Wrapper.Unit#UNIT + local IsEmpty = CarrierUnit:IsCargoEmpty() + self:I({ IsEmpty = IsEmpty }) + if not IsEmpty then + AllUnloaded = false + break + end + end + + if AllUnloaded == true then + if DeployZone == true then + self.Carrier_Cargo = {} + end + self.CargoCarrier = Carrier + end + end + + if AllUnloaded == true then + self:__Deployed( 5, DeployZone, Defend ) + end + +end + +--- On after Deployed event. +-- @param #AI_CARGO self +-- @param Wrapper.Group#GROUP Carrier +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. +-- @param #boolean Defend Defend for APCs. +function AI_CARGO:onafterDeployed( Carrier, From, Event, To, DeployZone, Defend ) + self:F( { Carrier, From, Event, To, DeployZone = DeployZone, Defend = Defend } ) + + if not Defend == true then + self.Transporting = false + else + self:F( "Defending" ) + + end + +end +--- **AI** - Models the intelligent transportation of cargo using ground vehicles. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_Cargo_APC +-- @image AI_Cargo_Dispatching_For_APC.JPG + +--- @type AI_CARGO_APC +-- @extends AI.AI_Cargo#AI_CARGO + + +--- Brings a dynamic cargo handling capability for an AI vehicle group. +-- +-- Armoured Personnel Carriers (APC), Trucks, Jeeps and other ground based carrier equipment can be mobilized to intelligently transport infantry and other cargo within the simulation. +-- +-- The AI_CARGO_APC class uses the @{Cargo.Cargo} capabilities within the MOOSE framework. +-- @{Cargo.Cargo} must be declared within the mission to make the AI_CARGO_APC object recognize the cargo. +-- Please consult the @{Cargo.Cargo} module for more information. +-- +-- ## Cargo loading. +-- +-- The module will load automatically cargo when the APCs are within boarding or loading radius. +-- The boarding or loading radius is specified when the cargo is created in the simulation, and therefore, this radius depends on the type of cargo +-- and the specified boarding radius. +-- +-- ## **Defending** the APCs when enemies nearby. +-- +-- Cargo will defend the carrier with its available arms, and to avoid cargo being lost within the battlefield. +-- +-- When the APCs are approaching enemy units, something special is happening. +-- The APCs will stop moving, and the loaded infantry will unboard and follow the APCs and will help to defend the group. +-- The carrier will hold the route once the unboarded infantry is further than 50 meters from the APCs, +-- to ensure that the APCs are not too far away from the following running infantry. +-- Once all enemies are cleared, the infantry will board again automatically into the APCs. Once boarded, the APCs will follow its pre-defined route. +-- +-- A combat radius needs to be specified in meters at the @{#AI_CARGO_APC.New}() method. +-- This combat radius will trigger the unboarding of troops when enemies are within the combat radius around the APCs. +-- During my tests, I've noticed that there is a balance between ensuring that the infantry is within sufficient hit radius (effectiveness) versus +-- vulnerability of the infantry. It all depends on the kind of enemies that are expected to be encountered. +-- A combat radius of 350 meters to 500 meters has been proven to be the most effective and efficient. +-- +-- However, when the defense of the carrier, is not required, it must be switched off. +-- This is done by disabling the defense of the carrier using the method @{#AI_CARGO_APC.SetCombatRadius}(), and providing a combat radius of 0 meters. +-- It can be switched on later when required by reenabling the defense using the method and providing a combat radius larger than 0. +-- +-- ## Infantry or cargo **health**. +-- +-- When infantry is unboarded from the APCs, the infantry is actually respawned into the battlefield. +-- As a result, the unboarding infantry is very _healthy_ every time it unboards. +-- This is due to the limitation of the DCS simulator, which is not able to specify the health of new spawned units as a parameter. +-- However, infantry that was destroyed when unboarded and following the APCs, won't be respawned again. Destroyed is destroyed. +-- As a result, there is some additional strength that is gained when an unboarding action happens, but in terms of simulation balance this has +-- marginal impact on the overall battlefield simulation. Fortunately, the firing strength of infantry is limited, and thus, respacing healthy infantry every +-- time is not so much of an issue ... +-- +-- ## Control the APCs on the map. +-- +-- It is possible also as a human ground commander to influence the path of the APCs, by pointing a new path using the DCS user interface on the map. +-- In this case, the APCs will change the direction towards its new indicated route. However, there is a catch! +-- Once the APCs are near the enemy, and infantry is unboarded, the APCs won't be able to hold the route until the infantry could catch up. +-- The APCs will simply drive on and won't stop! This is a limitation in ED that prevents user actions being controlled by the scripting engine. +-- No workaround is possible on this. +-- +-- ## Cargo deployment. +-- +-- Using the @{#AI_CARGO_APC.Deploy}() method, you are able to direct the APCs towards a point on the battlefield to unboard/unload the cargo at the specific coordinate. +-- The APCs will follow nearby roads as much as possible, to ensure fast and clean cargo transportation between the objects and villages in the simulation environment. +-- +-- ## Cargo pickup. +-- +-- Using the @{#AI_CARGO_APC.Pickup}() method, you are able to direct the APCs towards a point on the battlefield to board/load the cargo at the specific coordinate. +-- The APCs will follow nearby roads as much as possible, to ensure fast and clean cargo transportation between the objects and villages in the simulation environment. +-- +-- +-- +-- @field #AI_CARGO_APC +AI_CARGO_APC = { + ClassName = "AI_CARGO_APC", + Coordinate = nil, -- Core.Point#COORDINATE, +} + +--- Creates a new AI_CARGO_APC object. +-- @param #AI_CARGO_APC self +-- @param Wrapper.Group#GROUP APC The carrier APC group. +-- @param Core.Set#SET_CARGO CargoSet The set of cargo to be transported. +-- @param #number CombatRadius Provide the combat radius to defend the carrier by unboarding the cargo when enemies are nearby. When the combat radius is 0, no defense will happen of the carrier. +-- @return #AI_CARGO_APC +function AI_CARGO_APC:New( APC, CargoSet, CombatRadius ) + + local self = BASE:Inherit( self, AI_CARGO:New( APC, CargoSet ) ) -- #AI_CARGO_APC + + self:AddTransition( "*", "Monitor", "*" ) + self:AddTransition( "*", "Follow", "Following" ) + self:AddTransition( "*", "Guard", "Unloaded" ) + self:AddTransition( "*", "Home", "*" ) + self:AddTransition( "*", "Reload", "Boarding" ) + self:AddTransition( "*", "Deployed", "*" ) + self:AddTransition( "*", "PickedUp", "*" ) + self:AddTransition( "*", "Destroyed", "Destroyed" ) + + self:SetCombatRadius( CombatRadius ) + + self:SetCarrier( APC ) + + return self +end + + +--- Set the Carrier. +-- @param #AI_CARGO_APC self +-- @param Wrapper.Group#GROUP CargoCarrier +-- @return #AI_CARGO_APC +function AI_CARGO_APC:SetCarrier( CargoCarrier ) + + self.CargoCarrier = CargoCarrier -- Wrapper.Group#GROUP + self.CargoCarrier:SetState( self.CargoCarrier, "AI_CARGO_APC", self ) + + CargoCarrier:HandleEvent( EVENTS.Dead ) + + function CargoCarrier:OnEventDead( EventData ) + self:F({"dead"}) + local AICargoTroops = self:GetState( self, "AI_CARGO_APC" ) + self:F({AICargoTroops=AICargoTroops}) + if AICargoTroops then + self:F({}) + if not AICargoTroops:Is( "Loaded" ) then + -- There are enemies within combat radius. Unload the CargoCarrier. + AICargoTroops:Destroyed() + end + end + end + +-- CargoCarrier:HandleEvent( EVENTS.Hit ) +-- +-- function CargoCarrier:OnEventHit( EventData ) +-- self:F({"hit"}) +-- local AICargoTroops = self:GetState( self, "AI_CARGO_APC" ) +-- if AICargoTroops then +-- self:F( { OnHitLoaded = AICargoTroops:Is( "Loaded" ) } ) +-- if AICargoTroops:Is( "Loaded" ) or AICargoTroops:Is( "Boarding" ) then +-- -- There are enemies within combat radius. Unload the CargoCarrier. +-- AICargoTroops:Unload( false ) +-- end +-- end +-- end + + self.Zone = ZONE_UNIT:New( self.CargoCarrier:GetName() .. "-Zone", self.CargoCarrier, self.CombatRadius ) + self.Coalition = self.CargoCarrier:GetCoalition() + + self:SetControllable( CargoCarrier ) + + self:Guard() + + return self +end + +--- Set whether or not the carrier will use roads to *pickup* and *deploy* the cargo. +-- @param #AI_CARGO_APC self +-- @param #boolean Offroad If true, carrier will not use roads. If `nil` or `false` the carrier will use roads when available. +-- @param #number Formation Offroad formation used. Default is `ENUMS.Formation.Vehicle.Offroad`. +-- @return #AI_CARGO_APC self +function AI_CARGO_APC:SetOffRoad(Offroad, Formation) + + self:SetPickupOffRoad(Offroad, Formation) + self:SetDeployOffRoad(Offroad, Formation) + + return self +end + +--- Set whether the carrier will *not* use roads to *pickup* the cargo. +-- @param #AI_CARGO_APC self +-- @param #boolean Offroad If true, carrier will not use roads. +-- @param #number Formation Offroad formation used. Default is `ENUMS.Formation.Vehicle.Offroad`. +-- @return #AI_CARGO_APC self +function AI_CARGO_APC:SetPickupOffRoad(Offroad, Formation) + + self.pickupOffroad=Offroad + self.pickupFormation=Formation or ENUMS.Formation.Vehicle.OffRoad + + return self +end + +--- Set whether the carrier will *not* use roads to *deploy* the cargo. +-- @param #AI_CARGO_APC self +-- @param #boolean Offroad If true, carrier will not use roads. +-- @param #number Formation Offroad formation used. Default is `ENUMS.Formation.Vehicle.Offroad`. +-- @return #AI_CARGO_APC self +function AI_CARGO_APC:SetDeployOffRoad(Offroad, Formation) + + self.deployOffroad=Offroad + self.deployFormation=Formation or ENUMS.Formation.Vehicle.OffRoad + + return self +end + + +--- Find a free Carrier within a radius. +-- @param #AI_CARGO_APC self +-- @param Core.Point#COORDINATE Coordinate +-- @param #number Radius +-- @return Wrapper.Group#GROUP NewCarrier +function AI_CARGO_APC:FindCarrier( Coordinate, Radius ) + + local CoordinateZone = ZONE_RADIUS:New( "Zone" , Coordinate:GetVec2(), Radius ) + CoordinateZone:Scan( { Object.Category.UNIT } ) + for _, DCSUnit in pairs( CoordinateZone:GetScannedUnits() ) do + local NearUnit = UNIT:Find( DCSUnit ) + self:F({NearUnit=NearUnit}) + if not NearUnit:GetState( NearUnit, "AI_CARGO_APC" ) then + local Attributes = NearUnit:GetDesc() + self:F({Desc=Attributes}) + if NearUnit:HasAttribute( "Trucks" ) then + return NearUnit:GetGroup() + end + end + end + + return nil + +end + +--- Enable/Disable unboarding of cargo (infantry) when enemies are nearby (to help defend the carrier). +-- This is only valid for APCs and trucks etc, thus ground vehicles. +-- @param #AI_CARGO_APC self +-- @param #number CombatRadius Provide the combat radius to defend the carrier by unboarding the cargo when enemies are nearby. +-- When the combat radius is 0, no defense will happen of the carrier. +-- When the combat radius is not provided, no defense will happen! +-- @return #AI_CARGO_APC +-- @usage +-- +-- -- Disembark the infantry when the carrier is under attack. +-- AICargoAPC:SetCombatRadius( true ) +-- +-- -- Keep the cargo in the carrier when the carrier is under attack. +-- AICargoAPC:SetCombatRadius( false ) +function AI_CARGO_APC:SetCombatRadius( CombatRadius ) + + self.CombatRadius = CombatRadius or 0 + + if self.CombatRadius > 0 then + self:__Monitor( -5 ) + end + + return self +end + + +--- Follow Infantry to the Carrier. +-- @param #AI_CARGO_APC self +-- @param #AI_CARGO_APC Me +-- @param Wrapper.Unit#UNIT APCUnit +-- @param Cargo.CargoGroup#CARGO_GROUP Cargo +-- @return #AI_CARGO_APC +function AI_CARGO_APC:FollowToCarrier( Me, APCUnit, CargoGroup ) + + local InfantryGroup = CargoGroup:GetGroup() + + self:F( { self = self:GetClassNameAndID(), InfantryGroup = InfantryGroup:GetName() } ) + + --if self:Is( "Following" ) then + + if APCUnit:IsAlive() then + -- We check if the Cargo is near to the CargoCarrier. + if InfantryGroup:IsPartlyInZone( ZONE_UNIT:New( "Radius", APCUnit, 25 ) ) then + + -- The Cargo does not need to follow the Carrier. + Me:Guard() + + else + + self:F( { InfantryGroup = InfantryGroup:GetName() } ) + + if InfantryGroup:IsAlive() then + + self:F( { InfantryGroup = InfantryGroup:GetName() } ) + + local Waypoints = {} + + -- Calculate the new Route. + local FromCoord = InfantryGroup:GetCoordinate() + local FromGround = FromCoord:WaypointGround( 10, "Diamond" ) + self:F({FromGround=FromGround}) + table.insert( Waypoints, FromGround ) + + local ToCoord = APCUnit:GetCoordinate():GetRandomCoordinateInRadius( 10, 5 ) + local ToGround = ToCoord:WaypointGround( 10, "Diamond" ) + self:F({ToGround=ToGround}) + table.insert( Waypoints, ToGround ) + + local TaskRoute = InfantryGroup:TaskFunction( "AI_CARGO_APC.FollowToCarrier", Me, APCUnit, CargoGroup ) + + self:F({Waypoints = Waypoints}) + local Waypoint = Waypoints[#Waypoints] + InfantryGroup:SetTaskWaypoint( Waypoint, TaskRoute ) -- Set for the given Route at Waypoint 2 the TaskRouteToZone. + + InfantryGroup:Route( Waypoints, 1 ) -- Move after a random seconds to the Route. See the Route method for details. + end + end + end +end + + +--- On after Monitor event. +-- @param #AI_CARGO_APC self +-- @param Wrapper.Group#GROUP APC +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AI_CARGO_APC:onafterMonitor( APC, From, Event, To ) + self:F( { APC, From, Event, To, IsTransporting = self:IsTransporting() } ) + + if self.CombatRadius > 0 then + if APC and APC:IsAlive() then + if self.CarrierCoordinate then + if self:IsTransporting() == true then + local Coordinate = APC:GetCoordinate() + if self:Is( "Unloaded" ) or self:Is( "Loaded" ) then + self.Zone:Scan( { Object.Category.UNIT } ) + if self.Zone:IsAllInZoneOfCoalition( self.Coalition ) then + if self:Is( "Unloaded" ) then + -- There are no enemies within combat radius. Reload the CargoCarrier. + self:Reload() + end + else + if self:Is( "Loaded" ) then + -- There are enemies within combat radius. Unload the CargoCarrier. + self:__Unload( 1, nil, true ) -- The 2nd parameter is true, which means that the unload is for defending the carrier, not to deploy! + else + if self:Is( "Unloaded" ) then + --self:Follow() + end + self:F( "I am here" .. self:GetCurrentState() ) + if self:Is( "Following" ) then + for Cargo, APCUnit in pairs( self.Carrier_Cargo ) do + local Cargo = Cargo -- Cargo.Cargo#CARGO + local APCUnit = APCUnit -- Wrapper.Unit#UNIT + if Cargo:IsAlive() then + if not Cargo:IsNear( APCUnit, 40 ) then + APCUnit:RouteStop() + self.CarrierStopped = true + else + if self.CarrierStopped then + if Cargo:IsNear( APCUnit, 25 ) then + APCUnit:RouteResume() + self.CarrierStopped = nil + end + end + end + end + end + end + end + end + end + end + + end + self.CarrierCoordinate = APC:GetCoordinate() + end + + self:__Monitor( -5 ) + end + +end + + +--- On after Follow event. +-- @param #AI_CARGO_APC self +-- @param Wrapper.Group#GROUP APC +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AI_CARGO_APC:onafterFollow( APC, From, Event, To ) + self:F( { APC, From, Event, To } ) + + self:F( "Follow" ) + if APC and APC:IsAlive() then + for Cargo, APCUnit in pairs( self.Carrier_Cargo ) do + local Cargo = Cargo -- Cargo.Cargo#CARGO + if Cargo:IsUnLoaded() then + self:FollowToCarrier( self, APCUnit, Cargo ) + APCUnit:RouteResume() + end + end + end + +end + +--- Pickup task function. Triggers Load event. +-- @param Wrapper.Group#GROUP APC The cargo carrier group. +-- @param #AI_CARGO_APC sel `AI_CARGO_APC` class. +-- @param Core.Point#COORDINATE Coordinate. The coordinate (not used). +-- @param #number Speed Speed (not used). +-- @param Core.Zone#ZONE PickupZone Pickup zone. +function AI_CARGO_APC._Pickup(APC, self, Coordinate, Speed, PickupZone) + + APC:F( { "AI_CARGO_APC._Pickup:", APC:GetName() } ) + + if APC:IsAlive() then + self:Load( PickupZone ) + end +end + +--- Deploy task function. Triggers Unload event. +-- @param Wrapper.Group#GROUP APC The cargo carrier group. +-- @param #AI_CARGO_APC self `AI_CARGO_APC` class. +-- @param Core.Point#COORDINATE Coordinate. The coordinate (not used). +-- @param Core.Zone#ZONE DeployZone Deploy zone. +function AI_CARGO_APC._Deploy(APC, self, Coordinate, DeployZone) + + APC:F( { "AI_CARGO_APC._Deploy:", APC } ) + + if APC:IsAlive() then + self:Unload( DeployZone ) + end +end + + + +--- On after Pickup event. +-- @param #AI_CARGO_APC self +-- @param Wrapper.Group#GROUP APC +-- @param From +-- @param Event +-- @param To +-- @param Core.Point#COORDINATE Coordinate of the pickup point. +-- @param #number Speed Speed in km/h to drive to the pickup coordinate. Default is 50% of max possible speed the unit can go. +-- @param #number Height Height in meters to move to the pickup coordinate. This parameter is ignored for APCs. +-- @param Core.Zone#ZONE PickupZone (optional) The zone where the cargo will be picked up. The PickupZone can be nil, if there wasn't any PickupZoneSet provided. +function AI_CARGO_APC:onafterPickup( APC, From, Event, To, Coordinate, Speed, Height, PickupZone ) + + if APC and APC:IsAlive() then + + if Coordinate then + self.RoutePickup = true + + local _speed=Speed or APC:GetSpeedMax()*0.5 + + -- Route on road. + local Waypoints = {} + + if self.pickupOffroad then + Waypoints[1]=APC:GetCoordinate():WaypointGround(Speed, self.pickupFormation) + Waypoints[2]=Coordinate:WaypointGround(_speed, self.pickupFormation, DCSTasks) + else + Waypoints=APC:TaskGroundOnRoad(Coordinate, _speed, ENUMS.Formation.Vehicle.OffRoad, true) + end + + + local TaskFunction = APC:TaskFunction( "AI_CARGO_APC._Pickup", self, Coordinate, Speed, PickupZone ) + + local Waypoint = Waypoints[#Waypoints] + APC:SetTaskWaypoint( Waypoint, TaskFunction ) -- Set for the given Route at Waypoint 2 the TaskRouteToZone. + + APC:Route( Waypoints, 1 ) -- Move after a random seconds to the Route. See the Route method for details. + else + AI_CARGO_APC._Pickup( APC, self, Coordinate, Speed, PickupZone ) + end + + self:GetParent( self, AI_CARGO_APC ).onafterPickup( self, APC, From, Event, To, Coordinate, Speed, Height, PickupZone ) + end + +end + + +--- On after Deploy event. +-- @param #AI_CARGO_APC self +-- @param Wrapper.Group#GROUP APC +-- @param From +-- @param Event +-- @param To +-- @param Core.Point#COORDINATE Coordinate Deploy place. +-- @param #number Speed Speed in km/h to drive to the depoly coordinate. Default is 50% of max possible speed the unit can go. +-- @param #number Height Height in meters to move to the deploy coordinate. This parameter is ignored for APCs. +-- @param Core.Zone#ZONE DeployZone The zone where the cargo will be deployed. +function AI_CARGO_APC:onafterDeploy( APC, From, Event, To, Coordinate, Speed, Height, DeployZone ) + + if APC and APC:IsAlive() then + + self.RouteDeploy = true + + -- Set speed in km/h. + local speedmax=APC:GetSpeedMax() + local _speed=Speed or speedmax*0.5 + _speed=math.min(_speed, speedmax) + + -- Route on road. + local Waypoints = {} + + if self.deployOffroad then + Waypoints[1]=APC:GetCoordinate():WaypointGround(Speed, self.deployFormation) + Waypoints[2]=Coordinate:WaypointGround(_speed, self.deployFormation, DCSTasks) + else + Waypoints=APC:TaskGroundOnRoad(Coordinate, _speed, ENUMS.Formation.Vehicle.OffRoad, true) + end + + -- Task function + local TaskFunction = APC:TaskFunction( "AI_CARGO_APC._Deploy", self, Coordinate, DeployZone ) + + -- Last waypoint + local Waypoint = Waypoints[#Waypoints] + + -- Set task function + APC:SetTaskWaypoint(Waypoint, TaskFunction) -- Set for the given Route at Waypoint 2 the TaskRouteToZone. + + -- Route group + APC:Route( Waypoints, 1 ) -- Move after a random seconds to the Route. See the Route method for details. + + -- Call parent function. + self:GetParent( self, AI_CARGO_APC ).onafterDeploy( self, APC, From, Event, To, Coordinate, Speed, Height, DeployZone ) + + end + +end + +--- On after Unloaded event. +-- @param #AI_CARGO_APC self +-- @param Wrapper.Group#GROUP Carrier +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string Cargo.Cargo#CARGO Cargo Cargo object. +-- @param #boolean Deployed Cargo is deployed. +-- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. +function AI_CARGO_APC:onafterUnloaded( Carrier, From, Event, To, Cargo, CarrierUnit, DeployZone, Defend ) + self:F( { Carrier, From, Event, To, DeployZone = DeployZone, Defend = Defend } ) + + + self:GetParent( self, AI_CARGO_APC ).onafterUnloaded( self, Carrier, From, Event, To, Cargo, CarrierUnit, DeployZone, Defend ) + + -- If Defend == true then we need to scan for possible enemies within combat zone and engage only ground forces. + if Defend == true then + self.Zone:Scan( { Object.Category.UNIT } ) + if not self.Zone:IsAllInZoneOfCoalition( self.Coalition ) then + -- OK, enemies nearby, now find the enemies and attack them. + local AttackUnits = self.Zone:GetScannedUnits() -- #list + local Move = {} + local CargoGroup = Cargo.CargoObject -- Wrapper.Group#GROUP + Move[#Move+1] = CargoGroup:GetCoordinate():WaypointGround( 70, "Custom" ) + for UnitId, AttackUnit in pairs( AttackUnits ) do + local MooseUnit = UNIT:Find( AttackUnit ) + if MooseUnit:GetCoalition() ~= CargoGroup:GetCoalition() then + Move[#Move+1] = MooseUnit:GetCoordinate():WaypointGround( 70, "Line abreast" ) + --MoveTo.Task = CargoGroup:TaskCombo( CargoGroup:TaskAttackUnit( MooseUnit, true ) ) + self:F( { MooseUnit = MooseUnit:GetName(), CargoGroup = CargoGroup:GetName() } ) + end + end + CargoGroup:RoutePush( Move, 0.1 ) + end + + end + +end + +--- On after Deployed event. +-- @param #AI_CARGO_APC self +-- @param Wrapper.Group#GROUP Carrier +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. +function AI_CARGO_APC:onafterDeployed( APC, From, Event, To, DeployZone, Defend ) + self:F( { APC, From, Event, To, DeployZone = DeployZone, Defend = Defend } ) + + self:__Guard( 0.1 ) + + self:GetParent( self, AI_CARGO_APC ).onafterDeployed( self, APC, From, Event, To, DeployZone, Defend ) + +end + + +--- On after Home event. +-- @param #AI_CARGO_APC self +-- @param Wrapper.Group#GROUP APC +-- @param From +-- @param Event +-- @param To +-- @param Core.Point#COORDINATE Coordinate Home place. +-- @param #number Speed Speed in km/h to drive to the pickup coordinate. Default is 50% of max possible speed the unit can go. +-- @param #number Height Height in meters to move to the home coordinate. This parameter is ignored for APCs. +function AI_CARGO_APC:onafterHome( APC, From, Event, To, Coordinate, Speed, Height, HomeZone ) + + if APC and APC:IsAlive() ~= nil then + + self.RouteHome = true + + Speed = Speed or APC:GetSpeedMax()*0.5 + + local Waypoints = APC:TaskGroundOnRoad( Coordinate, Speed, "Line abreast", true ) + + self:F({Waypoints = Waypoints}) + local Waypoint = Waypoints[#Waypoints] + + APC:Route( Waypoints, 1 ) -- Move after a random seconds to the Route. See the Route method for details. + + end + +end +--- **AI** - Models the intelligent transportation of cargo using helicopters. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_Cargo_Helicopter +-- @image AI_Cargo_Dispatching_For_Helicopters.JPG + +--- @type AI_CARGO_HELICOPTER +-- @extends Core.Fsm#FSM_CONTROLLABLE + + +--- Brings a dynamic cargo handling capability for an AI helicopter group. +-- +-- Helicopter carriers can be mobilized to intelligently transport infantry and other cargo within the simulation. +-- +-- The AI_CARGO_HELICOPTER class uses the @{Cargo.Cargo} capabilities within the MOOSE framework. +-- @{Cargo.Cargo} must be declared within the mission to make the AI_CARGO_HELICOPTER object recognize the cargo. +-- Please consult the @{Cargo.Cargo} module for more information. +-- +-- ## Cargo pickup. +-- +-- Using the @{#AI_CARGO_HELICOPTER.Pickup}() method, you are able to direct the helicopters towards a point on the battlefield to board/load the cargo at the specific coordinate. +-- Ensure that the landing zone is horizontally flat, and that trees cannot be found in the landing vicinity, or the helicopters won't land or will even crash! +-- +-- ## Cargo deployment. +-- +-- Using the @{#AI_CARGO_HELICOPTER.Deploy}() method, you are able to direct the helicopters towards a point on the battlefield to unboard/unload the cargo at the specific coordinate. +-- Ensure that the landing zone is horizontally flat, and that trees cannot be found in the landing vicinity, or the helicopters won't land or will even crash! +-- +-- ## Infantry health. +-- +-- When infantry is unboarded from the helicopters, the infantry is actually respawned into the battlefield. +-- As a result, the unboarding infantry is very _healthy_ every time it unboards. +-- This is due to the limitation of the DCS simulator, which is not able to specify the health of new spawned units as a parameter. +-- However, infantry that was destroyed when unboarded, won't be respawned again. Destroyed is destroyed. +-- As a result, there is some additional strength that is gained when an unboarding action happens, but in terms of simulation balance this has +-- marginal impact on the overall battlefield simulation. Fortunately, the firing strength of infantry is limited, and thus, respacing healthy infantry every +-- time is not so much of an issue ... +-- +-- +-- === +-- +-- @field #AI_CARGO_HELICOPTER +AI_CARGO_HELICOPTER = { + ClassName = "AI_CARGO_HELICOPTER", + Coordinate = nil, -- Core.Point#COORDINATE, +} + +AI_CARGO_QUEUE = {} + +--- Creates a new AI_CARGO_HELICOPTER object. +-- @param #AI_CARGO_HELICOPTER self +-- @param Wrapper.Group#GROUP Helicopter +-- @param Core.Set#SET_CARGO CargoSet +-- @return #AI_CARGO_HELICOPTER +function AI_CARGO_HELICOPTER:New( Helicopter, CargoSet ) + + local self = BASE:Inherit( self, AI_CARGO:New( Helicopter, CargoSet ) ) -- #AI_CARGO_HELICOPTER + + self.Zone = ZONE_GROUP:New( Helicopter:GetName(), Helicopter, 300 ) + + self:SetStartState( "Unloaded" ) + -- Boarding + self:AddTransition( "Unloaded", "Pickup", "Unloaded" ) + self:AddTransition( "*", "Landed", "*" ) + self:AddTransition( "*", "Load", "*" ) + self:AddTransition( "*", "Loaded", "Loaded" ) + self:AddTransition( "Loaded", "PickedUp", "Loaded" ) + + -- Unboarding + self:AddTransition( "Loaded", "Deploy", "*" ) + self:AddTransition( "*", "Queue", "*" ) + self:AddTransition( "*", "Orbit" , "*" ) + self:AddTransition( "*", "Destroyed", "*" ) + self:AddTransition( "*", "Unload", "*" ) + self:AddTransition( "*", "Unloaded", "Unloaded" ) + self:AddTransition( "Unloaded", "Deployed", "Unloaded" ) + + -- RTB + self:AddTransition( "*", "Home" , "*" ) + + --- Pickup Handler OnBefore for AI_CARGO_HELICOPTER + -- @function [parent=#AI_CARGO_HELICOPTER] OnBeforePickup + -- @param #AI_CARGO_HELICOPTER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Core.Point#COORDINATE Coordinate + -- @return #boolean + + --- Pickup Handler OnAfter for AI_CARGO_HELICOPTER + -- @function [parent=#AI_CARGO_HELICOPTER] OnAfterPickup + -- @param #AI_CARGO_HELICOPTER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Core.Point#COORDINATE Coordinate + -- @param #number Speed Speed in km/h to fly to the pickup coordinate. Default is 50% of max possible speed the unit can go. + + --- PickedUp Handler OnAfter for AI_CARGO_HELICOPTER - Cargo set has been picked up, ready to deploy + -- @function [parent=#AI_CARGO_HELICOPTER] OnAfterPickedUp + -- @param #AI_CARGO_HELICOPTER self + -- @param Wrapper.Group#GROUP Helicopter The helicopter #GROUP object + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Wrapper.Unit#UNIT Unit The helicopter #UNIT object + + --- Unloaded Handler OnAfter for AI_CARGO_HELICOPTER - Cargo unloaded, carrier is empty + -- @function [parent=#AI_CARGO_HELICOPTER] OnAfterUnloaded + -- @param #AI_CARGO_HELICOPTER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Cargo.CargoGroup#CARGO_GROUP Cargo The #CARGO_GROUP object. + -- @param Wrapper.Unit#UNIT Unit The helicopter #UNIT object + + --- Pickup Trigger for AI_CARGO_HELICOPTER + -- @function [parent=#AI_CARGO_HELICOPTER] Pickup + -- @param #AI_CARGO_HELICOPTER self + -- @param Core.Point#COORDINATE Coordinate + -- @param #number Speed Speed in km/h to fly to the pickup coordinate. Default is 50% of max possible speed the unit can go. + + --- Pickup Asynchronous Trigger for AI_CARGO_HELICOPTER + -- @function [parent=#AI_CARGO_HELICOPTER] __Pickup + -- @param #AI_CARGO_HELICOPTER self + -- @param #number Delay Delay in seconds. + -- @param Core.Point#COORDINATE Coordinate + -- @param #number Speed Speed in km/h to go to the pickup coordinate. Default is 50% of max possible speed the unit can go. + + --- Deploy Handler OnBefore for AI_CARGO_HELICOPTER + -- @function [parent=#AI_CARGO_HELICOPTER] OnBeforeDeploy + -- @param #AI_CARGO_HELICOPTER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Core.Point#COORDINATE Coordinate Place at which cargo is deployed. + -- @param #number Speed Speed in km/h to fly to the pickup coordinate. Default is 50% of max possible speed the unit can go. + -- @return #boolean + + --- Deploy Handler OnAfter for AI_CARGO_HELICOPTER + -- @function [parent=#AI_CARGO_HELICOPTER] OnAfterDeploy + -- @param #AI_CARGO_HELICOPTER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Core.Point#COORDINATE Coordinate + -- @param #number Speed Speed in km/h to fly to the pickup coordinate. Default is 50% of max possible speed the unit can go. + + --- Deployed Handler OnAfter for AI_CARGO_HELICOPTER + -- @function [parent=#AI_CARGO_HELICOPTER] OnAfterDeployed + -- @param #AI_CARGO_HELICOPTER self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Deploy Trigger for AI_CARGO_HELICOPTER + -- @function [parent=#AI_CARGO_HELICOPTER] Deploy + -- @param #AI_CARGO_HELICOPTER self + -- @param Core.Point#COORDINATE Coordinate Place at which the cargo is deployed. + -- @param #number Speed Speed in km/h to fly to the pickup coordinate. Default is 50% of max possible speed the unit can go. + + --- Deploy Asynchronous Trigger for AI_CARGO_HELICOPTER + -- @function [parent=#AI_CARGO_HELICOPTER] __Deploy + -- @param #number Delay Delay in seconds. + -- @param #AI_CARGO_HELICOPTER self + -- @param Core.Point#COORDINATE Coordinate Place at which the cargo is deployed. + -- @param #number Speed Speed in km/h to fly to the pickup coordinate. Default is 50% of max possible speed the unit can go. + + --- Home Trigger for AI_CARGO_HELICOPTER + -- @function [parent=#AI_CARGO_HELICOPTER] Home + -- @param #AI_CARGO_HELICOPTER self + -- @param Core.Point#COORDINATE Coordinate Place to which the helicopter will go. + -- @param #number Speed (optional) Speed in km/h to fly to the pickup coordinate. Default is 50% of max possible speed the unit can go. + -- @param #number Height (optional) Height the Helicopter should be flying at. + + --- Home Asynchronous Trigger for AI_CARGO_HELICOPTER + -- @function [parent=#AI_CARGO_HELICOPTER] __Home + -- @param #number Delay Delay in seconds. + -- @param #AI_CARGO_HELICOPTER self + -- @param Core.Point#COORDINATE Coordinate Place to which the helicopter will go. + -- @param #number Speed (optional) Speed in km/h to fly to the pickup coordinate. Default is 50% of max possible speed the unit can go. + -- @param #number Height (optional) Height the Helicopter should be flying at. + + -- We need to capture the Crash events for the helicopters. + -- The helicopter reference is used in the semaphore AI_CARGO_QUEUE. + -- So, we need to unlock this when the helo is not anymore ... + Helicopter:HandleEvent( EVENTS.Crash, + function( Helicopter, EventData ) + AI_CARGO_QUEUE[Helicopter] = nil + end + ) + + -- We need to capture the Land events for the helicopters. + -- The helicopter reference is used in the semaphore AI_CARGO_QUEUE. + -- So, we need to unlock this when the helo has landed, which can be anywhere ... + -- But only free the landing coordinate after 1 minute, to ensure that all helos have left. + Helicopter:HandleEvent( EVENTS.Land, + function( Helicopter, EventData ) + self:ScheduleOnce( 60, + function( Helicopter ) + AI_CARGO_QUEUE[Helicopter] = nil + end, Helicopter + ) + end + ) + + self:SetCarrier( Helicopter ) + + self.landingspeed = 15 -- kph + self.landingheight = 5.5 -- meter + + return self +end + + + + + +--- Set the Carrier. +-- @param #AI_CARGO_HELICOPTER self +-- @param Wrapper.Group#GROUP Helicopter +-- @return #AI_CARGO_HELICOPTER +function AI_CARGO_HELICOPTER:SetCarrier( Helicopter ) + + local AICargo = self + + self.Helicopter = Helicopter -- Wrapper.Group#GROUP + self.Helicopter:SetState( self.Helicopter, "AI_CARGO_HELICOPTER", self ) + + self.RoutePickup = false + self.RouteDeploy = false + + Helicopter:HandleEvent( EVENTS.Dead ) + Helicopter:HandleEvent( EVENTS.Hit ) + Helicopter:HandleEvent( EVENTS.Land ) + + function Helicopter:OnEventDead( EventData ) + local AICargoTroops = self:GetState( self, "AI_CARGO_HELICOPTER" ) + self:F({AICargoTroops=AICargoTroops}) + if AICargoTroops then + self:F({}) + if not AICargoTroops:Is( "Loaded" ) then + -- There are enemies within combat range. Unload the Helicopter. + AICargoTroops:Destroyed() + end + end + end + + function Helicopter:OnEventLand( EventData ) + AICargo:Landed() + end + + self.Coalition = self.Helicopter:GetCoalition() + + self:SetControllable( Helicopter ) + + return self +end + +--- Set landingspeed and -height for helicopter landings. Adjust after tracing if your helis get stuck after landing. +-- @param #AI_CARGO_HELICOPTER self +-- @param #number speed Landing speed in kph(!), e.g. 15 +-- @param #number height Landing height in meters(!), e.g. 5.5 +-- @return #AI_CARGO_HELICOPTER self +-- @usage If your choppers get stuck, add tracing to your script to determine if they hit the right parameters like so: +-- +-- BASE:TraceOn() +-- BASE:TraceClass("AI_CARGO_HELICOPTER") +-- +-- Watch the DCS.log for entries stating `Helicopter:, Height = Helicopter:, Velocity = Helicopter:` +-- Adjust if necessary. +function AI_CARGO_HELICOPTER:SetLandingSpeedAndHeight(speed, height) + local _speed = speed or 15 + local _height = height or 5.5 + self.landingheight = _height + self.landingspeed = _speed + return self +end + +--- @param #AI_CARGO_HELICOPTER self +-- @param Wrapper.Group#GROUP Helicopter +-- @param From +-- @param Event +-- @param To +function AI_CARGO_HELICOPTER:onafterLanded( Helicopter, From, Event, To ) + self:F({From, Event, To}) + Helicopter:F( { Name = Helicopter:GetName() } ) + + if Helicopter and Helicopter:IsAlive() then + + -- S_EVENT_LAND is directly called in two situations: + -- 1 - When the helo lands normally on the ground. + -- 2 - when the helo is hit and goes RTB or even when it is destroyed. + -- For point 2, this is an issue, the infantry may not unload in this case! + -- So we check if the helo is on the ground, and velocity< 15. + -- Only then the infantry can unload (and load too, for consistency)! + + self:T( { Helicopter:GetName(), Height = Helicopter:GetHeight( true ), Velocity = Helicopter:GetVelocityKMH() } ) + + if self.RoutePickup == true then + if Helicopter:GetHeight( true ) <= self.landingheight then --and Helicopter:GetVelocityKMH() < self.landingspeed then + --self:Load( Helicopter:GetPointVec2() ) + self:Load( self.PickupZone ) + self.RoutePickup = false + end + end + + if self.RouteDeploy == true then + if Helicopter:GetHeight( true ) <= self.landingheight then --and Helicopter:GetVelocityKMH() < self.landingspeed then + self:Unload( self.DeployZone ) + self.RouteDeploy = false + end + end + + end + +end + +--- @param #AI_CARGO_HELICOPTER self +-- @param Wrapper.Group#GROUP Helicopter +-- @param From +-- @param Event +-- @param To +-- @param Core.Point#COORDINATE Coordinate +-- @param #number Speed +function AI_CARGO_HELICOPTER:onafterQueue( Helicopter, From, Event, To, Coordinate, Speed, DeployZone ) + self:F({From, Event, To, Coordinate, Speed, DeployZone}) + local HelicopterInZone = false + + if Helicopter and Helicopter:IsAlive() == true then + + local Distance = Coordinate:DistanceFromPointVec2( Helicopter:GetCoordinate() ) + + if Distance > 2000 then + self:__Queue( -10, Coordinate, Speed, DeployZone ) + else + + local ZoneFree = true + + for Helicopter, ZoneQueue in pairs( AI_CARGO_QUEUE ) do + local ZoneQueue = ZoneQueue -- Core.Zone#ZONE_RADIUS + if ZoneQueue:IsCoordinateInZone( Coordinate ) then + ZoneFree = false + end + end + + self:F({ZoneFree=ZoneFree}) + + if ZoneFree == true then + + local ZoneQueue = ZONE_RADIUS:New( Helicopter:GetName(), Coordinate:GetVec2(), 100 ) + + AI_CARGO_QUEUE[Helicopter] = ZoneQueue + + local Route = {} + +-- local CoordinateFrom = Helicopter:GetCoordinate() +-- local WaypointFrom = CoordinateFrom:WaypointAir( +-- "RADIO", +-- POINT_VEC3.RoutePointType.TurningPoint, +-- POINT_VEC3.RoutePointAction.TurningPoint, +-- Speed, +-- true +-- ) +-- Route[#Route+1] = WaypointFrom + local CoordinateTo = Coordinate + + local landheight = CoordinateTo:GetLandHeight() -- get target height + CoordinateTo.y = landheight + 50 -- flight height should be 50m above ground + + local WaypointTo = CoordinateTo:WaypointAir( + "RADIO", + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + 50, + true + ) + Route[#Route+1] = WaypointTo + + local Tasks = {} + Tasks[#Tasks+1] = Helicopter:TaskLandAtVec2( CoordinateTo:GetVec2() ) + Route[#Route].task = Helicopter:TaskCombo( Tasks ) + + Route[#Route+1] = WaypointTo + + -- Now route the helicopter + Helicopter:Route( Route, 0 ) + + -- Keep the DeployZone, because when the helo has landed, we want to provide the DeployZone to the mission designer as part of the Unloaded event. + self.DeployZone = DeployZone + + else + self:__Queue( -10, Coordinate, Speed, DeployZone ) + end + end + else + AI_CARGO_QUEUE[Helicopter] = nil + end +end + + +--- @param #AI_CARGO_HELICOPTER self +-- @param Wrapper.Group#GROUP Helicopter +-- @param From +-- @param Event +-- @param To +-- @param Core.Point#COORDINATE Coordinate +-- @param #number Speed +function AI_CARGO_HELICOPTER:onafterOrbit( Helicopter, From, Event, To, Coordinate ) + self:F({From, Event, To, Coordinate}) + + if Helicopter and Helicopter:IsAlive() then + + local Route = {} + + local CoordinateTo = Coordinate + local landheight = CoordinateTo:GetLandHeight() -- get target height + CoordinateTo.y = landheight + 50 -- flight height should be 50m above ground + + local WaypointTo = CoordinateTo:WaypointAir("RADIO", POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, 50, true) + Route[#Route+1] = WaypointTo + + local Tasks = {} + Tasks[#Tasks+1] = Helicopter:TaskOrbitCircle( math.random( 30, 80 ), 150, CoordinateTo:GetRandomCoordinateInRadius( 800, 500 ) ) + Route[#Route].task = Helicopter:TaskCombo( Tasks ) + + Route[#Route+1] = WaypointTo + + -- Now route the helicopter + Helicopter:Route(Route, 0) + end +end + + + +--- On after Deployed event. +-- @param #AI_CARGO_HELICOPTER self +-- @param Wrapper.Group#GROUP Helicopter +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Cargo.Cargo#CARGO Cargo Cargo object. +-- @param #boolean Deployed Cargo is deployed. +-- @return #boolean True if all cargo has been unloaded. +function AI_CARGO_HELICOPTER:onafterDeployed( Helicopter, From, Event, To, DeployZone ) + self:F( { From, Event, To, DeployZone = DeployZone } ) + + self:Orbit( Helicopter:GetCoordinate(), 50 ) + + -- Free the coordinate zone after 30 seconds, so that the original helicopter can fly away first. + self:ScheduleOnce( 30, + function( Helicopter ) + AI_CARGO_QUEUE[Helicopter] = nil + end, Helicopter + ) + + self:GetParent( self, AI_CARGO_HELICOPTER ).onafterDeployed( self, Helicopter, From, Event, To, DeployZone ) + +end + +--- On after Pickup event. +-- @param #AI_CARGO_HELICOPTER self +-- @param Wrapper.Group#GROUP Helicopter +-- @param From +-- @param Event +-- @param To +-- @param Core.Point#COORDINATE Coordinate Pickup place. +-- @param #number Speed Speed in km/h to fly to the pickup coordinate. Default is 50% of max possible speed the unit can go. +-- @param #number Height Height in meters to move to the pickup coordinate. This parameter is ignored for APCs. +-- @param Core.Zone#ZONE PickupZone (optional) The zone where the cargo will be picked up. The PickupZone can be nil, if there wasn't any PickupZoneSet provided. +function AI_CARGO_HELICOPTER:onafterPickup( Helicopter, From, Event, To, Coordinate, Speed, Height, PickupZone ) + self:F({Coordinate, Speed, Height, PickupZone }) + + if Helicopter and Helicopter:IsAlive() ~= nil then + + Helicopter:Activate() + + self.RoutePickup = true + Coordinate.y = Height + + local _speed=Speed or Helicopter:GetSpeedMax()*0.5 + + local Route = {} + + --- Calculate the target route point. + local CoordinateFrom = Helicopter:GetCoordinate() + + --- Create a route point of type air. + local WaypointFrom = CoordinateFrom:WaypointAir("RADIO", POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, _speed, true) + + --- Create a route point of type air. + local CoordinateTo = Coordinate + local landheight = CoordinateTo:GetLandHeight() -- get target height + CoordinateTo.y = landheight + 50 -- flight height should be 50m above ground + + local WaypointTo = CoordinateTo:WaypointAir("RADIO", POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint,_speed, true) + + Route[#Route+1] = WaypointFrom + Route[#Route+1] = WaypointTo + + --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... + Helicopter:WayPointInitialize( Route ) + + local Tasks = {} + + Tasks[#Tasks+1] = Helicopter:TaskLandAtVec2( CoordinateTo:GetVec2() ) + Route[#Route].task = Helicopter:TaskCombo( Tasks ) + + Route[#Route+1] = WaypointTo + + -- Now route the helicopter + Helicopter:Route( Route, 1 ) + + self.PickupZone = PickupZone + + self:GetParent( self, AI_CARGO_HELICOPTER ).onafterPickup( self, Helicopter, From, Event, To, Coordinate, Speed, Height, PickupZone ) + + end + +end + +--- Depoloy function and queue. +-- @param #AI_CARGO_HELICOPTER self +-- @param Wrapper.Group#GROUP AICargoHelicopter +-- @param Core.Point#COORDINATE Coordinate Coordinate +function AI_CARGO_HELICOPTER:_Deploy( AICargoHelicopter, Coordinate, DeployZone ) + AICargoHelicopter:__Queue( -10, Coordinate, 100, DeployZone ) +end + +--- On after Deploy event. +-- @param #AI_CARGO_HELICOPTER self +-- @param Wrapper.Group#GROUP Helicopter Transport helicopter. +-- @param From +-- @param Event +-- @param To +-- @param Core.Point#COORDINATE Coordinate Place at which the cargo is deployed. +-- @param #number Speed Speed in km/h to fly to the pickup coordinate. Default is 50% of max possible speed the unit can go. +-- @param #number Height Height in meters to move to the deploy coordinate. +function AI_CARGO_HELICOPTER:onafterDeploy( Helicopter, From, Event, To, Coordinate, Speed, Height, DeployZone ) + self:F({From, Event, To, Coordinate, Speed, Height, DeployZone}) + if Helicopter and Helicopter:IsAlive() ~= nil then + + self.RouteDeploy = true + + + local Route = {} + + --- Calculate the target route point. + + Coordinate.y = Height + + local _speed=Speed or Helicopter:GetSpeedMax()*0.5 + + --- Create a route point of type air. + local CoordinateFrom = Helicopter:GetCoordinate() + local WaypointFrom = CoordinateFrom:WaypointAir("RADIO", POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, _speed, true) + Route[#Route+1] = WaypointFrom + Route[#Route+1] = WaypointFrom + + --- Create a route point of type air. + + local CoordinateTo = Coordinate + local landheight = CoordinateTo:GetLandHeight() -- get target height + CoordinateTo.y = landheight + 50 -- flight height should be 50m above ground + + local WaypointTo = CoordinateTo:WaypointAir("RADIO", POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, _speed, true) + + Route[#Route+1] = WaypointTo + Route[#Route+1] = WaypointTo + + --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... + Helicopter:WayPointInitialize( Route ) + + local Tasks = {} + + -- The _Deploy function does not exist. + Tasks[#Tasks+1] = Helicopter:TaskFunction( "AI_CARGO_HELICOPTER._Deploy", self, Coordinate, DeployZone ) + + Tasks[#Tasks+1] = Helicopter:TaskOrbitCircle( math.random( 30, 100 ), _speed, CoordinateTo:GetRandomCoordinateInRadius( 800, 500 ) ) + + --Tasks[#Tasks+1] = Helicopter:TaskLandAtVec2( CoordinateTo:GetVec2() ) + Route[#Route].task = Helicopter:TaskCombo( Tasks ) + + Route[#Route+1] = WaypointTo + + -- Now route the helicopter + Helicopter:Route( Route, 0 ) + + self:GetParent( self, AI_CARGO_HELICOPTER ).onafterDeploy( self, Helicopter, From, Event, To, Coordinate, Speed, Height, DeployZone ) + end + +end + + +--- On after Home event. +-- @param #AI_CARGO_HELICOPTER self +-- @param Wrapper.Group#GROUP Helicopter +-- @param From +-- @param Event +-- @param To +-- @param Core.Point#COORDINATE Coordinate Home place. +-- @param #number Speed Speed in km/h to fly to the pickup coordinate. Default is 50% of max possible speed the unit can go. +-- @param #number Height Height in meters to move to the home coordinate. +-- @param Core.Zone#ZONE HomeZone The zone wherein the carrier will return when all cargo has been transported. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. +function AI_CARGO_HELICOPTER:onafterHome( Helicopter, From, Event, To, Coordinate, Speed, Height, HomeZone ) + self:F({From, Event, To, Coordinate, Speed, Height}) + + if Helicopter and Helicopter:IsAlive() ~= nil then + + self.RouteHome = true + + local Route = {} + + --- Calculate the target route point. + + --Coordinate.y = Height + Height = Height or 50 + + Speed = Speed or Helicopter:GetSpeedMax()*0.5 + + --- Create a route point of type air. + local CoordinateFrom = Helicopter:GetCoordinate() + + local WaypointFrom = CoordinateFrom:WaypointAir("RADIO", POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, Speed, true) + Route[#Route+1] = WaypointFrom + + --- Create a route point of type air. + local CoordinateTo = Coordinate + local landheight = CoordinateTo:GetLandHeight() -- get target height + CoordinateTo.y = landheight + Height -- flight height should be 50m above ground + + local WaypointTo = CoordinateTo:WaypointAir("RADIO", POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, Speed, true) + + Route[#Route+1] = WaypointTo + + --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... + Helicopter:WayPointInitialize( Route ) + + local Tasks = {} + + Tasks[#Tasks+1] = Helicopter:TaskLandAtVec2( CoordinateTo:GetVec2() ) + Route[#Route].task = Helicopter:TaskCombo( Tasks ) + + Route[#Route+1] = WaypointTo + + -- Now route the helicopter + Helicopter:Route(Route, 0) + end + +end + +--- **AI** - Models the intelligent transportation of cargo using airplanes. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_Cargo_Airplane +-- @image AI_Cargo_Dispatching_For_Airplanes.JPG + +--- @type AI_CARGO_AIRPLANE +-- @extends Core.Fsm#FSM_CONTROLLABLE + + +--- Brings a dynamic cargo handling capability for an AI airplane group. +-- +-- Airplane carrier equipment can be mobilized to intelligently transport infantry and other cargo within the simulation between airbases. +-- +-- The AI_CARGO_AIRPLANE module uses the @{Cargo.Cargo} capabilities within the MOOSE framework. +-- @{Cargo.Cargo} must be declared within the mission to make AI_CARGO_AIRPLANE recognize the cargo. +-- Please consult the @{Cargo.Cargo} module for more information. +-- +-- ## Cargo pickup. +-- +-- Using the @{#AI_CARGO_AIRPLANE.Pickup}() method, you are able to direct the helicopters towards a point on the battlefield to board/load the cargo at the specific coordinate. +-- Ensure that the landing zone is horizontally flat, and that trees cannot be found in the landing vicinity, or the helicopters won't land or will even crash! +-- +-- ## Cargo deployment. +-- +-- Using the @{#AI_CARGO_AIRPLANE.Deploy}() method, you are able to direct the helicopters towards a point on the battlefield to unboard/unload the cargo at the specific coordinate. +-- Ensure that the landing zone is horizontally flat, and that trees cannot be found in the landing vicinity, or the helicopters won't land or will even crash! +-- +-- ## Infantry health. +-- +-- When infantry is unboarded from the APCs, the infantry is actually respawned into the battlefield. +-- As a result, the unboarding infantry is very _healthy_ every time it unboards. +-- This is due to the limitation of the DCS simulator, which is not able to specify the health of new spawned units as a parameter. +-- However, infantry that was destroyed when unboarded, won't be respawned again. Destroyed is destroyed. +-- As a result, there is some additional strength that is gained when an unboarding action happens, but in terms of simulation balance this has +-- marginal impact on the overall battlefield simulation. Fortunately, the firing strength of infantry is limited, and thus, respacing healthy infantry every +-- time is not so much of an issue ... +-- +-- +-- @field #AI_CARGO_AIRPLANE +AI_CARGO_AIRPLANE = { + ClassName = "AI_CARGO_AIRPLANE", + Coordinate = nil, -- Core.Point#COORDINATE +} + +--- Creates a new AI_CARGO_AIRPLANE object. +-- @param #AI_CARGO_AIRPLANE self +-- @param Wrapper.Group#GROUP Airplane Plane used for transportation of cargo. +-- @param Core.Set#SET_CARGO CargoSet Cargo set to be transported. +-- @return #AI_CARGO_AIRPLANE +function AI_CARGO_AIRPLANE:New( Airplane, CargoSet ) + + local self = BASE:Inherit( self, AI_CARGO:New( Airplane, CargoSet ) ) -- #AI_CARGO_AIRPLANE + + self:AddTransition( "*", "Landed", "*" ) + self:AddTransition( "*", "Home" , "*" ) + + self:AddTransition( "*", "Destroyed", "Destroyed" ) + + --- Pickup Handler OnBefore for AI_CARGO_AIRPLANE + -- @function [parent=#AI_CARGO_AIRPLANE] OnBeforePickup + -- @param #AI_CARGO_AIRPLANE self + -- @param Wrapper.Group#GROUP Airplane Cargo transport plane. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Airbase#AIRBASE Airbase Airbase where troops are picked up. + -- @param #number Speed in km/h for travelling to pickup base. + -- @return #boolean + + --- Pickup Handler OnAfter for AI_CARGO_AIRPLANE + -- @function [parent=#AI_CARGO_AIRPLANE] OnAfterPickup + -- @param #AI_CARGO_AIRPLANE self + -- @param Wrapper.Group#GROUP Airplane Cargo transport plane. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Point#COORDINATE Coordinate The coordinate where to pickup stuff. + -- @param #number Speed Speed in km/h for travelling to pickup base. + -- @param #number Height Height in meters to move to the pickup coordinate. + -- @param Core.Zone#ZONE_AIRBASE PickupZone The airbase zone where the cargo will be picked up. + + --- Pickup Trigger for AI_CARGO_AIRPLANE + -- @function [parent=#AI_CARGO_AIRPLANE] Pickup + -- @param #AI_CARGO_AIRPLANE self + -- @param Core.Point#COORDINATE Coordinate The coordinate where to pickup stuff. + -- @param #number Speed Speed in km/h for travelling to pickup base. + -- @param #number Height Height in meters to move to the pickup coordinate. + -- @param Core.Zone#ZONE_AIRBASE PickupZone The airbase zone where the cargo will be picked up. + + --- Pickup Asynchronous Trigger for AI_CARGO_AIRPLANE + -- @function [parent=#AI_CARGO_AIRPLANE] __Pickup + -- @param #AI_CARGO_AIRPLANE self + -- @param #number Delay Delay in seconds. + -- @param Core.Point#COORDINATE Coordinate The coordinate where to pickup stuff. + -- @param #number Speed Speed in km/h for travelling to pickup base. + -- @param #number Height Height in meters to move to the pickup coordinate. + -- @param Core.Zone#ZONE_AIRBASE PickupZone The airbase zone where the cargo will be picked up. + + --- Deploy Handler OnBefore for AI_CARGO_AIRPLANE + -- @function [parent=#AI_CARGO_AIRPLANE] OnBeforeDeploy + -- @param #AI_CARGO_AIRPLANE self + -- @param Wrapper.Group#GROUP Airplane Cargo plane. + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Wrapper.Airbase#AIRBASE Airbase Destination airbase where troops are deployed. + -- @param #number Speed Speed in km/h for travelling to deploy base. + -- @return #boolean + + --- Deploy Handler OnAfter for AI_CARGO_AIRPLANE + -- @function [parent=#AI_CARGO_AIRPLANE] OnAfterDeploy + -- @param #AI_CARGO_AIRPLANE self + -- @param Wrapper.Group#GROUP Airplane Cargo plane. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Point#COORDINATE Coordinate Coordinate where to deploy stuff. + -- @param #number Speed Speed in km/h for travelling to the deploy base. + -- @param #number Height Height in meters to move to the home coordinate. + -- @param Core.Zone#ZONE_AIRBASE DeployZone The airbase zone where the cargo will be deployed. + + --- Deploy Trigger for AI_CARGO_AIRPLANE + -- @function [parent=#AI_CARGO_AIRPLANE] Deploy + -- @param #AI_CARGO_AIRPLANE self + -- @param Core.Point#COORDINATE Coordinate Coordinate where to deploy stuff. + -- @param #number Speed Speed in km/h for travelling to the deploy base. + -- @param #number Height Height in meters to move to the home coordinate. + -- @param Core.Zone#ZONE_AIRBASE DeployZone The airbase zone where the cargo will be deployed. + + --- Deploy Asynchronous Trigger for AI_CARGO_AIRPLANE + -- @function [parent=#AI_CARGO_AIRPLANE] __Deploy + -- @param #AI_CARGO_AIRPLANE self + -- @param #number Delay Delay in seconds. + -- @param Core.Point#COORDINATE Coordinate Coordinate where to deploy stuff. + -- @param #number Speed Speed in km/h for travelling to the deploy base. + -- @param #number Height Height in meters to move to the home coordinate. + -- @param Core.Zone#ZONE_AIRBASE DeployZone The airbase zone where the cargo will be deployed. + + --- On after Loaded event, i.e. triggered when the cargo is inside the carrier. + -- @function [parent=#AI_CARGO_AIRPLANE] OnAfterLoaded + -- @param #AI_CARGO_AIRPLANE self + -- @param Wrapper.Group#GROUP Airplane Cargo plane. + -- @param From + -- @param Event + -- @param To + + + --- On after Deployed event. + -- @function [parent=#AI_CARGO_AIRPLANE] OnAfterDeployed + -- @param #AI_CARGO_AIRPLANE self + -- @param Wrapper.Group#GROUP Airplane Cargo plane. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. + + -- Set carrier. + self:SetCarrier( Airplane ) + + return self +end + + +--- Set the Carrier (controllable). Also initializes events for carrier and defines the coalition. +-- @param #AI_CARGO_AIRPLANE self +-- @param Wrapper.Group#GROUP Airplane Transport plane. +-- @return #AI_CARGO_AIRPLANE self +function AI_CARGO_AIRPLANE:SetCarrier( Airplane ) + + local AICargo = self + + self.Airplane = Airplane -- Wrapper.Group#GROUP + self.Airplane:SetState( self.Airplane, "AI_CARGO_AIRPLANE", self ) + + self.RoutePickup = false + self.RouteDeploy = false + + Airplane:HandleEvent( EVENTS.Dead ) + Airplane:HandleEvent( EVENTS.Hit ) + Airplane:HandleEvent( EVENTS.EngineShutdown ) + + function Airplane:OnEventDead( EventData ) + local AICargoTroops = self:GetState( self, "AI_CARGO_AIRPLANE" ) + self:F({AICargoTroops=AICargoTroops}) + if AICargoTroops then + self:F({}) + if not AICargoTroops:Is( "Loaded" ) then + -- There are enemies within combat range. Unload the Airplane. + AICargoTroops:Destroyed() + end + end + end + + + function Airplane:OnEventHit( EventData ) + local AICargoTroops = self:GetState( self, "AI_CARGO_AIRPLANE" ) + if AICargoTroops then + self:F( { OnHitLoaded = AICargoTroops:Is( "Loaded" ) } ) + if AICargoTroops:Is( "Loaded" ) or AICargoTroops:Is( "Boarding" ) then + -- There are enemies within combat range. Unload the Airplane. + AICargoTroops:Unload() + end + end + end + + + function Airplane:OnEventEngineShutdown( EventData ) + AICargo.Relocating = false + AICargo:Landed( self.Airplane ) + end + + self.Coalition = self.Airplane:GetCoalition() + + self:SetControllable( Airplane ) + + return self +end + + +--- Find a free Carrier within a range. +-- @param #AI_CARGO_AIRPLANE self +-- @param Wrapper.Airbase#AIRBASE Airbase +-- @param #number Radius +-- @return Wrapper.Group#GROUP NewCarrier +function AI_CARGO_AIRPLANE:FindCarrier( Coordinate, Radius ) + + local CoordinateZone = ZONE_RADIUS:New( "Zone" , Coordinate:GetVec2(), Radius ) + CoordinateZone:Scan( { Object.Category.UNIT } ) + for _, DCSUnit in pairs( CoordinateZone:GetScannedUnits() ) do + local NearUnit = UNIT:Find( DCSUnit ) + self:F({NearUnit=NearUnit}) + if not NearUnit:GetState( NearUnit, "AI_CARGO_AIRPLANE" ) then + local Attributes = NearUnit:GetDesc() + self:F({Desc=Attributes}) + if NearUnit:HasAttribute( "Trucks" ) then + self:SetCarrier( NearUnit ) + break + end + end + end + +end + +--- On after "Landed" event. Called on engine shutdown and initiates the pickup mission or unloading event. +-- @param #AI_CARGO_AIRPLANE self +-- @param Wrapper.Group#GROUP Airplane Cargo transport plane. +-- @param From +-- @param Event +-- @param To +function AI_CARGO_AIRPLANE:onafterLanded( Airplane, From, Event, To ) + + self:F({Airplane, From, Event, To}) + + if Airplane and Airplane:IsAlive()~=nil then + + -- Aircraft was sent to this airbase to pickup troops. Initiate loadling. + if self.RoutePickup == true then + self:Load( self.PickupZone ) + end + + -- Aircraft was send to this airbase to deploy troops. Initiate unloading. + if self.RouteDeploy == true then + self:Unload() + self.RouteDeploy = false + end + + end + +end + + +--- On after "Pickup" event. Routes transport to pickup airbase. +-- @param #AI_CARGO_AIRPLANE self +-- @param Wrapper.Group#GROUP Airplane Cargo transport plane. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Point#COORDINATE Coordinate The coordinate where to pickup stuff. +-- @param #number Speed Speed in km/h for travelling to pickup base. +-- @param #number Height Height in meters to move to the pickup coordinate. +-- @param Core.Zone#ZONE_AIRBASE PickupZone The airbase zone where the cargo will be picked up. +function AI_CARGO_AIRPLANE:onafterPickup( Airplane, From, Event, To, Coordinate, Speed, Height, PickupZone ) + + if Airplane and Airplane:IsAlive() then + + local airbasepickup=Coordinate:GetClosestAirbase() + + self.PickupZone = PickupZone or ZONE_AIRBASE:New(airbasepickup:GetName()) + + -- Get closest airbase of current position. + local ClosestAirbase, DistToAirbase=Airplane:GetCoordinate():GetClosestAirbase() + + -- Two cases. Aircraft spawned in air or at an airbase. + if Airplane:InAir() then + self.Airbase=nil --> route will start in air + else + self.Airbase=ClosestAirbase + end + + -- Set pickup airbase. + local Airbase = self.PickupZone:GetAirbase() + + -- Distance from closest to pickup airbase ==> we need to know if we are already at the pickup airbase. + local Dist = Airbase:GetCoordinate():Get2DDistance(ClosestAirbase:GetCoordinate()) + + if Airplane:InAir() or Dist>500 then + + -- Route aircraft to pickup airbase. + self:Route( Airplane, Airbase, Speed, Height ) + + -- Set airbase as starting point in the next Route() call. + self.Airbase = Airbase + + -- Aircraft is on a pickup mission. + self.RoutePickup = true + + else + + -- We are already at the right airbase ==> Landed ==> triggers loading of troops. Is usually called at engine shutdown event. + self.RoutePickup=true + self:Landed() + + end + + self:GetParent( self, AI_CARGO_AIRPLANE ).onafterPickup( self, Airplane, From, Event, To, Coordinate, Speed, Height, self.PickupZone ) + + end + + +end + +--- On after Depoly event. Routes plane to the airbase where the troops are deployed. +-- @param #AI_CARGO_AIRPLANE self +-- @param Wrapper.Group#GROUP Airplane Cargo transport plane. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Point#COORDINATE Coordinate Coordinate where to deploy stuff. +-- @param #number Speed Speed in km/h for travelling to the deploy base. +-- @param #number Height Height in meters to move to the home coordinate. +-- @param Core.Zone#ZONE_AIRBASE DeployZone The airbase zone where the cargo will be deployed. +function AI_CARGO_AIRPLANE:onafterDeploy( Airplane, From, Event, To, Coordinate, Speed, Height, DeployZone ) + + if Airplane and Airplane:IsAlive()~=nil then + + local Airbase = Coordinate:GetClosestAirbase() + + if DeployZone then + Airbase=DeployZone:GetAirbase() + end + + -- Activate uncontrolled airplane. + if Airplane:IsAlive()==false then + Airplane:SetCommand({id = 'Start', params = {}}) + end + + -- Route to destination airbase. + self:Route( Airplane, Airbase, Speed, Height ) + + -- Aircraft is on a depoly mission. + self.RouteDeploy = true + + -- Set destination airbase for next :Route() command. + self.Airbase = Airbase + + self:GetParent( self, AI_CARGO_AIRPLANE ).onafterDeploy( self, Airplane, From, Event, To, Coordinate, Speed, Height, DeployZone ) + end + +end + + +--- On after Unload event. Cargo is beeing unloaded, i.e. the unboarding process is started. +-- @param #AI_CARGO_AIRPLANE self +-- @param Wrapper.Group#GROUP Airplane Cargo transport plane. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Zone#ZONE_AIRBASE DeployZone The airbase zone where the cargo will be deployed. +function AI_CARGO_AIRPLANE:onafterUnload( Airplane, From, Event, To, DeployZone ) + + local UnboardInterval = 10 + local UnboardDelay = 10 + + if Airplane and Airplane:IsAlive() then + for _, AirplaneUnit in pairs( Airplane:GetUnits() ) do + local Cargos = AirplaneUnit:GetCargo() + for CargoID, Cargo in pairs( Cargos ) do + + local Angle = 180 + local CargoCarrierHeading = Airplane:GetHeading() -- Get Heading of object in degrees. + local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) + self:T( { CargoCarrierHeading, CargoDeployHeading } ) + local CargoDeployCoordinate = Airplane:GetPointVec2():Translate( 150, CargoDeployHeading ) + + Cargo:__UnBoard( UnboardDelay, CargoDeployCoordinate ) + UnboardDelay = UnboardDelay + UnboardInterval + Cargo:SetDeployed( true ) + self:__Unboard( UnboardDelay, Cargo, AirplaneUnit, DeployZone ) + end + end + end + +end + +--- Route the airplane from one airport or it's current position to another airbase. +-- @param #AI_CARGO_AIRPLANE self +-- @param Wrapper.Group#GROUP Airplane Airplane group to be routed. +-- @param Wrapper.Airbase#AIRBASE Airbase Destination airbase. +-- @param #number Speed Speed in km/h. Default is 80% of max possible speed the group can do. +-- @param #number Height Height in meters to move to the Airbase. +-- @param #boolean Uncontrolled If true, spawn group in uncontrolled state. +function AI_CARGO_AIRPLANE:Route( Airplane, Airbase, Speed, Height, Uncontrolled ) + + if Airplane and Airplane:IsAlive() then + + -- Set takeoff type. + local Takeoff = SPAWN.Takeoff.Cold + + -- Get template of group. + local Template = Airplane:GetTemplate() + + -- Nil check + if Template==nil then + return + end + + -- Waypoints of the route. + local Points={} + + -- To point. + local AirbasePointVec2 = Airbase:GetPointVec2() + local ToWaypoint = AirbasePointVec2:WaypointAir(POINT_VEC3.RoutePointAltType.BARO, "Land", "Landing", Speed or Airplane:GetSpeedMax()*0.8, true, Airbase) + + --ToWaypoint["airdromeId"] = Airbase:GetID() + --ToWaypoint["speed_locked"] = true + + + -- If self.Airbase~=nil then group is currently at an airbase, where it should be respawned. + if self.Airbase then + + -- Second point of the route. First point is done in RespawnAtCurrentAirbase() routine. + Template.route.points[2] = ToWaypoint + + -- Respawn group at the current airbase. + Airplane:RespawnAtCurrentAirbase(Template, Takeoff, Uncontrolled) + + else + + -- From point. + local GroupPoint = Airplane:GetVec2() + local FromWaypoint = {} + FromWaypoint.x = GroupPoint.x + FromWaypoint.y = GroupPoint.y + FromWaypoint.type = "Turning Point" + FromWaypoint.action = "Turning Point" + FromWaypoint.speed = Airplane:GetSpeedMax()*0.8 + + -- The two route points. + Points[1] = FromWaypoint + Points[2] = ToWaypoint + + local PointVec3 = Airplane:GetPointVec3() + Template.x = PointVec3.x + Template.y = PointVec3.z + + Template.route.points = Points + + local GroupSpawned = Airplane:Respawn(Template) + + end + end +end + +--- On after Home event. Aircraft will be routed to their home base. +-- @param #AI_CARGO_AIRPLANE self +-- @param Wrapper.Group#GROUP Airplane The cargo plane. +-- @param From From state. +-- @param Event Event. +-- @param To To State. +-- @param Core.Point#COORDINATE Coordinate Home place (not used). +-- @param #number Speed Speed in km/h to fly to the home airbase (zone). Default is 80% of max possible speed the unit can go. +-- @param #number Height Height in meters to move to the home coordinate. +-- @param Core.Zone#ZONE_AIRBASE HomeZone The home airbase (zone) where the plane should return to. +function AI_CARGO_AIRPLANE:onafterHome(Airplane, From, Event, To, Coordinate, Speed, Height, HomeZone ) + if Airplane and Airplane:IsAlive() then + + -- We are going home! + self.RouteHome = true + + -- Home Base. + local HomeBase=HomeZone:GetAirbase() + self.Airbase=HomeBase + + -- Now route the airplane home + self:Route( Airplane, HomeBase, Speed, Height ) + + end + +end +--- **AI** -- (R2.5.1) - Models the intelligent transportation of infantry and other cargo. +-- +-- === +-- +-- ### Author: **acrojason** (derived from AI_Cargo_APC by FlightControl) +-- +-- === +-- +-- @module AI.AI_Cargo_Ship +-- @image AI_Cargo_Dispatcher.JPG + +--- @type AI_CARGO_SHIP +-- @extends AI.AI_Cargo#AI_CARGO + +--- Brings a dynamic cargo handling capability for an AI naval group. +-- +-- Naval ships can be utilized to transport cargo around the map following naval shipping lanes. +-- The AI_CARGO_SHIP class uses the @{Cargo.Cargo} capabilities within the MOOSE framework. +-- @{Cargo.Cargo} must be declared within the mission or warehouse to make the AI_CARGO_SHIP recognize the cargo. +-- Please consult the @{Cargo.Cargo} module for more information. +-- +-- ## Cargo loading. +-- +-- The module will automatically load cargo when the Ship is within boarding or loading radius. +-- The boarding or loading radius is specified when the cargo is created in the simulation and depends on the type of +-- cargo and the specified boarding radius. +-- +-- ## Defending the Ship when enemies are nearby +-- This is not supported for naval cargo because most tanks don't float. Protect your transports... +-- +-- ## Infantry or cargo **health**. +-- When cargo is unboarded from the Ship, the cargo is actually respawned into the battlefield. +-- As a result, the unboarding cargo is very _healthy_ every time it unboards. +-- This is due to the limitation of the DCS simulator, which is not able to specify the health of newly spawned units as a parameter. +-- However, cargo that was destroyed when unboarded and following the Ship won't be respawned again (this is likely not a thing for +-- naval cargo due to the lack of support for defending the Ship mentioned above). Destroyed is destroyed. +-- As a result, there is some additional strength that is gained when an unboarding action happens, but in terms of simulation balance +-- this has marginal impact on the overall battlefield simulation. Given the relatively short duration of DCS missions and the somewhat +-- lengthy naval transport times, most units entering the Ship as cargo will be freshly en route to an amphibious landing or transporting +-- between warehouses. +-- +-- ## Control the Ships on the map. +-- +-- Currently, naval transports can only be controlled via scripts due to their reliance upon predefined Shipping Lanes created in the Mission +-- Editor. An interesting future enhancement could leverage new pathfinding functionality for ships in the Ops module. +-- +-- ## Cargo deployment. +-- +-- Using the @{AI_CARGO_SHIP.Deploy}() method, you are able to direct the Ship towards a Deploy zone to unboard/unload the cargo at the +-- specified coordinate. The Ship will follow the Shipping Lane to ensure consistent cargo transportation within the simulation environment. +-- +-- ## Cargo pickup. +-- +-- Using the @{AI_CARGO_SHIP.Pickup}() method, you are able to direct the Ship towards a Pickup zone to board/load the cargo at the specified +-- coordinate. The Ship will follow the Shipping Lane to ensure consistent cargo transportation within the simulation environment. +-- +-- +-- @field #AI_CARGO_SHIP +AI_CARGO_SHIP = { + ClassName = "AI_CARGO_SHIP", + Coordinate = nil -- Core.Point#COORDINATE +} + +--- Creates a new AI_CARGO_SHIP object. +-- @param #AI_CARGO_SHIP self +-- @param Wrapper.Group#GROUP Ship The carrier Ship group +-- @param Core.Set#SET_CARGO CargoSet The set of cargo to be transported +-- @param #number CombatRadius Provide the combat radius to defend the carrier by unboarding the cargo when enemies are nearby. When CombatRadius is 0, no defense will occur. +-- @param #table ShippingLane Table containing list of Shipping Lanes to be used +-- @return #AI_CARGO_SHIP +function AI_CARGO_SHIP:New( Ship, CargoSet, CombatRadius, ShippingLane ) + + local self = BASE:Inherit( self, AI_CARGO:New( Ship, CargoSet ) ) -- #AI_CARGO_SHIP + + self:AddTransition( "*", "Monitor", "*" ) + self:AddTransition( "*", "Destroyed", "Destroyed" ) + self:AddTransition( "*", "Home", "*" ) + + self:SetCombatRadius( 0 ) -- Don't want to deploy cargo in middle of water to defend Ship, so set CombatRadius to 0 + self:SetShippingLane ( ShippingLane ) + + self:SetCarrier( Ship ) + + return self +end + +--- Set the Carrier +-- @param #AI_CARGO_SHIP self +-- @param Wrapper.Group#GROUP CargoCarrier +-- @return #AI_CARGO_SHIP +function AI_CARGO_SHIP:SetCarrier( CargoCarrier ) + self.CargoCarrier = CargoCarrier -- Wrapper.Group#GROUIP + self.CargoCarrier:SetState( self.CargoCarrier, "AI_CARGO_SHIP", self ) + + CargoCarrier:HandleEvent( EVENTS.Dead ) + + function CargoCarrier:OnEventDead( EventData ) + self:F({"dead"}) + local AICargoTroops = self:GetState( self, "AI_CARGO_SHIP" ) + self:F({AICargoTroops=AICargoTroops}) + if AICargoTroops then + self:F({}) + if not AICargoTroops:Is( "Loaded" ) then + -- Better hope they can swim! + AICargoTroops:Destroyed() + end + end + end + + self.Zone = ZONE_UNIT:New( self.CargoCarrier:GetName() .. "-Zone", self.CargoCarrier, self.CombatRadius ) + self.Coalition = self.CargoCarrier:GetCoalition() + + self:SetControllable( CargoCarrier ) + + return self +end + + +--- FInd a free Carrier within a radius +-- @param #AI_CARGO_SHIP self +-- @param Core.Point#COORDINATE Coordinate +-- @param #number Radius +-- @return Wrapper.Group#GROUP NewCarrier +function AI_CARGO_SHIP:FindCarrier( Coordinate, Radius ) + + local CoordinateZone = ZONE_RADIUS:New( "Zone", Coordinate:GetVec2(), Radius ) + CoordinateZone:Scan( { Object.Category.UNIT } ) + for _, DCSUnit in pairs( CoordinateZone:GetScannedUnits() ) do + local NearUnit = UNIT:Find( DCSUnit ) + self:F({NearUnit=NearUnit}) + if not NearUnit:GetState( NearUnit, "AI_CARGO_SHIP" ) then + local Attributes = NearUnit:GetDesc() + self:F({Desc=Attributes}) + if NearUnit:HasAttributes( "Trucks" ) then + return NearUnit:GetGroup() + end + end + end + + return nil +end + +function AI_CARGO_SHIP:SetShippingLane( ShippingLane ) + self.ShippingLane = ShippingLane + + return self +end + +function AI_CARGO_SHIP:SetCombatRadius( CombatRadius ) + self.CombatRadius = CombatRadius or 0 + + return self +end + + +--- Follow Infantry to the Carrier +-- @param #AI_CARGO_SHIP self +-- @param #AI_CARGO_SHIP Me +-- @param Wrapper.Unit#UNIT ShipUnit +-- @param Cargo.CargoGroup#CARGO_GROUP Cargo +-- @return #AI_CARGO_SHIP +function AI_CARGO_SHIP:FollowToCarrier( Me, ShipUnit, CargoGroup ) + + local InfantryGroup = CargoGroup:GetGroup() + + self:F( { self=self:GetClassNameAndID(), InfantryGroup = InfantryGroup:GetName() } ) + + if ShipUnit:IsAlive() then + -- Check if the Cargo is near the CargoCarrier + if InfantryGroup:IsPartlyInZone( ZONE_UNIT:New( "Radius", ShipUnit, 1000 ) ) then + + -- Cargo does not need to navigate to Carrier + Me:Guard() + else + + self:F( { InfantryGroup = InfantryGroup:GetName() } ) + if InfantryGroup:IsAlive() then + + self:F( { InfantryGroup = InfantryGroup:GetName() } ) + local Waypoints = {} + + -- Calculate new route + local FromCoord = InfantryGroup:GetCoordinate() + local FromGround = FromCoord:WaypointGround( 10, "Diamond" ) + self:F({FromGround=FromGround}) + table.insert( Waypoints, FromGround ) + + local ToCoord = ShipUnit:GetCoordinate():GetRandomCoordinateInRadius( 10, 5 ) + local ToGround = ToCoord:WaypointGround( 10, "Diamond" ) + self:F({ToGround=ToGround}) + table.insert( Waypoints, ToGround ) + + local TaskRoute = InfantryGroup:TaskFunction( "AI_CARGO_SHIP.FollowToCarrier", Me, ShipUnit, CargoGroup ) + + self:F({Waypoints=Waypoints}) + local Waypoint = Waypoints[#Waypoints] + InfantryGroup:SetTaskWaypoint( Waypoint, TaskRoute ) -- Set for the given Route at Waypoint 2 the TaskRouteToZone + + InfantryGroup:Route( Waypoints, 1 ) -- Move after a random number of seconds to the Route. See Route method for details + end + end + end +end + + +function AI_CARGO_SHIP:onafterMonitor( Ship, From, Event, To ) + self:F( { Ship, From, Event, To, IsTransporting = self:IsTransporting() } ) + + if self.CombatRadius > 0 then + -- We really shouldn't find ourselves in here for Ships since the CombatRadius should always be 0. + -- This is to avoid Unloading the Ship in the middle of the sea. + if Ship and Ship:IsAlive() then + if self.CarrierCoordinate then + if self:IsTransporting() == true then + local Coordinate = Ship:GetCoordinate() + if self:Is( "Unloaded" ) or self:Is( "Loaded" ) then + self.Zone:Scan( { Object.Category.UNIT } ) + if self.Zone:IsAllInZoneOfCoalition( self.Coalition ) then + if self:Is( "Unloaded" ) then + -- There are no enemies within combat radius. Reload the CargoCarrier. + self:Reload() + end + else + if self:Is( "Loaded" ) then + -- There are enemies within combat radius. Unload the CargoCarrier. + self:__Unload( 1, nil, true ) -- The 2nd parameter is true, which means that the unload is for defending the carrier, not to deploy! + else + if self:Is( "Unloaded" ) then + --self:Follow() + end + self:F( "I am here" .. self:GetCurrentState() ) + if self:Is( "Following" ) then + for Cargo, ShipUnit in pairs( self.Carrier_Cargo ) do + local Cargo = Cargo -- Cargo.Cargo#CARGO + local ShipUnit = ShipUnit -- Wrapper.Unit#UNIT + if Cargo:IsAlive() then + if not Cargo:IsNear( ShipUnit, 40 ) then + ShipUnit:RouteStop() + self.CarrierStopped = true + else + if self.CarrierStopped then + if Cargo:IsNear( ShipUnit, 25 ) then + ShipUnit:RouteResume() + self.CarrierStopped = nil + end + end + end + end + end + end + end + end + end + end + end + self.CarrierCoordinate = Ship:GetCoordinate() + end + self:__Monitor( -5 ) + end +end + +--- Check if cargo ship is alive and trigger Load event +-- @param Wrapper.Group#Group Ship +-- @param #AI_CARGO_SHIP self +function AI_CARGO_SHIP._Pickup( Ship, self, Coordinate, Speed, PickupZone ) + + Ship:F( { "AI_CARGO_Ship._Pickup:", Ship:GetName() } ) + + if Ship:IsAlive() then + self:Load( PickupZone ) + end +end + +--- Check if cargo ship is alive and trigger Unload event. Good time to remind people that Lua is case sensitive and Unload != UnLoad +-- @param Wrapper.Group#GROUP Ship +-- @param #AI_CARGO_SHIP self +function AI_CARGO_SHIP._Deploy( Ship, self, Coordinate, DeployZone ) + Ship:F( { "AI_CARGO_Ship._Deploy:", Ship } ) + + if Ship:IsAlive() then + self:Unload( DeployZone ) + end +end + +--- on after Pickup event. +-- @param AI_CARGO_SHIP Ship +-- @param From +-- @param Event +-- @param To +-- @param Core.Point#COORDINATE Coordinate of the pickup point +-- @param #number Speed Speed in km/h to sail to the pickup coordinate. Default is 50% of max speed for the unit +-- @param #number Height Altitude in meters to move to the pickup coordinate. This parameter is ignored for Ships +-- @param Core.Zone#ZONE PickupZone (optional) The zone where the cargo will be picked up. The PickupZone can be nil if there was no PickupZoneSet provided +function AI_CARGO_SHIP:onafterPickup( Ship, From, Event, To, Coordinate, Speed, Height, PickupZone ) + + if Ship and Ship:IsAlive() then + AI_CARGO_SHIP._Pickup( Ship, self, Coordinate, Speed, PickupZone ) + self:GetParent( self, AI_CARGO_SHIP ).onafterPickup( self, Ship, From, Event, To, Coordinate, Speed, Height, PickupZone ) + end +end + +--- On after Deploy event. +-- @param #AI_CARGO_SHIP self +-- @param Wrapper.Group#GROUP SHIP +-- @param From +-- @param Event +-- @param To +-- @param Core.Point#COORDINATE Coordinate Coordinate of the deploy point +-- @param #number Speed Speed in km/h to sail to the deploy coordinate. Default is 50% of max speed for the unit +-- @param #number Height Altitude in meters to move to the deploy coordinate. This parameter is ignored for Ships +-- @param Core.Zone#ZONE DeployZone The zone where the cargo will be deployed. +function AI_CARGO_SHIP:onafterDeploy( Ship, From, Event, To, Coordinate, Speed, Height, DeployZone ) + + if Ship and Ship:IsAlive() then + + Speed = Speed or Ship:GetSpeedMax()*0.8 + local lane = self.ShippingLane + + if lane then + local Waypoints = {} + + for i=1, #lane do + local coord = lane[i] + local Waypoint = coord:WaypointGround(_speed) + table.insert(Waypoints, Waypoint) + end + + local TaskFunction = Ship:TaskFunction( "AI_CARGO_SHIP._Deploy", self, Coordinate, DeployZone ) + local Waypoint = Waypoints[#Waypoints] + Ship:SetTaskWaypoint( Waypoint, TaskFunction ) + Ship:Route(Waypoints, 1) + self:GetParent( self, AI_CARGO_SHIP ).onafterDeploy( self, Ship, From, Event, To, Coordinate, Speed, Height, DeployZone ) + else + self:E(self.lid.."ERROR: No shipping lane defined for Naval Transport!") + end + end +end + +--- On after Unload event. +-- @param #AI_CARGO_SHIP self +-- @param Wrapper.Group#GROUP Ship +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. +function AI_CARGO_SHIP:onafterUnload( Ship, From, Event, To, DeployZone, Defend ) + self:F( { Ship, From, Event, To, DeployZone, Defend = Defend } ) + + local UnboardInterval = 5 + local UnboardDelay = 5 + + if Ship and Ship:IsAlive() then + for _, ShipUnit in pairs( Ship:GetUnits() ) do + local ShipUnit = ShipUnit -- Wrapper.Unit#UNIT + Ship:RouteStop() + for _, Cargo in pairs( ShipUnit:GetCargo() ) do + self:F( { Cargo = Cargo:GetName(), Isloaded = Cargo:IsLoaded() } ) + if Cargo:IsLoaded() then + local unboardCoord = DeployZone:GetRandomPointVec2() + Cargo:__UnBoard( UnboardDelay, unboardCoord, 1000) + UnboardDelay = UnboardDelay + Cargo:GetCount() * UnboardInterval + self:__Unboard( UnboardDelay, Cargo, ShipUnit, DeployZone, Defend ) + if not Defend == true then + Cargo:SetDeployed( true ) + end + end + end + end + end +end + +function AI_CARGO_SHIP:onafterHome( Ship, From, Event, To, Coordinate, Speed, Height, HomeZone ) + if Ship and Ship:IsAlive() then + + self.RouteHome = true + Speed = Speed or Ship:GetSpeedMax()*0.8 + local lane = self.ShippingLane + + if lane then + local Waypoints = {} + + -- Need to find a more generalized way to do this instead of reversing the shipping lane. + -- This only works if the Source/Dest route waypoints are numbered 1..n and not n..1 + for i=#lane, 1, -1 do + local coord = lane[i] + local Waypoint = coord:WaypointGround(_speed) + table.insert(Waypoints, Waypoint) + end + + local Waypoint = Waypoints[#Waypoints] + Ship:Route(Waypoints, 1) + + else + self:E(self.lid.."ERROR: No shipping lane defined for Naval Transport!") + end + end +end +--- **AI** - Models the intelligent transportation of infantry and other cargo. +-- +-- ## Features: +-- +-- * AI_CARGO_DISPATCHER is the **base class** for: +-- +-- * @{AI.AI_Cargo_Dispatcher_APC#AI_CARGO_DISPATCHER_APC} +-- * @{AI.AI_Cargo_Dispatcher_Helicopter#AI_CARGO_DISPATCHER_HELICOPTER} +-- * @{AI.AI_Cargo_Dispatcher_Airplane#AI_CARGO_DISPATCHER_AIRPLANE} +-- +-- * Provides the facilities to transport cargo over the battle field for the above classes. +-- * Dispatches transport tasks to a common set of cargo transporting groups. +-- * Different options can be setup to tweak the cargo transporation behaviour. +-- +-- === +-- +-- ## Test Missions: +-- +-- Test missions can be located on the main GITHUB site. +-- +-- [FlightControl-Master/MOOSE_MISSIONS/AID - AI Dispatching/AID-CGO - AI Cargo Dispatching/](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/AID%20-%20AI%20Dispatching/AID-CGO%20-%20AI%20Cargo%20Dispatching) +-- +-- === +-- +-- # The dispatcher concept. +-- +-- Carrier equipment can be mobilized to intelligently transport infantry and other cargo within the simulation. +-- The AI_CARGO_DISPATCHER module uses the @{Cargo.Cargo} capabilities within the MOOSE framework, to enable Carrier GROUP objects +-- to transport @{Cargo.Cargo} towards several deploy zones. +-- @{Cargo.Cargo} must be declared within the mission to make the AI_CARGO_DISPATCHER object recognize the cargo. +-- Please consult the @{Cargo.Cargo} module for more information. +-- +-- +-- ## Why cargo dispatching? +-- +-- It provides a realistic way of distributing your army forces around the battlefield, and to provide a quick means of cargo transportation. +-- Instead of having troops or cargo to "appear" suddenly at certain locations, the dispatchers will pickup the cargo and transport it. +-- It also allows to enforce or retreat your army from certain zones when needed, using helicopters or APCs. +-- Airplanes can transport cargo over larger distances between the airfields. +-- +-- +-- ## What is a cargo object then? +-- +-- In order to make use of the MOOSE cargo system, you need to **declare** the DCS objects as MOOSE cargo objects! +-- This sounds complicated, but it is actually quite simple. +-- +-- See here an example: +-- +-- local EngineerCargoGroup = CARGO_GROUP:New( GROUP:FindByName( "Engineers" ), "Workmaterials", "Engineers", 250 ) +-- +-- The above code declares a MOOSE cargo object called `EngineerCargoGroup`. +-- It actually just refers to an infantry group created within the sim called `"Engineers"`. +-- The infantry group now becomes controlled by the MOOSE cargo object `EngineerCargoGroup`. +-- A MOOSE cargo object also has properties, like the type of cargo, the logical name, and the reporting range. +-- +-- For more information, please consult the @{Cargo.Cargo} module documentation. Please read through it, because it will explain how to setup the cargo objects for use +-- within your dispatchers. +-- +-- +-- ## Do I need to do a lot of coding to setup a dispatcher? +-- +-- No! It requires a bit of studying to set it up, but once you understand the different components that use the cargo dispatcher, it becomes very easy. +-- Also, the dispatchers work in a true dynamic environment. The carriers and cargo, pickup and deploy zones can be created dynamically in your mission, +-- and will automatically be recognized by the dispatcher. +-- +-- +-- ## Is the dispatcher causing a lot of CPU overhead? +-- +-- A little yes, but once the cargo is properly loaded into the carrier, the CPU consumption is very little. +-- When infantry or vehicles board into a carrier, or unboard from a carrier, you may perceive certain performance lags. +-- We are working to minimize the impact of those. +-- That being said, the DCS simulator is limited. It is just impossible to deploy hundreds of cargo over the battlefield, hundreds of helicopters transporting, +-- without any performance impact. The amount of helicopters that are active and flying in your simulation influences more the performance than the dispatchers. +-- It really comes down to trying it out and getting experienced with what is possible and what is not (or too much). +-- +-- +-- ## Are the dispatchers a "black box" in terms of the logic? +-- +-- No. You can tailor the dispatcher mechanisms using event handlers, and create additional logic to enhance the behaviour and dynamism in your own mission. +-- The events are listed below, and so are the options, but here are a couple of examples of what is possible: +-- +-- * You could handle the **Deployed** event, when all the cargo is unloaded from a carrier in the dispatcher. +-- Adding your own code to the event handler, you could move the deployed cargo (infantry) to specific points to engage in the battlefield. +-- +-- * When a carrier is picking up cargo, the *Pickup** event is triggered, and you can inform the coalition of this event, +-- because it is an indication that troops are planned to join. +-- +-- +-- ## Are there options that you can set to modify the behaviour of the carries? +-- +-- Yes, there are options to configure: +-- +-- * the location where carriers will park or land near the cargo for pickup. +-- * the location where carriers will park or land in the deploy zone for cargo deployment. +-- * the height for airborne carriers when they fly to and from pickup and deploy zones. +-- * the speed of the carriers. This is an important parameter, because depending on the tactication situation, speed will influence the detection by radars. +-- +-- +-- ## Can the zones be of any zone type? +-- +-- Yes, please ensure that the zones are declared using the @{Core.Zone} classes. +-- Possible zones that function at the moment are ZONE, ZONE_GROUP, ZONE_UNIT, ZONE_POLYGON. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_Cargo_Dispatcher +-- @image AI_Cargo_Dispatcher.JPG + + +--- @type AI_CARGO_DISPATCHER +-- @field Core.Set#SET_GROUP CarrierSet The set of @{Wrapper.Group#GROUP} objects of carriers that will transport the cargo. +-- @field Core.Set#SET_CARGO CargoSet The set of @{Cargo.Cargo#CARGO} objects, which can be CARGO_GROUP, CARGO_CRATE, CARGO_SLINGLOAD objects. +-- @field Core.Zone#SET_ZONE PickupZoneSet The set of pickup zones, which are used to where the cargo can be picked up by the carriers. If nil, then cargo can be picked up everywhere. +-- @field Core.Zone#SET_ZONE DeployZoneSet The set of deploy zones, which are used to where the cargo will be deployed by the carriers. +-- @field #number PickupMaxSpeed The maximum speed to move to the cargo pickup location. +-- @field #number PickupMinSpeed The minimum speed to move to the cargo pickup location. +-- @field #number DeployMaxSpeed The maximum speed to move to the cargo deploy location. +-- @field #number DeployMinSpeed The minimum speed to move to the cargo deploy location. +-- @field #number PickupMaxHeight The maximum height to fly to the cargo pickup location. +-- @field #number PickupMinHeight The minimum height to fly to the cargo pickup location. +-- @field #number DeployMaxHeight The maximum height to fly to the cargo deploy location. +-- @field #number DeployMinHeight The minimum height to fly to the cargo deploy location. +-- @field #number PickupOuterRadius The outer radius in meters around the cargo coordinate to pickup the cargo. +-- @field #number PickupInnerRadius The inner radius in meters around the cargo coordinate to pickup the cargo. +-- @field #number DeployOuterRadius The outer radius in meters around the cargo coordinate to deploy the cargo. +-- @field #number DeployInnerRadius The inner radius in meters around the cargo coordinate to deploy the cargo. +-- @field Core.Zone#ZONE_BASE HomeZone The home zone where the carriers will return when there is no more cargo to pickup. +-- @field #number MonitorTimeInterval The interval in seconds when the cargo dispatcher will search for new cargo to be picked up. +-- @extends Core.Fsm#FSM + + +--- A dynamic cargo handling capability for AI groups. +-- +-- --- +-- +-- Carrier equipment can be mobilized to intelligently transport infantry and other cargo within the simulation. +-- The AI_CARGO_DISPATCHER module uses the @{Cargo.Cargo} capabilities within the MOOSE framework, to enable Carrier GROUP objects +-- to transport @{Cargo.Cargo} towards several deploy zones. +-- @{Cargo.Cargo} must be declared within the mission to make the AI_CARGO_DISPATCHER object recognize the cargo. +-- Please consult the @{Cargo.Cargo} module for more information. +-- +-- # 1) AI_CARGO_DISPATCHER constructor. +-- +-- * @{#AI_CARGO_DISPATCHER.New}(): Creates a new AI_CARGO_DISPATCHER object. +-- +-- Find below some examples of AI cargo dispatcher objects created. +-- +-- ### An AI dispatcher object for a helicopter squadron, moving infantry from pickup zones to deploy zones. +-- +-- local SetCargoInfantry = SET_CARGO:New():FilterTypes( "Infantry" ):FilterStart() +-- local SetHelicopter = SET_GROUP:New():FilterPrefixes( "Helicopter" ):FilterStart() +-- local SetPickupZones = SET_ZONE:New():FilterPrefixes( "Pickup" ):FilterStart() +-- local SetDeployZones = SET_ZONE:New():FilterPrefixes( "Deploy" ):FilterStart() +-- +-- AICargoDispatcherHelicopter = AI_CARGO_DISPATCHER_HELICOPTER:New( SetHelicopter, SetCargoInfantry, SetPickupZones, SetDeployZones ) +-- AICargoDispatcherHelicopter:SetHomeZone( ZONE:FindByName( "Home" ) ) +-- +-- ### An AI dispatcher object for a vehicle squadron, moving infantry from pickup zones to deploy zones. +-- +-- local SetCargoInfantry = SET_CARGO:New():FilterTypes( "Infantry" ):FilterStart() +-- local SetAPC = SET_GROUP:New():FilterPrefixes( "APC" ):FilterStart() +-- local SetDeployZones = SET_ZONE:New():FilterPrefixes( "Deploy" ):FilterStart() +-- +-- AICargoDispatcherAPC = AI_CARGO_DISPATCHER_APC:New( SetAPC, SetCargoInfantry, nil, SetDeployZones ) +-- AICargoDispatcherAPC:Start() +-- +-- ### An AI dispatcher object for an airplane squadron, moving infantry and vehicles from pickup airbases to deploy airbases. +-- +-- local CargoInfantrySet = SET_CARGO:New():FilterTypes( "Infantry" ):FilterStart() +-- local AirplanesSet = SET_GROUP:New():FilterPrefixes( "Airplane" ):FilterStart() +-- local PickupZoneSet = SET_ZONE:New() +-- local DeployZoneSet = SET_ZONE:New() +-- +-- PickupZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Gudauta ) ) +-- DeployZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Sochi_Adler ) ) +-- DeployZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Maykop_Khanskaya ) ) +-- DeployZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Mineralnye_Vody ) ) +-- DeployZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Vaziani ) ) +-- +-- AICargoDispatcherAirplanes = AI_CARGO_DISPATCHER_AIRPLANE:New( AirplanesSet, CargoInfantrySet, PickupZoneSet, DeployZoneSet ) +-- AICargoDispatcherAirplanes:SetHomeZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Kobuleti ) ) +-- +-- --- +-- +-- # 2) AI_CARGO_DISPATCHER is a Finite State Machine. +-- +-- This section must be read as follows. Each of the rows indicate a state transition, triggered through an event, and with an ending state of the event was executed. +-- The first column is the **From** state, the second column the **Event**, and the third column the **To** state. +-- +-- So, each of the rows have the following structure. +-- +-- * **From** => **Event** => **To** +-- +-- Important to know is that an event can only be executed if the **current state** is the **From** state. +-- This, when an **Event** that is being triggered has a **From** state that is equal to the **Current** state of the state machine, the event will be executed, +-- and the resulting state will be the **To** state. +-- +-- These are the different possible state transitions of this state machine implementation: +-- +-- * Idle => Start => Monitoring +-- * Monitoring => Monitor => Monitoring +-- * Monitoring => Stop => Idle +-- +-- * Monitoring => Pickup => Monitoring +-- * Monitoring => Load => Monitoring +-- * Monitoring => Loading => Monitoring +-- * Monitoring => Loaded => Monitoring +-- * Monitoring => PickedUp => Monitoring +-- * Monitoring => Deploy => Monitoring +-- * Monitoring => Unload => Monitoring +-- * Monitoring => Unloaded => Monitoring +-- * Monitoring => Deployed => Monitoring +-- * Monitoring => Home => Monitoring +-- +-- ## 2.1) AI_CARGO_DISPATCHER States. +-- +-- * **Monitoring**: The process is dispatching. +-- * **Idle**: The process is idle. +-- +-- ## 2.2) AI_CARGO_DISPATCHER Events. +-- +-- * **Start**: Start the transport process. +-- * **Stop**: Stop the transport process. +-- * **Monitor**: Monitor and take action. +-- +-- * **Pickup**: Pickup cargo. +-- * **Load**: Load the cargo. +-- * **Loading**: The dispatcher is coordinating the loading of a cargo. +-- * **Loaded**: Flag that the cargo is loaded. +-- * **PickedUp**: The dispatcher has loaded all requested cargo into the CarrierGroup. +-- * **Deploy**: Deploy cargo to a location. +-- * **Unload**: Unload the cargo. +-- * **Unloaded**: Flag that the cargo is unloaded. +-- * **Deployed**: All cargo is unloaded from the carriers in the group. +-- * **Home**: A Carrier is going home. +-- +-- --- +-- +-- # 3) Enhance your mission scripts with **Tailored** Event Handling! +-- +-- Use these methods to capture the events and tailor the events with your own code! +-- All classes derived from AI_CARGO_DISPATCHER can capture these events, and you can write your own code. +-- +-- In order to properly capture the events, it is mandatory that you execute the following actions using your script: +-- +-- * Copy / Paste the code section into your script. +-- * Change the CLASS literal to the object name you have in your script. +-- * Within the function, you can now write your own code! +-- * IntelliSense will recognize the type of the variables provided by the function. Note: the From, Event and To variables can be safely ignored, +-- but you need to declare them as they are automatically provided by the event handling system of MOOSE. +-- +-- You can send messages or fire off any other events within the code section. The sky is the limit! +-- +-- Mission AID-CGO-140, AID-CGO-240 and AID-CGO-340 contain examples how these events can be tailored. +-- +-- For those who don't have the time to check the test missions, find the underlying example of a Deployed event that is tailored. +-- +-- --- Deployed Handler OnAfter for AI_CARGO_DISPATCHER. +-- -- Use this event handler to tailor the event when a carrier has deployed all cargo objects from the CarrierGroup. +-- -- You can use this event handler to post messages to players, or provide status updates etc. +-- -- @function OnAfterDeployed +-- -- @param #AICargoDispatcherHelicopter self +-- -- @param #string From A string that contains the "*from state name*" when the event was fired. +-- -- @param #string Event A string that contains the "*event name*" when the event was fired. +-- -- @param #string To A string that contains the "*to state name*" when the event was fired. +-- -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. +-- -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. +-- function AICargoDispatcherHelicopter:OnAfterDeployed( From, Event, To, CarrierGroup, DeployZone ) +-- +-- MESSAGE:NewType( "Group " .. CarrierGroup:GetName() .. " deployed all cargo in zone " .. DeployZone:GetName(), MESSAGE.Type.Information ):ToAll() +-- +-- end +-- +-- +-- ## 3.1) Tailor the **Pickup** event +-- +-- Use this event handler to tailor the event when a CarrierGroup is routed towards a new pickup Coordinate and a specified Speed. +-- You can use this event handler to post messages to players, or provide status updates etc. +-- +-- +-- --- Pickup event handler OnAfter for CLASS. +-- -- Use this event handler to tailor the event when a CarrierGroup is routed towards a new pickup Coordinate and a specified Speed. +-- -- You can use this event handler to post messages to players, or provide status updates etc. +-- -- @param #CLASS self +-- -- @param #string From A string that contains the "*from state name*" when the event was triggered. +-- -- @param #string Event A string that contains the "*event name*" when the event was triggered. +-- -- @param #string To A string that contains the "*to state name*" when the event was triggered. +-- -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. +-- -- @param Core.Point#COORDINATE Coordinate The coordinate of the pickup location. +-- -- @param #number Speed The velocity in meters per second on which the CarrierGroup is routed towards the pickup Coordinate. +-- -- @param #number Height Height in meters to move to the pickup coordinate. +-- -- @param Core.Zone#ZONE_AIRBASE PickupZone (optional) The zone from where the cargo is picked up. Note that the zone is optional and may not be provided, but for AI_CARGO_DISPATCHER_AIRBASE there will always be a PickupZone, as the pickup location is an airbase zone. +-- function CLASS:OnAfterPickup( From, Event, To, CarrierGroup, Coordinate, Speed, Height, PickupZone ) +-- +-- -- Write here your own code. +-- +-- end +-- +-- +-- ## 3.2) Tailor the **Load** event +-- +-- Use this event handler to tailor the event when a CarrierGroup has initiated the loading or boarding of cargo within reporting or near range. +-- You can use this event handler to post messages to players, or provide status updates etc. +-- +-- +-- --- Load event handler OnAfter for CLASS. +-- -- Use this event handler to tailor the event when a CarrierGroup has initiated the loading or boarding of cargo within reporting or near range. +-- -- You can use this event handler to post messages to players, or provide status updates etc. +-- -- @param #CLASS self +-- -- @param #string From A string that contains the "*from state name*" when the event was triggered. +-- -- @param #string Event A string that contains the "*event name*" when the event was triggered. +-- -- @param #string To A string that contains the "*to state name*" when the event was triggered. +-- -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. +-- -- @param Core.Zone#ZONE_AIRBASE PickupZone (optional) The zone from where the cargo is picked up. Note that the zone is optional and may not be provided, but for AI_CARGO_DISPATCHER_AIRBASE there will always be a PickupZone, as the pickup location is an airbase zone. +-- function CLASS:OnAfterLoad( From, Event, To, CarrierGroup, PickupZone ) +-- +-- -- Write here your own code. +-- +-- end +-- +-- +-- ## 3.3) Tailor the **Loading** event +-- +-- Use this event handler to tailor the event when a CarrierUnit of a CarrierGroup is in the process of loading or boarding of a cargo object. +-- You can use this event handler to post messages to players, or provide status updates etc. +-- +-- +-- --- Loading event handler OnAfter for CLASS. +-- -- Use this event handler to tailor the event when a CarrierUnit of a CarrierGroup is in the process of loading or boarding of a cargo object. +-- -- You can use this event handler to post messages to players, or provide status updates etc. +-- -- Note that this event is triggered repeatedly until all cargo (units) have been boarded into the carrier. +-- -- @param #CLASS self +-- -- @param #string From A string that contains the "*from state name*" when the event was triggered. +-- -- @param #string Event A string that contains the "*event name*" when the event was triggered. +-- -- @param #string To A string that contains the "*to state name*" when the event was triggered. +-- -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. +-- -- @param Cargo.Cargo#CARGO Cargo The cargo object. +-- -- @param Wrapper.Unit#UNIT CarrierUnit The carrier unit that is executing the cargo loading operation. +-- -- @param Core.Zone#ZONE_AIRBASE PickupZone (optional) The zone from where the cargo is picked up. Note that the zone is optional and may not be provided, but for AI_CARGO_DISPATCHER_AIRBASE there will always be a PickupZone, as the pickup location is an airbase zone. +-- function CLASS:OnAfterLoading( From, Event, To, CarrierGroup, Cargo, CarrierUnit, PickupZone ) +-- +-- -- Write here your own code. +-- +-- end +-- +-- +-- ## 3.4) Tailor the **Loaded** event +-- +-- Use this event handler to tailor the event when a CarrierUnit of a CarrierGroup has loaded a cargo object. +-- You can use this event handler to post messages to players, or provide status updates etc. +-- Note that if more cargo objects were loading or boarding into the CarrierUnit, then this event can be triggered multiple times for each different Cargo/CarrierUnit. +-- +-- The function provides the CarrierGroup, which is the main group that was loading the Cargo into the CarrierUnit. +-- A CarrierUnit is part of the larger CarrierGroup. +-- +-- +-- --- Loaded event handler OnAfter for CLASS. +-- -- Use this event handler to tailor the event when a CarrierUnit of a CarrierGroup has loaded a cargo object. +-- -- You can use this event handler to post messages to players, or provide status updates etc. +-- -- Note that if more cargo objects were loading or boarding into the CarrierUnit, then this event can be triggered multiple times for each different Cargo/CarrierUnit. +-- -- A CarrierUnit can be part of the larger CarrierGroup. +-- -- @param #CLASS self +-- -- @param #string From A string that contains the "*from state name*" when the event was triggered. +-- -- @param #string Event A string that contains the "*event name*" when the event was triggered. +-- -- @param #string To A string that contains the "*to state name*" when the event was triggered. +-- -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. +-- -- @param Cargo.Cargo#CARGO Cargo The cargo object. +-- -- @param Wrapper.Unit#UNIT CarrierUnit The carrier unit that is executing the cargo loading operation. +-- -- @param Core.Zone#ZONE_AIRBASE PickupZone (optional) The zone from where the cargo is picked up. Note that the zone is optional and may not be provided, but for AI_CARGO_DISPATCHER_AIRBASE there will always be a PickupZone, as the pickup location is an airbase zone. +-- function CLASS:OnAfterLoaded( From, Event, To, CarrierGroup, Cargo, CarrierUnit, PickupZone ) +-- +-- -- Write here your own code. +-- +-- end +-- +-- +-- ## 3.5) Tailor the **PickedUp** event +-- +-- Use this event handler to tailor the event when a carrier has picked up all cargo objects into the CarrierGroup. +-- You can use this event handler to post messages to players, or provide status updates etc. +-- +-- +-- --- PickedUp event handler OnAfter for CLASS. +-- -- Use this event handler to tailor the event when a carrier has picked up all cargo objects into the CarrierGroup. +-- -- You can use this event handler to post messages to players, or provide status updates etc. +-- -- @param #CLASS self +-- -- @param #string From A string that contains the "*from state name*" when the event was triggered. +-- -- @param #string Event A string that contains the "*event name*" when the event was triggered. +-- -- @param #string To A string that contains the "*to state name*" when the event was triggered. +-- -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. +-- -- @param Core.Zone#ZONE_AIRBASE PickupZone (optional) The zone from where the cargo is picked up. Note that the zone is optional and may not be provided, but for AI_CARGO_DISPATCHER_AIRBASE there will always be a PickupZone, as the pickup location is an airbase zone. +-- function CLASS:OnAfterPickedUp( From, Event, To, CarrierGroup, PickupZone ) +-- +-- -- Write here your own code. +-- +-- end +-- +-- +-- ## 3.6) Tailor the **Deploy** event +-- +-- Use this event handler to tailor the event when a CarrierGroup is routed to a deploy coordinate, to Unload all cargo objects in each CarrierUnit. +-- You can use this event handler to post messages to players, or provide status updates etc. +-- +-- +-- --- Deploy event handler OnAfter for CLASS. +-- -- Use this event handler to tailor the event when a CarrierGroup is routed to a deploy coordinate, to Unload all cargo objects in each CarrierUnit. +-- -- You can use this event handler to post messages to players, or provide status updates etc. +-- -- @param #CLASS self +-- -- @param #string From A string that contains the "*from state name*" when the event was triggered. +-- -- @param #string Event A string that contains the "*event name*" when the event was triggered. +-- -- @param #string To A string that contains the "*to state name*" when the event was triggered. +-- -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. +-- -- @param Core.Point#COORDINATE Coordinate The deploy coordinate. +-- -- @param #number Speed The velocity in meters per second on which the CarrierGroup is routed towards the deploy Coordinate. +-- -- @param #number Height Height in meters to move to the deploy coordinate. +-- -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. +-- function CLASS:OnAfterDeploy( From, Event, To, CarrierGroup, Coordinate, Speed, Height, DeployZone ) +-- +-- -- Write here your own code. +-- +-- end +-- +-- +-- ## 3.7) Tailor the **Unload** event +-- +-- Use this event handler to tailor the event when a CarrierGroup has initiated the unloading or unboarding of cargo. +-- You can use this event handler to post messages to players, or provide status updates etc. +-- +-- +-- --- Unload event handler OnAfter for CLASS. +-- -- Use this event handler to tailor the event when a CarrierGroup has initiated the unloading or unboarding of cargo. +-- -- You can use this event handler to post messages to players, or provide status updates etc. +-- -- @param #CLASS self +-- -- @param #string From A string that contains the "*from state name*" when the event was triggered. +-- -- @param #string Event A string that contains the "*event name*" when the event was triggered. +-- -- @param #string To A string that contains the "*to state name*" when the event was triggered. +-- -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. +-- -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. +-- function CLASS:OnAfterUnload( From, Event, To, CarrierGroup, DeployZone ) +-- +-- -- Write here your own code. +-- +-- end +-- +-- +-- ## 3.8) Tailor the **Unloading** event +-- +-- +-- --- UnLoading event handler OnAfter for CLASS. +-- -- Use this event handler to tailor the event when a CarrierUnit of a CarrierGroup is in the process of unloading or unboarding of a cargo object. +-- -- You can use this event handler to post messages to players, or provide status updates etc. +-- -- Note that this event is triggered repeatedly until all cargo (units) have been unboarded from the CarrierUnit. +-- -- @param #CLASS self +-- -- @param #string From A string that contains the "*from state name*" when the event was triggered. +-- -- @param #string Event A string that contains the "*event name*" when the event was triggered. +-- -- @param #string To A string that contains the "*to state name*" when the event was triggered. +-- -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. +-- -- @param Cargo.Cargo#CARGO Cargo The cargo object. +-- -- @param Wrapper.Unit#UNIT CarrierUnit The carrier unit that is executing the cargo unloading operation. +-- -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. +-- function CLASS:OnAfterUnload( From, Event, To, CarrierGroup, Cargo, CarrierUnit, DeployZone ) +-- +-- -- Write here your own code. +-- +-- end +-- +-- +-- ## 3.9) Tailor the **Unloaded** event +-- +-- +-- Use this event handler to tailor the event when a CarrierUnit of a CarrierGroup has unloaded a cargo object. +-- You can use this event handler to post messages to players, or provide status updates etc. +-- +-- --- Unloaded event handler OnAfter for CLASS. +-- -- Use this event handler to tailor the event when a CarrierUnit of a CarrierGroup has unloaded a cargo object. +-- -- You can use this event handler to post messages to players, or provide status updates etc. +-- -- Note that if more cargo objects were unloading or unboarding from the CarrierUnit, then this event can be triggered multiple times for each different Cargo/CarrierUnit. +-- -- A CarrierUnit can be part of the larger CarrierGroup. +-- -- @param #CLASS self +-- -- @param #string From A string that contains the "*from state name*" when the event was triggered. +-- -- @param #string Event A string that contains the "*event name*" when the event was triggered. +-- -- @param #string To A string that contains the "*to state name*" when the event was triggered. +-- -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. +-- -- @param Cargo.Cargo#CARGO Cargo The cargo object. +-- -- @param Wrapper.Unit#UNIT CarrierUnit The carrier unit that is executing the cargo unloading operation. +-- -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. +-- function CLASS:OnAfterUnloaded( From, Event, To, CarrierGroup, Cargo, CarrierUnit, DeployZone ) +-- +-- -- Write here your own code. +-- +-- end +-- +-- +-- ## 3.10) Tailor the **Deployed** event +-- +-- Use this event handler to tailor the event when a carrier has deployed all cargo objects from the CarrierGroup. +-- You can use this event handler to post messages to players, or provide status updates etc. +-- +-- +-- --- Deployed event handler OnAfter for CLASS. +-- -- Use this event handler to tailor the event when a carrier has deployed all cargo objects from the CarrierGroup. +-- -- You can use this event handler to post messages to players, or provide status updates etc. +-- -- @param #CLASS self +-- -- @param #string From A string that contains the "*from state name*" when the event was triggered. +-- -- @param #string Event A string that contains the "*event name*" when the event was triggered. +-- -- @param #string To A string that contains the "*to state name*" when the event was triggered. +-- -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. +-- -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. +-- function CLASS:OnAfterDeployed( From, Event, To, CarrierGroup, DeployZone ) +-- +-- -- Write here your own code. +-- +-- end +-- +-- ## 3.11) Tailor the **Home** event +-- +-- Use this event handler to tailor the event when a CarrierGroup is returning to the HomeZone, after it has deployed all cargo objects from the CarrierGroup. +-- You can use this event handler to post messages to players, or provide status updates etc. +-- +-- --- Home event handler OnAfter for CLASS. +-- -- Use this event handler to tailor the event when a CarrierGroup is returning to the HomeZone, after it has deployed all cargo objects from the CarrierGroup. +-- -- You can use this event handler to post messages to players, or provide status updates etc. +-- -- If there is no HomeZone is specified, the CarrierGroup will stay at the current location after having deployed all cargo and this event won't be triggered. +-- -- @param #CLASS self +-- -- @param #string From A string that contains the "*from state name*" when the event was triggered. +-- -- @param #string Event A string that contains the "*event name*" when the event was triggered. +-- -- @param #string To A string that contains the "*to state name*" when the event was triggered. +-- -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. +-- -- @param Core.Point#COORDINATE Coordinate The home coordinate the Carrier will arrive and stop it's activities. +-- -- @param #number Speed The velocity in meters per second on which the CarrierGroup is routed towards the home Coordinate. +-- -- @param #number Height Height in meters to move to the home coordinate. +-- -- @param Core.Zone#ZONE HomeZone The zone wherein the carrier will return when all cargo has been transported. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. +-- function CLASS:OnAfterHome( From, Event, To, CarrierGroup, Coordinate, Speed, Height, HomeZone ) +-- +-- -- Write here your own code. +-- +-- end +-- +-- --- +-- +-- # 4) Set the pickup parameters. +-- +-- Several parameters can be set to pickup cargo: +-- +-- * @{#AI_CARGO_DISPATCHER.SetPickupRadius}(): Sets or randomizes the pickup location for the carrier around the cargo coordinate in a radius defined an outer and optional inner radius. +-- * @{#AI_CARGO_DISPATCHER.SetPickupSpeed}(): Set the speed or randomizes the speed in km/h to pickup the cargo. +-- * @{#AI_CARGO_DISPATCHER.SetPickupHeight}(): Set the height or randomizes the height in meters to pickup the cargo. +-- +-- --- +-- +-- # 5) Set the deploy parameters. +-- +-- Several parameters can be set to deploy cargo: +-- +-- * @{#AI_CARGO_DISPATCHER.SetDeployRadius}(): Sets or randomizes the deploy location for the carrier around the cargo coordinate in a radius defined an outer and an optional inner radius. +-- * @{#AI_CARGO_DISPATCHER.SetDeploySpeed}(): Set the speed or randomizes the speed in km/h to deploy the cargo. +-- * @{#AI_CARGO_DISPATCHER.SetDeployHeight}(): Set the height or randomizes the height in meters to deploy the cargo. +-- +-- --- +-- +-- # 6) Set the home zone when there isn't any more cargo to pickup. +-- +-- A home zone can be specified to where the Carriers will move when there isn't any cargo left for pickup. +-- Use @{#AI_CARGO_DISPATCHER.SetHomeZone}() to specify the home zone. +-- +-- If no home zone is specified, the carriers will wait near the deploy zone for a new pickup command. +-- +-- === +-- +-- @field #AI_CARGO_DISPATCHER +AI_CARGO_DISPATCHER = { + ClassName = "AI_CARGO_DISPATCHER", + AI_Cargo = {}, + PickupCargo = {} +} + +--- @field #list +AI_CARGO_DISPATCHER.AI_Cargo = {} + +--- @field #list +AI_CARGO_DISPATCHER.PickupCargo = {} + + +--- Creates a new AI_CARGO_DISPATCHER object. +-- @param #AI_CARGO_DISPATCHER self +-- @param Core.Set#SET_GROUP CarrierSet The set of @{Wrapper.Group#GROUP} objects of carriers that will transport the cargo. +-- @param Core.Set#SET_CARGO CargoSet The set of @{Cargo.Cargo#CARGO} objects, which can be CARGO_GROUP, CARGO_CRATE, CARGO_SLINGLOAD objects. +-- @param Core.Set#SET_ZONE PickupZoneSet (optional) The set of pickup zones, which are used to where the cargo can be picked up by the carriers. If nil, then cargo can be picked up everywhere. +-- @param Core.Set#SET_ZONE DeployZoneSet The set of deploy zones, which are used to where the cargo will be deployed by the carriers. +-- @return #AI_CARGO_DISPATCHER +-- @usage +-- +-- -- An AI dispatcher object for a helicopter squadron, moving infantry from pickup zones to deploy zones. +-- +-- local SetCargoInfantry = SET_CARGO:New():FilterTypes( "Infantry" ):FilterStart() +-- local SetHelicopter = SET_GROUP:New():FilterPrefixes( "Helicopter" ):FilterStart() +-- local SetPickupZones = SET_ZONE:New():FilterPrefixes( "Pickup" ):FilterStart() +-- local SetDeployZones = SET_ZONE:New():FilterPrefixes( "Deploy" ):FilterStart() +-- +-- AICargoDispatcherHelicopter = AI_CARGO_DISPATCHER_HELICOPTER:New( SetHelicopter, SetCargoInfantry, SetPickupZones, SetDeployZones ) +-- AICargoDispatcherHelicopter:Start() +-- +-- @usage +-- +-- -- An AI dispatcher object for a vehicle squadron, moving infantry from pickup zones to deploy zones. +-- +-- local SetCargoInfantry = SET_CARGO:New():FilterTypes( "Infantry" ):FilterStart() +-- local SetAPC = SET_GROUP:New():FilterPrefixes( "APC" ):FilterStart() +-- local SetDeployZones = SET_ZONE:New():FilterPrefixes( "Deploy" ):FilterStart() +-- +-- AICargoDispatcherAPC = AI_CARGO_DISPATCHER_APC:New( SetAPC, SetCargoInfantry, nil, SetDeployZones ) +-- AICargoDispatcherAPC:Start() +-- +-- @usage +-- +-- -- An AI dispatcher object for an airplane squadron, moving infantry and vehicles from pickup airbases to deploy airbases. +-- +-- local CargoInfantrySet = SET_CARGO:New():FilterTypes( "Infantry" ):FilterStart() +-- local AirplanesSet = SET_GROUP:New():FilterPrefixes( "Airplane" ):FilterStart() +-- local PickupZoneSet = SET_ZONE:New() +-- local DeployZoneSet = SET_ZONE:New() +-- +-- PickupZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Gudauta ) ) +-- DeployZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Sochi_Adler ) ) +-- DeployZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Maykop_Khanskaya ) ) +-- DeployZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Mineralnye_Vody ) ) +-- DeployZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Vaziani ) ) +-- +-- AICargoDispatcherAirplanes = AI_CARGO_DISPATCHER_AIRPLANE:New( AirplanesSet, CargoInfantrySet, PickupZoneSet, DeployZoneSet ) +-- AICargoDispatcherAirplanes:Start() +-- +function AI_CARGO_DISPATCHER:New( CarrierSet, CargoSet, PickupZoneSet, DeployZoneSet ) + + local self = BASE:Inherit( self, FSM:New() ) -- #AI_CARGO_DISPATCHER + + self.SetCarrier = CarrierSet -- Core.Set#SET_GROUP + self.SetCargo = CargoSet -- Core.Set#SET_CARGO + + + self.PickupZoneSet=PickupZoneSet + self.DeployZoneSet=DeployZoneSet + + self:SetStartState( "Idle" ) + + self:AddTransition( "Monitoring", "Monitor", "Monitoring" ) + + self:AddTransition( "Idle", "Start", "Monitoring" ) + self:AddTransition( "Monitoring", "Stop", "Idle" ) + + + self:AddTransition( "Monitoring", "Pickup", "Monitoring" ) + self:AddTransition( "Monitoring", "Load", "Monitoring" ) + self:AddTransition( "Monitoring", "Loading", "Monitoring" ) + self:AddTransition( "Monitoring", "Loaded", "Monitoring" ) + self:AddTransition( "Monitoring", "PickedUp", "Monitoring" ) + + self:AddTransition( "Monitoring", "Transport", "Monitoring" ) + + self:AddTransition( "Monitoring", "Deploy", "Monitoring" ) + self:AddTransition( "Monitoring", "Unload", "Monitoring" ) + self:AddTransition( "Monitoring", "Unloading", "Monitoring" ) + self:AddTransition( "Monitoring", "Unloaded", "Monitoring" ) + self:AddTransition( "Monitoring", "Deployed", "Monitoring" ) + + self:AddTransition( "Monitoring", "Home", "Monitoring" ) + + self:SetMonitorTimeInterval( 30 ) + + self:SetDeployRadius( 500, 200 ) + + self.PickupCargo = {} + self.CarrierHome = {} + + -- Put a Dead event handler on SetCarrier, to ensure that when a carrier is destroyed, that all internal parameters are reset. + function self.SetCarrier.OnAfterRemoved( SetCarrier, From, Event, To, CarrierName, Carrier ) + self:F( { Carrier = Carrier:GetName() } ) + self.PickupCargo[Carrier] = nil + self.CarrierHome[Carrier] = nil + end + + return self +end + + +--- Set the monitor time interval. +-- @param #AI_CARGO_DISPATCHER self +-- @param #number MonitorTimeInterval The interval in seconds when the cargo dispatcher will search for new cargo to be picked up. +-- @return #AI_CARGO_DISPATCHER +function AI_CARGO_DISPATCHER:SetMonitorTimeInterval( MonitorTimeInterval ) + + self.MonitorTimeInterval = MonitorTimeInterval + + return self +end + + +--- Set the home zone. +-- When there is nothing anymore to pickup, the carriers will go to a random coordinate in this zone. +-- They will await here new orders. +-- @param #AI_CARGO_DISPATCHER self +-- @param Core.Zone#ZONE_BASE HomeZone The home zone where the carriers will return when there is no more cargo to pickup. +-- @return #AI_CARGO_DISPATCHER +-- @usage +-- +-- -- Create a new cargo dispatcher +-- AICargoDispatcherHelicopter = AI_CARGO_DISPATCHER_HELICOPTER:New( SetHelicopter, SetCargoInfantry, SetPickupZones, SetDeployZones ) +-- +-- -- Set the home coordinate +-- local HomeZone = ZONE:New( "Home" ) +-- AICargoDispatcherHelicopter:SetHomeZone( HomeZone ) +-- +function AI_CARGO_DISPATCHER:SetHomeZone( HomeZone ) + + self.HomeZone = HomeZone + + return self +end + + +--- Sets or randomizes the pickup location for the carrier around the cargo coordinate in a radius defined an outer and optional inner radius. +-- This radius is influencing the location where the carrier will land to pickup the cargo. +-- There are two aspects that are very important to remember and take into account: +-- +-- - Ensure that the outer and inner radius are within reporting radius set by the cargo. +-- For example, if the cargo has a reporting radius of 400 meters, and the outer and inner radius is set to 500 and 450 respectively, +-- then no cargo will be loaded!!! +-- - Also take care of the potential cargo position and possible reasons to crash the carrier. This is especially important +-- for locations which are crowded with other objects, like in the middle of villages or cities. +-- So, for the best operation of cargo operations, always ensure that the cargo is located at open spaces. +-- +-- The default radius is 0, so the center. In case of a polygon zone, a random location will be selected as the center in the zone. +-- @param #AI_CARGO_DISPATCHER self +-- @param #number OuterRadius The outer radius in meters around the cargo coordinate. +-- @param #number InnerRadius (optional) The inner radius in meters around the cargo coordinate. +-- @return #AI_CARGO_DISPATCHER +-- @usage +-- +-- -- Create a new cargo dispatcher +-- AICargoDispatcherHelicopter = AI_CARGO_DISPATCHER_HELICOPTER:New( SetHelicopter, SetCargoInfantry, SetPickupZones, SetDeployZones ) +-- +-- -- Set the carrier to land within a band around the cargo coordinate between 500 and 300 meters! +-- AICargoDispatcherHelicopter:SetPickupRadius( 500, 300 ) +-- +function AI_CARGO_DISPATCHER:SetPickupRadius( OuterRadius, InnerRadius ) + + OuterRadius = OuterRadius or 0 + InnerRadius = InnerRadius or OuterRadius + + self.PickupOuterRadius = OuterRadius + self.PickupInnerRadius = InnerRadius + + return self +end + + +--- Set the speed or randomizes the speed in km/h to pickup the cargo. +-- @param #AI_CARGO_DISPATCHER self +-- @param #number MaxSpeed (optional) The maximum speed to move to the cargo pickup location. +-- @param #number MinSpeed The minimum speed to move to the cargo pickup location. +-- @return #AI_CARGO_DISPATCHER +-- @usage +-- +-- -- Create a new cargo dispatcher +-- AICargoDispatcherHelicopter = AI_CARGO_DISPATCHER_HELICOPTER:New( SetHelicopter, SetCargoInfantry, SetPickupZones, SetDeployZones ) +-- +-- -- Set the minimum pickup speed to be 100 km/h and the maximum speed to be 200 km/h. +-- AICargoDispatcherHelicopter:SetPickupSpeed( 200, 100 ) +-- +function AI_CARGO_DISPATCHER:SetPickupSpeed( MaxSpeed, MinSpeed ) + + MaxSpeed = MaxSpeed or 999 + MinSpeed = MinSpeed or MaxSpeed + + self.PickupMinSpeed = MinSpeed + self.PickupMaxSpeed = MaxSpeed + + return self +end + + +--- Sets or randomizes the deploy location for the carrier around the cargo coordinate in a radius defined an outer and an optional inner radius. +-- This radius is influencing the location where the carrier will land to deploy the cargo. +-- There is an aspect that is very important to remember and take into account: +-- +-- - Take care of the potential cargo position and possible reasons to crash the carrier. This is especially important +-- for locations which are crowded with other objects, like in the middle of villages or cities. +-- So, for the best operation of cargo operations, always ensure that the cargo is located at open spaces. +-- +-- The default radius is 0, so the center. In case of a polygon zone, a random location will be selected as the center in the zone. +-- @param #AI_CARGO_DISPATCHER self +-- @param #number OuterRadius The outer radius in meters around the cargo coordinate. +-- @param #number InnerRadius (optional) The inner radius in meters around the cargo coordinate. +-- @return #AI_CARGO_DISPATCHER +-- @usage +-- +-- -- Create a new cargo dispatcher +-- AICargoDispatcherHelicopter = AI_CARGO_DISPATCHER_HELICOPTER:New( SetHelicopter, SetCargoInfantry, SetPickupZones, SetDeployZones ) +-- +-- -- Set the carrier to land within a band around the cargo coordinate between 500 and 300 meters! +-- AICargoDispatcherHelicopter:SetDeployRadius( 500, 300 ) +-- +function AI_CARGO_DISPATCHER:SetDeployRadius( OuterRadius, InnerRadius ) + + OuterRadius = OuterRadius or 0 + InnerRadius = InnerRadius or OuterRadius + + self.DeployOuterRadius = OuterRadius + self.DeployInnerRadius = InnerRadius + + return self +end + + +--- Sets or randomizes the speed in km/h to deploy the cargo. +-- @param #AI_CARGO_DISPATCHER self +-- @param #number MaxSpeed The maximum speed to move to the cargo deploy location. +-- @param #number MinSpeed (optional) The minimum speed to move to the cargo deploy location. +-- @return #AI_CARGO_DISPATCHER +-- @usage +-- +-- -- Create a new cargo dispatcher +-- AICargoDispatcherHelicopter = AI_CARGO_DISPATCHER_HELICOPTER:New( SetHelicopter, SetCargoInfantry, SetPickupZones, SetDeployZones ) +-- +-- -- Set the minimum deploy speed to be 100 km/h and the maximum speed to be 200 km/h. +-- AICargoDispatcherHelicopter:SetDeploySpeed( 200, 100 ) +-- +function AI_CARGO_DISPATCHER:SetDeploySpeed( MaxSpeed, MinSpeed ) + + MaxSpeed = MaxSpeed or 999 + MinSpeed = MinSpeed or MaxSpeed + + self.DeployMinSpeed = MinSpeed + self.DeployMaxSpeed = MaxSpeed + + return self +end + + +--- Set the height or randomizes the height in meters to fly and pickup the cargo. The default height is 200 meters. +-- @param #AI_CARGO_DISPATCHER self +-- @param #number MaxHeight (optional) The maximum height to fly to the cargo pickup location. +-- @param #number MinHeight (optional) The minimum height to fly to the cargo pickup location. +-- @return #AI_CARGO_DISPATCHER +-- @usage +-- +-- -- Create a new cargo dispatcher +-- AICargoDispatcherHelicopter = AI_CARGO_DISPATCHER_HELICOPTER:New( SetHelicopter, SetCargoInfantry, SetPickupZones, SetDeployZones ) +-- +-- -- Set the minimum pickup fly height to be 50 meters and the maximum height to be 200 meters. +-- AICargoDispatcherHelicopter:SetPickupHeight( 200, 50 ) +-- +function AI_CARGO_DISPATCHER:SetPickupHeight( MaxHeight, MinHeight ) + + MaxHeight = MaxHeight or 200 + MinHeight = MinHeight or MaxHeight + + self.PickupMinHeight = MinHeight + self.PickupMaxHeight = MaxHeight + + return self +end + + +--- Set the height or randomizes the height in meters to fly and deploy the cargo. The default height is 200 meters. +-- @param #AI_CARGO_DISPATCHER self +-- @param #number MaxHeight (optional) The maximum height to fly to the cargo deploy location. +-- @param #number MinHeight (optional) The minimum height to fly to the cargo deploy location. +-- @return #AI_CARGO_DISPATCHER +-- @usage +-- +-- -- Create a new cargo dispatcher +-- AICargoDispatcherHelicopter = AI_CARGO_DISPATCHER_HELICOPTER:New( SetHelicopter, SetCargoInfantry, SetPickupZones, SetDeployZones ) +-- +-- -- Set the minimum deploy fly height to be 50 meters and the maximum height to be 200 meters. +-- AICargoDispatcherHelicopter:SetDeployHeight( 200, 50 ) +-- +function AI_CARGO_DISPATCHER:SetDeployHeight( MaxHeight, MinHeight ) + + MaxHeight = MaxHeight or 200 + MinHeight = MinHeight or MaxHeight + + self.DeployMinHeight = MinHeight + self.DeployMaxHeight = MaxHeight + + return self +end + + +--- The Start trigger event, which actually takes action at the specified time interval. +-- @param #AI_CARGO_DISPATCHER self +function AI_CARGO_DISPATCHER:onafterMonitor() + + self:F("Carriers") + self.SetCarrier:Flush() + + for CarrierGroupName, Carrier in pairs( self.SetCarrier:GetSet() ) do + local Carrier = Carrier -- Wrapper.Group#GROUP + if Carrier:IsAlive() ~= nil then + local AI_Cargo = self.AI_Cargo[Carrier] + if not AI_Cargo then + + -- ok, so this Carrier does not have yet an AI_CARGO handling object... + -- let's create one and also declare the Loaded and UnLoaded handlers. + self.AI_Cargo[Carrier] = self:AICargo( Carrier, self.SetCargo, self.CombatRadius ) + AI_Cargo = self.AI_Cargo[Carrier] + + --- Pickup event handler OnAfter for AI_CARGO_DISPATCHER. + -- Use this event handler to tailor the event when a CarrierGroup is routed towards a new pickup Coordinate and a specified Speed. + -- You can use this event handler to post messages to players, or provide status updates etc. + -- @function [parent=#AI_CARGO_DISPATCHER] OnAfterPickup + -- @param #AI_CARGO_DISPATCHER self + -- @param #string From A string that contains the "*from state name*" when the event was triggered. + -- @param #string Event A string that contains the "*event name*" when the event was triggered. + -- @param #string To A string that contains the "*to state name*" when the event was triggered. + -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. + -- @param Core.Point#COORDINATE Coordinate The coordinate of the pickup location. + -- @param #number Speed The velocity in meters per second on which the CarrierGroup is routed towards the pickup Coordinate. + -- @param #number Height Height in meters to move to the pickup coordinate. + -- @param Core.Zone#ZONE_AIRBASE PickupZone (optional) The zone from where the cargo is picked up. Note that the zone is optional and may not be provided, but for AI_CARGO_DISPATCHER_AIRBASE there will always be a PickupZone, as the pickup location is an airbase zone. + function AI_Cargo.OnAfterPickup( AI_Cargo, CarrierGroup, From, Event, To, Coordinate, Speed, Height, PickupZone ) + self:Pickup( CarrierGroup, Coordinate, Speed, Height, PickupZone ) + end + + --- Load event handler OnAfter for AI_CARGO_DISPATCHER. + -- Use this event handler to tailor the event when a CarrierGroup has initiated the loading or boarding of cargo within reporting or near range. + -- You can use this event handler to post messages to players, or provide status updates etc. + -- @function [parent=#AI_CARGO_DISPATCHER] OnAfterLoad + -- @param #AI_CARGO_DISPATCHER self + -- @param #string From A string that contains the "*from state name*" when the event was triggered. + -- @param #string Event A string that contains the "*event name*" when the event was triggered. + -- @param #string To A string that contains the "*to state name*" when the event was triggered. + -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. + -- @param Core.Zone#ZONE_AIRBASE PickupZone (optional) The zone from where the cargo is picked up. Note that the zone is optional and may not be provided, but for AI_CARGO_DISPATCHER_AIRBASE there will always be a PickupZone, as the pickup location is an airbase zone. + + function AI_Cargo.OnAfterLoad( AI_Cargo, CarrierGroup, From, Event, To, PickupZone ) + self:Load( CarrierGroup, PickupZone ) + end + + --- Loading event handler OnAfter for AI_CARGO_DISPATCHER. + -- Use this event handler to tailor the event when a CarrierUnit of a CarrierGroup is in the process of loading or boarding of a cargo object. + -- You can use this event handler to post messages to players, or provide status updates etc. + -- Note that this event is triggered repeatedly until all cargo (units) have been boarded into the carrier. + -- @function [parent=#AI_CARGO_DISPATCHER] OnAfterLoading + -- @param #AI_CARGO_DISPATCHER self + -- @param #string From A string that contains the "*from state name*" when the event was triggered. + -- @param #string Event A string that contains the "*event name*" when the event was triggered. + -- @param #string To A string that contains the "*to state name*" when the event was triggered. + -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. + -- @param Cargo.Cargo#CARGO Cargo The cargo object. + -- @param Wrapper.Unit#UNIT CarrierUnit The carrier unit that is executing the cargo loading operation. + -- @param Core.Zone#ZONE_AIRBASE PickupZone (optional) The zone from where the cargo is picked up. Note that the zone is optional and may not be provided, but for AI_CARGO_DISPATCHER_AIRBASE there will always be a PickupZone, as the pickup location is an airbase zone. + + function AI_Cargo.OnAfterBoard( AI_Cargo, CarrierGroup, From, Event, To, Cargo, CarrierUnit, PickupZone ) + self:Loading( CarrierGroup, Cargo, CarrierUnit, PickupZone ) + end + + --- Loaded event handler OnAfter for AI_CARGO_DISPATCHER. + -- Use this event handler to tailor the event when a CarrierUnit of a CarrierGroup has loaded a cargo object. + -- You can use this event handler to post messages to players, or provide status updates etc. + -- Note that if more cargo objects were loading or boarding into the CarrierUnit, then this event can be triggered multiple times for each different Cargo/CarrierUnit. + -- A CarrierUnit can be part of the larger CarrierGroup. + -- @function [parent=#AI_CARGO_DISPATCHER] OnAfterLoaded + -- @param #AI_CARGO_DISPATCHER self + -- @param #string From A string that contains the "*from state name*" when the event was triggered. + -- @param #string Event A string that contains the "*event name*" when the event was triggered. + -- @param #string To A string that contains the "*to state name*" when the event was triggered. + -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. + -- @param Cargo.Cargo#CARGO Cargo The cargo object. + -- @param Wrapper.Unit#UNIT CarrierUnit The carrier unit that is executing the cargo loading operation. + -- @param Core.Zone#ZONE_AIRBASE PickupZone (optional) The zone from where the cargo is picked up. Note that the zone is optional and may not be provided, but for AI_CARGO_DISPATCHER_AIRBASE there will always be a PickupZone, as the pickup location is an airbase zone. + + function AI_Cargo.OnAfterLoaded( AI_Cargo, CarrierGroup, From, Event, To, Cargo, CarrierUnit, PickupZone ) + self:Loaded( CarrierGroup, Cargo, CarrierUnit, PickupZone ) + end + + --- PickedUp event handler OnAfter for AI_CARGO_DISPATCHER. + -- Use this event handler to tailor the event when a carrier has picked up all cargo objects into the CarrierGroup. + -- You can use this event handler to post messages to players, or provide status updates etc. + -- @function [parent=#AI_CARGO_DISPATCHER] OnAfterPickedUp + -- @param #AI_CARGO_DISPATCHER self + -- @param #string From A string that contains the "*from state name*" when the event was triggered. + -- @param #string Event A string that contains the "*event name*" when the event was triggered. + -- @param #string To A string that contains the "*to state name*" when the event was triggered. + -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. + -- @param Core.Zone#ZONE_AIRBASE PickupZone (optional) The zone from where the cargo is picked up. Note that the zone is optional and may not be provided, but for AI_CARGO_DISPATCHER_AIRBASE there will always be a PickupZone, as the pickup location is an airbase zone. + + function AI_Cargo.OnAfterPickedUp( AI_Cargo, CarrierGroup, From, Event, To, PickupZone ) + self:PickedUp( CarrierGroup, PickupZone ) + self:Transport( CarrierGroup ) + end + + + --- Deploy event handler OnAfter for AI_CARGO_DISPATCHER. + -- Use this event handler to tailor the event when a CarrierGroup is routed to a deploy coordinate, to Unload all cargo objects in each CarrierUnit. + -- You can use this event handler to post messages to players, or provide status updates etc. + -- @function [parent=#AI_CARGO_DISPATCHER] OnAfterDeploy + -- @param #AI_CARGO_DISPATCHER self + -- @param #string From A string that contains the "*from state name*" when the event was triggered. + -- @param #string Event A string that contains the "*event name*" when the event was triggered. + -- @param #string To A string that contains the "*to state name*" when the event was triggered. + -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. + -- @param Core.Point#COORDINATE Coordinate The deploy coordinate. + -- @param #number Speed The velocity in meters per second on which the CarrierGroup is routed towards the deploy Coordinate. + -- @param #number Height Height in meters to move to the deploy coordinate. + -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. + + function AI_Cargo.OnAfterDeploy( AI_Cargo, CarrierGroup, From, Event, To, Coordinate, Speed, Height, DeployZone ) + self:Deploy( CarrierGroup, Coordinate, Speed, Height, DeployZone ) + end + + + --- Unload event handler OnAfter for AI_CARGO_DISPATCHER. + -- Use this event handler to tailor the event when a CarrierGroup has initiated the unloading or unboarding of cargo. + -- You can use this event handler to post messages to players, or provide status updates etc. + -- @function [parent=#AI_CARGO_DISPATCHER] OnAfterUnload + -- @param #AI_CARGO_DISPATCHER self + -- @param #string From A string that contains the "*from state name*" when the event was triggered. + -- @param #string Event A string that contains the "*event name*" when the event was triggered. + -- @param #string To A string that contains the "*to state name*" when the event was triggered. + -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. + -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. + + function AI_Cargo.OnAfterUnload( AI_Cargo, Carrier, From, Event, To, Cargo, CarrierUnit, DeployZone ) + self:Unloading( Carrier, Cargo, CarrierUnit, DeployZone ) + end + + --- UnLoading event handler OnAfter for AI_CARGO_DISPATCHER. + -- Use this event handler to tailor the event when a CarrierUnit of a CarrierGroup is in the process of unloading or unboarding of a cargo object. + -- You can use this event handler to post messages to players, or provide status updates etc. + -- Note that this event is triggered repeatedly until all cargo (units) have been unboarded from the CarrierUnit. + -- @function [parent=#AI_CARGO_DISPATCHER] OnAfterUnloading + -- @param #AI_CARGO_DISPATCHER self + -- @param #string From A string that contains the "*from state name*" when the event was triggered. + -- @param #string Event A string that contains the "*event name*" when the event was triggered. + -- @param #string To A string that contains the "*to state name*" when the event was triggered. + -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. + -- @param Cargo.Cargo#CARGO Cargo The cargo object. + -- @param Wrapper.Unit#UNIT CarrierUnit The carrier unit that is executing the cargo unloading operation. + -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. + + function AI_Cargo.OnAfterUnboard( AI_Cargo, CarrierGroup, From, Event, To, Cargo, CarrierUnit, DeployZone ) + self:Unloading( CarrierGroup, Cargo, CarrierUnit, DeployZone ) + end + + + --- Unloaded event handler OnAfter for AI_CARGO_DISPATCHER. + -- Use this event handler to tailor the event when a CarrierUnit of a CarrierGroup has unloaded a cargo object. + -- You can use this event handler to post messages to players, or provide status updates etc. + -- Note that if more cargo objects were unloading or unboarding from the CarrierUnit, then this event can be triggered multiple times for each different Cargo/CarrierUnit. + -- A CarrierUnit can be part of the larger CarrierGroup. + -- @function [parent=#AI_CARGO_DISPATCHER] OnAfterUnloaded + -- @param #AI_CARGO_DISPATCHER self + -- @param #string From A string that contains the "*from state name*" when the event was triggered. + -- @param #string Event A string that contains the "*event name*" when the event was triggered. + -- @param #string To A string that contains the "*to state name*" when the event was triggered. + -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. + -- @param Cargo.Cargo#CARGO Cargo The cargo object. + -- @param Wrapper.Unit#UNIT CarrierUnit The carrier unit that is executing the cargo unloading operation. + -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. + + function AI_Cargo.OnAfterUnloaded( AI_Cargo, Carrier, From, Event, To, Cargo, CarrierUnit, DeployZone ) + self:Unloaded( Carrier, Cargo, CarrierUnit, DeployZone ) + end + + --- Deployed event handler OnAfter for AI_CARGO_DISPATCHER. + -- Use this event handler to tailor the event when a carrier has deployed all cargo objects from the CarrierGroup. + -- You can use this event handler to post messages to players, or provide status updates etc. + -- @function [parent=#AI_CARGO_DISPATCHER] OnAfterDeployed + -- @param #AI_CARGO_DISPATCHER self + -- @param #string From A string that contains the "*from state name*" when the event was triggered. + -- @param #string Event A string that contains the "*event name*" when the event was triggered. + -- @param #string To A string that contains the "*to state name*" when the event was triggered. + -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. + -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. + + function AI_Cargo.OnAfterDeployed( AI_Cargo, Carrier, From, Event, To, DeployZone ) + self:Deployed( Carrier, DeployZone ) + end + + --- Home event handler OnAfter for AI_CARGO_DISPATCHER. + -- Use this event handler to tailor the event when a CarrierGroup is returning to the HomeZone, after it has deployed all cargo objects from the CarrierGroup. + -- You can use this event handler to post messages to players, or provide status updates etc. + -- If there is no HomeZone is specified, the CarrierGroup will stay at the current location after having deployed all cargo. + -- @function [parent=#AI_CARGO_DISPATCHER] OnAfterHome + -- @param #AI_CARGO_DISPATCHER self + -- @param #string From A string that contains the "*from state name*" when the event was triggered. + -- @param #string Event A string that contains the "*event name*" when the event was triggered. + -- @param #string To A string that contains the "*to state name*" when the event was triggered. + -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. + -- @param Core.Point#COORDINATE Coordinate The home coordinate the Carrier will arrive and stop it's activities. + -- @param #number Speed The velocity in meters per second on which the CarrierGroup is routed towards the home Coordinate. + -- @param #number Height Height in meters to move to the home coordinate. + -- @param Core.Zone#ZONE HomeZone The zone wherein the carrier will return when all cargo has been transported. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. + + function AI_Cargo.OnAfterHome( AI_Cargo, Carrier, From, Event, To, Coordinate, Speed, Height, HomeZone ) + self:Home( Carrier, Coordinate, Speed, Height, HomeZone ) + end + end + + -- The Pickup sequence ... + -- Check if this Carrier need to go and Pickup something... + -- So, if the cargo bay is not full yet with cargo to be loaded ... + self:T( { Carrier = CarrierGroupName, IsRelocating = AI_Cargo:IsRelocating(), IsTransporting = AI_Cargo:IsTransporting() } ) + if AI_Cargo:IsRelocating() == false and AI_Cargo:IsTransporting() == false then + -- ok, so there is a free Carrier + -- now find the first cargo that is Unloaded + + local PickupCargo = nil + local PickupZone = nil + + self.SetCargo:Flush() + for CargoName, Cargo in UTILS.spairs( self.SetCargo:GetSet(), function( t, a, b ) return t[a]:GetWeight() < t[b]:GetWeight() end ) do + local Cargo = Cargo -- Cargo.Cargo#CARGO + self:F( { Cargo = Cargo:GetName(), UnLoaded = Cargo:IsUnLoaded(), Deployed = Cargo:IsDeployed(), PickupCargo = self.PickupCargo[Carrier] ~= nil } ) + if Cargo:IsUnLoaded() == true and Cargo:IsDeployed() == false then + local CargoCoordinate = Cargo:GetCoordinate() + local CoordinateFree = true + --self.PickupZoneSet:Flush() + --PickupZone = self.PickupZoneSet:GetRandomZone() + PickupZone = self.PickupZoneSet and self.PickupZoneSet:IsCoordinateInZone( CargoCoordinate ) + if not self.PickupZoneSet or PickupZone then + for CarrierPickup, Coordinate in pairs( self.PickupCargo ) do + if CarrierPickup:IsAlive() == true then + if CargoCoordinate:Get2DDistance( Coordinate ) <= 25 then + self:F( { "Coordinate not free for ", Cargo = Cargo:GetName(), Carrier:GetName(), PickupCargo = self.PickupCargo[Carrier] ~= nil } ) + CoordinateFree = false + break + end + else + self.PickupCargo[CarrierPickup] = nil + end + end + if CoordinateFree == true then + -- Check if this cargo can be picked-up by at least one carrier unit of AI_Cargo. + local LargestLoadCapacity = 0 + for _, Carrier in pairs( Carrier:GetUnits() ) do + local LoadCapacity = Carrier:GetCargoBayFreeWeight() + if LargestLoadCapacity < LoadCapacity then + LargestLoadCapacity = LoadCapacity + end + end + -- So if there is a carrier that has the required load capacity to load the total weight of the cargo, dispatch the carrier. + -- Otherwise break and go to the next carrier. + -- This will skip cargo which is too large to be able to be loaded by carriers + -- and will secure an efficient dispatching scheme. + if LargestLoadCapacity >= Cargo:GetWeight() then + self.PickupCargo[Carrier] = CargoCoordinate + PickupCargo = Cargo + break + else + local text=string.format("WARNING: Cargo %s is too heavy to be loaded into transport. Cargo weight %.1f > %.1f load capacity of carrier %s.", + tostring(Cargo:GetName()), Cargo:GetWeight(), LargestLoadCapacity, tostring(Carrier:GetName())) + self:I(text) + end + end + end + end + end + + if PickupCargo then + self.CarrierHome[Carrier] = nil + local PickupCoordinate = PickupCargo:GetCoordinate():GetRandomCoordinateInRadius( self.PickupOuterRadius, self.PickupInnerRadius ) + AI_Cargo:Pickup( PickupCoordinate, math.random( self.PickupMinSpeed, self.PickupMaxSpeed ), math.random( self.PickupMinHeight, self.PickupMaxHeight ), PickupZone ) + break + else + if self.HomeZone then + if not self.CarrierHome[Carrier] then + self.CarrierHome[Carrier] = true + AI_Cargo:Home( self.HomeZone:GetRandomPointVec2(), math.random( self.PickupMinSpeed, self.PickupMaxSpeed ), math.random( self.PickupMinHeight, self.PickupMaxHeight ), self.HomeZone ) + end + end + end + end + end + end + + self:__Monitor( self.MonitorTimeInterval ) +end + + +--- Start Trigger for AI_CARGO_DISPATCHER +-- @function [parent=#AI_CARGO_DISPATCHER] Start +-- @param #AI_CARGO_DISPATCHER self + +--- Start Asynchronous Trigger for AI_CARGO_DISPATCHER +-- @function [parent=#AI_CARGO_DISPATCHER] __Start +-- @param #AI_CARGO_DISPATCHER self +-- @param #number Delay + +function AI_CARGO_DISPATCHER:onafterStart( From, Event, To ) + self:__Monitor( -1 ) +end + + +--- Stop Trigger for AI_CARGO_DISPATCHER +-- @function [parent=#AI_CARGO_DISPATCHER] Stop +-- @param #AI_CARGO_DISPATCHER self + +--- Stop Asynchronous Trigger for AI_CARGO_DISPATCHER +-- @function [parent=#AI_CARGO_DISPATCHER] __Stop +-- @param #AI_CARGO_DISPATCHER self +-- @param #number Delay + + +--- Make a Carrier run for a cargo deploy action after the cargo has been loaded, by default. +-- @param #AI_CARGO_DISPATCHER self +-- @param From +-- @param Event +-- @param To +-- @param Wrapper.Group#GROUP Carrier +-- @param Cargo.Cargo#CARGO Cargo +-- @return #AI_CARGO_DISPATCHER +function AI_CARGO_DISPATCHER:onafterTransport( From, Event, To, Carrier, Cargo ) + + if self.DeployZoneSet then + if self.AI_Cargo[Carrier]:IsTransporting() == true then + local DeployZone = self.DeployZoneSet:GetRandomZone() + + local DeployCoordinate = DeployZone:GetCoordinate():GetRandomCoordinateInRadius( self.DeployOuterRadius, self.DeployInnerRadius ) + self.AI_Cargo[Carrier]:__Deploy( 0.1, DeployCoordinate, math.random( self.DeployMinSpeed, self.DeployMaxSpeed ), math.random( self.DeployMinHeight, self.DeployMaxHeight ), DeployZone ) + end + end + + self:F( { Carrier = Carrier:GetName(), PickupCargo = self.PickupCargo } ) + self.PickupCargo[Carrier] = nil +end + +--- **AI** - Models the intelligent transportation of infantry and other cargo using APCs. +-- +-- ## Features: +-- +-- * Quickly transport cargo to various deploy zones using ground vehicles (APCs, trucks ...). +-- * Various @{Cargo.Cargo#CARGO} types can be transported. These are infantry groups and crates. +-- * Define a list of deploy zones of various types to transport the cargo to. +-- * The vehicles follow the roads to ensure the fastest possible cargo transportation over the ground. +-- * Multiple vehicles can transport multiple cargo as one vehicle group. +-- * Multiple vehicle groups can be enabled as one collaborating transportation process. +-- * Infantry loaded as cargo, will unboard in case enemies are nearby and will help defending the vehicles. +-- * Different ranges can be setup for enemy defenses. +-- * Different options can be setup to tweak the cargo transporation behaviour. +-- +-- === +-- +-- ## Test Missions: +-- +-- Test missions can be located on the main GITHUB site. +-- +-- [FlightControl-Master/MOOSE_MISSIONS/AID - AI Dispatching/AID-CGO - AI Cargo Dispatching/] +-- (https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/AID%20-%20AI%20Dispatching/AID-CGO%20-%20AI%20Cargo%20Dispatching) +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_Cargo_Dispatcher_APC +-- @image AI_Cargo_Dispatching_For_APC.JPG + +--- @type AI_CARGO_DISPATCHER_APC +-- @extends AI.AI_Cargo_Dispatcher#AI_CARGO_DISPATCHER + + +--- A dynamic cargo transportation capability for AI groups. +-- +-- Armoured Personnel APCs (APC), Trucks, Jeeps and other carrier equipment can be mobilized to intelligently transport infantry and other cargo within the simulation. +-- +-- The AI_CARGO_DISPATCHER_APC module is derived from the AI_CARGO_DISPATCHER module. +-- +-- ## Note! In order to fully understand the mechanisms of the AI_CARGO_DISPATCHER_APC class, it is recommended that you first consult and READ the documentation of the @{AI.AI_Cargo_Dispatcher} module!!! +-- +-- Especially to learn how to **Tailor the different cargo handling events**, this will be very useful! +-- +-- On top, the AI_CARGO_DISPATCHER_APC class uses the @{Cargo.Cargo} capabilities within the MOOSE framework. +-- Also ensure that you fully understand how to declare and setup Cargo objects within the MOOSE framework before using this class. +-- CARGO derived objects must be declared within the mission to make the AI_CARGO_DISPATCHER_HELICOPTER object recognize the cargo. +-- +-- +-- # 1) AI_CARGO_DISPATCHER_APC constructor. +-- +-- * @{#AI_CARGO_DISPATCHER_APC.New}(): Creates a new AI_CARGO_DISPATCHER_APC object. +-- +-- --- +-- +-- # 2) AI_CARGO_DISPATCHER_APC is a Finite State Machine. +-- +-- This section must be read as follows. Each of the rows indicate a state transition, triggered through an event, and with an ending state of the event was executed. +-- The first column is the **From** state, the second column the **Event**, and the third column the **To** state. +-- +-- So, each of the rows have the following structure. +-- +-- * **From** => **Event** => **To** +-- +-- Important to know is that an event can only be executed if the **current state** is the **From** state. +-- This, when an **Event** that is being triggered has a **From** state that is equal to the **Current** state of the state machine, the event will be executed, +-- and the resulting state will be the **To** state. +-- +-- These are the different possible state transitions of this state machine implementation: +-- +-- * Idle => Start => Monitoring +-- * Monitoring => Monitor => Monitoring +-- * Monitoring => Stop => Idle +-- +-- * Monitoring => Pickup => Monitoring +-- * Monitoring => Load => Monitoring +-- * Monitoring => Loading => Monitoring +-- * Monitoring => Loaded => Monitoring +-- * Monitoring => PickedUp => Monitoring +-- * Monitoring => Deploy => Monitoring +-- * Monitoring => Unload => Monitoring +-- * Monitoring => Unloaded => Monitoring +-- * Monitoring => Deployed => Monitoring +-- * Monitoring => Home => Monitoring +-- +-- +-- ## 2.1) AI_CARGO_DISPATCHER States. +-- +-- * **Monitoring**: The process is dispatching. +-- * **Idle**: The process is idle. +-- +-- ## 2.2) AI_CARGO_DISPATCHER Events. +-- +-- * **Start**: Start the transport process. +-- * **Stop**: Stop the transport process. +-- * **Monitor**: Monitor and take action. +-- +-- * **Pickup**: Pickup cargo. +-- * **Load**: Load the cargo. +-- * **Loading**: The dispatcher is coordinating the loading of a cargo. +-- * **Loaded**: Flag that the cargo is loaded. +-- * **PickedUp**: The dispatcher has loaded all requested cargo into the CarrierGroup. +-- * **Deploy**: Deploy cargo to a location. +-- * **Unload**: Unload the cargo. +-- * **Unloaded**: Flag that the cargo is unloaded. +-- * **Deployed**: All cargo is unloaded from the carriers in the group. +-- * **Home**: A Carrier is going home. +-- +-- ## 2.3) Enhance your mission scripts with **Tailored** Event Handling! +-- +-- Within your mission, you can capture these events when triggered, and tailor the events with your own code! +-- Check out the @{AI.AI_Cargo_Dispatcher#AI_CARGO_DISPATCHER} class at chapter 3 for details on the different event handlers that are available and how to use them. +-- +-- **There are a lot of templates available that allows you to quickly setup an event handler for a specific event type!** +-- +-- --- +-- +-- # 3) Set the pickup parameters. +-- +-- Several parameters can be set to pickup cargo: +-- +-- * @{#AI_CARGO_DISPATCHER_APC.SetPickupRadius}(): Sets or randomizes the pickup location for the APC around the cargo coordinate in a radius defined an outer and optional inner radius. +-- * @{#AI_CARGO_DISPATCHER_APC.SetPickupSpeed}(): Set the speed or randomizes the speed in km/h to pickup the cargo. +-- +-- # 4) Set the deploy parameters. +-- +-- Several parameters can be set to deploy cargo: +-- +-- * @{#AI_CARGO_DISPATCHER_APC.SetDeployRadius}(): Sets or randomizes the deploy location for the APC around the cargo coordinate in a radius defined an outer and an optional inner radius. +-- * @{#AI_CARGO_DISPATCHER_APC.SetDeploySpeed}(): Set the speed or randomizes the speed in km/h to deploy the cargo. +-- +-- # 5) Set the home zone when there isn't any more cargo to pickup. +-- +-- A home zone can be specified to where the APCs will move when there isn't any cargo left for pickup. +-- Use @{#AI_CARGO_DISPATCHER_APC.SetHomeZone}() to specify the home zone. +-- +-- If no home zone is specified, the APCs will wait near the deploy zone for a new pickup command. +-- +-- === +-- +-- @field #AI_CARGO_DISPATCHER_APC +AI_CARGO_DISPATCHER_APC = { + ClassName = "AI_CARGO_DISPATCHER_APC", +} + +--- Creates a new AI_CARGO_DISPATCHER_APC object. +-- @param #AI_CARGO_DISPATCHER_APC self +-- @param Core.Set#SET_GROUP APCSet The set of @{Wrapper.Group#GROUP} objects of vehicles, trucks, APCs that will transport the cargo. +-- @param Core.Set#SET_CARGO CargoSet The set of @{Cargo.Cargo#CARGO} objects, which can be CARGO_GROUP, CARGO_CRATE, CARGO_SLINGLOAD objects. +-- @param Core.Set#SET_ZONE PickupZoneSet (optional) The set of pickup zones, which are used to where the cargo can be picked up by the APCs. If nil, then cargo can be picked up everywhere. +-- @param Core.Set#SET_ZONE DeployZoneSet The set of deploy zones, which are used to where the cargo will be deployed by the APCs. +-- @param DCS#Distance CombatRadius The cargo will be unloaded from the APC and engage the enemy if the enemy is within CombatRadius range. The radius is in meters, the default value is 500 meters. +-- @return #AI_CARGO_DISPATCHER_APC +-- @usage +-- +-- -- An AI dispatcher object for a vehicle squadron, moving infantry from pickup zones to deploy zones. +-- +-- local SetCargoInfantry = SET_CARGO:New():FilterTypes( "Infantry" ):FilterStart() +-- local SetAPC = SET_GROUP:New():FilterPrefixes( "APC" ):FilterStart() +-- local SetDeployZones = SET_ZONE:New():FilterPrefixes( "Deploy" ):FilterStart() +-- +-- AICargoDispatcherAPC = AI_CARGO_DISPATCHER_APC:New( SetAPC, SetCargoInfantry, nil, SetDeployZones ) +-- AICargoDispatcherAPC:Start() +-- +function AI_CARGO_DISPATCHER_APC:New( APCSet, CargoSet, PickupZoneSet, DeployZoneSet, CombatRadius ) + + local self = BASE:Inherit( self, AI_CARGO_DISPATCHER:New( APCSet, CargoSet, PickupZoneSet, DeployZoneSet ) ) -- #AI_CARGO_DISPATCHER_APC + + self:SetDeploySpeed( 120, 70 ) + self:SetPickupSpeed( 120, 70 ) + self:SetPickupRadius( 0, 0 ) + self:SetDeployRadius( 0, 0 ) + + self:SetPickupHeight() + self:SetDeployHeight() + + self:SetCombatRadius( CombatRadius ) + + return self +end + + +--- AI cargo +-- @param #AI_CARGO_DISPATCHER_APC self +-- @param Wrapper.Group#GROUP APC The APC carrier. +-- @param Core.Set#SET_CARGO CargoSet Cargo set. +-- @return AI.AI_Cargo_APC#AI_CARGO_DISPATCHER_APC AI cargo APC object. +function AI_CARGO_DISPATCHER_APC:AICargo( APC, CargoSet ) + + local aicargoapc=AI_CARGO_APC:New(APC, CargoSet, self.CombatRadius) + + aicargoapc:SetDeployOffRoad(self.deployOffroad, self.deployFormation) + aicargoapc:SetPickupOffRoad(self.pickupOffroad, self.pickupFormation) + + return aicargoapc +end + +--- Enable/Disable unboarding of cargo (infantry) when enemies are nearby (to help defend the carrier). +-- This is only valid for APCs and trucks etc, thus ground vehicles. +-- @param #AI_CARGO_DISPATCHER_APC self +-- @param #number CombatRadius Provide the combat radius to defend the carrier by unboarding the cargo when enemies are nearby. +-- When the combat radius is 0 (default), no defense will happen of the carrier. +-- When the combat radius is not provided, no defense will happen! +-- @return #AI_CARGO_DISPATCHER_APC +-- @usage +-- +-- -- Disembark the infantry when the carrier is under attack. +-- AICargoDispatcher:SetCombatRadius( 500 ) +-- +-- -- Keep the cargo in the carrier when the carrier is under attack. +-- AICargoDispatcher:SetCombatRadius( 0 ) +function AI_CARGO_DISPATCHER_APC:SetCombatRadius( CombatRadius ) + + self.CombatRadius = CombatRadius or 0 + + return self +end + +--- Set whether the carrier will *not* use roads to *pickup* and *deploy* the cargo. +-- @param #AI_CARGO_DISPATCHER_APC self +-- @param #boolean Offroad If true, carrier will not use roads. +-- @param #number Formation Offroad formation used. Default is `ENUMS.Formation.Vehicle.Offroad`. +-- @return #AI_CARGO_DISPATCHER_APC self +function AI_CARGO_DISPATCHER_APC:SetOffRoad(Offroad, Formation) + + self:SetPickupOffRoad(Offroad, Formation) + self:SetDeployOffRoad(Offroad, Formation) + + return self +end + +--- Set whether the carrier will *not* use roads to *pickup* the cargo. +-- @param #AI_CARGO_DISPATCHER_APC self +-- @param #boolean Offroad If true, carrier will not use roads. +-- @param #number Formation Offroad formation used. Default is `ENUMS.Formation.Vehicle.Offroad`. +-- @return #AI_CARGO_DISPATCHER_APC self +function AI_CARGO_DISPATCHER_APC:SetPickupOffRoad(Offroad, Formation) + + self.pickupOffroad=Offroad + self.pickupFormation=Formation or ENUMS.Formation.Vehicle.OffRoad + + return self +end + +--- Set whether the carrier will *not* use roads to *deploy* the cargo. +-- @param #AI_CARGO_DISPATCHER_APC self +-- @param #boolean Offroad If true, carrier will not use roads. +-- @param #number Formation Offroad formation used. Default is `ENUMS.Formation.Vehicle.Offroad`. +-- @return #AI_CARGO_DISPATCHER_APC self +function AI_CARGO_DISPATCHER_APC:SetDeployOffRoad(Offroad, Formation) + + self.deployOffroad=Offroad + self.deployFormation=Formation or ENUMS.Formation.Vehicle.OffRoad + + return self +end--- **AI** -- (2.4) - Models the intelligent transportation of infantry and other cargo using Helicopters. +-- +-- ## Features: +-- +-- * The helicopters will fly towards the pickup locations to pickup the cargo. +-- * The helicopters will fly towards the deploy zones to deploy the cargo. +-- * Precision deployment as well as randomized deployment within the deploy zones are possible. +-- * Helicopters will orbit the deploy zones when there is no space for landing until the deploy zone is free. +-- +-- === +-- +-- ## Test Missions: +-- +-- Test missions can be located on the main GITHUB site. +-- +-- [FlightControl-Master/MOOSE_MISSIONS/AID - AI Dispatching/AID-CGO - AI Cargo Dispatching/] +-- (https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/AID%20-%20AI%20Dispatching/AID-CGO%20-%20AI%20Cargo%20Dispatching) +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_Cargo_Dispatcher_Helicopter +-- @image AI_Cargo_Dispatching_For_Helicopters.JPG + +--- @type AI_CARGO_DISPATCHER_HELICOPTER +-- @extends AI.AI_Cargo_Dispatcher#AI_CARGO_DISPATCHER + + +--- A dynamic cargo handling capability for AI helicopter groups. +-- +-- Helicopters can be mobilized to intelligently transport infantry and other cargo within the simulation. +-- +-- +-- The AI_CARGO_DISPATCHER_HELICOPTER module is derived from the AI_CARGO_DISPATCHER module. +-- +-- ## Note! In order to fully understand the mechanisms of the AI_CARGO_DISPATCHER_HELICOPTER class, it is recommended that you first consult and READ the documentation of the @{AI.AI_Cargo_Dispatcher} module!!!** +-- +-- Especially to learn how to **Tailor the different cargo handling events**, this will be very useful! +-- +-- On top, the AI_CARGO_DISPATCHER_HELICOPTER class uses the @{Cargo.Cargo} capabilities within the MOOSE framework. +-- Also ensure that you fully understand how to declare and setup Cargo objects within the MOOSE framework before using this class. +-- CARGO derived objects must be declared within the mission to make the AI_CARGO_DISPATCHER_HELICOPTER object recognize the cargo. +-- +-- --- +-- +-- # 1. AI\_CARGO\_DISPATCHER\_HELICOPTER constructor. +-- +-- * @{#AI_CARGO_DISPATCHER\_HELICOPTER.New}(): Creates a new AI\_CARGO\_DISPATCHER\_HELICOPTER object. +-- +-- --- +-- +-- # 2. AI\_CARGO\_DISPATCHER\_HELICOPTER is a Finite State Machine. +-- +-- This section must be read as follows. Each of the rows indicate a state transition, triggered through an event, and with an ending state of the event was executed. +-- The first column is the **From** state, the second column the **Event**, and the third column the **To** state. +-- +-- So, each of the rows have the following structure. +-- +-- * **From** => **Event** => **To** +-- +-- Important to know is that an event can only be executed if the **current state** is the **From** state. +-- This, when an **Event** that is being triggered has a **From** state that is equal to the **Current** state of the state machine, the event will be executed, +-- and the resulting state will be the **To** state. +-- +-- These are the different possible state transitions of this state machine implementation: +-- +-- * Idle => Start => Monitoring +-- * Monitoring => Monitor => Monitoring +-- * Monitoring => Stop => Idle +-- +-- * Monitoring => Pickup => Monitoring +-- * Monitoring => Load => Monitoring +-- * Monitoring => Loading => Monitoring +-- * Monitoring => Loaded => Monitoring +-- * Monitoring => PickedUp => Monitoring +-- * Monitoring => Deploy => Monitoring +-- * Monitoring => Unload => Monitoring +-- * Monitoring => Unloaded => Monitoring +-- * Monitoring => Deployed => Monitoring +-- * Monitoring => Home => Monitoring +-- +-- +-- ## 2.1) AI_CARGO_DISPATCHER States. +-- +-- * **Monitoring**: The process is dispatching. +-- * **Idle**: The process is idle. +-- +-- ## 2.2) AI_CARGO_DISPATCHER Events. +-- +-- * **Start**: Start the transport process. +-- * **Stop**: Stop the transport process. +-- * **Monitor**: Monitor and take action. +-- +-- * **Pickup**: Pickup cargo. +-- * **Load**: Load the cargo. +-- * **Loading**: The dispatcher is coordinating the loading of a cargo. +-- * **Loaded**: Flag that the cargo is loaded. +-- * **PickedUp**: The dispatcher has loaded all requested cargo into the CarrierGroup. +-- * **Deploy**: Deploy cargo to a location. +-- * **Unload**: Unload the cargo. +-- * **Unloaded**: Flag that the cargo is unloaded. +-- * **Deployed**: All cargo is unloaded from the carriers in the group. +-- * **Home**: A Carrier is going home. +-- +-- ## 2.3) Enhance your mission scripts with **Tailored** Event Handling! +-- +-- Within your mission, you can capture these events when triggered, and tailor the events with your own code! +-- Check out the @{AI.AI_Cargo_Dispatcher#AI_CARGO_DISPATCHER} class at chapter 3 for details on the different event handlers that are available and how to use them. +-- +-- **There are a lot of templates available that allows you to quickly setup an event handler for a specific event type!** +-- +-- --- +-- +-- ## 3. Set the pickup parameters. +-- +-- Several parameters can be set to pickup cargo: +-- +-- * @{#AI_CARGO_DISPATCHER_HELICOPTER.SetPickupRadius}(): Sets or randomizes the pickup location for the helicopter around the cargo coordinate in a radius defined an outer and optional inner radius. +-- * @{#AI_CARGO_DISPATCHER_HELICOPTER.SetPickupSpeed}(): Set the speed or randomizes the speed in km/h to pickup the cargo. +-- * @{#AI_CARGO_DISPATCHER_HELICOPTER.SetPickupHeight}(): Set the height or randomizes the height in meters to pickup the cargo. +-- +-- --- +-- +-- ## 4. Set the deploy parameters. +-- +-- Several parameters can be set to deploy cargo: +-- +-- * @{#AI_CARGO_DISPATCHER_HELICOPTER.SetDeployRadius}(): Sets or randomizes the deploy location for the helicopter around the cargo coordinate in a radius defined an outer and an optional inner radius. +-- * @{#AI_CARGO_DISPATCHER_HELICOPTER.SetDeploySpeed}(): Set the speed or randomizes the speed in km/h to deploy the cargo. +-- * @{#AI_CARGO_DISPATCHER_HELICOPTER.SetDeployHeight}(): Set the height or randomizes the height in meters to deploy the cargo. +-- +-- --- +-- +-- ## 5. Set the home zone when there isn't any more cargo to pickup. +-- +-- A home zone can be specified to where the Helicopters will move when there isn't any cargo left for pickup. +-- Use @{#AI_CARGO_DISPATCHER_HELICOPTER.SetHomeZone}() to specify the home zone. +-- +-- If no home zone is specified, the helicopters will wait near the deploy zone for a new pickup command. +-- +-- === +-- +-- @field #AI_CARGO_DISPATCHER_HELICOPTER +AI_CARGO_DISPATCHER_HELICOPTER = { + ClassName = "AI_CARGO_DISPATCHER_HELICOPTER", +} + +--- Creates a new AI_CARGO_DISPATCHER_HELICOPTER object. +-- @param #AI_CARGO_DISPATCHER_HELICOPTER self +-- @param Core.Set#SET_GROUP HelicopterSet The set of @{Wrapper.Group#GROUP} objects of helicopters that will transport the cargo. +-- @param Core.Set#SET_CARGO CargoSet The set of @{Cargo.Cargo#CARGO} objects, which can be CARGO_GROUP, CARGO_CRATE, CARGO_SLINGLOAD objects. +-- @param Core.Set#SET_ZONE PickupZoneSet (optional) The set of pickup zones, which are used to where the cargo can be picked up by the APCs. If nil, then cargo can be picked up everywhere. +-- @param Core.Set#SET_ZONE DeployZoneSet The set of deploy zones, which are used to where the cargo will be deployed by the Helicopters. +-- @return #AI_CARGO_DISPATCHER_HELICOPTER +-- @usage +-- +-- -- An AI dispatcher object for a helicopter squadron, moving infantry from pickup zones to deploy zones. +-- +-- local SetCargoInfantry = SET_CARGO:New():FilterTypes( "Infantry" ):FilterStart() +-- local SetHelicopter = SET_GROUP:New():FilterPrefixes( "Helicopter" ):FilterStart() +-- local SetPickupZones = SET_ZONE:New():FilterPrefixes( "Pickup" ):FilterStart() +-- local SetDeployZones = SET_ZONE:New():FilterPrefixes( "Deploy" ):FilterStart() +-- +-- AICargoDispatcherHelicopter = AI_CARGO_DISPATCHER_HELICOPTER:New( SetHelicopter, SetCargoInfantry, SetPickupZones, SetDeployZones ) +-- AICargoDispatcherHelicopter:Start() +-- +function AI_CARGO_DISPATCHER_HELICOPTER:New( HelicopterSet, CargoSet, PickupZoneSet, DeployZoneSet ) + + local self = BASE:Inherit( self, AI_CARGO_DISPATCHER:New( HelicopterSet, CargoSet, PickupZoneSet, DeployZoneSet ) ) -- #AI_CARGO_DISPATCHER_HELICOPTER + + self:SetPickupSpeed( 350, 150 ) + self:SetDeploySpeed( 350, 150 ) + + self:SetPickupRadius( 40, 12 ) + self:SetDeployRadius( 40, 12 ) + + self:SetPickupHeight( 500, 200 ) + self:SetDeployHeight( 500, 200 ) + + return self +end + + +function AI_CARGO_DISPATCHER_HELICOPTER:AICargo( Helicopter, CargoSet ) + + local dispatcher = AI_CARGO_HELICOPTER:New( Helicopter, CargoSet ) + dispatcher:SetLandingSpeedAndHeight(27, 6) + return dispatcher + +end + +--- **AI** -- (R2.4) - Models the intelligent transportation of infantry and other cargo using Planes. +-- +-- ## Features: +-- +-- * The airplanes will fly towards the pickup airbases to pickup the cargo. +-- * The airplanes will fly towards the deploy airbases to deploy the cargo. +-- +-- === +-- +-- ## Test Missions: +-- +-- Test missions can be located on the main GITHUB site. +-- +-- [FlightControl-Master/MOOSE_MISSIONS/AID - AI Dispatching/AID-CGO - AI Cargo Dispatching/] +-- (https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/AID%20-%20AI%20Dispatching/AID-CGO%20-%20AI%20Cargo%20Dispatching) +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_Cargo_Dispatcher_Airplane +-- @image AI_Cargo_Dispatching_For_Airplanes.JPG + + +--- @type AI_CARGO_DISPATCHER_AIRPLANE +-- @extends AI.AI_Cargo_Dispatcher#AI_CARGO_DISPATCHER + + +--- Brings a dynamic cargo handling capability for AI groups. +-- +-- Airplanes can be mobilized to intelligently transport infantry and other cargo within the simulation. +-- +-- The AI_CARGO_DISPATCHER_AIRPLANE module is derived from the AI_CARGO_DISPATCHER module. +-- +-- ## Note! In order to fully understand the mechanisms of the AI_CARGO_DISPATCHER_AIRPLANE class, it is recommended that you first consult and READ the documentation of the @{AI.AI_Cargo_Dispatcher} module!!!** +-- +-- Especially to learn how to **Tailor the different cargo handling events**, this will be very useful! +-- +-- On top, the AI_CARGO_DISPATCHER_AIRPLANE class uses the @{Cargo.Cargo} capabilities within the MOOSE framework. +-- Also ensure that you fully understand how to declare and setup Cargo objects within the MOOSE framework before using this class. +-- CARGO derived objects must be declared within the mission to make the AI_CARGO_DISPATCHER_HELICOPTER object recognize the cargo. +-- +-- # 1) AI_CARGO_DISPATCHER_AIRPLANE constructor. +-- +-- * @{#AI_CARGO_DISPATCHER_AIRPLANE.New}(): Creates a new AI_CARGO_DISPATCHER_AIRPLANE object. +-- +-- --- +-- +-- # 2) AI_CARGO_DISPATCHER_AIRPLANE is a Finite State Machine. +-- +-- This section must be read as follows. Each of the rows indicate a state transition, triggered through an event, and with an ending state of the event was executed. +-- The first column is the **From** state, the second column the **Event**, and the third column the **To** state. +-- +-- So, each of the rows have the following structure. +-- +-- * **From** => **Event** => **To** +-- +-- Important to know is that an event can only be executed if the **current state** is the **From** state. +-- This, when an **Event** that is being triggered has a **From** state that is equal to the **Current** state of the state machine, the event will be executed, +-- and the resulting state will be the **To** state. +-- +-- These are the different possible state transitions of this state machine implementation: +-- +-- * Idle => Start => Monitoring +-- * Monitoring => Monitor => Monitoring +-- * Monitoring => Stop => Idle +-- +-- * Monitoring => Pickup => Monitoring +-- * Monitoring => Load => Monitoring +-- * Monitoring => Loading => Monitoring +-- * Monitoring => Loaded => Monitoring +-- * Monitoring => PickedUp => Monitoring +-- * Monitoring => Deploy => Monitoring +-- * Monitoring => Unload => Monitoring +-- * Monitoring => Unloaded => Monitoring +-- * Monitoring => Deployed => Monitoring +-- * Monitoring => Home => Monitoring +-- +-- +-- ## 2.1) AI_CARGO_DISPATCHER States. +-- +-- * **Monitoring**: The process is dispatching. +-- * **Idle**: The process is idle. +-- +-- ## 2.2) AI_CARGO_DISPATCHER Events. +-- +-- * **Start**: Start the transport process. +-- * **Stop**: Stop the transport process. +-- * **Monitor**: Monitor and take action. +-- +-- * **Pickup**: Pickup cargo. +-- * **Load**: Load the cargo. +-- * **Loading**: The dispatcher is coordinating the loading of a cargo. +-- * **Loaded**: Flag that the cargo is loaded. +-- * **PickedUp**: The dispatcher has loaded all requested cargo into the CarrierGroup. +-- * **Deploy**: Deploy cargo to a location. +-- * **Unload**: Unload the cargo. +-- * **Unloaded**: Flag that the cargo is unloaded. +-- * **Deployed**: All cargo is unloaded from the carriers in the group. +-- * **Home**: A Carrier is going home. +-- +-- ## 2.3) Enhance your mission scripts with **Tailored** Event Handling! +-- +-- Within your mission, you can capture these events when triggered, and tailor the events with your own code! +-- Check out the @{AI.AI_Cargo_Dispatcher#AI_CARGO_DISPATCHER} class at chapter 3 for details on the different event handlers that are available and how to use them. +-- +-- **There are a lot of templates available that allows you to quickly setup an event handler for a specific event type!** +-- +-- +-- +-- @field #AI_CARGO_DISPATCHER_AIRPLANE +AI_CARGO_DISPATCHER_AIRPLANE = { + ClassName = "AI_CARGO_DISPATCHER_AIRPLANE", +} + +--- Creates a new AI_CARGO_DISPATCHER_AIRPLANE object. +-- @param #AI_CARGO_DISPATCHER_AIRPLANE self +-- @param Core.Set#SET_GROUP AirplaneSet The set of @{Wrapper.Group#GROUP} objects of airplanes that will transport the cargo. +-- @param Core.Set#SET_CARGO CargoSet The set of @{Cargo.Cargo#CARGO} objects, which can be CARGO_GROUP, CARGO_CRATE, CARGO_SLINGLOAD objects. +-- @param Core.Zone#SET_ZONE PickupZoneSet The set of zone airbases where the cargo has to be picked up. +-- @param Core.Zone#SET_ZONE DeployZoneSet The set of zone airbases where the cargo is deployed. Choice for each cargo is random. +-- @return #AI_CARGO_DISPATCHER_AIRPLANE self +-- @usage +-- +-- -- An AI dispatcher object for an airplane squadron, moving infantry and vehicles from pickup airbases to deploy airbases. +-- +-- local CargoInfantrySet = SET_CARGO:New():FilterTypes( "Infantry" ):FilterStart() +-- local AirplanesSet = SET_GROUP:New():FilterPrefixes( "Airplane" ):FilterStart() +-- local PickupZoneSet = SET_ZONE:New() +-- local DeployZoneSet = SET_ZONE:New() +-- +-- PickupZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Gudauta ) ) +-- DeployZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Sochi_Adler ) ) +-- DeployZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Maykop_Khanskaya ) ) +-- DeployZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Mineralnye_Vody ) ) +-- DeployZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Vaziani ) ) +-- +-- AICargoDispatcherAirplanes = AI_CARGO_DISPATCHER_AIRPLANE:New( AirplanesSet, CargoInfantrySet, PickupZoneSet, DeployZoneSet ) +-- AICargoDispatcherAirplanes:Start() +-- +function AI_CARGO_DISPATCHER_AIRPLANE:New( AirplaneSet, CargoSet, PickupZoneSet, DeployZoneSet ) + + local self = BASE:Inherit( self, AI_CARGO_DISPATCHER:New( AirplaneSet, CargoSet, PickupZoneSet, DeployZoneSet ) ) -- #AI_CARGO_DISPATCHER_AIRPLANE + + self:SetPickupSpeed( 1200, 600 ) + self:SetDeploySpeed( 1200, 600 ) + + self:SetPickupRadius( 0, 0 ) + self:SetDeployRadius( 0, 0 ) + + self:SetPickupHeight( 8000, 6000 ) + self:SetDeployHeight( 8000, 6000 ) + + self:SetMonitorTimeInterval( 600 ) + + return self +end + +function AI_CARGO_DISPATCHER_AIRPLANE:AICargo( Airplane, CargoSet ) + + return AI_CARGO_AIRPLANE:New( Airplane, CargoSet ) +end +--- **AI** -- (2.5.1) - Models the intelligent transportation of infantry and other cargo using Ships +-- +-- ## Features: +-- +-- * Transport cargo to various deploy zones using naval vehicles. +-- * Various @{Cargo.Cargo#CARGO} types can be transported, including infantry, vehicles, and crates. +-- * Define a deploy zone of various types to determine the destination of the cargo. +-- * Ships will follow shipping lanes as defined in the Mission Editor. +-- * Multiple ships can transport multiple cargo as a single group. +-- +-- === +-- +-- ## Test Missions: +-- +-- NEED TO DO +-- +-- === +-- +-- ### Author: **acrojason** (derived from AI_Cargo_Dispatcher_APC by FlightControl) +-- +-- === +-- +-- @module AI.AI_Cargo_Dispatcher_Ship +-- @image AI_Cargo_Dispatcher.JPG + +--- @type AI_CARGO_DISPATCHER_SHIP +-- @extends AI.AI_Cargo_Dispatcher#AI_CARGO_DISPATCHER + + +--- A dynamic cargo transportation capability for AI groups. +-- +-- Naval vessels can be mobilized to semi-intelligently transport cargo within the simulation. +-- +-- The AI_CARGO_DISPATCHER_SHIP module is derived from the AI_CARGO_DISPATCHER module. +-- +-- ## Note! In order to fully understand the mechanisms of the AI_CARGO_DISPATCHER_SHIP class, it is recommended that you first consult and READ the documentation of the @{AI.AI_Cargo_Dispatcher} module!!! +-- +-- This will be particularly helpful in order to determine how to **Tailor the different cargo handling events**. +-- +-- The AI_CARGO_DISPATCHER_SHIP class uses the @{Cargo.Cargo} capabilities within the MOOSE framwork. +-- Also ensure that you fully understand how to declare and setup Cargo objects within the MOOSE framework before using this class. +-- CARGO derived objects must generally be declared within the mission to make the AI_CARGO_DISPATCHER_SHIP object recognize the cargo. +-- +-- +-- # 1) AI_CARGO_DISPATCHER_SHIP constructor. +-- +-- * @{AI_CARGO_DISPATCHER_SHIP.New}(): Creates a new AI_CARGO_DISPATCHER_SHIP object. +-- +-- --- +-- +-- # 2) AI_CARGO_DISPATCHER_SHIP is a Finite State Machine. +-- +-- This section must be read as follows... Each of the rows indicate a state transition, triggered through an event, and with an ending state of the event was executed. +-- The first column is the **From** state, the second column the **Event**, and the third column the **To** state. +-- +-- So, each of the rows have the following structure. +-- +-- * **From** => **Event** => **To** +-- +-- Important to know is that an event can only be executed if the **current state** is the **From** state. +-- This, when an **Event** that is being triggered has a **From** state that is equal to the **Current** state of the state machine, the event will be executed, +-- and the resulting state will be the **To** state. +-- +-- These are the different possible state transitions of this state machine implementation: +-- +-- * Idle => Start => Monitoring +-- * Monitoring => Monitor => Monitoring +-- * Monitoring => Stop => Idle +-- +-- * Monitoring => Pickup => Monitoring +-- * Monitoring => Load => Monitoring +-- * Monitoring => Loading => Monitoring +-- * Monitoring => Loaded => Monitoring +-- * Monitoring => PickedUp => Monitoring +-- * Monitoring => Deploy => Monitoring +-- * Monitoring => Unload => Monitoring +-- * Monitoring => Unloaded => Monitoring +-- * Monitoring => Deployed => Monitoring +-- * Monitoring => Home => Monitoring +-- +-- +-- ## 2.1) AI_CARGO_DISPATCHER States. +-- +-- * **Monitoring**: The process is dispatching. +-- * **Idle**: The process is idle. +-- +-- ## 2.2) AI_CARGO_DISPATCHER Events. +-- +-- * **Start**: Start the transport process. +-- * **Stop**: Stop the transport process. +-- * **Monitor**: Monitor and take action. +-- +-- * **Pickup**: Pickup cargo. +-- * **Load**: Load the cargo. +-- * **Loading**: The dispatcher is coordinating the loading of a cargo. +-- * **Loaded**: Flag that the cargo is loaded. +-- * **PickedUp**: The dispatcher has loaded all requested cargo into the CarrierGroup. +-- * **Deploy**: Deploy cargo to a location. +-- * **Unload**: Unload the cargo. +-- * **Unloaded**: Flag that the cargo is unloaded. +-- * **Deployed**: All cargo is unloaded from the carriers in the group. +-- * **Home**: A Carrier is going home. +-- +-- ## 2.3) Enhance your mission scripts with **Tailored** Event Handling! +-- +-- Within your mission, you can capture these events when triggered, and tailor the events with your own code! +-- Check out the @{AI.AI_Cargo_Dispatcher#AI_CARGO_DISPATCHER} class at chapter 3 for details on the different event handlers that are available and how to use them. +-- +-- **There are a lot of templates available that allows you to quickly setup an event handler for a specific event type!** +-- +-- --- +-- +-- # 3) Set the pickup parameters. +-- +-- Several parameters can be set to pickup cargo: +-- +-- * @{#AI_CARGO_DISPATCHER_SHIP.SetPickupRadius}(): Sets or randomizes the pickup location for the Ship around the cargo coordinate in a radius defined an outer and optional inner radius. +-- * @{#AI_CARGO_DISPATCHER_SHIP.SetPickupSpeed}(): Set the speed or randomizes the speed in km/h to pickup the cargo. +-- +-- # 4) Set the deploy parameters. +-- +-- Several parameters can be set to deploy cargo: +-- +-- * @{#AI_CARGO_DISPATCHER_SHIP.SetDeployRadius}(): Sets or randomizes the deploy location for the Ship around the cargo coordinate in a radius defined an outer and an optional inner radius. +-- * @{#AI_CARGO_DISPATCHER_SHIP.SetDeploySpeed}(): Set the speed or randomizes the speed in km/h to deploy the cargo. +-- +-- # 5) Set the home zone when there isn't any more cargo to pickup. +-- +-- A home zone can be specified to where the Ship will move when there isn't any cargo left for pickup. +-- Use @{#AI_CARGO_DISPATCHER_SHIP.SetHomeZone}() to specify the home zone. +-- +-- If no home zone is specified, the Ship will wait near the deploy zone for a new pickup command. +-- +-- === +-- +-- @field #AI_CARGO_DISPATCHER_SHIP +AI_CARGO_DISPATCHER_SHIP = { + ClassName = "AI_CARGO_DISPATCHER_SHIP" + } + +--- Creates a new AI_CARGO_DISPATCHER_SHIP object. +-- @param #AI_CARGO_DISPATCHER_SHIP self +-- @param Core.Set#SET_GROUP ShipSet The set of @{Wrapper.Group#GROUP} objects of Ships that will transport the cargo +-- @param Core.Set#SET_CARGO CargoSet The set of @{Cargo.Cargo#CARGO} objects, which can be CARGO_GROUP, CARGO_CRATE, or CARGO_SLINGLOAD objects. +-- @param Core.Set#SET_ZONE PickupZoneSet The set of pickup zones which are used to determine from where the cargo can be picked up by the Ship. +-- @param Core.Set#SET_ZONE DeployZoneSet The set of deploy zones which determine where the cargo will be deployed by the Ship. +-- @param #table ShippingLane Table containing list of Shipping Lanes to be used +-- @return #AI_CARGO_DISPATCHER_SHIP +-- @usage +-- +-- -- An AI dispatcher object for a naval group, moving cargo from pickup zones to deploy zones via a predetermined Shipping Lane +-- +-- local SetCargoInfantry = SET_CARGO:New():FilterTypes( "Infantry" ):FilterStart() +-- local SetShip = SET_GROUP:New():FilterPrefixes( "Ship" ):FilterStart() +-- local SetPickupZones = SET_ZONE:New():FilterPrefixes( "Pickup" ):FilterStart() +-- local SetDeployZones = SET_ZONE:New():FilterPrefixes( "Deploy" ):FilterStart() +-- NEED MORE THOUGHT - ShippingLane is part of Warehouse....... +-- local ShippingLane = GROUP:New():FilterPrefixes( "ShippingLane" ):FilterStart() +-- +-- AICargoDispatcherShip = AI_CARGO_DISPATCHER_SHIP:New( SetShip, SetCargoInfantry, SetPickupZones, SetDeployZones, ShippingLane ) +-- AICargoDispatcherShip:Start() +-- +function AI_CARGO_DISPATCHER_SHIP:New( ShipSet, CargoSet, PickupZoneSet, DeployZoneSet, ShippingLane ) + + local self = BASE:Inherit( self, AI_CARGO_DISPATCHER:New( ShipSet, CargoSet, PickupZoneSet, DeployZoneSet ) ) + + self:SetPickupSpeed( 60, 10 ) + self:SetDeploySpeed( 60, 10 ) + + self:SetPickupRadius( 500, 6000 ) + self:SetDeployRadius( 500, 6000 ) + + self:SetPickupHeight( 0, 0 ) + self:SetDeployHeight( 0, 0 ) + + self:SetShippingLane( ShippingLane ) + + self:SetMonitorTimeInterval( 600 ) + + return self +end + +function AI_CARGO_DISPATCHER_SHIP:SetShippingLane( ShippingLane ) + self.ShippingLane = ShippingLane + + return self + +end + +function AI_CARGO_DISPATCHER_SHIP:AICargo( Ship, CargoSet ) + + return AI_CARGO_SHIP:New( Ship, CargoSet, 0, self.ShippingLane ) +end--- (SP) (MP) (FSM) Accept or reject process for player (task) assignments. +-- +-- === +-- +-- # @{#ACT_ASSIGN} FSM template class, extends @{Core.Fsm#FSM_PROCESS} +-- +-- ## ACT_ASSIGN state machine: +-- +-- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur. +-- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below. +-- Each derived class follows exactly the same process, using the same events and following the same state transitions, +-- but will have **different implementation behaviour** upon each event or state transition. +-- +-- ### ACT_ASSIGN **Events**: +-- +-- These are the events defined in this class: +-- +-- * **Start**: Start the tasking acceptance process. +-- * **Assign**: Assign the task. +-- * **Reject**: Reject the task.. +-- +-- ### ACT_ASSIGN **Event methods**: +-- +-- Event methods are available (dynamically allocated by the state machine), that accomodate for state transitions occurring in the process. +-- There are two types of event methods, which you can use to influence the normal mechanisms in the state machine: +-- +-- * **Immediate**: The event method has exactly the name of the event. +-- * **Delayed**: The event method starts with a __ + the name of the event. The first parameter of the event method is a number value, expressing the delay in seconds when the event will be executed. +-- +-- ### ACT_ASSIGN **States**: +-- +-- * **UnAssigned**: The player has not accepted the task. +-- * **Assigned (*)**: The player has accepted the task. +-- * **Rejected (*)**: The player has not accepted the task. +-- * **Waiting**: The process is awaiting player feedback. +-- * **Failed (*)**: The process has failed. +-- +-- (*) End states of the process. +-- +-- ### ACT_ASSIGN state transition methods: +-- +-- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. +-- There are 2 moments when state transition methods will be called by the state machine: +-- +-- * **Before** the state transition. +-- The state transition method needs to start with the name **OnBefore + the name of the state**. +-- If the state transition method returns false, then the processing of the state transition will not be done! +-- If you want to change the behaviour of the AIControllable at this event, return false, +-- but then you'll need to specify your own logic using the AIControllable! +-- +-- * **After** the state transition. +-- The state transition method needs to start with the name **OnAfter + the name of the state**. +-- These state transition methods need to provide a return value, which is specified at the function description. +-- +-- === +-- +-- # 1) @{#ACT_ASSIGN_ACCEPT} class, extends @{Core.Fsm.Assign#ACT_ASSIGN} +-- +-- The ACT_ASSIGN_ACCEPT class accepts by default a task for a player. No player intervention is allowed to reject the task. +-- +-- ## 1.1) ACT_ASSIGN_ACCEPT constructor: +-- +-- * @{#ACT_ASSIGN_ACCEPT.New}(): Creates a new ACT_ASSIGN_ACCEPT object. +-- +-- === +-- +-- # 2) @{#ACT_ASSIGN_MENU_ACCEPT} class, extends @{Core.Fsm.Assign#ACT_ASSIGN} +-- +-- The ACT_ASSIGN_MENU_ACCEPT class accepts a task when the player accepts the task through an added menu option. +-- This assignment type is useful to conditionally allow the player to choose whether or not he would accept the task. +-- The assignment type also allows to reject the task. +-- +-- ## 2.1) ACT_ASSIGN_MENU_ACCEPT constructor: +-- ----------------------------------------- +-- +-- * @{#ACT_ASSIGN_MENU_ACCEPT.New}(): Creates a new ACT_ASSIGN_MENU_ACCEPT object. +-- +-- === +-- +-- @module Actions.Assign +-- @image MOOSE.JPG + + +do -- ACT_ASSIGN + + --- ACT_ASSIGN class + -- @type ACT_ASSIGN + -- @field Tasking.Task#TASK Task + -- @field Wrapper.Unit#UNIT ProcessUnit + -- @field Core.Zone#ZONE_BASE TargetZone + -- @extends Core.Fsm#FSM_PROCESS + ACT_ASSIGN = { + ClassName = "ACT_ASSIGN", + } + + + --- Creates a new task assignment state machine. The process will accept the task by default, no player intervention accepted. + -- @param #ACT_ASSIGN self + -- @return #ACT_ASSIGN The task acceptance process. + function ACT_ASSIGN:New() + + -- Inherits from BASE + local self = BASE:Inherit( self, FSM_PROCESS:New( "ACT_ASSIGN" ) ) -- Core.Fsm#FSM_PROCESS + + self:AddTransition( "UnAssigned", "Start", "Waiting" ) + self:AddTransition( "Waiting", "Assign", "Assigned" ) + self:AddTransition( "Waiting", "Reject", "Rejected" ) + self:AddTransition( "*", "Fail", "Failed" ) + + self:AddEndState( "Assigned" ) + self:AddEndState( "Rejected" ) + self:AddEndState( "Failed" ) + + self:SetStartState( "UnAssigned" ) + + return self + end + +end -- ACT_ASSIGN + + + +do -- ACT_ASSIGN_ACCEPT + + --- ACT_ASSIGN_ACCEPT class + -- @type ACT_ASSIGN_ACCEPT + -- @field Tasking.Task#TASK Task + -- @field Wrapper.Unit#UNIT ProcessUnit + -- @field Core.Zone#ZONE_BASE TargetZone + -- @extends #ACT_ASSIGN + ACT_ASSIGN_ACCEPT = { + ClassName = "ACT_ASSIGN_ACCEPT", + } + + + --- Creates a new task assignment state machine. The process will accept the task by default, no player intervention accepted. + -- @param #ACT_ASSIGN_ACCEPT self + -- @param #string TaskBriefing + function ACT_ASSIGN_ACCEPT:New( TaskBriefing ) + + local self = BASE:Inherit( self, ACT_ASSIGN:New() ) -- #ACT_ASSIGN_ACCEPT + + self.TaskBriefing = TaskBriefing + + return self + end + + function ACT_ASSIGN_ACCEPT:Init( FsmAssign ) + + self.TaskBriefing = FsmAssign.TaskBriefing + end + + --- StateMachine callback function + -- @param #ACT_ASSIGN_ACCEPT self + -- @param Wrapper.Unit#UNIT ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ASSIGN_ACCEPT:onafterStart( ProcessUnit, Task, From, Event, To ) + + self:__Assign( 1 ) + end + + --- StateMachine callback function + -- @param #ACT_ASSIGN_ACCEPT self + -- @param Wrapper.Unit#UNIT ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ASSIGN_ACCEPT:onenterAssigned( ProcessUnit, Task, From, Event, To, TaskGroup ) + + self.Task:Assign( ProcessUnit, ProcessUnit:GetPlayerName() ) + end + +end -- ACT_ASSIGN_ACCEPT + + +do -- ACT_ASSIGN_MENU_ACCEPT + + --- ACT_ASSIGN_MENU_ACCEPT class + -- @type ACT_ASSIGN_MENU_ACCEPT + -- @field Tasking.Task#TASK Task + -- @field Wrapper.Unit#UNIT ProcessUnit + -- @field Core.Zone#ZONE_BASE TargetZone + -- @extends #ACT_ASSIGN + ACT_ASSIGN_MENU_ACCEPT = { + ClassName = "ACT_ASSIGN_MENU_ACCEPT", + } + + --- Init. + -- @param #ACT_ASSIGN_MENU_ACCEPT self + -- @param #string TaskBriefing + -- @return #ACT_ASSIGN_MENU_ACCEPT self + function ACT_ASSIGN_MENU_ACCEPT:New( TaskBriefing ) + + -- Inherits from BASE + local self = BASE:Inherit( self, ACT_ASSIGN:New() ) -- #ACT_ASSIGN_MENU_ACCEPT + + self.TaskBriefing = TaskBriefing + + return self + end + + + --- Creates a new task assignment state machine. The process will request from the menu if it accepts the task, if not, the unit is removed from the simulator. + -- @param #ACT_ASSIGN_MENU_ACCEPT self + -- @param #string TaskBriefing + -- @return #ACT_ASSIGN_MENU_ACCEPT self + function ACT_ASSIGN_MENU_ACCEPT:Init( TaskBriefing ) + + self.TaskBriefing = TaskBriefing + + return self + end + + --- StateMachine callback function + -- @param #ACT_ASSIGN_MENU_ACCEPT self + -- @param Wrapper.Unit#UNIT ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ASSIGN_MENU_ACCEPT:onafterStart( ProcessUnit, Task, From, Event, To ) + + self:GetCommandCenter():MessageToGroup( "Task " .. self.Task:GetName() .. " has been assigned to you and your group!\nRead the briefing and use the Radio Menu (F10) / Task ... CONFIRMATION menu to accept or reject the task.\nYou have 2 minutes to accept, or the task assignment will be cancelled!", ProcessUnit:GetGroup(), 120 ) + + local TaskGroup = ProcessUnit:GetGroup() + + self.Menu = MENU_GROUP:New( TaskGroup, "Task " .. self.Task:GetName() .. " CONFIRMATION" ) + self.MenuAcceptTask = MENU_GROUP_COMMAND:New( TaskGroup, "Accept task " .. self.Task:GetName(), self.Menu, self.MenuAssign, self, TaskGroup ) + self.MenuRejectTask = MENU_GROUP_COMMAND:New( TaskGroup, "Reject task " .. self.Task:GetName(), self.Menu, self.MenuReject, self, TaskGroup ) + + self:__Reject( 120, TaskGroup ) + end + + --- Menu function. + -- @param #ACT_ASSIGN_MENU_ACCEPT self + function ACT_ASSIGN_MENU_ACCEPT:MenuAssign( TaskGroup ) + + self:__Assign( -1, TaskGroup ) + end + + --- Menu function. + -- @param #ACT_ASSIGN_MENU_ACCEPT self + function ACT_ASSIGN_MENU_ACCEPT:MenuReject( TaskGroup ) + + self:__Reject( -1, TaskGroup ) + end + + --- StateMachine callback function + -- @param #ACT_ASSIGN_MENU_ACCEPT self + -- @param Wrapper.Unit#UNIT ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ASSIGN_MENU_ACCEPT:onafterAssign( ProcessUnit, Task, From, Event, To, TaskGroup ) + + self.Menu:Remove() + end + + --- StateMachine callback function + -- @param #ACT_ASSIGN_MENU_ACCEPT self + -- @param Wrapper.Unit#UNIT ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ASSIGN_MENU_ACCEPT:onafterReject( ProcessUnit, Task, From, Event, To, TaskGroup ) + self:F( { TaskGroup = TaskGroup } ) + + self.Menu:Remove() + --TODO: need to resolve this problem ... it has to do with the events ... + --self.Task:UnAssignFromUnit( ProcessUnit )needs to become a callback funtion call upon the event + self.Task:RejectGroup( TaskGroup ) + end + + --- StateMachine callback function + -- @param #ACT_ASSIGN_ACCEPT self + -- @param Wrapper.Unit#UNIT ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ASSIGN_MENU_ACCEPT:onenterAssigned( ProcessUnit, Task, From, Event, To, TaskGroup ) + + --self.Task:AssignToGroup( TaskGroup ) + self.Task:Assign( ProcessUnit, ProcessUnit:GetPlayerName() ) + end + +end -- ACT_ASSIGN_MENU_ACCEPT +--- (SP) (MP) (FSM) Route AI or players through waypoints or to zones. +-- +-- === +-- +-- # @{#ACT_ROUTE} FSM class, extends @{Core.Fsm#FSM_PROCESS} +-- +-- ## ACT_ROUTE state machine: +-- +-- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur. +-- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below. +-- Each derived class follows exactly the same process, using the same events and following the same state transitions, +-- but will have **different implementation behaviour** upon each event or state transition. +-- +-- ### ACT_ROUTE **Events**: +-- +-- These are the events defined in this class: +-- +-- * **Start**: The process is started. The process will go into the Report state. +-- * **Report**: The process is reporting to the player the route to be followed. +-- * **Route**: The process is routing the controllable. +-- * **Pause**: The process is pausing the route of the controllable. +-- * **Arrive**: The controllable has arrived at a route point. +-- * **More**: There are more route points that need to be followed. The process will go back into the Report state. +-- * **NoMore**: There are no more route points that need to be followed. The process will go into the Success state. +-- +-- ### ACT_ROUTE **Event methods**: +-- +-- Event methods are available (dynamically allocated by the state machine), that accomodate for state transitions occurring in the process. +-- There are two types of event methods, which you can use to influence the normal mechanisms in the state machine: +-- +-- * **Immediate**: The event method has exactly the name of the event. +-- * **Delayed**: The event method starts with a __ + the name of the event. The first parameter of the event method is a number value, expressing the delay in seconds when the event will be executed. +-- +-- ### ACT_ROUTE **States**: +-- +-- * **None**: The controllable did not receive route commands. +-- * **Arrived (*)**: The controllable has arrived at a route point. +-- * **Aborted (*)**: The controllable has aborted the route path. +-- * **Routing**: The controllable is understay to the route point. +-- * **Pausing**: The process is pausing the routing. AI air will go into hover, AI ground will stop moving. Players can fly around. +-- * **Success (*)**: All route points were reached. +-- * **Failed (*)**: The process has failed. +-- +-- (*) End states of the process. +-- +-- ### ACT_ROUTE state transition methods: +-- +-- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. +-- There are 2 moments when state transition methods will be called by the state machine: +-- +-- * **Before** the state transition. +-- The state transition method needs to start with the name **OnBefore + the name of the state**. +-- If the state transition method returns false, then the processing of the state transition will not be done! +-- If you want to change the behaviour of the AIControllable at this event, return false, +-- but then you'll need to specify your own logic using the AIControllable! +-- +-- * **After** the state transition. +-- The state transition method needs to start with the name **OnAfter + the name of the state**. +-- These state transition methods need to provide a return value, which is specified at the function description. +-- +-- === +-- +-- # 1) @{#ACT_ROUTE_ZONE} class, extends @{Core.Fsm.Route#ACT_ROUTE} +-- +-- The ACT_ROUTE_ZONE class implements the core functions to route an AIR @{Wrapper.Controllable} player @{Wrapper.Unit} to a @{Zone}. +-- The player receives on perioding times messages with the coordinates of the route to follow. +-- Upon arrival at the zone, a confirmation of arrival is sent, and the process will be ended. +-- +-- # 1.1) ACT_ROUTE_ZONE constructor: +-- +-- * @{#ACT_ROUTE_ZONE.New}(): Creates a new ACT_ROUTE_ZONE object. +-- +-- === +-- +-- @module Actions.Route +-- @image MOOSE.JPG + + +do -- ACT_ROUTE + + --- ACT_ROUTE class + -- @type ACT_ROUTE + -- @field Tasking.Task#TASK TASK + -- @field Wrapper.Unit#UNIT ProcessUnit + -- @field Core.Zone#ZONE_BASE Zone + -- @field Core.Point#COORDINATE Coordinate + -- @extends Core.Fsm#FSM_PROCESS + ACT_ROUTE = { + ClassName = "ACT_ROUTE", + } + + + --- Creates a new routing state machine. The process will route a CLIENT to a ZONE until the CLIENT is within that ZONE. + -- @param #ACT_ROUTE self + -- @return #ACT_ROUTE self + function ACT_ROUTE:New() + + -- Inherits from BASE + local self = BASE:Inherit( self, FSM_PROCESS:New( "ACT_ROUTE" ) ) -- Core.Fsm#FSM_PROCESS + + self:AddTransition( "*", "Reset", "None" ) + self:AddTransition( "None", "Start", "Routing" ) + self:AddTransition( "*", "Report", "*" ) + self:AddTransition( "Routing", "Route", "Routing" ) + self:AddTransition( "Routing", "Pause", "Pausing" ) + self:AddTransition( "Routing", "Arrive", "Arrived" ) + self:AddTransition( "*", "Cancel", "Cancelled" ) + self:AddTransition( "Arrived", "Success", "Success" ) + self:AddTransition( "*", "Fail", "Failed" ) + self:AddTransition( "", "", "" ) + self:AddTransition( "", "", "" ) + + self:AddEndState( "Arrived" ) + self:AddEndState( "Failed" ) + self:AddEndState( "Cancelled" ) + + self:SetStartState( "None" ) + + self:SetRouteMode( "C" ) + + return self + end + + --- Set a Cancel Menu item. + -- @param #ACT_ROUTE self + -- @return #ACT_ROUTE + function ACT_ROUTE:SetMenuCancel( MenuGroup, MenuText, ParentMenu, MenuTime, MenuTag ) + + self.CancelMenuGroupCommand = MENU_GROUP_COMMAND:New( + MenuGroup, + MenuText, + ParentMenu, + self.MenuCancel, + self + ):SetTime( MenuTime ):SetTag( MenuTag ) + + ParentMenu:SetTime( MenuTime ) + + ParentMenu:Remove( MenuTime, MenuTag ) + + return self + end + + --- Set the route mode. + -- There are 2 route modes supported: + -- + -- * SetRouteMode( "B" ): Route mode is Bearing and Range. + -- * SetRouteMode( "C" ): Route mode is LL or MGRS according coordinate system setup. + -- + -- @param #ACT_ROUTE self + -- @return #ACT_ROUTE + function ACT_ROUTE:SetRouteMode( RouteMode ) + + self.RouteMode = RouteMode + + return self + end + + --- Get the routing text to be displayed. + -- The route mode determines the text displayed. + -- @param #ACT_ROUTE self + -- @param Wrapper.Unit#UNIT Controllable + -- @return #string + function ACT_ROUTE:GetRouteText( Controllable ) + + local RouteText = "" + + local Coordinate = nil -- Core.Point#COORDINATE + + if self.Coordinate then + Coordinate = self.Coordinate + end + + if self.Zone then + Coordinate = self.Zone:GetPointVec3( self.Altitude ) + Coordinate:SetHeading( self.Heading ) + end + + + local Task = self:GetTask() -- This is to dermine that the coordinates are for a specific task mode (A2A or A2G). + local CC = self:GetTask():GetMission():GetCommandCenter() + if CC then + if CC:IsModeWWII() then + -- Find closest reference point to the target. + local ShortestDistance = 0 + local ShortestReferencePoint = nil + local ShortestReferenceName = "" + self:F( { CC.ReferencePoints } ) + for ZoneName, Zone in pairs( CC.ReferencePoints ) do + self:F( { ZoneName = ZoneName } ) + local Zone = Zone -- Core.Zone#ZONE + local ZoneCoord = Zone:GetCoordinate() + local ZoneDistance = ZoneCoord:Get2DDistance( Coordinate ) + self:F( { ShortestDistance, ShortestReferenceName } ) + if ShortestDistance == 0 or ZoneDistance < ShortestDistance then + ShortestDistance = ZoneDistance + ShortestReferencePoint = ZoneCoord + ShortestReferenceName = CC.ReferenceNames[ZoneName] + end + end + if ShortestReferencePoint then + RouteText = Coordinate:ToStringFromRP( ShortestReferencePoint, ShortestReferenceName, Controllable ) + end + else + RouteText = Coordinate:ToString( Controllable, nil, Task ) + end + end + + return RouteText + end + + + function ACT_ROUTE:MenuCancel() + self:F("Cancelled") + self.CancelMenuGroupCommand:Remove() + self:__Cancel( 1 ) + end + + --- Task Events + + --- StateMachine callback function + -- @param #ACT_ROUTE self + -- @param Wrapper.Unit#UNIT ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ROUTE:onafterStart( ProcessUnit, From, Event, To ) + + + self:__Route( 1 ) + end + + --- Check if the controllable has arrived. + -- @param #ACT_ROUTE self + -- @param Wrapper.Unit#UNIT ProcessUnit + -- @return #boolean + function ACT_ROUTE:onfuncHasArrived( ProcessUnit ) + return false + end + + --- StateMachine callback function + -- @param #ACT_ROUTE self + -- @param Wrapper.Unit#UNIT ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ROUTE:onbeforeRoute( ProcessUnit, From, Event, To ) + + if ProcessUnit:IsAlive() then + local HasArrived = self:onfuncHasArrived( ProcessUnit ) -- Polymorphic + if self.DisplayCount >= self.DisplayInterval then + self:T( { HasArrived = HasArrived } ) + if not HasArrived then + self:Report() + end + self.DisplayCount = 1 + else + self.DisplayCount = self.DisplayCount + 1 + end + + if HasArrived then + self:__Arrive( 1 ) + else + self:__Route( 1 ) + end + + return HasArrived -- if false, then the event will not be executed... + end + + return false + + end + +end -- ACT_ROUTE + + +do -- ACT_ROUTE_POINT + + --- ACT_ROUTE_POINT class + -- @type ACT_ROUTE_POINT + -- @field Tasking.Task#TASK TASK + -- @extends #ACT_ROUTE + ACT_ROUTE_POINT = { + ClassName = "ACT_ROUTE_POINT", + } + + + --- Creates a new routing state machine. + -- The task will route a controllable to a Coordinate until the controllable is within the Range. + -- @param #ACT_ROUTE_POINT self + -- @param Core.Point#COORDINATE The Coordinate to Target. + -- @param #number Range The Distance to Target. + -- @param Core.Zone#ZONE_BASE Zone + function ACT_ROUTE_POINT:New( Coordinate, Range ) + local self = BASE:Inherit( self, ACT_ROUTE:New() ) -- #ACT_ROUTE_POINT + + self.Coordinate = Coordinate + self.Range = Range or 0 + + self.DisplayInterval = 30 + self.DisplayCount = 30 + self.DisplayMessage = true + self.DisplayTime = 10 -- 10 seconds is the default + + return self + end + + --- Creates a new routing state machine. + -- The task will route a controllable to a Coordinate until the controllable is within the Range. + -- @param #ACT_ROUTE_POINT self + function ACT_ROUTE_POINT:Init( FsmRoute ) + + self.Coordinate = FsmRoute.Coordinate + self.Range = FsmRoute.Range or 0 + + self.DisplayInterval = 30 + self.DisplayCount = 30 + self.DisplayMessage = true + self.DisplayTime = 10 -- 10 seconds is the default + self:SetStartState("None") + end + + --- Set Coordinate + -- @param #ACT_ROUTE_POINT self + -- @param Core.Point#COORDINATE Coordinate The Coordinate to route to. + function ACT_ROUTE_POINT:SetCoordinate( Coordinate ) + self:F2( { Coordinate } ) + self.Coordinate = Coordinate + end + + --- Get Coordinate + -- @param #ACT_ROUTE_POINT self + -- @return Core.Point#COORDINATE Coordinate The Coordinate to route to. + function ACT_ROUTE_POINT:GetCoordinate() + self:F2( { self.Coordinate } ) + return self.Coordinate + end + + --- Set Range around Coordinate + -- @param #ACT_ROUTE_POINT self + -- @param #number Range The Range to consider the arrival. Default is 10000 meters. + function ACT_ROUTE_POINT:SetRange( Range ) + self:F2( { Range } ) + self.Range = Range or 10000 + end + + --- Get Range around Coordinate + -- @param #ACT_ROUTE_POINT self + -- @return #number The Range to consider the arrival. Default is 10000 meters. + function ACT_ROUTE_POINT:GetRange() + self:F2( { self.Range } ) + return self.Range + end + + --- Method override to check if the controllable has arrived. + -- @param #ACT_ROUTE_POINT self + -- @param Wrapper.Unit#UNIT ProcessUnit + -- @return #boolean + function ACT_ROUTE_POINT:onfuncHasArrived( ProcessUnit ) + + if ProcessUnit:IsAlive() then + local Distance = self.Coordinate:Get2DDistance( ProcessUnit:GetCoordinate() ) + + if Distance <= self.Range then + local RouteText = "Task \"" .. self:GetTask():GetName() .. "\", you have arrived." + self:GetCommandCenter():MessageTypeToGroup( RouteText, ProcessUnit:GetGroup(), MESSAGE.Type.Information ) + return true + end + end + + return false + end + + --- Task Events + + --- StateMachine callback function + -- @param #ACT_ROUTE_POINT self + -- @param Wrapper.Unit#UNIT ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ROUTE_POINT:onafterReport( ProcessUnit, From, Event, To ) + + local RouteText = "Task \"" .. self:GetTask():GetName() .. "\", " .. self:GetRouteText( ProcessUnit ) + + self:GetCommandCenter():MessageTypeToGroup( RouteText, ProcessUnit:GetGroup(), MESSAGE.Type.Update ) + end + +end -- ACT_ROUTE_POINT + + +do -- ACT_ROUTE_ZONE + + --- ACT_ROUTE_ZONE class + -- @type ACT_ROUTE_ZONE + -- @field Tasking.Task#TASK TASK + -- @field Wrapper.Unit#UNIT ProcessUnit + -- @field Core.Zone#ZONE_BASE Zone + -- @extends #ACT_ROUTE + ACT_ROUTE_ZONE = { + ClassName = "ACT_ROUTE_ZONE", + } + + + --- Creates a new routing state machine. The task will route a controllable to a ZONE until the controllable is within that ZONE. + -- @param #ACT_ROUTE_ZONE self + -- @param Core.Zone#ZONE_BASE Zone + function ACT_ROUTE_ZONE:New( Zone ) + local self = BASE:Inherit( self, ACT_ROUTE:New() ) -- #ACT_ROUTE_ZONE + + self.Zone = Zone + + self.DisplayInterval = 30 + self.DisplayCount = 30 + self.DisplayMessage = true + self.DisplayTime = 10 -- 10 seconds is the default + + return self + end + + function ACT_ROUTE_ZONE:Init( FsmRoute ) + + self.Zone = FsmRoute.Zone + + self.DisplayInterval = 30 + self.DisplayCount = 30 + self.DisplayMessage = true + self.DisplayTime = 10 -- 10 seconds is the default + end + + --- Set Zone + -- @param #ACT_ROUTE_ZONE self + -- @param Core.Zone#ZONE_BASE Zone The Zone object where to route to. + -- @param #number Altitude + -- @param #number Heading + function ACT_ROUTE_ZONE:SetZone( Zone, Altitude, Heading ) -- R2.2 Added altitude and heading + self.Zone = Zone + self.Altitude = Altitude + self.Heading = Heading + end + + --- Get Zone + -- @param #ACT_ROUTE_ZONE self + -- @return Core.Zone#ZONE_BASE Zone The Zone object where to route to. + function ACT_ROUTE_ZONE:GetZone() + return self.Zone + end + + --- Method override to check if the controllable has arrived. + -- @param #ACT_ROUTE self + -- @param Wrapper.Unit#UNIT ProcessUnit + -- @return #boolean + function ACT_ROUTE_ZONE:onfuncHasArrived( ProcessUnit ) + + if ProcessUnit:IsInZone( self.Zone ) then + local RouteText = "Task \"" .. self:GetTask():GetName() .. "\", you have arrived within the zone." + self:GetCommandCenter():MessageTypeToGroup( RouteText, ProcessUnit:GetGroup(), MESSAGE.Type.Information ) + end + + return ProcessUnit:IsInZone( self.Zone ) + end + + --- Task Events + + --- StateMachine callback function + -- @param #ACT_ROUTE_ZONE self + -- @param Wrapper.Unit#UNIT ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ROUTE_ZONE:onafterReport( ProcessUnit, From, Event, To ) + self:F( { ProcessUnit = ProcessUnit } ) + + local RouteText = "Task \"" .. self:GetTask():GetName() .. "\", " .. self:GetRouteText( ProcessUnit ) + self:GetCommandCenter():MessageTypeToGroup( RouteText, ProcessUnit:GetGroup(), MESSAGE.Type.Update ) + end + +end -- ACT_ROUTE_ZONE +--- **Actions** - ACT_ACCOUNT_ classes **account for** (detect, count & report) various DCS events occuring on @{Wrapper.Unit}s. +-- +-- ![Banner Image](..\Presentations\ACT_ACCOUNT\Dia1.JPG) +-- +-- === +-- +-- @module Actions.Account +-- @image MOOSE.JPG + +do -- ACT_ACCOUNT + + --- # @{#ACT_ACCOUNT} FSM class, extends @{Core.Fsm#FSM_PROCESS} + -- + -- ## ACT_ACCOUNT state machine: + -- + -- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur. + -- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below. + -- Each derived class follows exactly the same process, using the same events and following the same state transitions, + -- but will have **different implementation behaviour** upon each event or state transition. + -- + -- ### ACT_ACCOUNT States + -- + -- * **Asigned**: The player is assigned. + -- * **Waiting**: Waiting for an event. + -- * **Report**: Reporting. + -- * **Account**: Account for an event. + -- * **Accounted**: All events have been accounted for, end of the process. + -- * **Failed**: Failed the process. + -- + -- ### ACT_ACCOUNT Events + -- + -- * **Start**: Start the process. + -- * **Wait**: Wait for an event. + -- * **Report**: Report the status of the accounting. + -- * **Event**: An event happened, process the event. + -- * **More**: More targets. + -- * **NoMore (*)**: No more targets. + -- * **Fail (*)**: The action process has failed. + -- + -- (*) End states of the process. + -- + -- ### ACT_ACCOUNT state transition methods: + -- + -- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. + -- There are 2 moments when state transition methods will be called by the state machine: + -- + -- * **Before** the state transition. + -- The state transition method needs to start with the name **OnBefore + the name of the state**. + -- If the state transition method returns false, then the processing of the state transition will not be done! + -- If you want to change the behaviour of the AIControllable at this event, return false, + -- but then you'll need to specify your own logic using the AIControllable! + -- + -- * **After** the state transition. + -- The state transition method needs to start with the name **OnAfter + the name of the state**. + -- These state transition methods need to provide a return value, which is specified at the function description. + -- + -- @type ACT_ACCOUNT + -- @field Core.Set#SET_UNIT TargetSetUnit + -- @extends Core.Fsm#FSM_PROCESS + ACT_ACCOUNT = { + ClassName = "ACT_ACCOUNT", + TargetSetUnit = nil, + } + + --- Creates a new DESTROY process. + -- @param #ACT_ACCOUNT self + -- @return #ACT_ACCOUNT + function ACT_ACCOUNT:New() + + -- Inherits from BASE + local self = BASE:Inherit( self, FSM_PROCESS:New() ) -- Core.Fsm#FSM_PROCESS + + self:AddTransition( "Assigned", "Start", "Waiting" ) + self:AddTransition( "*", "Wait", "Waiting" ) + self:AddTransition( "*", "Report", "Report" ) + self:AddTransition( "*", "Event", "Account" ) + self:AddTransition( "Account", "Player", "AccountForPlayer" ) + self:AddTransition( "Account", "Other", "AccountForOther" ) + self:AddTransition( { "Account", "AccountForPlayer", "AccountForOther" }, "More", "Wait" ) + self:AddTransition( { "Account", "AccountForPlayer", "AccountForOther" }, "NoMore", "Accounted" ) + self:AddTransition( "*", "Fail", "Failed" ) + + self:AddEndState( "Failed" ) + + self:SetStartState( "Assigned" ) + + return self + end + + --- Process Events + + --- StateMachine callback function + -- @param #ACT_ACCOUNT self + -- @param Wrapper.Unit#UNIT ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ACCOUNT:onafterStart( ProcessUnit, From, Event, To ) + + self:HandleEvent( EVENTS.Dead, self.onfuncEventDead ) + self:HandleEvent( EVENTS.Crash, self.onfuncEventCrash ) + self:HandleEvent( EVENTS.Hit ) + + self:__Wait( 1 ) + end + + + --- StateMachine callback function + -- @param #ACT_ACCOUNT self + -- @param Wrapper.Unit#UNIT ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ACCOUNT:onenterWaiting( ProcessUnit, From, Event, To ) + + if self.DisplayCount >= self.DisplayInterval then + self:Report() + self.DisplayCount = 1 + else + self.DisplayCount = self.DisplayCount + 1 + end + + return true -- Process always the event. + end + + --- StateMachine callback function + -- @param #ACT_ACCOUNT self + -- @param Wrapper.Unit#UNIT ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ACCOUNT:onafterEvent( ProcessUnit, From, Event, To, Event ) + + self:__NoMore( 1 ) + end + +end -- ACT_ACCOUNT + +do -- ACT_ACCOUNT_DEADS + + --- # @{#ACT_ACCOUNT_DEADS} FSM class, extends @{Core.Fsm.Account#ACT_ACCOUNT} + -- + -- The ACT_ACCOUNT_DEADS class accounts (detects, counts and reports) successful kills of DCS units. + -- The process is given a @{Set} of units that will be tracked upon successful destruction. + -- The process will end after each target has been successfully destroyed. + -- Each successful dead will trigger an Account state transition that can be scored, modified or administered. + -- + -- + -- ## ACT_ACCOUNT_DEADS constructor: + -- + -- * @{#ACT_ACCOUNT_DEADS.New}(): Creates a new ACT_ACCOUNT_DEADS object. + -- + -- @type ACT_ACCOUNT_DEADS + -- @field Core.Set#SET_UNIT TargetSetUnit + -- @extends #ACT_ACCOUNT + ACT_ACCOUNT_DEADS = { + ClassName = "ACT_ACCOUNT_DEADS", + } + + + --- Creates a new DESTROY process. + -- @param #ACT_ACCOUNT_DEADS self + -- @param Core.Set#SET_UNIT TargetSetUnit + -- @param #string TaskName + function ACT_ACCOUNT_DEADS:New() + -- Inherits from BASE + local self = BASE:Inherit( self, ACT_ACCOUNT:New() ) -- #ACT_ACCOUNT_DEADS + + self.DisplayInterval = 30 + self.DisplayCount = 30 + self.DisplayMessage = true + self.DisplayTime = 10 -- 10 seconds is the default + self.DisplayCategory = "HQ" -- Targets is the default display category + + return self + end + + function ACT_ACCOUNT_DEADS:Init( FsmAccount ) + + self.Task = self:GetTask() + self.TaskName = self.Task:GetName() + end + + --- Process Events + + --- StateMachine callback function + -- @param #ACT_ACCOUNT_DEADS self + -- @param Wrapper.Unit#UNIT ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ACCOUNT_DEADS:onenterReport( ProcessUnit, Task, From, Event, To ) + + local MessageText = "Your group with assigned " .. self.TaskName .. " task has " .. Task.TargetSetUnit:GetUnitTypesText() .. " targets left to be destroyed." + self:GetCommandCenter():MessageTypeToGroup( MessageText, ProcessUnit:GetGroup(), MESSAGE.Type.Information ) + end + + + --- StateMachine callback function + -- @param #ACT_ACCOUNT_DEADS self + -- @param Wrapper.Unit#UNIT ProcessUnit + -- @param Tasking.Task#TASK Task + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Core.Event#EVENTDATA EventData + function ACT_ACCOUNT_DEADS:onafterEvent( ProcessUnit, Task, From, Event, To, EventData ) + self:T( { ProcessUnit:GetName(), Task:GetName(), From, Event, To, EventData } ) + + if Task.TargetSetUnit:FindUnit( EventData.IniUnitName ) then + local PlayerName = ProcessUnit:GetPlayerName() + local PlayerHit = self.PlayerHits and self.PlayerHits[EventData.IniUnitName] + if PlayerHit == PlayerName then + self:Player( EventData ) + else + self:Other( EventData ) + end + end + end + + --- StateMachine callback function + -- @param #ACT_ACCOUNT_DEADS self + -- @param Wrapper.Unit#UNIT ProcessUnit + -- @param Tasking.Task#TASK Task + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Core.Event#EVENTDATA EventData + function ACT_ACCOUNT_DEADS:onenterAccountForPlayer( ProcessUnit, Task, From, Event, To, EventData ) + self:T( { ProcessUnit:GetName(), Task:GetName(), From, Event, To, EventData } ) + + local TaskGroup = ProcessUnit:GetGroup() + + Task.TargetSetUnit:Remove( EventData.IniUnitName ) + + local MessageText = "You have destroyed a target.\nYour group assigned with task " .. self.TaskName .. " has\n" .. Task.TargetSetUnit:Count() .. " targets ( " .. Task.TargetSetUnit:GetUnitTypesText() .. " ) left to be destroyed." + self:GetCommandCenter():MessageTypeToGroup( MessageText, ProcessUnit:GetGroup(), MESSAGE.Type.Information ) + + local PlayerName = ProcessUnit:GetPlayerName() + Task:AddProgress( PlayerName, "Destroyed " .. EventData.IniTypeName, timer.getTime(), 1 ) + + if Task.TargetSetUnit:Count() > 0 then + self:__More( 1 ) + else + self:__NoMore( 1 ) + end + end + + --- StateMachine callback function + -- @param #ACT_ACCOUNT_DEADS self + -- @param Wrapper.Unit#UNIT ProcessUnit + -- @param Tasking.Task#TASK Task + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Core.Event#EVENTDATA EventData + function ACT_ACCOUNT_DEADS:onenterAccountForOther( ProcessUnit, Task, From, Event, To, EventData ) + self:T( { ProcessUnit:GetName(), Task:GetName(), From, Event, To, EventData } ) + + local TaskGroup = ProcessUnit:GetGroup() + Task.TargetSetUnit:Remove( EventData.IniUnitName ) + + local MessageText = "One of the task targets has been destroyed.\nYour group assigned with task " .. self.TaskName .. " has\n" .. Task.TargetSetUnit:Count() .. " targets ( " .. Task.TargetSetUnit:GetUnitTypesText() .. " ) left to be destroyed." + self:GetCommandCenter():MessageTypeToGroup( MessageText, ProcessUnit:GetGroup(), MESSAGE.Type.Information ) + + if Task.TargetSetUnit:Count() > 0 then + self:__More( 1 ) + else + self:__NoMore( 1 ) + end + end + + + --- DCS Events + + --- @param #ACT_ACCOUNT_DEADS self + -- @param Core.Event#EVENTDATA EventData + function ACT_ACCOUNT_DEADS:OnEventHit( EventData ) + self:T( { "EventDead", EventData } ) + + if EventData.IniPlayerName and EventData.TgtDCSUnitName then + self.PlayerHits = self.PlayerHits or {} + self.PlayerHits[EventData.TgtDCSUnitName] = EventData.IniPlayerName + end + end + + --- @param #ACT_ACCOUNT_DEADS self + -- @param Core.Event#EVENTDATA EventData + function ACT_ACCOUNT_DEADS:onfuncEventDead( EventData ) + self:T( { "EventDead", EventData } ) + + if EventData.IniDCSUnit then + self:Event( EventData ) + end + end + + --- DCS Events + + --- @param #ACT_ACCOUNT_DEADS self + -- @param Core.Event#EVENTDATA EventData + function ACT_ACCOUNT_DEADS:onfuncEventCrash( EventData ) + self:T( { "EventDead", EventData } ) + + if EventData.IniDCSUnit then + self:Event( EventData ) + end + end + +end -- ACT_ACCOUNT DEADS +--- (SP) (MP) (FSM) Route AI or players through waypoints or to zones. +-- +-- ## ACT_ASSIST state machine: +-- +-- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur. +-- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below. +-- Each derived class follows exactly the same process, using the same events and following the same state transitions, +-- but will have **different implementation behaviour** upon each event or state transition. +-- +-- ### ACT_ASSIST **Events**: +-- +-- These are the events defined in this class: +-- +-- * **Start**: The process is started. +-- * **Next**: The process is smoking the targets in the given zone. +-- +-- ### ACT_ASSIST **Event methods**: +-- +-- Event methods are available (dynamically allocated by the state machine), that accomodate for state transitions occurring in the process. +-- There are two types of event methods, which you can use to influence the normal mechanisms in the state machine: +-- +-- * **Immediate**: The event method has exactly the name of the event. +-- * **Delayed**: The event method starts with a __ + the name of the event. The first parameter of the event method is a number value, expressing the delay in seconds when the event will be executed. +-- +-- ### ACT_ASSIST **States**: +-- +-- * **None**: The controllable did not receive route commands. +-- * **AwaitSmoke (*)**: The process is awaiting to smoke the targets in the zone. +-- * **Smoking (*)**: The process is smoking the targets in the zone. +-- * **Failed (*)**: The process has failed. +-- +-- (*) End states of the process. +-- +-- ### ACT_ASSIST state transition methods: +-- +-- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. +-- There are 2 moments when state transition methods will be called by the state machine: +-- +-- * **Before** the state transition. +-- The state transition method needs to start with the name **OnBefore + the name of the state**. +-- If the state transition method returns false, then the processing of the state transition will not be done! +-- If you want to change the behaviour of the AIControllable at this event, return false, +-- but then you'll need to specify your own logic using the AIControllable! +-- +-- * **After** the state transition. +-- The state transition method needs to start with the name **OnAfter + the name of the state**. +-- These state transition methods need to provide a return value, which is specified at the function description. +-- +-- === +-- +-- # 1) @{#ACT_ASSIST_SMOKE_TARGETS_ZONE} class, extends @{Core.Fsm.Route#ACT_ASSIST} +-- +-- The ACT_ASSIST_SMOKE_TARGETS_ZONE class implements the core functions to smoke targets in a @{Zone}. +-- The targets are smoked within a certain range around each target, simulating a realistic smoking behaviour. +-- At random intervals, a new target is smoked. +-- +-- # 1.1) ACT_ASSIST_SMOKE_TARGETS_ZONE constructor: +-- +-- * @{#ACT_ASSIST_SMOKE_TARGETS_ZONE.New}(): Creates a new ACT_ASSIST_SMOKE_TARGETS_ZONE object. +-- +-- === +-- +-- @module Actions.Assist +-- @image MOOSE.JPG + + +do -- ACT_ASSIST + + --- ACT_ASSIST class + -- @type ACT_ASSIST + -- @extends Core.Fsm#FSM_PROCESS + ACT_ASSIST = { + ClassName = "ACT_ASSIST", + } + + --- Creates a new target smoking state machine. The process will request from the menu if it accepts the task, if not, the unit is removed from the simulator. + -- @param #ACT_ASSIST self + -- @return #ACT_ASSIST + function ACT_ASSIST:New() + + -- Inherits from BASE + local self = BASE:Inherit( self, FSM_PROCESS:New( "ACT_ASSIST" ) ) -- Core.Fsm#FSM_PROCESS + + self:AddTransition( "None", "Start", "AwaitSmoke" ) + self:AddTransition( "AwaitSmoke", "Next", "Smoking" ) + self:AddTransition( "Smoking", "Next", "AwaitSmoke" ) + self:AddTransition( "*", "Stop", "Success" ) + self:AddTransition( "*", "Fail", "Failed" ) + + self:AddEndState( "Failed" ) + self:AddEndState( "Success" ) + + self:SetStartState( "None" ) + + return self + end + + --- Task Events + + --- StateMachine callback function + -- @param #ACT_ASSIST self + -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ASSIST:onafterStart( ProcessUnit, From, Event, To ) + + local ProcessGroup = ProcessUnit:GetGroup() + local MissionMenu = self:GetMission():GetMenu( ProcessGroup ) + + local function MenuSmoke( MenuParam ) + local self = MenuParam.self + local SmokeColor = MenuParam.SmokeColor + self.SmokeColor = SmokeColor + self:__Next( 1 ) + end + + self.Menu = MENU_GROUP:New( ProcessGroup, "Target acquisition", MissionMenu ) + self.MenuSmokeBlue = MENU_GROUP_COMMAND:New( ProcessGroup, "Drop blue smoke on targets", self.Menu, MenuSmoke, { self = self, SmokeColor = SMOKECOLOR.Blue } ) + self.MenuSmokeGreen = MENU_GROUP_COMMAND:New( ProcessGroup, "Drop green smoke on targets", self.Menu, MenuSmoke, { self = self, SmokeColor = SMOKECOLOR.Green } ) + self.MenuSmokeOrange = MENU_GROUP_COMMAND:New( ProcessGroup, "Drop Orange smoke on targets", self.Menu, MenuSmoke, { self = self, SmokeColor = SMOKECOLOR.Orange } ) + self.MenuSmokeRed = MENU_GROUP_COMMAND:New( ProcessGroup, "Drop Red smoke on targets", self.Menu, MenuSmoke, { self = self, SmokeColor = SMOKECOLOR.Red } ) + self.MenuSmokeWhite = MENU_GROUP_COMMAND:New( ProcessGroup, "Drop White smoke on targets", self.Menu, MenuSmoke, { self = self, SmokeColor = SMOKECOLOR.White } ) + end + + --- StateMachine callback function + -- @param #ACT_ASSIST self + -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ASSIST:onafterStop( ProcessUnit, From, Event, To ) + + self.Menu:Remove() -- When stopped, remove the menus + end + +end + +do -- ACT_ASSIST_SMOKE_TARGETS_ZONE + + --- ACT_ASSIST_SMOKE_TARGETS_ZONE class + -- @type ACT_ASSIST_SMOKE_TARGETS_ZONE + -- @field Core.Set#SET_UNIT TargetSetUnit + -- @field Core.Zone#ZONE_BASE TargetZone + -- @extends #ACT_ASSIST + ACT_ASSIST_SMOKE_TARGETS_ZONE = { + ClassName = "ACT_ASSIST_SMOKE_TARGETS_ZONE", + } + +-- function ACT_ASSIST_SMOKE_TARGETS_ZONE:_Destructor() +-- self:E("_Destructor") +-- +-- self.Menu:Remove() +-- self:EventRemoveAll() +-- end + + --- Creates a new target smoking state machine. The process will request from the menu if it accepts the task, if not, the unit is removed from the simulator. + -- @param #ACT_ASSIST_SMOKE_TARGETS_ZONE self + -- @param Core.Set#SET_UNIT TargetSetUnit + -- @param Core.Zone#ZONE_BASE TargetZone + function ACT_ASSIST_SMOKE_TARGETS_ZONE:New( TargetSetUnit, TargetZone ) + local self = BASE:Inherit( self, ACT_ASSIST:New() ) -- #ACT_ASSIST + + self.TargetSetUnit = TargetSetUnit + self.TargetZone = TargetZone + + return self + end + + function ACT_ASSIST_SMOKE_TARGETS_ZONE:Init( FsmSmoke ) + + self.TargetSetUnit = FsmSmoke.TargetSetUnit + self.TargetZone = FsmSmoke.TargetZone + end + + --- Creates a new target smoking state machine. The process will request from the menu if it accepts the task, if not, the unit is removed from the simulator. + -- @param #ACT_ASSIST_SMOKE_TARGETS_ZONE self + -- @param Core.Set#SET_UNIT TargetSetUnit + -- @param Core.Zone#ZONE_BASE TargetZone + -- @return #ACT_ASSIST_SMOKE_TARGETS_ZONE self + function ACT_ASSIST_SMOKE_TARGETS_ZONE:Init( TargetSetUnit, TargetZone ) + + self.TargetSetUnit = TargetSetUnit + self.TargetZone = TargetZone + + return self + end + + --- StateMachine callback function + -- @param #ACT_ASSIST_SMOKE_TARGETS_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ASSIST_SMOKE_TARGETS_ZONE:onenterSmoking( ProcessUnit, From, Event, To ) + + self.TargetSetUnit:ForEachUnit( + --- @param Wrapper.Unit#UNIT SmokeUnit + function( SmokeUnit ) + if math.random( 1, ( 100 * self.TargetSetUnit:Count() ) / 4 ) <= 100 then + SCHEDULER:New( self, + function() + if SmokeUnit:IsAlive() then + SmokeUnit:Smoke( self.SmokeColor, 150 ) + end + end, {}, math.random( 10, 60 ) + ) + end + end + ) + + end + +end +--- **Sound** - Manage user sound. +-- +-- === +-- +-- ## Features: +-- +-- * Play sounds wihtin running missions. +-- +-- === +-- +-- Management of DCS User Sound. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module Sound.UserSound +-- @image Core_Usersound.JPG + +do -- UserSound + + --- @type USERSOUND + -- @extends Core.Base#BASE + + + --- Management of DCS User Sound. + -- + -- ## USERSOUND constructor + -- + -- * @{#USERSOUND.New}(): Creates a new USERSOUND object. + -- + -- @field #USERSOUND + USERSOUND = { + ClassName = "USERSOUND", + } + + --- USERSOUND Constructor. + -- @param #USERSOUND self + -- @param #string UserSoundFileName The filename of the usersound. + -- @return #USERSOUND + function USERSOUND:New( UserSoundFileName ) --R2.3 + + local self = BASE:Inherit( self, BASE:New() ) -- #USERSOUND + + self.UserSoundFileName = UserSoundFileName + + return self + end + + + --- Set usersound filename. + -- @param #USERSOUND self + -- @param #string UserSoundFileName The filename of the usersound. + -- @return #USERSOUND The usersound instance. + -- @usage + -- local BlueVictory = USERSOUND:New( "BlueVictory.ogg" ) + -- BlueVictory:SetFileName( "BlueVictoryLoud.ogg" ) -- Set the BlueVictory to change the file name to play a louder sound. + -- + function USERSOUND:SetFileName( UserSoundFileName ) --R2.3 + + self.UserSoundFileName = UserSoundFileName + + return self + end + + + + + --- Play the usersound to all players. + -- @param #USERSOUND self + -- @return #USERSOUND The usersound instance. + -- @usage + -- local BlueVictory = USERSOUND:New( "BlueVictory.ogg" ) + -- BlueVictory:ToAll() -- Play the sound that Blue has won. + -- + function USERSOUND:ToAll() --R2.3 + + trigger.action.outSound( self.UserSoundFileName ) + + return self + end + + + --- Play the usersound to the given coalition. + -- @param #USERSOUND self + -- @param DCS#coalition Coalition The coalition to play the usersound to. + -- @return #USERSOUND The usersound instance. + -- @usage + -- local BlueVictory = USERSOUND:New( "BlueVictory.ogg" ) + -- BlueVictory:ToCoalition( coalition.side.BLUE ) -- Play the sound that Blue has won to the blue coalition. + -- + function USERSOUND:ToCoalition( Coalition ) --R2.3 + + trigger.action.outSoundForCoalition(Coalition, self.UserSoundFileName ) + + return self + end + + + --- Play the usersound to the given country. + -- @param #USERSOUND self + -- @param DCS#country Country The country to play the usersound to. + -- @return #USERSOUND The usersound instance. + -- @usage + -- local BlueVictory = USERSOUND:New( "BlueVictory.ogg" ) + -- BlueVictory:ToCountry( country.id.USA ) -- Play the sound that Blue has won to the USA country. + -- + function USERSOUND:ToCountry( Country ) --R2.3 + + trigger.action.outSoundForCountry( Country, self.UserSoundFileName ) + + return self + end + + + --- Play the usersound to the given @{Wrapper.Group}. + -- @param #USERSOUND self + -- @param Wrapper.Group#GROUP Group The @{Wrapper.Group} to play the usersound to. + -- @param #number Delay (Optional) Delay in seconds, before the sound is played. Default 0. + -- @return #USERSOUND The usersound instance. + -- @usage + -- local BlueVictory = USERSOUND:New( "BlueVictory.ogg" ) + -- local PlayerGroup = GROUP:FindByName( "PlayerGroup" ) -- Search for the active group named "PlayerGroup", that contains a human player. + -- BlueVictory:ToGroup( PlayerGroup ) -- Play the sound that Blue has won to the player group. + -- + function USERSOUND:ToGroup( Group, Delay ) --R2.3 + + Delay=Delay or 0 + if Delay>0 then + SCHEDULER:New(nil, USERSOUND.ToGroup,{self, Group}, Delay) + else + trigger.action.outSoundForGroup( Group:GetID(), self.UserSoundFileName ) + end + + return self + end + +end--- **Sound** - Sound output classes. +-- +-- === +-- +-- ## Features: +-- +-- * Create a SOUNDFILE object (mp3 or ogg) to be played via DCS or SRS transmissions +-- * Create a SOUNDTEXT object for text-to-speech output vis SRS Simple-Text-To-Speech (STTS) +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- === +-- +-- There are two classes, SOUNDFILE and SOUNDTEXT, defined in this section that deal with playing +-- sound files or arbitrary text (via SRS Simple-Text-To-Speech), respectively. +-- +-- The SOUNDFILE and SOUNDTEXT objects can be defined and used in other MOOSE classes. +-- +-- +-- @module Sound.SoundOutput +-- @image Sound_SoundOutput.png + +do -- Sound Base + + --- @type SOUNDBASE + -- @field #string ClassName Name of the class. + -- @extends Core.Base#BASE + + + --- Basic sound output inherited by other classes suche as SOUNDFILE and SOUNDTEXT. + -- + -- This class is **not** meant to be used by "ordinary" users. + -- + -- @field #SOUNDBASE + SOUNDBASE={ + ClassName = "SOUNDBASE", + } + + --- Constructor to create a new SOUNDBASE object. + -- @param #SOUNDBASE self + -- @return #SOUNDBASE self + function SOUNDBASE:New() + + -- Inherit BASE. + local self=BASE:Inherit(self, BASE:New()) -- #SOUNDBASE + + + + return self + end + + --- Function returns estimated speech time in seconds. + -- Assumptions for time calc: 100 Words per min, avarage of 5 letters for english word so + -- + -- * 5 chars * 100wpm = 500 characters per min = 8.3 chars per second + -- + -- So lengh of msg / 8.3 = number of seconds needed to read it. rounded down to 8 chars per sec map function: + -- + -- * (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min + -- + -- @param #string Text The text string to analyze. + -- @param #number Speed Speed factor. Default 1. + -- @param #boolean isGoogle If true, google text-to-speech is used. + function SOUNDBASE:GetSpeechTime(length,speed,isGoogle) + + local maxRateRatio = 3 + + speed = speed or 1.0 + isGoogle = isGoogle or false + + local speedFactor = 1.0 + if isGoogle then + speedFactor = speed + else + if speed ~= 0 then + speedFactor = math.abs(speed) * (maxRateRatio - 1) / 10 + 1 + end + if speed < 0 then + speedFactor = 1/speedFactor + end + end + + -- Words per minute. + local wpm = math.ceil(100 * speedFactor) + + -- Characters per second. + local cps = math.floor((wpm * 5)/60) + + if type(length) == "string" then + length = string.len(length) + end + + return math.ceil(length/cps) + end + +end + + +do -- Sound File + + --- @type SOUNDFILE + -- @field #string ClassName Name of the class + -- @field #string filename Name of the flag. + -- @field #string path Directory path, where the sound file is located. This includes the final slash "/". + -- @field #string duration Duration of the sound file in seconds. + -- @field #string subtitle Subtitle of the transmission. + -- @field #number subduration Duration in seconds how long the subtitle is displayed. + -- @field #boolean useSRS If true, sound file is played via SRS. Sound file needs to be on local disk not inside the miz file! + -- @extends Core.Base#BASE + + + --- Sound files used by other classes. + -- + -- # The SOUNDFILE Concept + -- + -- A SOUNDFILE object hold the important properties that are necessary to play the sound file, e.g. its file name, path, duration. + -- + -- It can be created with the @{#SOUNDFILE.New}(*FileName*, *Path*, *Duration*) function: + -- + -- local soundfile=SOUNDFILE:New("My Soundfile.ogg", "Sound File/", 3.5) + -- + -- ## SRS + -- + -- If sound files are supposed to be played via SRS, you need to use the @{#SOUNDFILE.SetPlayWithSRS}() function. + -- + -- # Location/Path + -- + -- ## DCS + -- + -- DCS can only play sound files that are located inside the mission (.miz) file. In particular, DCS cannot make use of files that are stored on + -- your hard drive. + -- + -- The default location where sound files are stored in DCS is the directory "l10n/DEFAULT/". This is where sound files are placed, if they are + -- added via the mission editor (TRIGGERS-->ACTIONS-->SOUND TO ALL). Note however, that sound files which are not added with a trigger command, + -- will be deleted each time the mission is saved! Therefore, this directory is not ideal to be used especially if many sound files are to + -- be included since for each file a trigger action needs to be created. Which is cumbersome, to say the least. + -- + -- The recommended way is to create a new folder inside the mission (.miz) file (a miz file is essentially zip file and can be opened, e.g., with 7-Zip) + -- and to place the sound files in there. Sound files in these folders are not wiped out by DCS on the next save. + -- + -- ## SRS + -- + -- SRS sound files need to be located on your local drive (not inside the miz). Therefore, you need to specify the full path. + -- + -- @field #SOUNDFILE + SOUNDFILE={ + ClassName = "SOUNDFILE", + filename = nil, + path = "l10n/DEFAULT/", + duration = 3, + subtitle = nil, + subduration = 0, + useSRS = false, + } + + --- Constructor to create a new SOUNDFILE object. + -- @param #SOUNDFILE self + -- @param #string FileName The name of the sound file, e.g. "Hello World.ogg". + -- @param #string Path The path of the directory, where the sound file is located. Default is "l10n/DEFAULT/" within the miz file. + -- @param #number Duration Duration in seconds, how long it takes to play the sound file. Default is 3 seconds. + -- @return #SOUNDFILE self + function SOUNDFILE:New(FileName, Path, Duration) + + -- Inherit BASE. + local self=BASE:Inherit(self, BASE:New()) -- #SOUNDFILE + + -- Set file name. + self:SetFileName(FileName) + + -- Set path. + self:SetPath(Path) + + -- Set duration. + self:SetDuration(Duration) + + -- Debug info: + self:T(string.format("New SOUNDFILE: file name=%s, path=%s", self.filename, self.path)) + + return self + end + + --- Set path, where the sound file is located. + -- @param #SOUNDFILE self + -- @param #string Path Path to the directory, where the sound file is located. + -- @return #SOUNDFILE self + function SOUNDFILE:SetPath(Path) + + -- Init path. + self.path=Path or "l10n/DEFAULT/" + + -- Remove (back)slashes. + local nmax=1000 ; local n=1 + while (self.path:sub(-1)=="/" or self.path:sub(-1)==[[\]]) and n<=nmax do + self.path=self.path:sub(1,#self.path-1) + n=n+1 + end + + -- Append slash. + self.path=self.path.."/" + + return self + end + + --- Get path of the directory, where the sound file is located. + -- @param #SOUNDFILE self + -- @return #string Path. + function SOUNDFILE:GetPath() + local path=self.path or "l10n/DEFAULT/" + return path + end + + --- Set sound file name. This must be a .ogg or .mp3 file! + -- @param #SOUNDFILE self + -- @param #string FileName Name of the file. Default is "Hello World.mp3". + -- @return #SOUNDFILE self + function SOUNDFILE:SetFileName(FileName) + --TODO: check that sound file is really .ogg or .mp3 + self.filename=FileName or "Hello World.mp3" + return self + end + + --- Get the sound file name. + -- @param #SOUNDFILE self + -- @return #string Name of the soud file. This does *not* include its path. + function SOUNDFILE:GetFileName() + return self.filename + end + + + --- Set duration how long it takes to play the sound file. + -- @param #SOUNDFILE self + -- @param #string Duration Duration in seconds. Default 3 seconds. + -- @return #SOUNDFILE self + function SOUNDFILE:SetDuration(Duration) + self.duration=Duration or 3 + return self + end + + --- Get duration how long the sound file takes to play. + -- @param #SOUNDFILE self + -- @return #number Duration in seconds. + function SOUNDFILE:GetDuration() + return self.duration or 3 + end + + --- Get the complete sound file name inlcuding its path. + -- @param #SOUNDFILE self + -- @return #string Name of the sound file. + function SOUNDFILE:GetName() + local path=self:GetPath() + local filename=self:GetFileName() + local name=string.format("%s%s", path, filename) + return name + end + + --- Set whether sound files should be played via SRS. + -- @param #SOUNDFILE self + -- @param #boolean Switch If true or nil, use SRS. If false, use DCS transmission. + -- @return #SOUNDFILE self + function SOUNDFILE:SetPlayWithSRS(Switch) + if Switch==true or Switch==nil then + self.useSRS=true + else + self.useSRS=false + end + return self + end + +end + +do -- Text-To-Speech + + --- @type SOUNDTEXT + -- @field #string ClassName Name of the class + -- @field #string text Text to speak. + -- @field #number duration Duration in seconds. + -- @field #string gender Gender: "male", "female". + -- @field #string culture Culture, e.g. "en-GB". + -- @field #string voice Specific voice to use. Overrules `gender` and `culture` settings. + -- @extends Core.Base#BASE + + + --- Text-to-speech objects for other classes. + -- + -- # The SOUNDTEXT Concept + -- + -- A SOUNDTEXT object holds all necessary information to play a general text via SRS Simple-Text-To-Speech. + -- + -- It can be created with the @{#SOUNDTEXT.New}(*Text*, *Duration*) function. + -- + -- * @{#SOUNDTEXT.New}(*Text, Duration*): Creates a new SOUNDTEXT object. + -- + -- # Options + -- + -- ## Gender + -- + -- You can choose a gender ("male" or "femal") with the @{#SOUNDTEXT.SetGender}(*Gender*) function. + -- Note that the gender voice needs to be installed on your windows machine for the used culture (see below). + -- + -- ## Culture + -- + -- You can choose a "culture" (accent) with the @{#SOUNDTEXT.SetCulture}(*Culture*) function, where the default (SRS) culture is "en-GB". + -- + -- Other examples for culture are: "en-US" (US accent), "de-DE" (German), "it-IT" (Italian), "ru-RU" (Russian), "zh-CN" (Chinese). + -- + -- Note that the chosen culture needs to be installed on your windows machine. + -- + -- ## Specific Voice + -- + -- You can use a specific voice for the transmission with the @{SOUNDTEXT.SetVoice}(*VoiceName*) function. Here are some examples + -- + -- * Name: Microsoft Hazel Desktop, Culture: en-GB, Gender: Female, Age: Adult, Desc: Microsoft Hazel Desktop - English (Great Britain) + -- * Name: Microsoft David Desktop, Culture: en-US, Gender: Male, Age: Adult, Desc: Microsoft David Desktop - English (United States) + -- * Name: Microsoft Zira Desktop, Culture: en-US, Gender: Female, Age: Adult, Desc: Microsoft Zira Desktop - English (United States) + -- * Name: Microsoft Hedda Desktop, Culture: de-DE, Gender: Female, Age: Adult, Desc: Microsoft Hedda Desktop - German + -- * Name: Microsoft Helena Desktop, Culture: es-ES, Gender: Female, Age: Adult, Desc: Microsoft Helena Desktop - Spanish (Spain) + -- * Name: Microsoft Hortense Desktop, Culture: fr-FR, Gender: Female, Age: Adult, Desc: Microsoft Hortense Desktop - French + -- * Name: Microsoft Elsa Desktop, Culture: it-IT, Gender: Female, Age: Adult, Desc: Microsoft Elsa Desktop - Italian (Italy) + -- * Name: Microsoft Irina Desktop, Culture: ru-RU, Gender: Female, Age: Adult, Desc: Microsoft Irina Desktop - Russian + -- * Name: Microsoft Huihui Desktop, Culture: zh-CN, Gender: Female, Age: Adult, Desc: Microsoft Huihui Desktop - Chinese (Simplified) + -- + -- Note that this must be installed on your windos machine. Also note that this overrides any culture and gender settings. + -- + -- @field #SOUNDTEXT + SOUNDTEXT={ + ClassName = "SOUNDTEXT", + } + + --- Constructor to create a new SOUNDTEXT object. + -- @param #SOUNDTEXT self + -- @param #string Text The text to speak. + -- @param #number Duration Duration in seconds, how long it takes to play the text. Default is 3 seconds. + -- @return #SOUNDTEXT self + function SOUNDTEXT:New(Text, Duration) + + -- Inherit BASE. + local self=BASE:Inherit(self, BASE:New()) -- #SOUNDTEXT + + self:SetText(Text) + self:SetDuration(Duration or STTS.getSpeechTime(Text)) + --self:SetGender() + --self:SetCulture() + + -- Debug info: + self:T(string.format("New SOUNDTEXT: text=%s, duration=%.1f sec", self.text, self.duration)) + + return self + end + + --- Set text. + -- @param #SOUNDTEXT self + -- @param #string Text Text to speak. Default "Hello World!". + -- @return #SOUNDTEXT self + function SOUNDTEXT:SetText(Text) + + self.text=Text or "Hello World!" + + return self + end + + --- Set duration, how long it takes to speak the text. + -- @param #SOUNDTEXT self + -- @param #number Duration Duration in seconds. Default 3 seconds. + -- @return #SOUNDTEXT self + function SOUNDTEXT:SetDuration(Duration) + + self.duration=Duration or 3 + + return self + end + + --- Set gender. + -- @param #SOUNDTEXT self + -- @param #string Gender Gender: "male" or "female" (default). + -- @return #SOUNDTEXT self + function SOUNDTEXT:SetGender(Gender) + + self.gender=Gender or "female" + + return self + end + + --- Set TTS culture - local for the voice. + -- @param #SOUNDTEXT self + -- @param #string Culture TTS culture. Default "en-GB". + -- @return #SOUNDTEXT self + function SOUNDTEXT:SetCulture(Culture) + + self.culture=Culture or "en-GB" + + return self + end + + --- Set to use a specific voice name. + -- See the list from `DCS-SR-ExternalAudio.exe --help` or if using google see [google voices](https://cloud.google.com/text-to-speech/docs/voices). + -- @param #SOUNDTEXT self + -- @param #string VoiceName Voice name. Note that this will overrule `Gender` and `Culture`. + -- @return #SOUNDTEXT self + function SOUNDTEXT:SetVoice(VoiceName) + + self.voice=VoiceName + + return self + end + +end--- **Sound** - Radio transmissions. +-- +-- === +-- +-- ## Features: +-- +-- * Provide radio functionality to broadcast radio transmissions. +-- +-- What are radio communications in DCS? +-- +-- * Radio transmissions consist of **sound files** that are broadcasted on a specific **frequency** (e.g. 115MHz) and **modulation** (e.g. AM), +-- * They can be **subtitled** for a specific **duration**, the **power** in Watts of the transmiter's antenna can be set, and the transmission can be **looped**. +-- +-- How to supply DCS my own Sound Files? +-- +-- * Your sound files need to be encoded in **.ogg** or .wav, +-- * Your sound files should be **as tiny as possible**. It is suggested you encode in .ogg with low bitrate and sampling settings, +-- * They need to be added in .\l10n\DEFAULT\ in you .miz file (wich can be decompressed like a .zip file), +-- * For simplicity sake, you can **let DCS' Mission Editor add the file** itself, by creating a new Trigger with the action "Sound to Country", and choosing your sound file and a country you don't use in your mission. +-- +-- Due to weird DCS quirks, **radio communications behave differently** if sent by a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP} or by any other @{Wrapper.Positionable#POSITIONABLE} +-- +-- * If the transmitter is a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP}, DCS will set the power of the transmission automatically, +-- * If the transmitter is any other @{Wrapper.Positionable#POSITIONABLE}, the transmisison can't be subtitled or looped. +-- +-- Note that obviously, the **frequency** and the **modulation** of the transmission are important only if the players are piloting an **Advanced System Modelling** enabled aircraft, +-- like the A10C or the Mirage 2000C. They will **hear the transmission** if they are tuned on the **right frequency and modulation** (and if they are close enough - more on that below). +-- If an FC3 aircraft is used, it will **hear every communication, whatever the frequency and the modulation** is set to. The same is true for TACAN beacons. If your aircraft isn't compatible, +-- you won't hear/be able to use the TACAN beacon informations. +-- +-- === +-- +-- ### Authors: Hugues "Grey_Echo" Bousquet, funkyfranky +-- +-- @module Sound.Radio +-- @image Core_Radio.JPG + + +--- *It's not true I had nothing on, I had the radio on.* -- Marilyn Monroe +-- +-- # RADIO usage +-- +-- There are 3 steps to a successful radio transmission. +-- +-- * First, you need to **"add a @{#RADIO} object** to your @{Wrapper.Positionable#POSITIONABLE}. This is done using the @{Wrapper.Positionable#POSITIONABLE.GetRadio}() function, +-- * Then, you will **set the relevant parameters** to the transmission (see below), +-- * When done, you can actually **broadcast the transmission** (i.e. play the sound) with the @{RADIO.Broadcast}() function. +-- +-- Methods to set relevant parameters for both a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP} or any other @{Wrapper.Positionable#POSITIONABLE} +-- +-- * @{#RADIO.SetFileName}() : Sets the file name of your sound file (e.g. "Noise.ogg"), +-- * @{#RADIO.SetFrequency}() : Sets the frequency of your transmission. +-- * @{#RADIO.SetModulation}() : Sets the modulation of your transmission. +-- * @{#RADIO.SetLoop}() : Choose if you want the transmission to be looped. If you need your transmission to be looped, you might need a @{#BEACON} instead... +-- +-- Additional Methods to set relevant parameters if the transmitter is a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP} +-- +-- * @{#RADIO.SetSubtitle}() : Set both the subtitle and its duration, +-- * @{#RADIO.NewUnitTransmission}() : Shortcut to set all the relevant parameters in one method call +-- +-- Additional Methods to set relevant parameters if the transmitter is any other @{Wrapper.Positionable#POSITIONABLE} +-- +-- * @{#RADIO.SetPower}() : Sets the power of the antenna in Watts +-- * @{#RADIO.NewGenericTransmission}() : Shortcut to set all the relevant parameters in one method call +-- +-- What is this power thing? +-- +-- * If your transmission is sent by a @{Wrapper.Positionable#POSITIONABLE} other than a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP}, you can set the power of the antenna, +-- * Otherwise, DCS sets it automatically, depending on what's available on your Unit, +-- * If the player gets **too far** from the transmitter, or if the antenna is **too weak**, the transmission will **fade** and **become noisyer**, +-- * This an automated DCS calculation you have no say on, +-- * For reference, a standard VOR station has a 100 W antenna, a standard AA TACAN has a 120 W antenna, and civilian ATC's antenna usually range between 300 and 500 W, +-- * Note that if the transmission has a subtitle, it will be readable, regardless of the quality of the transmission. +-- +-- @type RADIO +-- @field Wrapper.Controllable#CONTROLLABLE Positionable The @{#CONTROLLABLE} that will transmit the radio calls. +-- @field #string FileName Name of the sound file played. +-- @field #number Frequency Frequency of the transmission in Hz. +-- @field #number Modulation Modulation of the transmission (either radio.modulation.AM or radio.modulation.FM). +-- @field #string Subtitle Subtitle of the transmission. +-- @field #number SubtitleDuration Duration of the Subtitle in seconds. +-- @field #number Power Power of the antenna is Watts. +-- @field #boolean Loop Transmission is repeated (default true). +-- @field #string alias Name of the radio transmitter. +-- @extends Core.Base#BASE +RADIO = { + ClassName = "RADIO", + FileName = "", + Frequency = 0, + Modulation = radio.modulation.AM, + Subtitle = "", + SubtitleDuration = 0, + Power = 100, + Loop = false, + alias = nil, +} + +--- Create a new RADIO Object. This doesn't broadcast a transmission, though, use @{#RADIO.Broadcast} to actually broadcast. +-- If you want to create a RADIO, you probably should use @{Wrapper.Positionable#POSITIONABLE.GetRadio}() instead. +-- @param #RADIO self +-- @param Wrapper.Positionable#POSITIONABLE Positionable The @{Positionable} that will receive radio capabilities. +-- @return #RADIO The RADIO object or #nil if Positionable is invalid. +function RADIO:New(Positionable) + + -- Inherit base + local self = BASE:Inherit( self, BASE:New() ) -- Core.Radio#RADIO + self:F(Positionable) + + if Positionable:GetPointVec2() then -- It's stupid, but the only way I found to make sure positionable is valid + self.Positionable = Positionable + return self + end + + self:E({error="The passed positionable is invalid, no RADIO created!", positionable=Positionable}) + return nil +end + +--- Set alias of the transmitter. +-- @param #RADIO self +-- @param #string alias Name of the radio transmitter. +-- @return #RADIO self +function RADIO:SetAlias(alias) + self.alias=tostring(alias) + return self +end + +--- Get alias of the transmitter. +-- @param #RADIO self +-- @return #string Name of the transmitter. +function RADIO:GetAlias() + return tostring(self.alias) +end + +--- Set the file name for the radio transmission. +-- @param #RADIO self +-- @param #string FileName File name of the sound file (i.e. "Noise.ogg") +-- @return #RADIO self +function RADIO:SetFileName(FileName) + self:F2(FileName) + + if type(FileName) == "string" then + + if FileName:find(".ogg") or FileName:find(".wav") then + if not FileName:find("l10n/DEFAULT/") then + FileName = "l10n/DEFAULT/" .. FileName + end + + self.FileName = FileName + return self + end + end + + self:E({"File name invalid. Maybe something wrong with the extension?", FileName}) + return self +end + +--- Set the frequency for the radio transmission. +-- If the transmitting positionable is a unit or group, this also set the command "SetFrequency" with the defined frequency and modulation. +-- @param #RADIO self +-- @param #number Frequency Frequency in MHz. +-- @return #RADIO self +function RADIO:SetFrequency(Frequency) + self:F2(Frequency) + + if type(Frequency) == "number" then + + -- If frequency is in range + --if (Frequency >= 30 and Frequency <= 87.995) or (Frequency >= 108 and Frequency <= 173.995) or (Frequency >= 225 and Frequency <= 399.975) then + + -- Convert frequency from MHz to Hz + self.Frequency = Frequency * 1000000 + + -- If the RADIO is attached to a UNIT or a GROUP, we need to send the DCS Command "SetFrequency" to change the UNIT or GROUP frequency + if self.Positionable.ClassName == "UNIT" or self.Positionable.ClassName == "GROUP" then + + local commandSetFrequency={ + id = "SetFrequency", + params = { + frequency = self.Frequency, + modulation = self.Modulation, + } + } + + self:T2(commandSetFrequency) + self.Positionable:SetCommand(commandSetFrequency) + end + + return self + --end + end + + self:E({"Frequency is not a number. Frequency unchanged.", Frequency}) + return self +end + +--- Set AM or FM modulation of the radio transmitter. +-- @param #RADIO self +-- @param #number Modulation Modulation is either radio.modulation.AM or radio.modulation.FM. +-- @return #RADIO self +function RADIO:SetModulation(Modulation) + self:F2(Modulation) + if type(Modulation) == "number" then + if Modulation == radio.modulation.AM or Modulation == radio.modulation.FM then --TODO Maybe make this future proof if ED decides to add an other modulation ? + self.Modulation = Modulation + return self + end + end + self:E({"Modulation is invalid. Use DCS's enum radio.modulation. Modulation unchanged.", self.Modulation}) + return self +end + +--- Check validity of the power passed and sets RADIO.Power +-- @param #RADIO self +-- @param #number Power Power in W. +-- @return #RADIO self +function RADIO:SetPower(Power) + self:F2(Power) + + if type(Power) == "number" then + self.Power = math.floor(math.abs(Power)) --TODO Find what is the maximum power allowed by DCS and limit power to that + else + self:E({"Power is invalid. Power unchanged.", self.Power}) + end + + return self +end + +--- Set message looping on or off. +-- @param #RADIO self +-- @param #boolean Loop If true, message is repeated indefinitely. +-- @return #RADIO self +function RADIO:SetLoop(Loop) + self:F2(Loop) + if type(Loop) == "boolean" then + self.Loop = Loop + return self + end + self:E({"Loop is invalid. Loop unchanged.", self.Loop}) + return self +end + +--- Check validity of the subtitle and the subtitleDuration passed and sets RADIO.subtitle and RADIO.subtitleDuration +-- Both parameters are mandatory, since it wouldn't make much sense to change the Subtitle and not its duration +-- @param #RADIO self +-- @param #string Subtitle +-- @param #number SubtitleDuration in s +-- @return #RADIO self +-- @usage +-- -- create the broadcaster and attaches it a RADIO +-- local MyUnit = UNIT:FindByName("MyUnit") +-- local MyUnitRadio = MyUnit:GetRadio() +-- +-- -- add a subtitle for the next transmission, which will be up for 10s +-- MyUnitRadio:SetSubtitle("My Subtitle, 10) +function RADIO:SetSubtitle(Subtitle, SubtitleDuration) + self:F2({Subtitle, SubtitleDuration}) + if type(Subtitle) == "string" then + self.Subtitle = Subtitle + else + self.Subtitle = "" + self:E({"Subtitle is invalid. Subtitle reset.", self.Subtitle}) + end + if type(SubtitleDuration) == "number" then + self.SubtitleDuration = SubtitleDuration + else + self.SubtitleDuration = 0 + self:E({"SubtitleDuration is invalid. SubtitleDuration reset.", self.SubtitleDuration}) + end + return self +end + +--- Create a new transmission, that is to say, populate the RADIO with relevant data +-- In this function the data is especially relevant if the broadcaster is anything but a UNIT or a GROUP, +-- but it will work with a UNIT or a GROUP anyway. +-- Only the #RADIO and the Filename are mandatory +-- @param #RADIO self +-- @param #string FileName Name of the sound file that will be transmitted. +-- @param #number Frequency Frequency in MHz. +-- @param #number Modulation Modulation of frequency, which is either radio.modulation.AM or radio.modulation.FM. +-- @param #number Power Power in W. +-- @return #RADIO self +function RADIO:NewGenericTransmission(FileName, Frequency, Modulation, Power, Loop) + self:F({FileName, Frequency, Modulation, Power}) + + self:SetFileName(FileName) + if Frequency then self:SetFrequency(Frequency) end + if Modulation then self:SetModulation(Modulation) end + if Power then self:SetPower(Power) end + if Loop then self:SetLoop(Loop) end + + return self +end + + +--- Create a new transmission, that is to say, populate the RADIO with relevant data +-- In this function the data is especially relevant if the broadcaster is a UNIT or a GROUP, +-- but it will work for any @{Wrapper.Positionable#POSITIONABLE}. +-- Only the RADIO and the Filename are mandatory. +-- @param #RADIO self +-- @param #string FileName Name of sound file. +-- @param #string Subtitle Subtitle to be displayed with sound file. +-- @param #number SubtitleDuration Duration of subtitle display in seconds. +-- @param #number Frequency Frequency in MHz. +-- @param #number Modulation Modulation which can be either radio.modulation.AM or radio.modulation.FM +-- @param #boolean Loop If true, loop message. +-- @return #RADIO self +function RADIO:NewUnitTransmission(FileName, Subtitle, SubtitleDuration, Frequency, Modulation, Loop) + self:F({FileName, Subtitle, SubtitleDuration, Frequency, Modulation, Loop}) + + -- Set file name. + self:SetFileName(FileName) + + -- Set modulation AM/FM. + if Modulation then + self:SetModulation(Modulation) + end + + -- Set frequency. + if Frequency then + self:SetFrequency(Frequency) + end + + -- Set subtitle. + if Subtitle then + self:SetSubtitle(Subtitle, SubtitleDuration or 0) + end + + -- Set Looping. + if Loop then + self:SetLoop(Loop) + end + + return self +end + +--- Broadcast the transmission. +-- * The Radio has to be populated with the new transmission before broadcasting. +-- * Please use RADIO setters or either @{#RADIO.NewGenericTransmission} or @{#RADIO.NewUnitTransmission} +-- * This class is in fact pretty smart, it determines the right DCS function to use depending on the type of POSITIONABLE +-- * If the POSITIONABLE is not a UNIT or a GROUP, we use the generic (but limited) trigger.action.radioTransmission() +-- * If the POSITIONABLE is a UNIT or a GROUP, we use the "TransmitMessage" Command +-- * If your POSITIONABLE is a UNIT or a GROUP, the Power is ignored. +-- * If your POSITIONABLE is not a UNIT or a GROUP, the Subtitle, SubtitleDuration are ignored +-- @param #RADIO self +-- @param #boolean viatrigger Use trigger.action.radioTransmission() in any case, i.e. also for UNITS and GROUPS. +-- @return #RADIO self +function RADIO:Broadcast(viatrigger) + self:F({viatrigger=viatrigger}) + + -- If the POSITIONABLE is actually a UNIT or a GROUP, use the more complicated DCS command system. + if (self.Positionable.ClassName=="UNIT" or self.Positionable.ClassName=="GROUP") and (not viatrigger) then + self:T("Broadcasting from a UNIT or a GROUP") + + local commandTransmitMessage={ + id = "TransmitMessage", + params = { + file = self.FileName, + duration = self.SubtitleDuration, + subtitle = self.Subtitle, + loop = self.Loop, + }} + + self:T3(commandTransmitMessage) + self.Positionable:SetCommand(commandTransmitMessage) + else + -- If the POSITIONABLE is anything else, we revert to the general singleton function + -- I need to give it a unique name, so that the transmission can be stopped later. I use the class ID + self:T("Broadcasting from a POSITIONABLE") + trigger.action.radioTransmission(self.FileName, self.Positionable:GetPositionVec3(), self.Modulation, self.Loop, self.Frequency, self.Power, tostring(self.ID)) + end + + return self +end + + + +--- Stops a transmission +-- This function is especially usefull to stop the broadcast of looped transmissions +-- @param #RADIO self +-- @return #RADIO self +function RADIO:StopBroadcast() + self:F() + -- If the POSITIONABLE is a UNIT or a GROUP, stop the transmission with the DCS "StopTransmission" command + if self.Positionable.ClassName == "UNIT" or self.Positionable.ClassName == "GROUP" then + + local commandStopTransmission={id="StopTransmission", params={}} + + self.Positionable:SetCommand(commandStopTransmission) + else + -- Else, we use the appropriate singleton funciton + trigger.action.stopRadioTransmission(tostring(self.ID)) + end + return self +end +--- **Sound** - Queues Radio Transmissions. +-- +-- === +-- +-- ## Features: +-- +-- * Manage Radio Transmissions +-- +-- === +-- +-- ### Authors: funkyfranky +-- +-- @module Sound.RadioQueue +-- @image Core_Radio.JPG + +--- Manages radio transmissions. +-- +-- The main goal of the RADIOQUEUE class is to string together multiple sound files to play a complete sentence. +-- The underlying problem is that radio transmissions in DCS are not queued but played "on top" of each other. +-- Therefore, to achive the goal, it is vital to know the precise duration how long it takes to play the sound file. +-- +-- @type RADIOQUEUE +-- @field #string ClassName Name of the class "RADIOQUEUE". +-- @field #boolean Debugmode Debug mode. More info. +-- @field #string lid ID for dcs.log. +-- @field #number frequency The radio frequency in Hz. +-- @field #number modulation The radio modulation. Either radio.modulation.AM or radio.modulation.FM. +-- @field Core.Scheduler#SCHEDULER scheduler The scheduler. +-- @field #string RQid The radio queue scheduler ID. +-- @field #table queue The queue of transmissions. +-- @field #string alias Name of the radio. +-- @field #number dt Time interval in seconds for checking the radio queue. +-- @field #number delay Time delay before starting the radio queue. +-- @field #number Tlast Time (abs) when the last transmission finished. +-- @field Core.Point#COORDINATE sendercoord Coordinate from where transmissions are broadcasted. +-- @field #number sendername Name of the sending unit or static. +-- @field #boolean senderinit Set frequency was initialized. +-- @field #number power Power of radio station in Watts. Default 100 W. +-- @field #table numbers Table of number transmission parameters. +-- @field #boolean checking Scheduler is checking the radio queue. +-- @field #boolean schedonce Call ScheduleOnce instead of normal scheduler. +-- @field Sound.SRS#MSRS msrs Moose SRS class. +-- @extends Core.Base#BASE +RADIOQUEUE = { + ClassName = "RADIOQUEUE", + Debugmode = nil, + lid = nil, + frequency = nil, + modulation = nil, + scheduler = nil, + RQid = nil, + queue = {}, + alias = nil, + dt = nil, + delay = nil, + Tlast = nil, + sendercoord = nil, + sendername = nil, + senderinit = nil, + power = nil, + numbers = {}, + checking = nil, + schedonce = false, +} + +--- Radio queue transmission data. +-- @type RADIOQUEUE.Transmission +-- @field #string filename Name of the file to be transmitted. +-- @field #string path Path in miz file where the file is located. +-- @field #number duration Duration in seconds. +-- @field #string subtitle Subtitle of the transmission. +-- @field #number subduration Duration of the subtitle being displayed. +-- @field #number Tstarted Mission time (abs) in seconds when the transmission started. +-- @field #boolean isplaying If true, transmission is currently playing. +-- @field #number Tplay Mission time (abs) in seconds when the transmission should be played. +-- @field #number interval Interval in seconds before next transmission. +-- @field Sound.SoundOutput#SOUNDFILE soundfile Sound file object to play via SRS. +-- @field Sound.SoundOutput#SOUNDTEXT soundtext Sound TTS object to play via SRS. + + +--- Create a new RADIOQUEUE object for a given radio frequency/modulation. +-- @param #RADIOQUEUE self +-- @param #number frequency The radio frequency in MHz. +-- @param #number modulation (Optional) The radio modulation. Default `radio.modulation.AM` (=0). +-- @param #string alias (Optional) Name of the radio queue. +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:New(frequency, modulation, alias) + + -- Inherit base + local self=BASE:Inherit(self, BASE:New()) -- #RADIOQUEUE + + self.alias=alias or "My Radio" + + self.lid=string.format("RADIOQUEUE %s | ", self.alias) + + if frequency==nil then + self:E(self.lid.."ERROR: No frequency specified as first parameter!") + return nil + end + + -- Frequency in Hz. + self.frequency=frequency*1000000 + + -- Modulation. + self.modulation=modulation or radio.modulation.AM + + -- Set radio power. + self:SetRadioPower() + + -- Scheduler. + self.scheduler=SCHEDULER:New() + self.scheduler:NoTrace() + + return self +end + +--- Start the radio queue. +-- @param #RADIOQUEUE self +-- @param #number delay (Optional) Delay in seconds, before the radio queue is started. Default 1 sec. +-- @param #number dt (Optional) Time step in seconds for checking the queue. Default 0.01 sec. +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:Start(delay, dt) + + -- Delay before start. + self.delay=delay or 1 + + -- Time interval for queue check. + self.dt=dt or 0.01 + + -- Debug message. + self:I(self.lid..string.format("Starting RADIOQUEUE %s on Frequency %.2f MHz [modulation=%d] in %.1f seconds (dt=%.3f sec)", self.alias, self.frequency/1000000, self.modulation, self.delay, self.dt)) + + -- Start Scheduler. + if self.schedonce then + self:_CheckRadioQueueDelayed(self.delay) + else + self.RQid=self.scheduler:Schedule(nil, RADIOQUEUE._CheckRadioQueue, {self}, self.delay, self.dt) + end + + return self +end + +--- Stop the radio queue. Stop scheduler and delete queue. +-- @param #RADIOQUEUE self +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:Stop() + self:I(self.lid.."Stopping RADIOQUEUE.") + self.scheduler:Stop(self.RQid) + self.queue={} + return self +end + +--- Set coordinate from where the transmission is broadcasted. +-- @param #RADIOQUEUE self +-- @param Core.Point#COORDINATE coordinate Coordinate of the sender. +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:SetSenderCoordinate(coordinate) + self.sendercoord=coordinate + return self +end + +--- Set name of unit or static from which transmissions are made. +-- @param #RADIOQUEUE self +-- @param #string name Name of the unit or static used for transmissions. +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:SetSenderUnitName(name) + self.sendername=name + return self +end + +--- Set radio power. Note that this only applies if no relay unit is used. +-- @param #RADIOQUEUE self +-- @param #number power Radio power in Watts. Default 100 W. +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:SetRadioPower(power) + self.power=power or 100 + return self +end + +--- Set SRS. +-- @param #RADIOQUEUE self +-- @param #string PathToSRS Path to SRS. +-- @param #number Port SRS port. Default 5002. +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:SetSRS(PathToSRS, Port) + self.msrs=MSRS:New(PathToSRS, self.frequency/1000000, self.modulation) + self.msrs:SetPort(Port) + return self +end + +--- Set parameters of a digit. +-- @param #RADIOQUEUE self +-- @param #number digit The digit 0-9. +-- @param #string filename The name of the sound file. +-- @param #number duration The duration of the sound file in seconds. +-- @param #string path The directory within the miz file where the sound is located. Default "l10n/DEFAULT/". +-- @param #string subtitle Subtitle of the transmission. +-- @param #number subduration Duration [sec] of the subtitle being displayed. Default 5 sec. +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:SetDigit(digit, filename, duration, path, subtitle, subduration) + + local transmission={} --#RADIOQUEUE.Transmission + transmission.filename=filename + transmission.duration=duration + transmission.path=path or "l10n/DEFAULT/" + transmission.subtitle=nil + transmission.subduration=nil + + -- Convert digit to string in case it is given as a number. + if type(digit)=="number" then + digit=tostring(digit) + end + + -- Set transmission. + self.numbers[digit]=transmission + + return self +end + +--- Add a transmission to the radio queue. +-- @param #RADIOQUEUE self +-- @param #RADIOQUEUE.Transmission transmission The transmission data table. +-- @return #RADIOQUEUE self +function RADIOQUEUE:AddTransmission(transmission) + self:F({transmission=transmission}) + + -- Init. + transmission.isplaying=false + transmission.Tstarted=nil + + -- Add to queue. + table.insert(self.queue, transmission) + + -- Start checking. + if self.schedonce and not self.checking then + self:_CheckRadioQueueDelayed() + end + + return self +end + +--- Create a new transmission and add it to the radio queue. +-- @param #RADIOQUEUE self +-- @param #string filename Name of the sound file. Usually an ogg or wav file type. +-- @param #number duration Duration in seconds the file lasts. +-- @param #number path Directory path inside the miz file where the sound file is located. Default "l10n/DEFAULT/". +-- @param #number tstart Start time (abs) seconds. Default now. +-- @param #number interval Interval in seconds after the last transmission finished. +-- @param #string subtitle Subtitle of the transmission. +-- @param #number subduration Duration [sec] of the subtitle being displayed. Default 5 sec. +-- @return #RADIOQUEUE.Transmission Radio transmission table. +function RADIOQUEUE:NewTransmission(filename, duration, path, tstart, interval, subtitle, subduration) + + -- Sanity checks. + if not filename then + self:E(self.lid.."ERROR: No filename specified.") + return nil + end + if type(filename)~="string" then + self:E(self.lid.."ERROR: Filename specified is NOT a string.") + return nil + end + + if not duration then + self:E(self.lid.."ERROR: No duration of transmission specified.") + return nil + end + if type(duration)~="number" then + self:E(self.lid.."ERROR: Duration specified is NOT a number.") + return nil + end + + + local transmission={} --#RADIOQUEUE.Transmission + transmission.filename=filename + transmission.duration=duration + transmission.path=path or "l10n/DEFAULT/" + transmission.Tplay=tstart or timer.getAbsTime() + transmission.subtitle=subtitle + transmission.interval=interval or 0 + if transmission.subtitle then + transmission.subduration=subduration or 5 + else + transmission.subduration=nil + end + + -- Add transmission to queue. + self:AddTransmission(transmission) + + return transmission +end + +--- Add a SOUNDFILE to the radio queue. +-- @param #RADIOQUEUE self +-- @param Sound.SoundOutput#SOUNDFILE soundfile Sound file object to be added. +-- @param #number tstart Start time (abs) seconds. Default now. +-- @param #number interval Interval in seconds after the last transmission finished. +-- @return #RADIOQUEUE self +function RADIOQUEUE:AddSoundFile(soundfile, tstart, interval) + --env.info(string.format("FF add soundfile: name=%s%s", soundfile:GetPath(), soundfile:GetFileName())) + local transmission=self:NewTransmission(soundfile:GetFileName(), soundfile.duration, soundfile:GetPath(), tstart, interval, soundfile.subtitle, soundfile.subduration) + transmission.soundfile=soundfile + return self +end + +--- Add a SOUNDTEXT to the radio queue. +-- @param #RADIOQUEUE self +-- @param Sound.SoundOutput#SOUNDTEXT soundtext Text-to-speech text. +-- @param #number tstart Start time (abs) seconds. Default now. +-- @param #number interval Interval in seconds after the last transmission finished. +-- @return #RADIOQUEUE self +function RADIOQUEUE:AddSoundText(soundtext, tstart, interval) + + local transmission=self:NewTransmission("SoundText.ogg", soundtext.duration, nil, tstart, interval, soundtext.subtitle, soundtext.subduration) + transmission.soundtext=soundtext + return self +end + + +--- Convert a number (as string) into a radio transmission. +-- E.g. for board number or headings. +-- @param #RADIOQUEUE self +-- @param #string number Number string, e.g. "032" or "183". +-- @param #number delay Delay before transmission in seconds. +-- @param #number interval Interval between the next call. +-- @return #number Duration of the call in seconds. +function RADIOQUEUE:Number2Transmission(number, delay, interval) + + -- Split string into characters. + local numbers=UTILS.GetCharacters(number) + + local wait=0 + for i=1,#numbers do + + -- Current number + local n=numbers[i] + + -- Radio call. + local transmission=UTILS.DeepCopy(self.numbers[n]) --#RADIOQUEUE.Transmission + + transmission.Tplay=timer.getAbsTime()+(delay or 0) + + if interval and i==1 then + transmission.interval=interval + end + + self:AddTransmission(transmission) + + -- Add up duration of the number. + wait=wait+transmission.duration + end + + -- Return the total duration of the call. + return wait +end + + +--- Broadcast radio message. +-- @param #RADIOQUEUE self +-- @param #RADIOQUEUE.Transmission transmission The transmission. +function RADIOQUEUE:Broadcast(transmission) + + if ((transmission.soundfile and transmission.soundfile.useSRS) or transmission.soundtext) and self.msrs then + self:_BroadcastSRS(transmission) + return + end + + -- Get unit sending the transmission. + local sender=self:_GetRadioSender() + + -- Construct file name. + local filename=string.format("%s%s", transmission.path, transmission.filename) + + if sender then + + -- Broadcasting from aircraft. Only players tuned in to the right frequency will see the message. + self:T(self.lid..string.format("Broadcasting from aircraft %s", sender:GetName())) + + + if not self.senderinit then + + -- Command to set the Frequency for the transmission. + local commandFrequency={ + id="SetFrequency", + params={ + frequency=self.frequency, -- Frequency in Hz. + modulation=self.modulation, + }} + + -- Set commend for frequency + sender:SetCommand(commandFrequency) + + self.senderinit=true + end + + -- Set subtitle only if duration>0 sec. + local subtitle=nil + local duration=nil + if transmission.subtitle and transmission.subduration and transmission.subduration>0 then + subtitle=transmission.subtitle + duration=transmission.subduration + end + + -- Command to tranmit the call. + local commandTransmit={ + id = "TransmitMessage", + params = { + file=filename, + duration=duration, + subtitle=subtitle, + loop=false, + }} + + -- Set command for radio transmission. + sender:SetCommand(commandTransmit) + + -- Debug message. + if self.Debugmode then + local text=string.format("file=%s, freq=%.2f MHz, duration=%.2f sec, subtitle=%s", filename, self.frequency/1000000, transmission.duration, transmission.subtitle or "") + MESSAGE:New(text, 2, "RADIOQUEUE "..self.alias):ToAll() + end + + else + + -- Broadcasting from carrier. No subtitle possible. Need to send messages to players. + self:T(self.lid..string.format("Broadcasting via trigger.action.radioTransmission().")) + + -- Position from where to transmit. + local vec3=nil + + -- Try to get positon from sender unit/static. + if self.sendername then + vec3=self:_GetRadioSenderCoord() + end + + -- Try to get fixed positon. + if self.sendercoord and not vec3 then + vec3=self.sendercoord:GetVec3() + end + + -- Transmit via trigger. + if vec3 then + self:T("Sending") + self:T( { filename = filename, vec3 = vec3, modulation = self.modulation, frequency = self.frequency, power = self.power } ) + + -- Trigger transmission. + trigger.action.radioTransmission(filename, vec3, self.modulation, false, self.frequency, self.power) + + -- Debug message. + if self.Debugmode then + local text=string.format("file=%s, freq=%.2f MHz, duration=%.2f sec, subtitle=%s", filename, self.frequency/1000000, transmission.duration, transmission.subtitle or "") + MESSAGE:New(string.format(text, filename, transmission.duration, transmission.subtitle or ""), 5, "RADIOQUEUE "..self.alias):ToAll() + end + end + + end +end + +--- Broadcast radio message. +-- @param #RADIOQUEUE self +-- @param #RADIOQUEUE.Transmission transmission The transmission. +function RADIOQUEUE:_BroadcastSRS(transmission) + + if transmission.soundfile and transmission.soundfile.useSRS then + self.msrs:PlaySoundFile(transmission.soundfile) + elseif transmission.soundtext then + self.msrs:PlaySoundText(transmission.soundtext) + end + +end + +--- Start checking the radio queue. +-- @param #RADIOQUEUE self +-- @param #number delay Delay in seconds before checking. +function RADIOQUEUE:_CheckRadioQueueDelayed(delay) + self.checking=true + self:ScheduleOnce(delay or self.dt, RADIOQUEUE._CheckRadioQueue, self) +end + +--- Check radio queue for transmissions to be broadcasted. +-- @param #RADIOQUEUE self +function RADIOQUEUE:_CheckRadioQueue() + --env.info("FF check radio queue "..self.alias) + + -- Check if queue is empty. + if #self.queue==0 then + -- Queue is now empty. Nothing to else to do. + self.checking=false + return + end + + -- Get current abs time. + local time=timer.getAbsTime() + + local playing=false + local next=nil --#RADIOQUEUE.Transmission + local remove=nil + for i,_transmission in ipairs(self.queue) do + local transmission=_transmission --#RADIOQUEUE.Transmission + + -- Check if transmission time has passed. + if time>=transmission.Tplay then + + -- Check if transmission is currently playing. + if transmission.isplaying then + + -- Check if transmission is finished. + if time>=transmission.Tstarted+transmission.duration then + + -- Transmission over. + transmission.isplaying=false + + -- Remove ith element in queue. + remove=i + + -- Store time last transmission finished. + self.Tlast=time + + else -- still playing + + -- Transmission is still playing. + playing=true + + end + + else -- not playing yet + + local Tlast=self.Tlast + + if transmission.interval==nil then + + -- Not playing ==> this will be next. + if next==nil then + next=transmission + end + + else + + if Tlast==nil or time-Tlast>=transmission.interval then + next=transmission + else + + end + end + + -- We got a transmission or one with an interval that is not due yet. No need for anything else. + if next or Tlast then + break + end + + end + + else + + -- Transmission not due yet. + + end + end + + -- Found a new transmission. + if next~=nil and not playing then + self:Broadcast(next) + next.isplaying=true + next.Tstarted=time + end + + -- Remove completed calls from queue. + if remove then + table.remove(self.queue, remove) + end + + -- Check queue. + if self.schedonce then + self:_CheckRadioQueueDelayed() + end + +end + +--- Get unit from which we want to transmit a radio message. This has to be an aircraft for subtitles to work. +-- @param #RADIOQUEUE self +-- @return Wrapper.Unit#UNIT Sending unit or nil if was not setup, is not an aircraft or ground unit or is not alive. +function RADIOQUEUE:_GetRadioSender() + + -- Check if we have a sending aircraft. + local sender=nil --Wrapper.Unit#UNIT + + -- Try the general default. + if self.sendername then + + -- First try to find a unit + sender=UNIT:FindByName(self.sendername) + + -- Check that sender is alive and an aircraft. + if sender and sender:IsAlive() and (sender:IsAir() or sender:IsGround()) then + return sender + end + + end + + return nil +end + +--- Get unit from which we want to transmit a radio message. This has to be an aircraft or ground unit for subtitles to work. +-- @param #RADIOQUEUE self +-- @return DCS#Vec3 Vector 3D. +function RADIOQUEUE:_GetRadioSenderCoord() + + local vec3=nil + + -- Try the general default. + if self.sendername then + + -- First try to find a unit + local sender=UNIT:FindByName(self.sendername) + + -- Check that sender is alive and an aircraft. + if sender and sender:IsAlive() then + return sender:GetVec3() + end + + -- Now try a static. + local sender=STATIC:FindByName( self.sendername, false ) + + -- Check that sender is alive and an aircraft. + if sender then + return sender:GetVec3() + end + + end + + return nil +end +--- **Core** - Makes the radio talk. +-- +-- === +-- +-- ## Features: +-- +-- * Send text strings using a vocabulary that is converted in spoken language. +-- * Possiblity to implement multiple language. +-- +-- === +-- +-- ### Authors: FlightControl +-- +-- @module Sound.RadioSpeech +-- @image Core_Radio.JPG + +--- Makes the radio speak. +-- +-- # RADIOSPEECH usage +-- +-- +-- @type RADIOSPEECH +-- @extends Core.RadioQueue#RADIOQUEUE +RADIOSPEECH = { + ClassName = "RADIOSPEECH", + Vocabulary = { + EN = {}, + DE = {}, + RU = {}, + } +} + + +RADIOSPEECH.Vocabulary.EN = { + ["1"] = { "1", 0.25 }, + ["2"] = { "2", 0.25 }, + ["3"] = { "3", 0.30 }, + ["4"] = { "4", 0.35 }, + ["5"] = { "5", 0.35 }, + ["6"] = { "6", 0.42 }, + ["7"] = { "7", 0.38 }, + ["8"] = { "8", 0.20 }, + ["9"] = { "9", 0.32 }, + ["10"] = { "10", 0.35 }, + ["11"] = { "11", 0.40 }, + ["12"] = { "12", 0.42 }, + ["13"] = { "13", 0.38 }, + ["14"] = { "14", 0.42 }, + ["15"] = { "15", 0.42 }, + ["16"] = { "16", 0.52 }, + ["17"] = { "17", 0.59 }, + ["18"] = { "18", 0.40 }, + ["19"] = { "19", 0.47 }, + ["20"] = { "20", 0.38 }, + ["30"] = { "30", 0.29 }, + ["40"] = { "40", 0.35 }, + ["50"] = { "50", 0.32 }, + ["60"] = { "60", 0.44 }, + ["70"] = { "70", 0.48 }, + ["80"] = { "80", 0.26 }, + ["90"] = { "90", 0.36 }, + ["100"] = { "100", 0.55 }, + ["200"] = { "200", 0.55 }, + ["300"] = { "300", 0.61 }, + ["400"] = { "400", 0.60 }, + ["500"] = { "500", 0.61 }, + ["600"] = { "600", 0.65 }, + ["700"] = { "700", 0.70 }, + ["800"] = { "800", 0.54 }, + ["900"] = { "900", 0.60 }, + ["1000"] = { "1000", 0.60 }, + ["2000"] = { "2000", 0.61 }, + ["3000"] = { "3000", 0.64 }, + ["4000"] = { "4000", 0.62 }, + ["5000"] = { "5000", 0.69 }, + ["6000"] = { "6000", 0.69 }, + ["7000"] = { "7000", 0.75 }, + ["8000"] = { "8000", 0.59 }, + ["9000"] = { "9000", 0.65 }, + + ["chevy"] = { "chevy", 0.35 }, + ["colt"] = { "colt", 0.35 }, + ["springfield"] = { "springfield", 0.65 }, + ["dodge"] = { "dodge", 0.35 }, + ["enfield"] = { "enfield", 0.5 }, + ["ford"] = { "ford", 0.32 }, + ["pontiac"] = { "pontiac", 0.55 }, + ["uzi"] = { "uzi", 0.28 }, + + ["degrees"] = { "degrees", 0.5 }, + ["kilometers"] = { "kilometers", 0.65 }, + ["km"] = { "kilometers", 0.65 }, + ["miles"] = { "miles", 0.45 }, + ["meters"] = { "meters", 0.41 }, + ["mi"] = { "miles", 0.45 }, + ["feet"] = { "feet", 0.29 }, + + ["br"] = { "br", 1.1 }, + ["bra"] = { "bra", 0.3 }, + + + ["returning to base"] = { "returning_to_base", 0.85 }, + ["on route to ground target"] = { "on_route_to_ground_target", 1.05 }, + ["intercepting bogeys"] = { "intercepting_bogeys", 1.00 }, + ["engaging ground target"] = { "engaging_ground_target", 1.20 }, + ["engaging bogeys"] = { "engaging_bogeys", 0.81 }, + ["wheels up"] = { "wheels_up", 0.42 }, + ["landing at base"] = { "landing at base", 0.8 }, + ["patrolling"] = { "patrolling", 0.55 }, + + ["for"] = { "for", 0.31 }, + ["and"] = { "and", 0.31 }, + ["at"] = { "at", 0.3 }, + ["dot"] = { "dot", 0.26 }, + ["defender"] = { "defender", 0.45 }, +} + +RADIOSPEECH.Vocabulary.RU = { + ["1"] = { "1", 0.34 }, + ["2"] = { "2", 0.30 }, + ["3"] = { "3", 0.23 }, + ["4"] = { "4", 0.51 }, + ["5"] = { "5", 0.31 }, + ["6"] = { "6", 0.44 }, + ["7"] = { "7", 0.25 }, + ["8"] = { "8", 0.43 }, + ["9"] = { "9", 0.45 }, + ["10"] = { "10", 0.53 }, + ["11"] = { "11", 0.66 }, + ["12"] = { "12", 0.70 }, + ["13"] = { "13", 0.66 }, + ["14"] = { "14", 0.80 }, + ["15"] = { "15", 0.65 }, + ["16"] = { "16", 0.75 }, + ["17"] = { "17", 0.74 }, + ["18"] = { "18", 0.85 }, + ["19"] = { "19", 0.80 }, + ["20"] = { "20", 0.58 }, + ["30"] = { "30", 0.51 }, + ["40"] = { "40", 0.51 }, + ["50"] = { "50", 0.67 }, + ["60"] = { "60", 0.76 }, + ["70"] = { "70", 0.68 }, + ["80"] = { "80", 0.84 }, + ["90"] = { "90", 0.71 }, + ["100"] = { "100", 0.35 }, + ["200"] = { "200", 0.59 }, + ["300"] = { "300", 0.53 }, + ["400"] = { "400", 0.70 }, + ["500"] = { "500", 0.50 }, + ["600"] = { "600", 0.58 }, + ["700"] = { "700", 0.64 }, + ["800"] = { "800", 0.77 }, + ["900"] = { "900", 0.75 }, + ["1000"] = { "1000", 0.87 }, + ["2000"] = { "2000", 0.83 }, + ["3000"] = { "3000", 0.84 }, + ["4000"] = { "4000", 1.00 }, + ["5000"] = { "5000", 0.77 }, + ["6000"] = { "6000", 0.90 }, + ["7000"] = { "7000", 0.77 }, + ["8000"] = { "8000", 0.92 }, + ["9000"] = { "9000", 0.87 }, + + ["градусы"] = { "degrees", 0.5 }, + ["километры"] = { "kilometers", 0.65 }, + ["km"] = { "kilometers", 0.65 }, + ["мили"] = { "miles", 0.45 }, + ["mi"] = { "miles", 0.45 }, + ["метров"] = { "meters", 0.41 }, + ["m"] = { "meters", 0.41 }, + ["ноги"] = { "feet", 0.37 }, + + ["br"] = { "br", 1.1 }, + ["bra"] = { "bra", 0.3 }, + + + ["возвращение на базу"] = { "returning_to_base", 1.40 }, + ["на пути к наземной цели"] = { "on_route_to_ground_target", 1.45 }, + ["перехват боги"] = { "intercepting_bogeys", 1.22 }, + ["поражение наземной цели"] = { "engaging_ground_target", 1.53 }, + ["привлечение болотных птиц"] = { "engaging_bogeys", 1.68 }, + ["колёса вверх..."] = { "wheels_up", 0.92 }, + ["посадка на базу"] = { "landing at base", 1.04 }, + ["патрулирование"] = { "patrolling", 0.96 }, + + ["для"] = { "for", 0.27 }, + ["и"] = { "and", 0.17 }, + ["на сайте"] = { "at", 0.19 }, + ["точка"] = { "dot", 0.51 }, + ["защитник"] = { "defender", 0.45 }, +} + +--- Create a new RADIOSPEECH object for a given radio frequency/modulation. +-- @param #RADIOSPEECH self +-- @param #number frequency The radio frequency in MHz. +-- @param #number modulation (Optional) The radio modulation. Default radio.modulation.AM. +-- @return #RADIOSPEECH self The RADIOSPEECH object. +function RADIOSPEECH:New(frequency, modulation) + + -- Inherit base + local self = BASE:Inherit( self, RADIOQUEUE:New( frequency, modulation ) ) -- #RADIOSPEECH + + self.Language = "EN" + + self:BuildTree() + + return self +end + +function RADIOSPEECH:SetLanguage( Langauge ) + + self.Language = Langauge +end + + +--- Add Sentence to the Speech collection. +-- @param #RADIOSPEECH self +-- @param #string RemainingSentence The remaining sentence during recursion. +-- @param #table Speech The speech node. +-- @param #string Sentence The full sentence. +-- @param #string Data The speech data. +-- @return #RADIOSPEECH self The RADIOSPEECH object. +function RADIOSPEECH:AddSentenceToSpeech( RemainingSentence, Speech, Sentence, Data ) + + self:I( { RemainingSentence, Speech, Sentence, Data } ) + + local Token, RemainingSentence = RemainingSentence:match( "^ *([^ ]+)(.*)" ) + self:I( { Token = Token, RemainingSentence = RemainingSentence } ) + + -- Is there a Token? + if Token then + + -- We check if the Token is already in the Speech collection. + if not Speech[Token] then + + -- There is not yet a vocabulary registered for this. + Speech[Token] = {} + + if RemainingSentence and RemainingSentence ~= "" then + -- We use recursion to iterate through the complete Sentence, and make a chain of Tokens. + -- The last Speech node in the collection contains the Sentence and the Data to be spoken. + -- This to ensure that during the actual speech: + -- - Complete sentences are being understood. + -- - Words without speech are ignored. + -- - Incorrect sequence of words are ignored. + Speech[Token].Next = {} + self:AddSentenceToSpeech( RemainingSentence, Speech[Token].Next, Sentence, Data ) + else + -- There is no remaining sentence, so we add speech to the Sentence. + -- The recursion stops here. + Speech[Token].Sentence = Sentence + Speech[Token].Data = Data + end + end + end +end + +--- Build the tree structure based on the language words, in order to find the correct sentences and to ignore incomprehensible words. +-- @param #RADIOSPEECH self +-- @return #RADIOSPEECH self The RADIOSPEECH object. +function RADIOSPEECH:BuildTree() + + self.Speech = {} + + for Language, Sentences in pairs( self.Vocabulary ) do + self:I( { Language = Language, Sentences = Sentences }) + self.Speech[Language] = {} + for Sentence, Data in pairs( Sentences ) do + self:I( { Sentence = Sentence, Data = Data } ) + self:AddSentenceToSpeech( Sentence, self.Speech[Language], Sentence, Data ) + end + end + + self:I( { Speech = self.Speech } ) + + return self +end + +--- Speak a sentence. +-- @param #RADIOSPEECH self +-- @param #string Sentence The sentence to be spoken. +function RADIOSPEECH:SpeakWords( Sentence, Speech, Language ) + + local OriginalSentence = Sentence + + -- lua does not parse UTF-8, so the match statement will fail on cyrillic using %a. + -- therefore, the only way to parse the statement is to use blank, comma or dot as a delimiter. + -- and then check if the character can be converted to a number or not. + local Word, RemainderSentence = Sentence:match( "^[., ]*([^ .,]+)(.*)" ) + + self:I( { Word = Word, Speech = Speech[Word], RemainderSentence = RemainderSentence } ) + + + if Word then + if Word ~= "" and tonumber(Word) == nil then + + -- Construct of words + Word = Word:lower() + if Speech[Word] then + -- The end of the sentence has been reached. Now Speech.Next should be nil, otherwise there is an error. + if Speech[Word].Next == nil then + self:I( { Sentence = Speech[Word].Sentence, Data = Speech[Word].Data } ) + self:NewTransmission( Speech[Word].Data[1] .. ".wav", Speech[Word].Data[2], Language .. "/" ) + else + if RemainderSentence and RemainderSentence ~= "" then + return self:SpeakWords( RemainderSentence, Speech[Word].Next, Language ) + end + end + end + return RemainderSentence + end + return OriginalSentence + else + return "" + end + +end + +--- Speak a sentence. +-- @param #RADIOSPEECH self +-- @param #string Sentence The sentence to be spoken. +function RADIOSPEECH:SpeakDigits( Sentence, Speech, Langauge ) + + local OriginalSentence = Sentence + + -- lua does not parse UTF-8, so the match statement will fail on cyrillic using %a. + -- therefore, the only way to parse the statement is to use blank, comma or dot as a delimiter. + -- and then check if the character can be converted to a number or not. + local Digits, RemainderSentence = Sentence:match( "^[., ]*([^ .,]+)(.*)" ) + + self:I( { Digits = Digits, Speech = Speech[Digits], RemainderSentence = RemainderSentence } ) + + if Digits then + if Digits ~= "" and tonumber( Digits ) ~= nil then + + -- Construct numbers + local Number = tonumber( Digits ) + local Multiple = nil + while Number >= 0 do + if Number > 1000 then + Multiple = math.floor( Number / 1000 ) * 1000 + elseif Number > 100 then + Multiple = math.floor( Number / 100 ) * 100 + elseif Number > 20 then + Multiple = math.floor( Number / 10 ) * 10 + elseif Number >= 0 then + Multiple = Number + end + Sentence = tostring( Multiple ) + if Speech[Sentence] then + self:I( { Speech = Speech[Sentence].Sentence, Data = Speech[Sentence].Data } ) + self:NewTransmission( Speech[Sentence].Data[1] .. ".wav", Speech[Sentence].Data[2], Langauge .. "/" ) + end + Number = Number - Multiple + Number = ( Number == 0 ) and -1 or Number + end + return RemainderSentence + end + return OriginalSentence + else + return "" + end + +end + + + +--- Speak a sentence. +-- @param #RADIOSPEECH self +-- @param #string Sentence The sentence to be spoken. +function RADIOSPEECH:Speak( Sentence, Language ) + + self:I( { Sentence, Language } ) + + local Language = Language or "EN" + + self:I( { Language = Language } ) + + -- If there is no node for Speech, then we start at the first nodes of the language. + local Speech = self.Speech[Language] + + self:I( { Speech = Speech, Language = Language } ) + + self:NewTransmission( "_In.wav", 0.52, Language .. "/" ) + + repeat + + Sentence = self:SpeakWords( Sentence, Speech, Language ) + + self:I( { Sentence = Sentence } ) + + Sentence = self:SpeakDigits( Sentence, Speech, Language ) + + self:I( { Sentence = Sentence } ) + +-- Sentence = self:SpeakSymbols( Sentence, Speech ) +-- +-- self:I( { Sentence = Sentence } ) + + until not Sentence or Sentence == "" + + self:NewTransmission( "_Out.wav", 0.28, Language .. "/" ) + +end +--- **Sound** - Simple Radio Standalone (SRS) Integration. +-- +-- === +-- +-- **Main Features:** +-- +-- * Play sound files via SRS +-- * Play text-to-speach via SRS +-- +-- === +-- +-- ## Youtube Videos: None yet +-- +-- === +-- +-- ## Missions: None yet +-- +-- === +-- +-- ## Sound files: [MOOSE Sound Files](https://github.com/FlightControl-Master/MOOSE_SOUND/releases) +-- +-- === +-- +-- The goal of the [SRS](https://github.com/ciribob/DCS-SimpleRadioStandalone) project is to bring VoIP communication into DCS and to make communication as frictionless as possible. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Sound.MSRS +-- @image Sound_MSRS.png + +--- MSRS class. +-- @type MSRS +-- @field #string ClassName Name of the class. +-- @field #string lid Class id string for output to DCS log file. +-- @field #table frequencies Frequencies used in the transmissions. +-- @field #table modulations Modulations used in the transmissions. +-- @field #number coalition Coalition of the transmission. +-- @field #number port Port. Default 5002. +-- @field #string name Name. Default "DCS-STTS". +-- @field #number volume Volume between 0 (min) and 1 (max). Default 1. +-- @field #string culture Culture. Default "en-GB". +-- @field #string gender Gender. Default "female". +-- @field #string voice Specifc voce. +-- @field Core.Point#COORDINATE coordinate Coordinate from where the transmission is send. +-- @field #string path Path to the SRS exe. This includes the final slash "/". +-- @field #string google Full path google credentials JSON file, e.g. "C:\Users\username\Downloads\service-account-file.json". +-- @extends Core.Base#BASE + +--- *It is a very sad thing that nowadays there is so little useless information.* - Oscar Wilde +-- +-- === +-- +-- ![Banner Image](..\Presentations\ATIS\ATIS_Main.png) +-- +-- # The MSRS Concept +-- +-- This class allows to broadcast sound files or text via Simple Radio Standalone (SRS). +-- +-- ## Prerequisites +-- +-- This script needs SRS version >= 1.9.6. +-- +-- # Play Sound Files +-- +-- local soundfile=SOUNDFILE:New("My Soundfile.ogg", "D:\\Sounds For DCS") +-- local msrs=MSRS:New("C:\\Path To SRS", 251, radio.modulation.AM) +-- msrs:PlaySoundFile(soundfile) +-- +-- # Play Text-To-Speech +-- +-- Basic example: +-- +-- -- Create a SOUNDTEXT object. +-- local text=SOUNDTEXT:New("All Enemies destroyed") +-- +-- -- MOOSE SRS +-- local msrs=MSRS:New("D:\\DCS\\_SRS\\", 305, radio.modulation.AM) +-- +-- -- Text-to speech with default voice after 2 seconds. +-- msrs:PlaySoundText(text, 2) +-- +-- ## Set Gender +-- +-- Use a specific gender with the @{#MSRS.SetGender} function, e.g. `SetGender("male")` or `:SetGender("female")`. +-- +-- ## Set Culture +-- +-- Use a specific "culture" with the @{#MSRS.SetCulture} function, e.g. `:SetCulture("en-US")` or `:SetCulture("de-DE")`. +-- +-- ## Set Voice +-- +-- Use a specifc voice with the @{#MSRS.SetVoice} function, e.g, `:SetVoice("Microsoft Hedda Desktop")`. +-- Note that this must be installed on your windows system. +-- +-- ## Set Coordinate +-- +-- Use @{#MSRS.SetCoordinate} to define the origin from where the transmission is broadcasted. +-- +-- @field #MSRS +MSRS = { + ClassName = "MSRS", + lid = nil, + port = 5002, + name = "MSRS", + frequencies = {}, + modulations = {}, + coalition = 0, + gender = "female", + culture = nil, + voice = nil, + volume = 1, + speed = 1, + coordinate = nil, +} + +--- MSRS class version. +-- @field #string version +MSRS.version="0.0.3" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Add functions to add/remove freqs and modulations. +-- DONE: Add coordinate. +-- DONE: Add google. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new MSRS object. +-- @param #MSRS self +-- @param #string PathToSRS Path to the directory, where SRS is located. +-- @param #number Frequency Radio frequency in MHz. Default 143.00 MHz. Can also be given as a #table of multiple frequencies. +-- @param #number Modulation Radio modulation: 0=AM (default), 1=FM. See `radio.modulation.AM` and `radio.modulation.FM` enumerators. Can also be given as a #table of multiple modulations. +-- @return #MSRS self +function MSRS:New(PathToSRS, Frequency, Modulation) + + -- Defaults. + Frequency =Frequency or 143 + Modulation= Modulation or radio.modulation.AM + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, BASE:New()) -- #MSRS + + self:SetPath(PathToSRS) + self:SetPort() + self:SetFrequencies(Frequency) + self:SetModulations(Modulation) + self:SetGender() + self:SetCoalition() + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set path to SRS install directory. More precisely, path to where the DCS- +-- @param #MSRS self +-- @param #string Path Path to the directory, where the sound file is located. This does **not** contain a final backslash or slash. +-- @return #MSRS self +function MSRS:SetPath(Path) + + if Path==nil then + self:E("ERROR: No path to SRS directory specified!") + return nil + end + + -- Set path. + self.path=Path + + -- Remove (back)slashes. + local n=1 ; local nmax=1000 + while (self.path:sub(-1)=="/" or self.path:sub(-1)==[[\]]) and n<=nmax do + self.path=self.path:sub(1,#self.path-1) + n=n+1 + end + + -- Debug output. + self:T(string.format("SRS path=%s", self:GetPath())) + + return self +end + +--- Get path to SRS directory. +-- @param #MSRS self +-- @return #string Path to the directory. This includes the final slash "/". +function MSRS:GetPath() + return self.path +end + +--- Set port. +-- @param #MSRS self +-- @param #number Port Port. Default 5002. +-- @return #MSRS self +function MSRS:SetPort(Port) + self.port=Port or 5002 +end + +--- Get port. +-- @param #MSRS self +-- @return #number Port. +function MSRS:GetPort() + return self.port +end + +--- Set coalition. +-- @param #MSRS self +-- @param #number Coalition Coalition. Default 0. +-- @return #MSRS self +function MSRS:SetCoalition(Coalition) + self.coalition=Coalition or 0 +end + +--- Get coalition. +-- @param #MSRS self +-- @return #number Coalition. +function MSRS:GetCoalition() + return self.coalition +end + + +--- Set frequencies. +-- @param #MSRS self +-- @param #table Frequencies Frequencies in MHz. Can also be given as a #number if only one frequency should be used. +-- @return #MSRS self +function MSRS:SetFrequencies(Frequencies) + + -- Ensure table. + if type(Frequencies)~="table" then + Frequencies={Frequencies} + end + + self.frequencies=Frequencies + + return self +end + +--- Get frequencies. +-- @param #MSRS self +-- @param #table Frequencies in MHz. +function MSRS:GetFrequencies() + return self.frequencies +end + + +--- Set modulations. +-- @param #MSRS self +-- @param #table Modulations Modulations. Can also be given as a #number if only one modulation should be used. +-- @return #MSRS self +function MSRS:SetModulations(Modulations) + + -- Ensure table. + if type(Modulations)~="table" then + Modulations={Modulations} + end + + self.modulations=Modulations + + return self +end + +--- Get modulations. +-- @param #MSRS self +-- @param #table Modulations. +function MSRS:GetModulations() + return self.modulations +end + +--- Set gender. +-- @param #MSRS self +-- @param #string Gender Gender: "male" or "female" (default). +-- @return #MSRS self +function MSRS:SetGender(Gender) + + Gender=Gender or "female" + + self.gender=Gender:lower() + + -- Debug output. + self:T("Setting gender to "..tostring(self.gender)) + + return self +end + +--- Set culture. +-- @param #MSRS self +-- @param #string Culture Culture, e.g. "en-GB" (default). +-- @return #MSRS self +function MSRS:SetCulture(Culture) + + self.culture=Culture + + return self +end + +--- Set to use a specific voice. Will override gender and culture settings. +-- @param #MSRS self +-- @param #string Voice Voice. +-- @return #MSRS self +function MSRS:SetVoice(Voice) + + self.voice=Voice + + return self +end + +--- Set the coordinate from which the transmissions will be broadcasted. +-- @param #MSRS self +-- @param Core.Point#COORDINATE Coordinate Origin of the transmission. +-- @return #MSRS self +function MSRS:SetCoordinate(Coordinate) + + self.coordinate=Coordinate + + return self +end + +--- Use google text-to-speech. +-- @param #MSRS self +-- @param PathToCredentials Full path to the google credentials JSON file, e.g. "C:\Users\username\Downloads\service-account-file.json". +-- @return #MSRS self +function MSRS:SetGoogle(PathToCredentials) + + self.google=PathToCredentials + + return self +end + +--- Print SRS STTS help to DCS log file. +-- @param #MSRS self +-- @return #MSRS self +function MSRS:Help() + + -- Path and exe. + local path=self:GetPath() or STTS.DIRECTORY + local exe=STTS.EXECUTABLE or "DCS-SR-ExternalAudio.exe" + + -- Text file for output. + local filename = os.getenv('TMP') .. "\\MSRS-help-"..STTS.uuid()..".txt" + + -- Print help. + local command=string.format("%s/%s --help > %s", path, exe, filename) + os.execute(command) + + local f=assert(io.open(filename, "rb")) + local data=f:read("*all") + f:close() + + -- Print to log file. + env.info("SRS STTS help output:") + env.info("======================================================================") + env.info(data) + env.info("======================================================================") + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Transmission Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Play sound file (ogg or mp3) via SRS. +-- @param #MSRS self +-- @param Sound.SoundFile#SOUNDFILE Soundfile Sound file to play. +-- @param #number Delay Delay in seconds, before the sound file is played. +-- @return #MSRS self +function MSRS:PlaySoundFile(Soundfile, Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, MSRS.PlaySoundFile, self, Soundfile, 0) + else + + -- Sound file name. + local soundfile=Soundfile:GetName() + + -- Get command. + local command=self:_GetCommand() + + -- Append file. + command=command.." --file="..tostring(soundfile) + + self:_ExecCommand(command) + + --[[ + + command=command.." > bla.txt" + + -- Debug output. + self:I(string.format("MSRS PlaySoundfile command=%s", command)) + + -- Execute SRS command. + local x=os.execute(command) + + ]] + + end + + return self +end + +--- Play a SOUNDTEXT text-to-speech object. +-- @param #MSRS self +-- @param Sound.SoundFile#SOUNDTEXT SoundText Sound text. +-- @param #number Delay Delay in seconds, before the sound file is played. +-- @return #MSRS self +function MSRS:PlaySoundText(SoundText, Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, MSRS.PlaySoundText, self, SoundText, 0) + else + + -- Get command. + local command=self:_GetCommand(nil, nil, nil, SoundText.gender, SoundText.voice, SoundText.culture, SoundText.volume, SoundText.speed) + + -- Append text. + command=command..string.format(" --text=\"%s\"", tostring(SoundText.text)) + + -- Execute command. + self:_ExecCommand(command) + + --[[ + command=command.." > bla.txt" + + -- Debug putput. + self:I(string.format("MSRS PlaySoundfile command=%s", command)) + + -- Execute SRS command. + local x=os.execute(command) + ]] + + end + + return self +end + +--- Play text message via STTS. +-- @param #MSRS self +-- @param #string Text Text message. +-- @param #number Delay Delay in seconds, before the message is played. +-- @return #MSRS self +function MSRS:PlayText(Text, Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, MSRS.PlayText, self, Text, 0) + else + + -- Get command line. + local command=self:_GetCommand() + + -- Append text. + command=command..string.format(" --text=\"%s\"", tostring(Text)) + + -- Execute command. + self:_ExecCommand(command) + + --[[ + + -- Check that length of command is max 255 chars or os.execute() will not work! + if string.len(command)>255 then + + -- Create a tmp file. + local filename = os.getenv('TMP') .. "\\MSRS-"..STTS.uuid()..".bat" + + local script = io.open(filename, "w+") + script:write(command.." && exit") + script:close() + + -- Play command. + command=string.format("\"%s\"", filename) + + -- Play file in 0.05 seconds + timer.scheduleFunction(os.execute, command, timer.getTime()+0.05) + + -- Remove file in 1 second. + timer.scheduleFunction(os.remove, filename, timer.getTime()+1) + else + + -- Debug output. + self:I(string.format("MSRS Text command=%s", command)) + + -- Execute SRS command. + local x=os.execute(command) + + end + + ]] + end + + return self +end + + +--- Play text file via STTS. +-- @param #MSRS self +-- @param #string TextFile Full path to the file. +-- @param #number Delay Delay in seconds, before the message is played. +-- @return #MSRS self +function MSRS:PlayTextFile(TextFile, Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, MSRS.PlayTextFile, self, TextFile, 0) + else + + -- First check if text file exists! + local exists=UTILS.FileExists(TextFile) + if not exists then + self:E("ERROR: MSRS Text file does not exist! File="..tostring(TextFile)) + return self + end + + -- Get command line. + local command=self:_GetCommand() + + -- Append text file. + command=command..string.format(" --textFile=\"%s\"", tostring(TextFile)) + + -- Debug output. + self:T(string.format("MSRS TextFile command=%s", command)) + + -- Count length of command. + local l=string.len(command) + + -- Execute command. + self:_ExecCommand(command) + + end + + return self +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Execute SRS command to play sound using the `DCS-SR-ExternalAudio.exe`. +-- @param #MSRS self +-- @param #string command Command to executer +-- @return #number Return value of os.execute() command. +function MSRS:_ExecCommand(command) + + -- Create a tmp file. + local filename=os.getenv('TMP').."\\MSRS-"..STTS.uuid()..".bat" + + local script=io.open(filename, "w+") + script:write(command.." && exit") + script:close() + + -- Play command. + command=string.format('start /b "" "%s"', filename) + + local res=nil + if true then + + -- Create a tmp file. + local filenvbs = os.getenv('TMP') .. "\\MSRS-"..STTS.uuid()..".vbs" + + -- VBS script + local script = io.open(filenvbs, "w+") + script:write(string.format('Dim WinScriptHost\n')) + script:write(string.format('Set WinScriptHost = CreateObject("WScript.Shell")\n')) + script:write(string.format('WinScriptHost.Run Chr(34) & "%s" & Chr(34), 0\n', filename)) + script:write(string.format('Set WinScriptHost = Nothing')) + script:close() + + -- Run visual basic script. This still pops up a window but very briefly and does not put the DCS window out of focus. + local runvbs=string.format('cscript.exe //Nologo //B "%s"', filenvbs) + + -- Debug output. + self:T("MSRS execute command="..command) + self:T("MSRS execute VBS command="..runvbs) + + -- Play file in 0.01 seconds + res=os.execute(runvbs) + + -- Remove file in 1 second. + timer.scheduleFunction(os.remove, filename, timer.getTime()+1) + timer.scheduleFunction(os.remove, filenvbs, timer.getTime()+1) + + + else + + -- Debug output. + self:T("MSRS execute command="..command) + + -- Execute command + res=os.execute(command) + + -- Remove file in 1 second. + timer.scheduleFunction(os.remove, filename, timer.getTime()+1) + + end + + + return res +end + +--- Get lat, long and alt from coordinate. +-- @param #MSRS self +-- @param Core.Point#Coordinate Coordinate Coordinate. Can also be a DCS#Vec3. +-- @return #number Latitude. +-- @return #number Longitude. +-- @return #number Altitude. +function MSRS:_GetLatLongAlt(Coordinate) + + local lat, lon, alt=coord.LOtoLL(Coordinate) + + return lat, lon, math.floor(alt) +end + + +--- Get SRS command to play sound using the `DCS-SR-ExternalAudio.exe`. +-- @param #MSRS self +-- @param #table freqs Frequencies in MHz. +-- @param #table modus Modulations. +-- @param #number coal Coalition. +-- @param #string gender Gender. +-- @param #string voice Voice. +-- @param #string culture Culture. +-- @param #number volume Volume. +-- @param #number speed Speed. +-- @param #number port Port. +-- @return #string Command. +function MSRS:_GetCommand(freqs, modus, coal, gender, voice, culture, volume, speed, port) + + local path=self:GetPath() or STTS.DIRECTORY + local exe=STTS.EXECUTABLE or "DCS-SR-ExternalAudio.exe" + freqs=table.concat(freqs or self.frequencies, ",") + modus=table.concat(modus or self.modulations, ",") + coal=coal or self.coalition + gender=gender or self.gender + voice=voice or self.voice + culture=culture or self.culture + volume=volume or self.volume + speed=speed or self.speed + port=port or self.port + + -- Replace modulation + modus=modus:gsub("0", "AM") + modus=modus:gsub("1", "FM") + + -- This did not work well. Stopped if the transmission was a bit longer with no apparent error. + --local command=string.format("%s --freqs=%s --modulations=%s --coalition=%d --port=%d --volume=%.2f --speed=%d", exe, freqs, modus, coal, port, volume, speed) + + -- Command from orig STTS script. Works better for some unknown reason! + local command=string.format("start /min \"\" /d \"%s\" /b \"%s\" -f %s -m %s -c %s -p %s -n \"%s\" -h", path, exe, freqs, modus, coal, port, "ROBOT") + + --local command=string.format('start /b "" /d "%s" "%s" -f %s -m %s -c %s -p %s -n "%s" > bla.txt', path, exe, freqs, modus, coal, port, "ROBOT") + + -- Command. + local command=string.format('%s/%s -f %s -m %s -c %s -p %s -n "%s"', path, exe, freqs, modus, coal, port, "ROBOT") + + -- Set voice or gender/culture. + if voice then + -- Use a specific voice (no need for gender and/or culture. + command=command..string.format(" --voice=\"%s\"", tostring(voice)) + else + -- Add gender. + if gender and gender~="female" then + command=command..string.format(" --gender=%s", tostring(gender)) + end + -- Add culture. + if culture and culture~="en-GB" then + command=command..string.format(" -l %s", tostring(culture)) + end + end + + -- Set coordinate. + if self.coordinate then + local lat,lon,alt=self:_GetLatLongAlt(self.coordinate) + command=command..string.format(" -L %.4f -O %.4f -A %d", lat, lon, alt) + end + + -- Set google. + if self.google then + command=command..string.format(' -G "%s"', self.google) + end + + -- Debug output. + self:T("MSRS command="..command) + + return command +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- **Tasking** -- A command center governs multiple missions, and takes care of the reporting and communications. +-- +-- **Features:** +-- +-- * Govern multiple missions. +-- * Communicate to coalitions, groups. +-- * Assign tasks. +-- * Manage the menus. +-- * Manage reference zones. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- ### Contributions: +-- +-- === +-- +-- @module Tasking.CommandCenter +-- @image Task_Command_Center.JPG + + +--- The COMMANDCENTER class +-- @type COMMANDCENTER +-- @field Wrapper.Group#GROUP HQ +-- @field DCS#coalition CommandCenterCoalition +-- @list Missions +-- @extends Core.Base#BASE + + +--- Governs multiple missions, the tasking and the reporting. +-- +-- Command centers govern missions, communicates the task assignments between human players of the coalition, and manages the menu flow. +-- It can assign a random task to a player when requested. +-- The commandcenter provides the facilitites to communicate between human players online, executing a task. +-- +-- ## 1. Create a command center object. +-- +-- * @{#COMMANDCENTER.New}(): Creates a new COMMANDCENTER object. +-- +-- ## 2. Command center mission management. +-- +-- Command centers manage missions. These can be added, removed and provides means to retrieve missions. +-- These methods are heavily used by the task dispatcher classes. +-- +-- * @{#COMMANDCENTER.AddMission}(): Adds a mission to the commandcenter control. +-- * @{#COMMANDCENTER.RemoveMission}(): Removes a mission to the commandcenter control. +-- * @{#COMMANDCENTER.GetMissions}(): Retrieves the missions table controlled by the commandcenter. +-- +-- ## 3. Communication management between players. +-- +-- Command center provide means of communication between players. +-- Because a command center is a central object governing multiple missions, +-- there are several levels at which communication needs to be done. +-- Within MOOSE, communication is facilitated using the message system within the DCS simulator. +-- +-- Messages can be sent between players at various levels: +-- +-- - On a global level, to all players. +-- - On a coalition level, only to the players belonging to the same coalition. +-- - On a group level, to the players belonging to the same group. +-- +-- Messages can be sent to **all players** by the command center using the method @{Tasking.CommandCenter#COMMANDCENTER.MessageToAll}(). +-- +-- To send messages to **the coalition of the command center**, there are two methods available: +-- +-- - Use the method @{Tasking.CommandCenter#COMMANDCENTER.MessageToCoalition}() to send a specific message to the coalition, with a given message display duration. +-- - You can send a specific type of message using the method @{Tasking.CommandCenter#COMMANDCENTER.MessageTypeToCoalition}(). +-- This will send a message of a specific type to the coalition, and as a result its display duration will be flexible according the message display time selection by the human player. +-- +-- To send messages **to the group** of human players, there are also two methods available: +-- +-- - Use the method @{Tasking.CommandCenter#COMMANDCENTER.MessageToGroup}() to send a specific message to a group, with a given message display duration. +-- - You can send a specific type of message using the method @{Tasking.CommandCenter#COMMANDCENTER.MessageTypeToGroup}(). +-- This will send a message of a specific type to the group, and as a result its display duration will be flexible according the message display time selection by the human player . +-- +-- Messages are considered to be sometimes disturbing for human players, therefore, the settings menu provides the means to activate or deactivate messages. +-- For more information on the message types and display timings that can be selected and configured using the menu, refer to the @{Core.Settings} menu description. +-- +-- ## 4. Command center detailed methods. +-- +-- Various methods are added to manage command centers. +-- +-- ### 4.1. Naming and description. +-- +-- There are 3 methods that can be used to retrieve the description of a command center: +-- +-- - Use the method @{Tasking.CommandCenter#COMMANDCENTER.GetName}() to retrieve the name of the command center. +-- This is the name given as part of the @{Tasking.CommandCenter#COMMANDCENTER.New}() constructor. +-- The returned name using this method, is not to be used for message communication. +-- +-- A textual description can be retrieved that provides the command center name to be used within message communication: +-- +-- - @{Tasking.CommandCenter#COMMANDCENTER.GetShortText}() returns the command center name as `CC [CommandCenterName]`. +-- - @{Tasking.CommandCenter#COMMANDCENTER.GetText}() returns the command center name as `Command Center [CommandCenterName]`. +-- +-- ### 4.2. The coalition of the command center. +-- +-- The method @{Tasking.CommandCenter#COMMANDCENTER.GetCoalition}() returns the coalition of the command center. +-- The return value is an enumeration of the type @{DCS#coalition.side}, which contains the RED, BLUE and NEUTRAL coalition. +-- +-- ### 4.3. The command center is a real object. +-- +-- The command center must be represented by a live object within the DCS simulator. As a result, the command center +-- can be a @{Wrapper.Unit}, a @{Wrapper.Group}, an @{Wrapper.Airbase} or a @{Wrapper.Static} object. +-- +-- Using the method @{Tasking.CommandCenter#COMMANDCENTER.GetPositionable}() you retrieve the polymorphic positionable object representing +-- the command center, but just be aware that you should be able to use the representable object derivation methods. +-- +-- ### 5. Command center reports. +-- +-- Because a command center giverns multiple missions, there are several reports available that are generated by command centers. +-- These reports are generated using the following methods: +-- +-- - @{Tasking.CommandCenter#COMMANDCENTER.ReportSummary}(): Creates a summary report of all missions governed by the command center. +-- - @{Tasking.CommandCenter#COMMANDCENTER.ReportDetails}(): Creates a detailed report of all missions governed by the command center. +-- - @{Tasking.CommandCenter#COMMANDCENTER.ReportMissionPlayers}(): Creates a report listing the players active at the missions governed by the command center. +-- +-- ## 6. Reference Zones. +-- +-- Command Centers may be aware of certain Reference Zones within the battleground. These Reference Zones can refer to +-- known areas, recognizable buildings or sites, or any other point of interest. +-- Command Centers will use these Reference Zones to help pilots with defining coordinates in terms of navigation +-- during the WWII era. +-- The Reference Zones are related to the WWII mode that the Command Center will operate in. +-- Use the method @{#COMMANDCENTER.SetModeWWII}() to set the mode of communication to the WWII mode. +-- +-- In WWII mode, the Command Center will receive detected targets, and will select for each target the closest +-- nearby Reference Zone. This allows pilots to navigate easier through the battle field readying for combat. +-- +-- The Reference Zones need to be set by the Mission Designer in the Mission Editor. +-- Reference Zones are set by normal trigger zones. One can color the zones in a specific color, +-- and the radius of the zones doesn't matter, only the point is important. Place the center of these Reference Zones at +-- specific scenery objects or points of interest (like cities, rivers, hills, crossing etc). +-- The trigger zones indicating a Reference Zone need to follow a specific syntax. +-- The name of each trigger zone expressing a Reference Zone need to start with a classification name of the object, +-- followed by a #, followed by a symbolic name of the Reference Zone. +-- A few examples: +-- +-- * A church at Tskinvali would be indicated as: *Church#Tskinvali* +-- * A train station near Kobuleti would be indicated as: *Station#Kobuleti* +-- +-- The COMMANDCENTER class contains a method to indicate which trigger zones need to be used as Reference Zones. +-- This is done by using the method @{#COMMANDCENTER.SetReferenceZones}(). +-- For the moment, only one Reference Zone class can be specified, but in the future, more classes will become possible. +-- +-- ## 7. Tasks. +-- +-- ### 7.1. Automatically assign tasks. +-- +-- One of the most important roles of the command center is the management of tasks. +-- The command center can assign automatically tasks to the players using the @{Tasking.CommandCenter#COMMANDCENTER.SetAutoAssignTasks}() method. +-- When this method is used with a parameter true; the command center will scan at regular intervals which players in a slot are not having a task assigned. +-- For those players; the tasking is enabled to assign automatically a task. +-- An Assign Menu will be accessible for the player under the command center menu, to configure the automatic tasking to switched on or off. +-- +-- ### 7.2. Automatically accept assigned tasks. +-- +-- When a task is assigned; the mission designer can decide if players are immediately assigned to the task; or they can accept/reject the assigned task. +-- Use the method @{Tasking.CommandCenter#COMMANDCENTER.SetAutoAcceptTasks}() to configure this behaviour. +-- If the tasks are not automatically accepted; the player will receive a message that he needs to access the command center menu and +-- choose from 2 added menu options either to accept or reject the assigned task within 30 seconds. +-- If the task is not accepted within 30 seconds; the task will be cancelled and a new task will be assigned. +-- +-- +-- @field #COMMANDCENTER +COMMANDCENTER = { + ClassName = "COMMANDCENTER", + CommandCenterName = "", + CommandCenterCoalition = nil, + CommandCenterPositionable = nil, + Name = "", + ReferencePoints = {}, + ReferenceNames = {}, + CommunicationMode = "80", +} + + +--- @type COMMANDCENTER.AutoAssignMethods +COMMANDCENTER.AutoAssignMethods = { + ["Random"] = 1, + ["Distance"] = 2, + ["Priority"] = 3, + } + +--- The constructor takes an IDENTIFIABLE as the HQ command center. +-- @param #COMMANDCENTER self +-- @param Wrapper.Positionable#POSITIONABLE CommandCenterPositionable +-- @param #string CommandCenterName +-- @return #COMMANDCENTER +function COMMANDCENTER:New( CommandCenterPositionable, CommandCenterName ) + + local self = BASE:Inherit( self, BASE:New() ) -- #COMMANDCENTER + + self.CommandCenterPositionable = CommandCenterPositionable + self.CommandCenterName = CommandCenterName or CommandCenterPositionable:GetName() + self.CommandCenterCoalition = CommandCenterPositionable:GetCoalition() + + self.Missions = {} + + self:SetAutoAssignTasks( false ) + self:SetAutoAcceptTasks( true ) + self:SetAutoAssignMethod( COMMANDCENTER.AutoAssignMethods.Distance ) + self:SetFlashStatus( false ) + self:SetMessageDuration(10) + + self:HandleEvent( EVENTS.Birth, + --- @param #COMMANDCENTER self + -- @param Core.Event#EVENTDATA EventData + function( self, EventData ) + if EventData.IniObjectCategory == 1 then + local EventGroup = GROUP:Find( EventData.IniDCSGroup ) + --self:E( { CommandCenter = self:GetName(), EventGroup = EventGroup:GetName(), HasGroup = self:HasGroup( EventGroup ), EventData = EventData } ) + if EventGroup and EventGroup:IsAlive() and self:HasGroup( EventGroup ) then + local CommandCenterMenu = MENU_GROUP:New( EventGroup, self:GetText() ) + local MenuReporting = MENU_GROUP:New( EventGroup, "Missions Reports", CommandCenterMenu ) + local MenuMissionsSummary = MENU_GROUP_COMMAND:New( EventGroup, "Missions Status Report", MenuReporting, self.ReportSummary, self, EventGroup ) + local MenuMissionsDetails = MENU_GROUP_COMMAND:New( EventGroup, "Missions Players Report", MenuReporting, self.ReportMissionsPlayers, self, EventGroup ) + --self:ReportSummary( EventGroup ) + local PlayerUnit = EventData.IniUnit + for MissionID, Mission in pairs( self:GetMissions() ) do + local Mission = Mission -- Tasking.Mission#MISSION + local PlayerGroup = EventData.IniGroup -- The GROUP object should be filled! + Mission:JoinUnit( PlayerUnit, PlayerGroup ) + end + self:SetMenu() + end + end + + end + ) + +-- -- When a player enters a client or a unit, the CommandCenter will check for each Mission and each Task in the Mission if the player has things to do. +-- -- For these elements, it will= +-- -- - Set the correct menu. +-- -- - Assign the PlayerUnit to the Task if required. +-- -- - Send a message to the other players in the group that this player has joined. +-- self:HandleEvent( EVENTS.PlayerEnterUnit, +-- --- @param #COMMANDCENTER self +-- -- @param Core.Event#EVENTDATA EventData +-- function( self, EventData ) +-- local PlayerUnit = EventData.IniUnit +-- for MissionID, Mission in pairs( self:GetMissions() ) do +-- local Mission = Mission -- Tasking.Mission#MISSION +-- local PlayerGroup = EventData.IniGroup -- The GROUP object should be filled! +-- Mission:JoinUnit( PlayerUnit, PlayerGroup ) +-- end +-- self:SetMenu() +-- end +-- ) + + -- Handle when a player leaves a slot and goes back to spectators ... + -- The PlayerUnit will be UnAssigned from the Task. + -- When there is no Unit left running the Task, the Task goes into Abort... + self:HandleEvent( EVENTS.MissionEnd, + --- @param #TASK self + -- @param Core.Event#EVENTDATA EventData + function( self, EventData ) + local PlayerUnit = EventData.IniUnit + for MissionID, Mission in pairs( self:GetMissions() ) do + local Mission = Mission -- Tasking.Mission#MISSION + Mission:Stop() + end + end + ) + + -- Handle when a player leaves a slot and goes back to spectators ... + -- The PlayerUnit will be UnAssigned from the Task. + -- When there is no Unit left running the Task, the Task goes into Abort... + self:HandleEvent( EVENTS.PlayerLeaveUnit, + --- @param #TASK self + -- @param Core.Event#EVENTDATA EventData + function( self, EventData ) + local PlayerUnit = EventData.IniUnit + for MissionID, Mission in pairs( self:GetMissions() ) do + local Mission = Mission -- Tasking.Mission#MISSION + if Mission:IsENGAGED() then + Mission:AbortUnit( PlayerUnit ) + end + end + end + ) + + -- Handle when a player crashes ... + -- The PlayerUnit will be UnAssigned from the Task. + -- When there is no Unit left running the Task, the Task goes into Abort... + self:HandleEvent( EVENTS.Crash, + --- @param #TASK self + -- @param Core.Event#EVENTDATA EventData + function( self, EventData ) + local PlayerUnit = EventData.IniUnit + for MissionID, Mission in pairs( self:GetMissions() ) do + local Mission = Mission -- Tasking.Mission#MISSION + if Mission:IsENGAGED() then + Mission:CrashUnit( PlayerUnit ) + end + end + end + ) + + self:SetMenu() + + _SETTINGS:SetSystemMenu( CommandCenterPositionable ) + + self:SetCommandMenu() + + return self +end + +--- Gets the name of the HQ command center. +-- @param #COMMANDCENTER self +-- @return #string +function COMMANDCENTER:GetName() + + return self.CommandCenterName +end + +--- Gets the text string of the HQ command center. +-- @param #COMMANDCENTER self +-- @return #string +function COMMANDCENTER:GetText() + + return "Command Center [" .. self.CommandCenterName .. "]" +end + +--- Gets the short text string of the HQ command center. +-- @param #COMMANDCENTER self +-- @return #string +function COMMANDCENTER:GetShortText() + + return "CC [" .. self.CommandCenterName .. "]" +end + + +--- Gets the coalition of the command center. +-- @param #COMMANDCENTER self +-- @return DCScoalition#coalition +function COMMANDCENTER:GetCoalition() + + return self.CommandCenterCoalition +end + + +--- Gets the POSITIONABLE of the HQ command center. +-- @param #COMMANDCENTER self +-- @return Wrapper.Positionable#POSITIONABLE +function COMMANDCENTER:GetPositionable() + return self.CommandCenterPositionable +end + +--- Get the Missions governed by the HQ command center. +-- @param #COMMANDCENTER self +-- @return #list +function COMMANDCENTER:GetMissions() + + return self.Missions or {} +end + +--- Add a MISSION to be governed by the HQ command center. +-- @param #COMMANDCENTER self +-- @param Tasking.Mission#MISSION Mission +-- @return Tasking.Mission#MISSION +function COMMANDCENTER:AddMission( Mission ) + + self.Missions[Mission] = Mission + + return Mission +end + +--- Removes a MISSION to be governed by the HQ command center. +-- The given Mission is not nilified. +-- @param #COMMANDCENTER self +-- @param Tasking.Mission#MISSION Mission +-- @return Tasking.Mission#MISSION +function COMMANDCENTER:RemoveMission( Mission ) + + self.Missions[Mission] = nil + + return Mission +end + +--- Set special Reference Zones known by the Command Center to guide airborne pilots during WWII. +-- +-- These Reference Zones are normal trigger zones, with a special naming. +-- The Reference Zones need to be set by the Mission Designer in the Mission Editor. +-- Reference Zones are set by normal trigger zones. One can color the zones in a specific color, +-- and the radius of the zones doesn't matter, only the center of the zone is important. Place the center of these Reference Zones at +-- specific scenery objects or points of interest (like cities, rivers, hills, crossing etc). +-- The trigger zones indicating a Reference Zone need to follow a specific syntax. +-- The name of each trigger zone expressing a Reference Zone need to start with a classification name of the object, +-- followed by a #, followed by a symbolic name of the Reference Zone. +-- A few examples: +-- +-- * A church at Tskinvali would be indicated as: *Church#Tskinvali* +-- * A train station near Kobuleti would be indicated as: *Station#Kobuleti* +-- +-- Taking the above example, this is how this method would be used: +-- +-- CC:SetReferenceZones( "Church" ) +-- CC:SetReferenceZones( "Station" ) +-- +-- +-- @param #COMMANDCENTER self +-- @param #string ReferenceZonePrefix The name before the #-mark indicating the class of the Reference Zones. +-- @return #COMMANDCENTER +function COMMANDCENTER:SetReferenceZones( ReferenceZonePrefix ) + local MatchPattern = "(.*)#(.*)" + self:F( { MatchPattern = MatchPattern } ) + for ReferenceZoneName in pairs( _DATABASE.ZONENAMES ) do + local ZoneName, ReferenceName = string.match( ReferenceZoneName, MatchPattern ) + self:F( { ZoneName = ZoneName, ReferenceName = ReferenceName } ) + if ZoneName and ReferenceName and ZoneName == ReferenceZonePrefix then + self.ReferencePoints[ReferenceZoneName] = ZONE:New( ReferenceZoneName ) + self.ReferenceNames[ReferenceZoneName] = ReferenceName + end + end + return self +end + +--- Set the commandcenter operations in WWII mode +-- This will disable LL, MGRS, BRA, BULLS navigatin messages sent by the Command Center, +-- and will be replaced by a navigation using Reference Zones. +-- It will also disable the settings at the settings menu for these. +-- @param #COMMANDCENTER self +-- @return #COMMANDCENTER +function COMMANDCENTER:SetModeWWII() + self.CommunicationMode = "WWII" + return self +end + + +--- Returns if the commandcenter operations is in WWII mode +-- @param #COMMANDCENTER self +-- @return #boolean true if in WWII mode. +function COMMANDCENTER:IsModeWWII() + return self.CommunicationMode == "WWII" +end + + + + +--- Sets the menu structure of the Missions governed by the HQ command center. +-- @param #COMMANDCENTER self +function COMMANDCENTER:SetMenu() + self:F2() + + local MenuTime = timer.getTime() + for MissionID, Mission in pairs( self:GetMissions() or {} ) do + local Mission = Mission -- Tasking.Mission#MISSION + Mission:SetMenu( MenuTime ) + end + + for MissionID, Mission in pairs( self:GetMissions() or {} ) do + Mission = Mission -- Tasking.Mission#MISSION + Mission:RemoveMenu( MenuTime ) + end + +end + +--- Gets the commandcenter menu structure governed by the HQ command center. +-- @param #COMMANDCENTER self +-- @param Wrapper.Group#Group TaskGroup Task Group. +-- @return Core.Menu#MENU_COALITION +function COMMANDCENTER:GetMenu( TaskGroup ) + + local MenuTime = timer.getTime() + + self.CommandCenterMenus = self.CommandCenterMenus or {} + local CommandCenterMenu + + local CommandCenterText = self:GetText() + CommandCenterMenu = MENU_GROUP:New( TaskGroup, CommandCenterText ):SetTime(MenuTime) + self.CommandCenterMenus[TaskGroup] = CommandCenterMenu + + if self.AutoAssignTasks == false then + local AssignTaskMenu = MENU_GROUP_COMMAND:New( TaskGroup, "Assign Task", CommandCenterMenu, self.AssignTask, self, TaskGroup ):SetTime(MenuTime):SetTag("AutoTask") + end + CommandCenterMenu:Remove( MenuTime, "AutoTask" ) + + return self.CommandCenterMenus[TaskGroup] +end + + +--- Assigns a random task to a TaskGroup. +-- @param #COMMANDCENTER self +-- @return #COMMANDCENTER +function COMMANDCENTER:AssignTask( TaskGroup ) + + local Tasks = {} + local AssignPriority = 99999999 + local AutoAssignMethod = self.AutoAssignMethod + + for MissionID, Mission in pairs( self:GetMissions() ) do + local Mission = Mission -- Tasking.Mission#MISSION + local MissionTasks = Mission:GetGroupTasks( TaskGroup ) + for MissionTaskName, MissionTask in pairs( MissionTasks or {} ) do + local MissionTask = MissionTask -- Tasking.Task#TASK + if MissionTask:IsStatePlanned() or MissionTask:IsStateReplanned() or MissionTask:IsStateAssigned() then + local TaskPriority = MissionTask:GetAutoAssignPriority( self.AutoAssignMethod, self, TaskGroup ) + if TaskPriority < AssignPriority then + AssignPriority = TaskPriority + Tasks = {} + end + if TaskPriority == AssignPriority then + Tasks[#Tasks+1] = MissionTask + end + end + end + end + + local Task = Tasks[ math.random( 1, #Tasks ) ] -- Tasking.Task#TASK + + if Task then + + self:I( "Assigning task " .. Task:GetName() .. " using auto assign method " .. self.AutoAssignMethod .. " to " .. TaskGroup:GetName() .. " with task priority " .. AssignPriority ) + + if not self.AutoAcceptTasks == true then + Task:SetAutoAssignMethod( ACT_ASSIGN_MENU_ACCEPT:New( Task.TaskBriefing ) ) + end + + Task:AssignToGroup( TaskGroup ) + + end + +end + + +--- Sets the menu of the command center. +-- This command is called within the :New() method. +-- @param #COMMANDCENTER self +function COMMANDCENTER:SetCommandMenu() + + local MenuTime = timer.getTime() + + if self.CommandCenterPositionable and self.CommandCenterPositionable:IsInstanceOf(GROUP) then + local CommandCenterText = self:GetText() + local CommandCenterMenu = MENU_GROUP:New( self.CommandCenterPositionable, CommandCenterText ):SetTime(MenuTime) + + if self.AutoAssignTasks == false then + local AutoAssignTaskMenu = MENU_GROUP_COMMAND:New( self.CommandCenterPositionable, "Assign Task On", CommandCenterMenu, self.SetAutoAssignTasks, self, true ):SetTime(MenuTime):SetTag("AutoTask") + else + local AutoAssignTaskMenu = MENU_GROUP_COMMAND:New( self.CommandCenterPositionable, "Assign Task Off", CommandCenterMenu, self.SetAutoAssignTasks, self, false ):SetTime(MenuTime):SetTag("AutoTask") + end + CommandCenterMenu:Remove( MenuTime, "AutoTask" ) + end + +end + + + +--- Automatically assigns tasks to all TaskGroups. +-- One of the most important roles of the command center is the management of tasks. +-- When this method is used with a parameter true; the command center will scan at regular intervals which players in a slot are not having a task assigned. +-- For those players; the tasking is enabled to assign automatically a task. +-- An Assign Menu will be accessible for the player under the command center menu, to configure the automatic tasking to switched on or off. +-- @param #COMMANDCENTER self +-- @param #boolean AutoAssign true for ON and false or nil for OFF. +function COMMANDCENTER:SetAutoAssignTasks( AutoAssign ) + + self.AutoAssignTasks = AutoAssign or false + + if self.AutoAssignTasks == true then + self:ScheduleRepeat( 10, 30, 0, nil, self.AssignTasks, self ) + else + self:ScheduleStop( self.AssignTasks ) + end + +end + +--- Automatically accept tasks for all TaskGroups. +-- When a task is assigned; the mission designer can decide if players are immediately assigned to the task; or they can accept/reject the assigned task. +-- If the tasks are not automatically accepted; the player will receive a message that he needs to access the command center menu and +-- choose from 2 added menu options either to accept or reject the assigned task within 30 seconds. +-- If the task is not accepted within 30 seconds; the task will be cancelled and a new task will be assigned. +-- @param #COMMANDCENTER self +-- @param #boolean AutoAccept true for ON and false or nil for OFF. +function COMMANDCENTER:SetAutoAcceptTasks( AutoAccept ) + + self.AutoAcceptTasks = AutoAccept or false + +end + + +--- Define the method to be used to assign automatically a task from the available tasks in the mission. +-- There are 3 types of methods that can be applied for the moment: +-- +-- 1. Random - assigns a random task in the mission to the player. +-- 2. Distance - assigns a task based on a distance evaluation from the player. The closest are to be assigned first. +-- 3. Priority - assigns a task based on the priority as defined by the mission designer, using the SetTaskPriority parameter. +-- +-- The different task classes implement the logic to determine the priority of automatic task assignment to a player, depending on one of the above methods. +-- The method @{Tasking.Task#TASK.GetAutoAssignPriority} calculate the priority of the tasks to be assigned. +-- @param #COMMANDCENTER self +-- @param #COMMANDCENTER.AutoAssignMethods AutoAssignMethod A selection of an assign method from the COMMANDCENTER.AutoAssignMethods enumeration. +function COMMANDCENTER:SetAutoAssignMethod( AutoAssignMethod ) + + self.AutoAssignMethod = AutoAssignMethod or COMMANDCENTER.AutoAssignMethods.Random + +end + +--- Automatically assigns tasks to all TaskGroups. +-- @param #COMMANDCENTER self +function COMMANDCENTER:AssignTasks() + + local GroupSet = self:AddGroups() + + for GroupID, TaskGroup in pairs( GroupSet:GetSet() ) do + local TaskGroup = TaskGroup -- Wrapper.Group#GROUP + + if TaskGroup:IsAlive() then + self:GetMenu( TaskGroup ) + + if self:IsGroupAssigned( TaskGroup ) then + else + -- Only groups with planes or helicopters will receive automatic tasks. + -- TODO Workaround DCS-BUG-3 - https://github.com/FlightControl-Master/MOOSE/issues/696 + if TaskGroup:IsAir() then + self:AssignTask( TaskGroup ) + end + end + end + end + +end + + +--- Get all the Groups active within the command center. +-- @param #COMMANDCENTER self +-- @return Core.Set#SET_GROUP The set of groups active within the command center. +function COMMANDCENTER:AddGroups() + + local GroupSet = SET_GROUP:New() + + for MissionID, Mission in pairs( self.Missions ) do + local Mission = Mission -- Tasking.Mission#MISSION + GroupSet = Mission:AddGroups( GroupSet ) + end + + return GroupSet +end + + +--- Checks of the TaskGroup has a Task. +-- @param #COMMANDCENTER self +-- @return #boolean When true, the TaskGroup has a Task, otherwise the returned value will be false. +function COMMANDCENTER:IsGroupAssigned( TaskGroup ) + + local Assigned = false + + for MissionID, Mission in pairs( self.Missions ) do + local Mission = Mission -- Tasking.Mission#MISSION + if Mission:IsGroupAssigned( TaskGroup ) then + Assigned = true + break + end + end + + return Assigned +end + + +--- Checks of the command center has the given MissionGroup. +-- @param #COMMANDCENTER self +-- @param Wrapper.Group#GROUP MissionGroup The group active within one of the missions governed by the command center. +-- @return #boolean +function COMMANDCENTER:HasGroup( MissionGroup ) + + local Has = false + + for MissionID, Mission in pairs( self.Missions ) do + local Mission = Mission -- Tasking.Mission#MISSION + if Mission:HasGroup( MissionGroup ) then + Has = true + break + end + end + + return Has +end + +--- Let the command center send a Message to all players. +-- @param #COMMANDCENTER self +-- @param #string Message The message text. +function COMMANDCENTER:MessageToAll( Message ) + + self:GetPositionable():MessageToAll( Message, self.MessageDuration, self:GetName() ) + +end + +--- Let the command center send a message to the MessageGroup. +-- @param #COMMANDCENTER self +-- @param #string Message The message text. +-- @param Wrapper.Group#GROUP MessageGroup The group to receive the message. +function COMMANDCENTER:MessageToGroup( Message, MessageGroup ) + + self:GetPositionable():MessageToGroup( Message, self.MessageDuration, MessageGroup, self:GetShortText() ) + +end + +--- Let the command center send a message to the MessageGroup. +-- @param #COMMANDCENTER self +-- @param #string Message The message text. +-- @param Wrapper.Group#GROUP MessageGroup The group to receive the message. +-- @param Core.Message#MESSAGE.MessageType MessageType The type of the message, resulting in automatic time duration and prefix of the message. +function COMMANDCENTER:MessageTypeToGroup( Message, MessageGroup, MessageType ) + + self:GetPositionable():MessageTypeToGroup( Message, MessageType, MessageGroup, self:GetShortText() ) + +end + +--- Let the command center send a message to the coalition of the command center. +-- @param #COMMANDCENTER self +-- @param #string Message The message text. +function COMMANDCENTER:MessageToCoalition( Message ) + + local CCCoalition = self:GetPositionable():GetCoalition() + --TODO: Fix coalition bug! + + self:GetPositionable():MessageToCoalition( Message, self.MessageDuration, CCCoalition, self:GetShortText() ) + +end + + +--- Let the command center send a message of a specified type to the coalition of the command center. +-- @param #COMMANDCENTER self +-- @param #string Message The message text. +-- @param Core.Message#MESSAGE.MessageType MessageType The type of the message, resulting in automatic time duration and prefix of the message. +function COMMANDCENTER:MessageTypeToCoalition( Message, MessageType ) + + local CCCoalition = self:GetPositionable():GetCoalition() + --TODO: Fix coalition bug! + + self:GetPositionable():MessageTypeToCoalition( Message, MessageType, CCCoalition, self:GetShortText() ) + +end + + +--- Let the command center send a report of the status of all missions to a group. +-- Each Mission is listed, with an indication how many Tasks are still to be completed. +-- @param #COMMANDCENTER self +-- @param Wrapper.Group#GROUP ReportGroup The group to receive the report. +function COMMANDCENTER:ReportSummary( ReportGroup ) + self:F( ReportGroup ) + + local Report = REPORT:New() + + -- List the name of the mission. + local Name = self:GetName() + Report:Add( string.format( '%s - Report Summary Missions', Name ) ) + + for MissionID, Mission in pairs( self.Missions ) do + local Mission = Mission -- Tasking.Mission#MISSION + Report:Add( " - " .. Mission:ReportSummary( ReportGroup ) ) + end + + self:MessageToGroup( Report:Text(), ReportGroup ) +end + +--- Let the command center send a report of the players of all missions to a group. +-- Each Mission is listed, with an indication how many Tasks are still to be completed. +-- @param #COMMANDCENTER self +-- @param Wrapper.Group#GROUP ReportGroup The group to receive the report. +function COMMANDCENTER:ReportMissionsPlayers( ReportGroup ) + self:F( ReportGroup ) + + local Report = REPORT:New() + + Report:Add( "Players active in all missions." ) + + for MissionID, MissionData in pairs( self.Missions ) do + local Mission = MissionData -- Tasking.Mission#MISSION + Report:Add( " - " .. Mission:ReportPlayersPerTask(ReportGroup) ) + end + + self:MessageToGroup( Report:Text(), ReportGroup ) +end + +--- Let the command center send a report of the status of a task to a group. +-- Report the details of a Mission, listing the Mission, and all the Task details. +-- @param #COMMANDCENTER self +-- @param Wrapper.Group#GROUP ReportGroup The group to receive the report. +-- @param Tasking.Task#TASK Task The task to be reported. +function COMMANDCENTER:ReportDetails( ReportGroup, Task ) + self:F( ReportGroup ) + + local Report = REPORT:New() + + for MissionID, Mission in pairs( self.Missions ) do + local Mission = Mission -- Tasking.Mission#MISSION + Report:Add( " - " .. Mission:ReportDetails() ) + end + + self:MessageToGroup( Report:Text(), ReportGroup ) +end + + +--- Let the command center flash a report of the status of the subscribed task to a group. +-- @param #COMMANDCENTER self +-- @param Flash #boolean +function COMMANDCENTER:SetFlashStatus( Flash ) + self:F() + + self.FlashStatus = Flash and true +end + +--- Duration a command center message is shown. +-- @param #COMMANDCENTER self +-- @param seconds #number +function COMMANDCENTER:SetMessageDuration(seconds) + self:F() + + self.MessageDuration = 10 or seconds +end +--- **Tasking** -- A mission models a goal to be achieved through the execution and completion of tasks by human players. +-- +-- **Features:** +-- +-- * A mission has a goal to be achieved, through the execution and completion of tasks of different categories by human players. +-- * A mission manages these tasks. +-- * A mission has a state, that indicates the fase of the mission. +-- * A mission has a menu structure, that facilitates mission reports and tasking menus. +-- * A mission can assign a task to a player. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- ### Contributions: +-- +-- === +-- +-- @module Tasking.Mission +-- @image Task_Mission.JPG + +--- @type MISSION +-- @field #MISSION.Clients _Clients +-- @field Core.Menu#MENU_COALITION MissionMenu +-- @field #string MissionBriefing +-- @extends Core.Fsm#FSM + +--- Models goals to be achieved and can contain multiple tasks to be executed to achieve the goals. +-- +-- A mission contains multiple tasks and can be of different task types. +-- These tasks need to be assigned to human players to be executed. +-- +-- A mission can have multiple states, which will evolve as the mission progresses during the DCS simulation. +-- +-- - **IDLE**: The mission is defined, but not started yet. No task has yet been joined by a human player as part of the mission. +-- - **ENGAGED**: The mission is ongoing, players have joined tasks to be executed. +-- - **COMPLETED**: The goals of the mission has been successfully reached, and the mission is flagged as completed. +-- - **FAILED**: For a certain reason, the goals of the mission has not been reached, and the mission is flagged as failed. +-- - **HOLD**: The mission was enaged, but for some reason it has been put on hold. +-- +-- Note that a mission goals need to be checked by a goal check trigger: @{#MISSION.OnBeforeMissionGoals}(), which may return false if the goal has not been reached. +-- This goal is checked automatically by the mission object every x seconds. +-- +-- - @{#MISSION.Start}() or @{#MISSION.__Start}() will start the mission, and will bring it from **IDLE** state to **ENGAGED** state. +-- - @{#MISSION.Stop}() or @{#MISSION.__Stop}() will stop the mission, and will bring it from **ENGAGED** state to **IDLE** state. +-- - @{#MISSION.Complete}() or @{#MISSION.__Complete}() will complete the mission, and will bring the mission state to **COMPLETED**. +-- Note that the mission must be in state **ENGAGED** to be able to complete the mission. +-- - @{#MISSION.Fail}() or @{#MISSION.__Fail}() will fail the mission, and will bring the mission state to **FAILED**. +-- Note that the mission must be in state **ENGAGED** to be able to fail the mission. +-- - @{#MISSION.Hold}() or @{#MISSION.__Hold}() will hold the mission, and will bring the mission state to **HOLD**. +-- Note that the mission must be in state **ENGAGED** to be able to hold the mission. +-- Re-engage the mission using the engage trigger. +-- +-- The following sections provide an overview of the most important methods that can be used as part of a mission object. +-- Note that the @{Tasking.CommandCenter} system is using most of these methods to manage the missions in its system. +-- +-- ## 1. Create a mission object. +-- +-- - @{#MISSION.New}(): Creates a new MISSION object. +-- +-- ## 2. Mission task management. +-- +-- Missions maintain tasks, which can be added or removed, or enquired. +-- +-- - @{#MISSION.AddTask}(): Adds a task to the mission. +-- - @{#MISSION.RemoveTask}(): Removes a task from the mission. +-- +-- ## 3. Mission detailed methods. +-- +-- Various methods are added to manage missions. +-- +-- ### 3.1. Naming and description. +-- +-- There are several methods that can be used to retrieve the properties of a mission: +-- +-- - Use the method @{#MISSION.GetName}() to retrieve the name of the mission. +-- This is the name given as part of the @{#MISSION.New}() constructor. +-- +-- A textual description can be retrieved that provides the mission name to be used within message communication: +-- +-- - @{#MISSION.GetShortText}() returns the mission name as `Mission "MissionName"`. +-- - @{#MISSION.GetText}() returns the mission name as `Mission "MissionName (MissionPriority)"`. A longer version including the priority text of the mission. +-- +-- ### 3.2. Get task information. +-- +-- - @{#MISSION.GetTasks}(): Retrieves a list of the tasks controlled by the mission. +-- - @{#MISSION.GetTask}(): Retrieves a specific task controlled by the mission. +-- - @{#MISSION.GetTasksRemaining}(): Retrieve a list of the tasks that aren't finished or failed, and are governed by the mission. +-- - @{#MISSION.GetGroupTasks}(): Retrieve a list of the tasks that can be asigned to a @{Wrapper.Group}. +-- - @{#MISSION.GetTaskTypes}(): Retrieve a list of the different task types governed by the mission. +-- +-- ### 3.3. Get the command center. +-- +-- - @{#MISSION.GetCommandCenter}(): Retrieves the @{Tasking.CommandCenter} governing the mission. +-- +-- ### 3.4. Get the groups active in the mission as a @{Core.Set}. +-- +-- - @{#MISSION.GetGroups}(): Retrieves a @{Core.Set#SET_GROUP} of all the groups active in the mission (as part of the tasks). +-- +-- ### 3.5. Get the names of the players. +-- +-- - @{#MISSION.GetPlayerNames}(): Retrieves the list of the players that were active within th mission.. +-- +-- ## 4. Menu management. +-- +-- A mission object is able to manage its own menu structure. Use the @{#MISSION.GetMenu}() and @{#MISSION.SetMenu}() to manage the underlying submenu +-- structure managing the tasks of the mission. +-- +-- ## 5. Reporting management. +-- +-- Several reports can be generated for a mission, and will return a text string that can be used to display using the @{Core.Message} system. +-- +-- - @{#MISSION.ReportBriefing}(): Generates the briefing for the mission. +-- - @{#MISSION.ReportOverview}(): Generates an overview of the tasks and status of the mission. +-- - @{#MISSION.ReportDetails}(): Generates a detailed report of the tasks of the mission. +-- - @{#MISSION.ReportSummary}(): Generates a summary report of the tasks of the mission. +-- - @{#MISSION.ReportPlayersPerTask}(): Generates a report showing the active players per task. +-- - @{#MISSION.ReportPlayersProgress}(): Generates a report showing the task progress per player. +-- +-- +-- @field #MISSION +MISSION = { + ClassName = "MISSION", + Name = "", + MissionStatus = "PENDING", + AssignedGroups = {}, +} + +--- This is the main MISSION declaration method. Each Mission is like the master or a Mission orchestration between, Clients, Tasks, Stages etc. +-- @param #MISSION self +-- @param Tasking.CommandCenter#COMMANDCENTER CommandCenter +-- @param #string MissionName Name of the mission. This name will be used to reference the status of each mission by the players. +-- @param #string MissionPriority String indicating the "priority" of the Mission. e.g. "Primary", "Secondary". It is free format and up to the Mission designer to choose. There are no rules behind this field. +-- @param #string MissionBriefing String indicating the mission briefing to be shown when a player joins a @{CLIENT}. +-- @param DCS#coaliton.side MissionCoalition Side of the coalition, i.e. and enumerator @{#DCS.coalition.side} corresponding to RED, BLUE or NEUTRAL. +-- @return #MISSION self +function MISSION:New( CommandCenter, MissionName, MissionPriority, MissionBriefing, MissionCoalition ) + + local self = BASE:Inherit( self, FSM:New() ) -- Core.Fsm#FSM + + self:T( { MissionName, MissionPriority, MissionBriefing, MissionCoalition } ) + + self.CommandCenter = CommandCenter + CommandCenter:AddMission( self ) + + self.Name = MissionName + self.MissionPriority = MissionPriority + self.MissionBriefing = MissionBriefing + self.MissionCoalition = MissionCoalition + + self.Tasks = {} + self.TaskNumber = 0 + self.PlayerNames = {} -- These are the players that achieved progress in the mission. + + self:SetStartState( "IDLE" ) + + self:AddTransition( "IDLE", "Start", "ENGAGED" ) + + --- OnLeave Transition Handler for State IDLE. + -- @function [parent=#MISSION] OnLeaveIDLE + -- @param #MISSION self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnEnter Transition Handler for State IDLE. + -- @function [parent=#MISSION] OnEnterIDLE + -- @param #MISSION self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- OnLeave Transition Handler for State ENGAGED. + -- @function [parent=#MISSION] OnLeaveENGAGED + -- @param #MISSION self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnEnter Transition Handler for State ENGAGED. + -- @function [parent=#MISSION] OnEnterENGAGED + -- @param #MISSION self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- OnBefore Transition Handler for Event Start. + -- @function [parent=#MISSION] OnBeforeStart + -- @param #MISSION self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Start. + -- @function [parent=#MISSION] OnAfterStart + -- @param #MISSION self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Start. + -- @function [parent=#MISSION] Start + -- @param #MISSION self + + --- Asynchronous Event Trigger for Event Start. + -- @function [parent=#MISSION] __Start + -- @param #MISSION self + -- @param #number Delay The delay in seconds. + + self:AddTransition( "ENGAGED", "Stop", "IDLE" ) + + --- OnLeave Transition Handler for State IDLE. + -- @function [parent=#MISSION] OnLeaveIDLE + -- @param #MISSION self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnEnter Transition Handler for State IDLE. + -- @function [parent=#MISSION] OnEnterIDLE + -- @param #MISSION self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- OnBefore Transition Handler for Event Stop. + -- @function [parent=#MISSION] OnBeforeStop + -- @param #MISSION self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Stop. + -- @function [parent=#MISSION] OnAfterStop + -- @param #MISSION self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Stop. + -- @function [parent=#MISSION] Stop + -- @param #MISSION self + + --- Asynchronous Event Trigger for Event Stop. + -- @function [parent=#MISSION] __Stop + -- @param #MISSION self + -- @param #number Delay The delay in seconds. + + self:AddTransition( "ENGAGED", "Complete", "COMPLETED" ) + + --- OnLeave Transition Handler for State COMPLETED. + -- @function [parent=#MISSION] OnLeaveCOMPLETED + -- @param #MISSION self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnEnter Transition Handler for State COMPLETED. + -- @function [parent=#MISSION] OnEnterCOMPLETED + -- @param #MISSION self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- OnBefore Transition Handler for Event Complete. + -- @function [parent=#MISSION] OnBeforeComplete + -- @param #MISSION self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Complete. + -- @function [parent=#MISSION] OnAfterComplete + -- @param #MISSION self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Complete. + -- @function [parent=#MISSION] Complete + -- @param #MISSION self + + --- Asynchronous Event Trigger for Event Complete. + -- @function [parent=#MISSION] __Complete + -- @param #MISSION self + -- @param #number Delay The delay in seconds. + + self:AddTransition( "*", "Fail", "FAILED" ) + + --- OnLeave Transition Handler for State FAILED. + -- @function [parent=#MISSION] OnLeaveFAILED + -- @param #MISSION self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnEnter Transition Handler for State FAILED. + -- @function [parent=#MISSION] OnEnterFAILED + -- @param #MISSION self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- OnBefore Transition Handler for Event Fail. + -- @function [parent=#MISSION] OnBeforeFail + -- @param #MISSION self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Fail. + -- @function [parent=#MISSION] OnAfterFail + -- @param #MISSION self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Fail. + -- @function [parent=#MISSION] Fail + -- @param #MISSION self + + --- Asynchronous Event Trigger for Event Fail. + -- @function [parent=#MISSION] __Fail + -- @param #MISSION self + -- @param #number Delay The delay in seconds. + + + self:AddTransition( "*", "MissionGoals", "*" ) + + --- MissionGoals Handler OnBefore for MISSION + -- @function [parent=#MISSION] OnBeforeMissionGoals + -- @param #MISSION self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- MissionGoals Handler OnAfter for MISSION + -- @function [parent=#MISSION] OnAfterMissionGoals + -- @param #MISSION self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- MissionGoals Trigger for MISSION + -- @function [parent=#MISSION] MissionGoals + -- @param #MISSION self + + --- MissionGoals Asynchronous Trigger for MISSION + -- @function [parent=#MISSION] __MissionGoals + -- @param #MISSION self + -- @param #number Delay + + -- Private implementations + + CommandCenter:SetMenu() + + return self +end + + + + +--- FSM function for a MISSION +-- @param #MISSION self +-- @param #string From +-- @param #string Event +-- @param #string To +function MISSION:onenterCOMPLETED( From, Event, To ) + + self:GetCommandCenter():MessageTypeToCoalition( self:GetText() .. " has been completed! Good job guys!", MESSAGE.Type.Information ) +end + +--- Gets the mission name. +-- @param #MISSION self +-- @return #MISSION self +function MISSION:GetName() + return self.Name +end + + +--- Gets the mission text. +-- @param #MISSION self +-- @return #MISSION self +function MISSION:GetText() + return string.format( 'Mission "%s (%s)"', self.Name, self.MissionPriority ) +end + + +--- Gets the short mission text. +-- @param #MISSION self +-- @return #MISSION self +function MISSION:GetShortText() + return string.format( 'Mission "%s"', self.Name ) +end + + +--- Add a Unit to join the Mission. +-- For each Task within the Mission, the Unit is joined with the Task. +-- If the Unit was not part of a Task in the Mission, false is returned. +-- If the Unit is part of a Task in the Mission, true is returned. +-- @param #MISSION self +-- @param Wrapper.Unit#UNIT PlayerUnit The CLIENT or UNIT of the Player joining the Mission. +-- @param Wrapper.Group#GROUP PlayerGroup The GROUP of the player joining the Mission. +-- @return #boolean true if Unit is part of a Task in the Mission. +function MISSION:JoinUnit( PlayerUnit, PlayerGroup ) + self:I( { Mission = self:GetName(), PlayerUnit = PlayerUnit, PlayerGroup = PlayerGroup } ) + + local PlayerUnitAdded = false + + for TaskID, Task in pairs( self:GetTasks() ) do + local Task = Task -- Tasking.Task#TASK + if Task:JoinUnit( PlayerUnit, PlayerGroup ) then + PlayerUnitAdded = true + end + end + + return PlayerUnitAdded +end + +--- Aborts a PlayerUnit from the Mission. +-- For each Task within the Mission, the PlayerUnit is removed from Task where it is assigned. +-- If the Unit was not part of a Task in the Mission, false is returned. +-- If the Unit is part of a Task in the Mission, true is returned. +-- @param #MISSION self +-- @param Wrapper.Unit#UNIT PlayerUnit The CLIENT or UNIT of the Player joining the Mission. +-- @return #MISSION +function MISSION:AbortUnit( PlayerUnit ) + self:F( { PlayerUnit = PlayerUnit } ) + + for TaskID, Task in pairs( self:GetTasks() ) do + local Task = Task -- Tasking.Task#TASK + local PlayerGroup = PlayerUnit:GetGroup() + Task:AbortGroup( PlayerGroup ) + end + + return self +end + +--- Handles a crash of a PlayerUnit from the Mission. +-- For each Task within the Mission, the PlayerUnit is removed from Task where it is assigned. +-- If the Unit was not part of a Task in the Mission, false is returned. +-- If the Unit is part of a Task in the Mission, true is returned. +-- @param #MISSION self +-- @param Wrapper.Unit#UNIT PlayerUnit The CLIENT or UNIT of the Player crashing. +-- @return #MISSION +function MISSION:CrashUnit( PlayerUnit ) + self:F( { PlayerUnit = PlayerUnit } ) + + for TaskID, Task in pairs( self:GetTasks() ) do + local Task = Task -- Tasking.Task#TASK + local PlayerGroup = PlayerUnit:GetGroup() + Task:CrashGroup( PlayerGroup ) + end + + return self +end + +--- Add a scoring to the mission. +-- @param #MISSION self +-- @return #MISSION self +function MISSION:AddScoring( Scoring ) + self.Scoring = Scoring + return self +end + +--- Get the scoring object of a mission. +-- @param #MISSION self +-- @return #SCORING Scoring +function MISSION:GetScoring() + return self.Scoring +end + +--- Gets the groups for which TASKS are given in the mission +-- @param #MISSION self +-- @param Core.Set#SET_GROUP GroupSet +-- @return Core.Set#SET_GROUP +function MISSION:GetGroups() + + return self:AddGroups() + +end + +--- Adds the groups for which TASKS are given in the mission +-- @param #MISSION self +-- @param Core.Set#SET_GROUP GroupSet +-- @return Core.Set#SET_GROUP +function MISSION:AddGroups( GroupSet ) + + GroupSet = GroupSet or SET_GROUP:New() + + for TaskID, Task in pairs( self:GetTasks() ) do + local Task = Task -- Tasking.Task#TASK + GroupSet = Task:AddGroups( GroupSet ) + end + + return GroupSet + +end + + +--- Sets the Planned Task menu. +-- @param #MISSION self +-- @param #number MenuTime +function MISSION:SetMenu( MenuTime ) + self:F( { self:GetName(), MenuTime } ) + + local MenuCount = {} + --for TaskID, Task in UTILS.spairs( self:GetTasks(), function( t, a, b ) return t[a]:ReportOrder( ReportGroup ) < t[b]:ReportOrder( ReportGroup ) end ) do + for TaskID, Task in pairs( self:GetTasks() ) do + local Task = Task -- Tasking.Task#TASK + local TaskType = Task:GetType() + MenuCount[TaskType] = MenuCount[TaskType] or 1 + if MenuCount[TaskType] <= 10 then + Task:SetMenu( MenuTime ) + MenuCount[TaskType] = MenuCount[TaskType] + 1 + end + end +end + +--- Removes the Planned Task menu. +-- @param #MISSION self +-- @param #number MenuTime +function MISSION:RemoveMenu( MenuTime ) + self:F( { self:GetName(), MenuTime } ) + + for _, Task in pairs( self:GetTasks() ) do + local Task = Task -- Tasking.Task#TASK + Task:RemoveMenu( MenuTime ) + end +end + + + +do -- Group Assignment + + --- Returns if the @{Mission} is assigned to the Group. + -- @param #MISSION self + -- @param Wrapper.Group#GROUP MissionGroup + -- @return #boolean + function MISSION:IsGroupAssigned( MissionGroup ) + + local MissionGroupName = MissionGroup:GetName() + + if self.AssignedGroups[MissionGroupName] == MissionGroup then + self:T2( { "Mission is assigned to:", MissionGroup:GetName() } ) + return true + end + + self:T2( { "Mission is not assigned to:", MissionGroup:GetName() } ) + return false + end + + + --- Set @{Wrapper.Group} assigned to the @{Mission}. + -- @param #MISSION self + -- @param Wrapper.Group#GROUP MissionGroup + -- @return #MISSION + function MISSION:SetGroupAssigned( MissionGroup ) + + local MissionName = self:GetName() + local MissionGroupName = MissionGroup:GetName() + + self.AssignedGroups[MissionGroupName] = MissionGroup + self:I( string.format( "Mission %s is assigned to %s", MissionName, MissionGroupName ) ) + + return self + end + + --- Clear the @{Wrapper.Group} assignment from the @{Mission}. + -- @param #MISSION self + -- @param Wrapper.Group#GROUP MissionGroup + -- @return #MISSION + function MISSION:ClearGroupAssignment( MissionGroup ) + + local MissionName = self:GetName() + local MissionGroupName = MissionGroup:GetName() + + self.AssignedGroups[MissionGroupName] = nil + --self:E( string.format( "Mission %s is unassigned to %s", MissionName, MissionGroupName ) ) + + return self + end + +end + +--- Gets the COMMANDCENTER. +-- @param #MISSION self +-- @return Tasking.CommandCenter#COMMANDCENTER +function MISSION:GetCommandCenter() + return self.CommandCenter +end + + +--- Removes a Task menu. +-- @param #MISSION self +-- @param Tasking.Task#TASK Task +-- @return #MISSION self +function MISSION:RemoveTaskMenu( Task ) + + Task:RemoveMenu() +end + + +--- Gets the root mission menu for the TaskGroup. Obsolete?! Originally no reference to TaskGroup parameter! +-- @param #MISSION self +-- @param Wrapper.Group#GROUP TaskGroup Task group. +-- @return Core.Menu#MENU_COALITION self +function MISSION:GetRootMenu( TaskGroup ) -- R2.2 + + local CommandCenter = self:GetCommandCenter() + local CommandCenterMenu = CommandCenter:GetMenu( TaskGroup ) + + local MissionName = self:GetText() + --local MissionMenu = CommandCenterMenu:GetMenu( MissionName ) + + self.MissionMenu = MENU_COALITION:New( self.MissionCoalition, MissionName, CommandCenterMenu ) + + return self.MissionMenu +end + +--- Gets the mission menu for the TaskGroup. +-- @param #MISSION self +-- @param Wrapper.Group#GROUP TaskGroup Task group. +-- @return Core.Menu#MENU_COALITION self +function MISSION:GetMenu( TaskGroup ) -- R2.1 -- Changed Menu Structure + + local CommandCenter = self:GetCommandCenter() + local CommandCenterMenu = CommandCenter:GetMenu( TaskGroup ) + + self.MissionGroupMenu = self.MissionGroupMenu or {} + self.MissionGroupMenu[TaskGroup] = self.MissionGroupMenu[TaskGroup] or {} + + local GroupMenu = self.MissionGroupMenu[TaskGroup] + + local MissionText = self:GetText() + self.MissionMenu = MENU_GROUP:New( TaskGroup, MissionText, CommandCenterMenu ) + + GroupMenu.BriefingMenu = MENU_GROUP_COMMAND:New( TaskGroup, "Mission Briefing", self.MissionMenu, self.MenuReportBriefing, self, TaskGroup ) + + GroupMenu.MarkTasks = MENU_GROUP_COMMAND:New( TaskGroup, "Mark Task Locations on Map", self.MissionMenu, self.MarkTargetLocations, self, TaskGroup ) + GroupMenu.TaskReportsMenu = MENU_GROUP:New( TaskGroup, "Task Reports", self.MissionMenu ) + GroupMenu.ReportTasksMenu = MENU_GROUP_COMMAND:New( TaskGroup, "Report Tasks Summary", GroupMenu.TaskReportsMenu, self.MenuReportTasksSummary, self, TaskGroup ) + GroupMenu.ReportPlannedTasksMenu = MENU_GROUP_COMMAND:New( TaskGroup, "Report Planned Tasks", GroupMenu.TaskReportsMenu, self.MenuReportTasksPerStatus, self, TaskGroup, "Planned" ) + GroupMenu.ReportAssignedTasksMenu = MENU_GROUP_COMMAND:New( TaskGroup, "Report Assigned Tasks", GroupMenu.TaskReportsMenu, self.MenuReportTasksPerStatus, self, TaskGroup, "Assigned" ) + GroupMenu.ReportSuccessTasksMenu = MENU_GROUP_COMMAND:New( TaskGroup, "Report Successful Tasks", GroupMenu.TaskReportsMenu, self.MenuReportTasksPerStatus, self, TaskGroup, "Success" ) + GroupMenu.ReportFailedTasksMenu = MENU_GROUP_COMMAND:New( TaskGroup, "Report Failed Tasks", GroupMenu.TaskReportsMenu, self.MenuReportTasksPerStatus, self, TaskGroup, "Failed" ) + GroupMenu.ReportHeldTasksMenu = MENU_GROUP_COMMAND:New( TaskGroup, "Report Held Tasks", GroupMenu.TaskReportsMenu, self.MenuReportTasksPerStatus, self, TaskGroup, "Hold" ) + + GroupMenu.PlayerReportsMenu = MENU_GROUP:New( TaskGroup, "Statistics Reports", self.MissionMenu ) + GroupMenu.ReportMissionHistory = MENU_GROUP_COMMAND:New( TaskGroup, "Report Mission Progress", GroupMenu.PlayerReportsMenu, self.MenuReportPlayersProgress, self, TaskGroup ) + GroupMenu.ReportPlayersPerTaskMenu = MENU_GROUP_COMMAND:New( TaskGroup, "Report Players per Task", GroupMenu.PlayerReportsMenu, self.MenuReportPlayersPerTask, self, TaskGroup ) + + return self.MissionMenu +end + + + + +--- Get the TASK identified by the TaskNumber from the Mission. This function is useful in GoalFunctions. +-- @param #string TaskName The Name of the @{Task} within the @{Mission}. +-- @return Tasking.Task#TASK The Task +-- @return #nil Returns nil if no task was found. +function MISSION:GetTask( TaskName ) + self:F( { TaskName } ) + + return self.Tasks[TaskName] +end + + +--- Return the next @{Task} ID to be completed within the @{Mission}. +-- @param #MISSION self +-- @param Tasking.Task#TASK Task is the @{Task} object. +-- @return Tasking.Task#TASK The task added. +function MISSION:GetNextTaskID( Task ) + + self.TaskNumber = self.TaskNumber + 1 + + return self.TaskNumber +end + + +--- Register a @{Task} to be completed within the @{Mission}. +-- Note that there can be multiple @{Task}s registered to be completed. +-- Each Task can be set a certain Goals. The Mission will not be completed until all Goals are reached. +-- @param #MISSION self +-- @param Tasking.Task#TASK Task is the @{Task} object. +-- @return Tasking.Task#TASK The task added. +function MISSION:AddTask( Task ) + + local TaskName = Task:GetTaskName() + self:I( { "==> Adding TASK ", MissionName = self:GetName(), TaskName = TaskName } ) + + self.Tasks[TaskName] = Task + + self:GetCommandCenter():SetMenu() + + return Task +end + + +--- Removes a @{Task} to be completed within the @{Mission}. +-- Note that there can be multiple @{Task}s registered to be completed. +-- Each Task can be set a certain Goals. The Mission will not be completed until all Goals are reached. +-- @param #MISSION self +-- @param Tasking.Task#TASK Task is the @{Task} object. +-- @return #nil The cleaned Task reference. +function MISSION:RemoveTask( Task ) + + local TaskName = Task:GetTaskName() + self:I( { "<== Removing TASK ", MissionName = self:GetName(), TaskName = TaskName } ) + + self:F( TaskName ) + self.Tasks[TaskName] = self.Tasks[TaskName] or { n = 0 } + + -- Ensure everything gets garbarge collected. + self.Tasks[TaskName] = nil + Task = nil + + collectgarbage() + + self:GetCommandCenter():SetMenu() + + return nil +end + +--- Is the @{Mission} **COMPLETED**. +-- @param #MISSION self +-- @return #boolean +function MISSION:IsCOMPLETED() + return self:Is( "COMPLETED" ) +end + +--- Is the @{Mission} **IDLE**. +-- @param #MISSION self +-- @return #boolean +function MISSION:IsIDLE() + return self:Is( "IDLE" ) +end + +--- Is the @{Mission} **ENGAGED**. +-- @param #MISSION self +-- @return #boolean +function MISSION:IsENGAGED() + return self:Is( "ENGAGED" ) +end + +--- Is the @{Mission} **FAILED**. +-- @param #MISSION self +-- @return #boolean +function MISSION:IsFAILED() + return self:Is( "FAILED" ) +end + +--- Is the @{Mission} **HOLD**. +-- @param #MISSION self +-- @return #boolean +function MISSION:IsHOLD() + return self:Is( "HOLD" ) +end + +--- Validates if the Mission has a Group +-- @param #MISSION +-- @return #boolean true if the Mission has a Group. +function MISSION:HasGroup( TaskGroup ) + local Has = false + + for TaskID, Task in pairs( self:GetTasks() ) do + local Task = Task -- Tasking.Task#TASK + if Task:HasGroup( TaskGroup ) then + Has = true + break + end + end + + return Has +end + +--- @param #MISSION self +-- @return #number +function MISSION:GetTasksRemaining() + -- Determine how many tasks are remaining. + local TasksRemaining = 0 + for TaskID, Task in pairs( self:GetTasks() ) do + local Task = Task -- Tasking.Task#TASK + if Task:IsStateSuccess() or Task:IsStateFailed() then + else + TasksRemaining = TasksRemaining + 1 + end + end + return TasksRemaining +end + +--- @param #MISSION self +-- @return #number +function MISSION:GetTaskTypes() + -- Determine how many tasks are remaining. + local TaskTypeList = {} + local TasksRemaining = 0 + for TaskID, Task in pairs( self:GetTasks() ) do + local Task = Task -- Tasking.Task#TASK + local TaskType = Task:GetType() + TaskTypeList[TaskType] = TaskType + end + return TaskTypeList +end + + +function MISSION:AddPlayerName( PlayerName ) + self.PlayerNames = self.PlayerNames or {} + self.PlayerNames[PlayerName] = PlayerName + return self +end + +function MISSION:GetPlayerNames() + return self.PlayerNames +end + + +--- Create a briefing report of the Mission. +-- @param #MISSION self +-- @return #string +function MISSION:ReportBriefing() + + local Report = REPORT:New() + + -- List the name of the mission. + local Name = self:GetText() + + -- Determine the status of the mission. + local Status = "<" .. self:GetState() .. ">" + + Report:Add( string.format( '%s - %s - Mission Briefing Report', Name, Status ) ) + + Report:Add( self.MissionBriefing ) + + return Report:Text() +end + + +----- Create a status report of the Mission. +---- This reports provides a one liner of the mission status. It indicates how many players and how many Tasks. +---- +---- Mission "" - Status "" +---- - Task Types: , +---- - Planned Tasks (xp) +---- - Assigned Tasks(xp) +---- - Success Tasks (xp) +---- - Hold Tasks (xp) +---- - Cancelled Tasks (xp) +---- - Aborted Tasks (xp) +---- - Failed Tasks (xp) +---- +---- @param #MISSION self +---- @return #string +--function MISSION:ReportSummary() +-- +-- local Report = REPORT:New() +-- +-- -- List the name of the mission. +-- local Name = self:GetText() +-- +-- -- Determine the status of the mission. +-- local Status = "<" .. self:GetState() .. ">" +-- +-- Report:Add( string.format( '%s - Status "%s"', Name, Status ) ) +-- +-- local TaskTypes = self:GetTaskTypes() +-- +-- Report:Add( string.format( " - Task Types: %s", table.concat(TaskTypes, ", " ) ) ) +-- +-- local TaskStatusList = { "Planned", "Assigned", "Success", "Hold", "Cancelled", "Aborted", "Failed" } +-- +-- for TaskStatusID, TaskStatus in pairs( TaskStatusList ) do +-- local TaskCount = 0 +-- local TaskPlayerCount = 0 +-- -- Determine how many tasks are remaining. +-- for TaskID, Task in pairs( self:GetTasks() ) do +-- local Task = Task -- Tasking.Task#TASK +-- if Task:Is( TaskStatus ) then +-- TaskCount = TaskCount + 1 +-- TaskPlayerCount = TaskPlayerCount + Task:GetPlayerCount() +-- end +-- end +-- if TaskCount > 0 then +-- Report:Add( string.format( " - %02d %s Tasks (%dp)", TaskCount, TaskStatus, TaskPlayerCount ) ) +-- end +-- end +-- +-- return Report:Text() +--end + + +--- Create an active player report of the Mission. +-- This reports provides a one liner of the mission status. It indicates how many players and how many Tasks. +-- +-- Mission "" - - Active Players Report +-- - Player ": Task , Task +-- - Player : Task , Task +-- - .. +-- +-- @param #MISSION self +-- @return #string +function MISSION:ReportPlayersPerTask( ReportGroup ) + + local Report = REPORT:New() + + -- List the name of the mission. + local Name = self:GetText() + + -- Determine the status of the mission. + local Status = "<" .. self:GetState() .. ">" + + Report:Add( string.format( '%s - %s - Players per Task Report', Name, Status ) ) + + local PlayerList = {} + + -- Determine how many tasks are remaining. + for TaskID, Task in pairs( self:GetTasks() ) do + local Task = Task -- Tasking.Task#TASK + local PlayerNames = Task:GetPlayerNames() + for PlayerName, PlayerGroup in pairs( PlayerNames ) do + PlayerList[PlayerName] = Task:GetName() + end + + end + + for PlayerName, TaskName in pairs( PlayerList ) do + Report:Add( string.format( ' - Player (%s): Task "%s"', PlayerName, TaskName ) ) + end + + return Report:Text() +end + +--- Create an Mission Progress report of the Mission. +-- This reports provides a one liner per player of the mission achievements per task. +-- +-- Mission "" - - Active Players Report +-- - Player : Task : +-- - Player : Task : +-- - .. +-- +-- @param #MISSION self +-- @return #string +function MISSION:ReportPlayersProgress( ReportGroup ) + + local Report = REPORT:New() + + -- List the name of the mission. + local Name = self:GetText() + + -- Determine the status of the mission. + local Status = "<" .. self:GetState() .. ">" + + Report:Add( string.format( '%s - %s - Players per Task Progress Report', Name, Status ) ) + + local PlayerList = {} + + -- Determine how many tasks are remaining. + for TaskID, Task in pairs( self:GetTasks() ) do + local Task = Task -- Tasking.Task#TASK + local TaskName = Task:GetName() + local Goal = Task:GetGoal() + PlayerList[TaskName] = PlayerList[TaskName] or {} + if Goal then + local TotalContributions = Goal:GetTotalContributions() + local PlayerContributions = Goal:GetPlayerContributions() + self:F( { TotalContributions = TotalContributions, PlayerContributions = PlayerContributions } ) + for PlayerName, PlayerContribution in pairs( PlayerContributions ) do + PlayerList[TaskName][PlayerName] = string.format( 'Player (%s): Task "%s": %d%%', PlayerName, TaskName, PlayerContributions[PlayerName] * 100 / TotalContributions ) + end + else + PlayerList[TaskName]["_"] = string.format( 'Player (---): Task "%s": %d%%', TaskName, 0 ) + end + end + + for TaskName, TaskData in pairs( PlayerList ) do + for PlayerName, TaskText in pairs( TaskData ) do + Report:Add( string.format( ' - %s', TaskText ) ) + end + end + + return Report:Text() +end + + +--- Mark all the target locations on the Map. +-- @param #MISSION self +-- @param Wrapper.Group#GROUP ReportGroup +-- @return #string +function MISSION:MarkTargetLocations( ReportGroup ) + + local Report = REPORT:New() + + -- List the name of the mission. + local Name = self:GetText() + + -- Determine the status of the mission. + local Status = "<" .. self:GetState() .. ">" + + Report:Add( string.format( '%s - %s - All Tasks are marked on the map. Select a Task from the Mission Menu and Join the Task!!!', Name, Status ) ) + + -- Determine how many tasks are remaining. + for TaskID, Task in UTILS.spairs( self:GetTasks(), function( t, a, b ) return t[a]:ReportOrder( ReportGroup ) < t[b]:ReportOrder( ReportGroup ) end ) do + local Task = Task -- Tasking.Task#TASK + Task:MenuMarkToGroup( ReportGroup ) + end + + return Report:Text() +end + + +--- Create a summary report of the Mission (one line). +-- @param #MISSION self +-- @param Wrapper.Group#GROUP ReportGroup +-- @return #string +function MISSION:ReportSummary( ReportGroup ) + + local Report = REPORT:New() + + -- List the name of the mission. + local Name = self:GetText() + + -- Determine the status of the mission. + local Status = "<" .. self:GetState() .. ">" + + Report:Add( string.format( '%s - %s - Task Overview Report', Name, Status ) ) + + -- Determine how many tasks are remaining. + for TaskID, Task in UTILS.spairs( self:GetTasks(), function( t, a, b ) return t[a]:ReportOrder( ReportGroup ) < t[b]:ReportOrder( ReportGroup ) end ) do + local Task = Task -- Tasking.Task#TASK + Report:Add( "- " .. Task:ReportSummary( ReportGroup ) ) + end + + return Report:Text() +end + +--- Create a overview report of the Mission (multiple lines). +-- @param #MISSION self +-- @return #string +function MISSION:ReportOverview( ReportGroup, TaskStatus ) + + self:F( { TaskStatus = TaskStatus } ) + + local Report = REPORT:New() + + -- List the name of the mission. + local Name = self:GetText() + + -- Determine the status of the mission. + local Status = "<" .. self:GetState() .. ">" + + Report:Add( string.format( '%s - %s - %s Tasks Report', Name, Status, TaskStatus ) ) + + -- Determine how many tasks are remaining. + local Tasks = 0 + for TaskID, Task in UTILS.spairs( self:GetTasks(), function( t, a, b ) return t[a]:ReportOrder( ReportGroup ) < t[b]:ReportOrder( ReportGroup ) end ) do + local Task = Task -- Tasking.Task#TASK + if Task:Is( TaskStatus ) then + Report:Add( string.rep( "-", 140 ) ) + Report:Add( Task:ReportOverview( ReportGroup ) ) + end + Tasks = Tasks + 1 + if Tasks >= 8 then + break + end + end + + return Report:Text() +end + +--- Create a detailed report of the Mission, listing all the details of the Task. +-- @param #MISSION self +-- @return #string +function MISSION:ReportDetails( ReportGroup ) + + local Report = REPORT:New() + + -- List the name of the mission. + local Name = self:GetText() + + -- Determine the status of the mission. + local Status = "<" .. self:GetState() .. ">" + + Report:Add( string.format( '%s - %s - Task Detailed Report', Name, Status ) ) + + -- Determine how many tasks are remaining. + local TasksRemaining = 0 + for TaskID, Task in pairs( self:GetTasks() ) do + local Task = Task -- Tasking.Task#TASK + Report:Add( string.rep( "-", 140 ) ) + Report:Add( Task:ReportDetails( ReportGroup ) ) + end + + return Report:Text() +end + +--- Get all the TASKs from the Mission. This function is useful in GoalFunctions. +-- @return {TASK,...} Structure of TASKS with the @{TASK} number as the key. +-- @usage +-- -- Get Tasks from the Mission. +-- Tasks = Mission:GetTasks() +-- env.info( "Task 2 Completion = " .. Tasks[2]:GetGoalPercentage() .. "%" ) +function MISSION:GetTasks() + + return self.Tasks or {} +end + +--- Get the relevant tasks of a TaskGroup. +-- @param #MISSION +-- @param Wrapper.Group#GROUP TaskGroup +-- @return #list +function MISSION:GetGroupTasks( TaskGroup ) + + local Tasks = {} + + for TaskID, Task in pairs( self:GetTasks() ) do + local Task = Task -- Tasking.Task#TASK + if Task:HasGroup( TaskGroup ) then + Tasks[#Tasks+1] = Task + end + end + + return Tasks +end + + +--- Reports the briefing. +-- @param #MISSION self +-- @param Wrapper.Group#GROUP ReportGroup The group to which the report needs to be sent. +function MISSION:MenuReportBriefing( ReportGroup ) + + local Report = self:ReportBriefing() + + self:GetCommandCenter():MessageTypeToGroup( Report, ReportGroup, MESSAGE.Type.Briefing ) +end + + +--- Mark all the targets of the Mission on the Map. +-- @param #MISSION self +-- @param Wrapper.Group#GROUP ReportGroup +function MISSION:MenuMarkTargetLocations( ReportGroup ) + + local Report = self:MarkTargetLocations( ReportGroup ) + + self:GetCommandCenter():MessageTypeToGroup( Report, ReportGroup, MESSAGE.Type.Overview ) +end + + + +--- Report the task summary. +-- @param #MISSION self +-- @param Wrapper.Group#GROUP ReportGroup +function MISSION:MenuReportTasksSummary( ReportGroup ) + + local Report = self:ReportSummary( ReportGroup ) + + self:GetCommandCenter():MessageTypeToGroup( Report, ReportGroup, MESSAGE.Type.Overview ) +end + + + + +--- @param #MISSION self +-- @param #string TaskStatus The status +-- @param Wrapper.Group#GROUP ReportGroup +function MISSION:MenuReportTasksPerStatus( ReportGroup, TaskStatus ) + + local Report = self:ReportOverview( ReportGroup, TaskStatus ) + + self:GetCommandCenter():MessageTypeToGroup( Report, ReportGroup, MESSAGE.Type.Overview ) +end + + +--- @param #MISSION self +-- @param Wrapper.Group#GROUP ReportGroup +function MISSION:MenuReportPlayersPerTask( ReportGroup ) + + local Report = self:ReportPlayersPerTask() + + self:GetCommandCenter():MessageTypeToGroup( Report, ReportGroup, MESSAGE.Type.Overview ) +end + +--- @param #MISSION self +-- @param Wrapper.Group#GROUP ReportGroup +function MISSION:MenuReportPlayersProgress( ReportGroup ) + + local Report = self:ReportPlayersProgress() + + self:GetCommandCenter():MessageTypeToGroup( Report, ReportGroup, MESSAGE.Type.Overview ) +end + + + + + +--- **Tasking** -- A task object governs the main engine to administer human taskings. +-- +-- **Features:** +-- +-- * A base class for other task classes filling in the details and making a concrete task process. +-- * Manage the overall task execution, following-up the progression made by the pilots and actors. +-- * Provide a mechanism to set a task status, depending on the progress made within the task. +-- * Manage a task briefing. +-- * Manage the players executing the task. +-- * Manage the task menu system. +-- * Manage the task goal and scoring. +-- +-- === +-- +-- # 1) Tasking from a player perspective. +-- +-- Tasking can be controlled by using the "other" menu in the radio menu of the player group. +-- +-- ![Other Menu](../Tasking/Menu_Main.JPG) +-- +-- ## 1.1) Command Centers govern multiple Missions. +-- +-- Depending on the tactical situation, your coalition may have one (or multiple) command center(s). +-- These command centers govern one (or multiple) mission(s). +-- +-- For each command center, there will be a separate **Command Center Menu** that focuses on the missions governed by that command center. +-- +-- ![Command Center](../Tasking/Menu_CommandCenter.JPG) +-- +-- In the above example menu structure, there is one command center with the name **`[Lima]`**. +-- The command center has one @{Tasking.Mission}, named **`"Overlord"`** with **`High`** priority. +-- +-- ## 1.2) Missions govern multiple Tasks. +-- +-- A mission has a mission goal to be achieved by the players within the coalition. +-- The mission goal is actually dependent on the tactical situation of the overall battlefield and the conditions set to achieve the goal. +-- So a mission can be much more than just shoot stuff ... It can be a combination of different conditions or events to complete a mission goal. +-- +-- A mission can be in a specific state during the simulation run. For more information about these states, please check the @{Tasking.Mission} section. +-- +-- To achieve the mission goal, a mission administers @{Tasking.Task}s that are set to achieve the mission goal by the human players. +-- Each of these tasks can be **dynamically created** using a task dispatcher, or **coded** by the mission designer. +-- Each mission has a separate **Mission Menu**, that focuses on the administration of these tasks. +-- +-- On top, a mission has a mission briefing, can help to allocate specific points of interest on the map, and provides various reports. +-- +-- ![Mission](../Tasking/Menu_Mission.JPG) +-- +-- The above shows a mission menu in detail of **`"Overlord"`**. +-- +-- The two other menus are related to task assignment. Which will be detailed later. +-- +-- ### 1.2.1) Mission briefing. +-- +-- The task briefing will show a message containing a description of the mission goal, and other tactical information. +-- +-- ![Mission](../Tasking/Report_Briefing.JPG) +-- +-- ### 1.2.2) Mission Map Locations. +-- +-- Various points of interest as part of the mission can be indicated on the map using the *Mark Task Locations on Map* menu. +-- As a result, the map will contain various points of interest for the player (group). +-- +-- ![Mission](../Tasking/Report_Mark_Task_Location.JPG) +-- +-- ### 1.2.3) Mission Task Reports. +-- +-- Various reports can be generated on the status of each task governed within the mission. +-- +-- ![Mission](../Tasking/Report_Task_Summary.JPG) +-- +-- The Task Overview Report will show each task, with its task status and a short coordinate information. +-- +-- ![Mission](../Tasking/Report_Tasks_Planned.JPG) +-- +-- The other Task Menus will show for each task more details, for example here the planned tasks report. +-- Note that the order of the tasks are shortest distance first to the unit position seated by the player. +-- +-- ### 1.2.4) Mission Statistics. +-- +-- Various statistics can be displayed regarding the mission. +-- +-- ![Mission](../Tasking/Report_Statistics_Progress.JPG) +-- +-- A statistic report on the progress of the mission. Each task achievement will increase the %-tage to 100% as a goal to complete the task. +-- +-- ## 1.3) Join a Task. +-- +-- The mission menu contains a very important option, that is to join a task governed within the mission. +-- In order to join a task, select the **Join Planned Task** menu, and a new menu will be given. +-- +-- ![Mission](../Tasking/Menu_Join_Planned_Tasks.JPG) +-- +-- A mission governs multiple tasks, as explained earlier. Each task is of a certain task type. +-- This task type was introduced to have some sort of task classification system in place for the player. +-- A short acronym is shown that indicates the task type. The meaning of each acronym can be found in the task types explanation. +-- +-- ![Mission](../Tasking/Menu_Join_Tasks.JPG) +-- +-- When the player selects a task type, a list of the available tasks of that type are listed... +-- In this case the **`SEAD`** task type was selected and a list of available **`SEAD`** tasks can be selected. +-- +-- ![Mission](../Tasking/Menu_Join_Planned_Task.JPG) +-- +-- A new list of menu options are now displayed that allow to join the task selected, but also to obtain first some more information on the task. +-- +-- ### 1.3.1) Report Task Details. +-- +-- ![Mission](../Tasking/Report_Task_Detailed.JPG) +-- +-- When selected, a message is displayed that shows detailed information on the task, like the coordinate, enemy target information, threat level etc. +-- +-- ### 1.3.2) Mark Task Location on Map. +-- +-- ![Mission](../Tasking/Report_Task_Detailed.JPG) +-- +-- When selected, the target location on the map is indicated with specific information on the task. +-- +-- ### 1.3.3) Join Task. +-- +-- ![Mission](../Tasking/Report_Task_Detailed.JPG) +-- +-- By joining a task, the player will indicate that the task is assigned to him, and the task is started. +-- The Command Center will communicate several task details to the player and the coalition of the player. +-- +-- ## 1.4) Task Control and Actions. +-- +-- ![Mission](../Tasking/Menu_Main_Task.JPG) +-- +-- When a player has joined a task, a **Task Action Menu** is available to be used by the player. +-- +-- ![Mission](../Tasking/Menu_Task.JPG) +-- +-- The task action menu contains now menu items specific to the task, but also one generic menu item, which is to control the task. +-- This **Task Control Menu** allows to display again the task details and the task map location information. +-- But it also allows to abort a task! +-- +-- Depending on the task type, the task action menu can contain more menu items which are specific to the task. +-- For example, cargo transportation tasks will contain various additional menu items to select relevant cargo coordinates, +-- or to load/unload cargo. +-- +-- ## 1.5) Automatic task assignment. +-- +-- ![Command Center](../Tasking/Menu_CommandCenter.JPG) +-- +-- When we take back the command center menu, you see two addtional **Assign Task** menu items. +-- The menu **Assign Task On** will automatically allocate a task to the player. +-- After the selection of this menu, the menu will change into **Assign Task Off**, +-- and will need to be selected again by the player to switch of the automatic task assignment. +-- +-- The other option is to select **Assign Task**, which will assign a new random task to the player. +-- +-- When a task is automatically assigned to a player, the task needs to be confirmed as accepted within 30 seconds. +-- If this is not the case, the task will be cancelled automatically, and a new random task will be assigned to the player. +-- This will continue to happen until the player accepts the task or switches off the automatic task assignment process. +-- +-- The player can accept the task using the menu **Confirm Task Acceptance** ... +-- +-- ## 1.6) Task states. +-- +-- A task has a state, reflecting the progress or completion status of the task: +-- +-- - **Planned**: Expresses that the task is created, but not yet in execution and is not assigned yet to a pilot. +-- - **Assigned**: Expresses that the task is assigned to a group of pilots, and that the task is in execution mode. +-- - **Success**: Expresses the successful execution and finalization of the task. +-- - **Failed**: Expresses the failure of a task. +-- - **Abort**: Expresses that the task is aborted by by the player using the abort menu. +-- - **Cancelled**: Expresses that the task is cancelled by HQ or through a logical situation where a cancellation of the task is required. +-- +-- ### 1.6.1) Task progress. +-- +-- The task governor takes care of the **progress** and **completion** of the task **goal(s)**. +-- Tasks are executed by **human pilots** and actors within a DCS simulation. +-- Pilots can use a **menu system** to engage or abort a task, and provides means to +-- understand the **task briefing** and goals, and the relevant **task locations** on the map and +-- obtain **various reports** related to the task. +-- +-- ### 1.6.2) Task completion. +-- +-- As the task progresses, the **task status** will change over time, from Planned state to Completed state. +-- **Multiple pilots** can execute the same task, as such, the tasking system provides a **co-operative model** for joint task execution. +-- Depending on the task progress, a **scoring** can be allocated to award pilots of the achievements made. +-- The scoring is fully flexible, and different levels of awarding can be provided depending on the task type and complexity. +-- +-- A normal flow of task status would evolve from the **Planned** state, to the **Assigned** state ending either in a **Success** or a **Failed** state. +-- +-- Planned -> Assigned -> Success +-- -> Failed +-- -> Cancelled +-- +-- The state completion is by default set to **Success**, if the goals of the task have been reached, but can be overruled by a goal method. +-- +-- Depending on the tactical situation, a task can be **Cancelled** by the mission governer. +-- It is actually the mission designer who has the flexibility to decide at which conditions a task would be set to **Success**, **Failed** or **Cancelled**. +-- This decision all depends on the task goals, and the phase/evolution of the task conditions that would accomplish the goals. +-- +-- For example, if the task goal is to merely destroy a target, and the target is mid-mission destroyed by another event than the pilot destroying the target, +-- the task goal could be set to **Failed**, or .. **Cancelled** ... +-- However, it could very well be also acceptable that the task would be flagged as **Success**. +-- +-- The tasking mechanism governs beside the progress also a scoring mechanism, and in case of goal completion without any active pilot involved +-- in the execution of the task, could result in a **Success** task completion status, but no score would be awared, as there were no players involved. +-- +-- These different completion states are important for the mission designer to reflect scoring to a player. +-- A success could mean a positive score to be given, while a failure could mean a negative score or penalties to be awarded. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- ### Contributions: +-- +-- === +-- +-- @module Tasking.Task +-- @image MOOSE.JPG + +--- @type TASK +-- @field Core.Scheduler#SCHEDULER TaskScheduler +-- @field Tasking.Mission#MISSION Mission +-- @field Core.Set#SET_GROUP SetGroup The Set of Groups assigned to the Task +-- @field Core.Fsm#FSM_PROCESS FsmTemplate +-- @field Tasking.Mission#MISSION Mission +-- @field Tasking.CommandCenter#COMMANDCENTER CommandCenter +-- @field Tasking.TaskInfo#TASKINFO TaskInfo +-- @extends Core.Fsm#FSM_TASK + +--- Governs the main engine to administer human taskings. +-- +-- A task is governed by a @{Tasking.Mission} object. Tasks are of different types. +-- The @{#TASK} object is used or derived by more detailed tasking classes that will implement the task execution mechanisms +-- and goals. +-- +-- # 1) Derived task classes. +-- +-- The following TASK_ classes are derived from @{#TASK}. +-- +-- TASK +-- TASK_A2A +-- TASK_A2A_ENGAGE +-- TASK_A2A_INTERCEPT +-- TASK_A2A_SWEEP +-- TASK_A2G +-- TASK_A2G_SEAD +-- TASK_A2G_CAS +-- TASK_A2G_BAI +-- TASK_CARGO +-- TASK_CARGO_TRANSPORT +-- TASK_CARGO_CSAR +-- +-- ## 1.1) A2A Tasks +-- +-- - @{Tasking.Task_A2A#TASK_A2A_ENGAGE} - Models an A2A engage task of a target group of airborne intruders mid-air. +-- - @{Tasking.Task_A2A#TASK_A2A_INTERCEPT} - Models an A2A ground intercept task of a target group of airborne intruders mid-air. +-- - @{Tasking.Task_A2A#TASK_A2A_SWEEP} - Models an A2A sweep task to clean an area of previously detected intruders mid-air. +-- +-- ## 1.2) A2G Tasks +-- +-- - @{Tasking.Task_A2G#TASK_A2G_SEAD} - Models an A2G Suppression or Extermination of Air Defenses task to clean an area of air to ground defense threats. +-- - @{Tasking.Task_A2G#TASK_A2G_CAS} - Models an A2G Close Air Support task to provide air support to nearby friendlies near the front-line. +-- - @{Tasking.Task_A2G#TASK_A2G_BAI} - Models an A2G Battlefield Air Interdiction task to provide air support to nearby friendlies near the front-line. +-- +-- ## 1.3) Cargo Tasks +-- +-- - @{Tasking.Task_Cargo#TASK_CARGO_TRANSPORT} - Models the transportation of cargo to deployment zones. +-- - @{Tasking.Task_Cargo#TASK_CARGO_CSAR} - Models the rescue of downed friendly pilots from behind enemy lines. +-- +-- +-- # 2) Task status events. +-- +-- The task statuses can be set by using the following methods: +-- +-- - @{#TASK.Success}() - Set the task to **Success** state. +-- - @{#TASK.Fail}() - Set the task to **Failed** state. +-- - @{#TASK.Hold}() - Set the task to **Hold** state. +-- - @{#TASK.Abort}() - Set the task to **Aborted** state, aborting the task. The task may be replanned. +-- - @{#TASK.Cancel}() - Set the task to **Cancelled** state, cancelling the task. +-- +-- The mentioned derived TASK_ classes are implementing the task status transitions out of the box. +-- So no extra logic needs to be written. +-- +-- # 3) Goal conditions for a task. +-- +-- Every 30 seconds, a @{#Task.Goal} trigger method is fired. +-- You as a mission designer, can capture the **Goal** event trigger to check your own task goal conditions and take action! +-- +-- ## 3.1) Goal event handler `OnAfterGoal()`. +-- +-- And this is a really great feature! Imagine a task which has **several conditions to check** before the task can move into **Success** state. +-- You can do this with the OnAfterGoal method. +-- +-- The following code provides an example of such a goal condition check implementation. +-- +-- function Task:OnAfterGoal() +-- if condition == true then +-- self:Success() -- This will flag the task to Succcess when the condition is true. +-- else +-- if condition2 == true and condition3 == true then +-- self:Fail() -- This will flag the task to Failed, when condition2 and condition3 would be true. +-- end +-- end +-- end +-- +-- So the @{#TASK.OnAfterGoal}() event handler would be called every 30 seconds automatically, +-- and within this method, you can now check the conditions and take respective action. +-- +-- ## 3.2) Goal event trigger `Goal()`. +-- +-- If you would need to check a goal at your own defined event timing, then just call the @{#TASK.Goal}() method within your logic. +-- The @{#TASK.OnAfterGoal}() event handler would then directly be called and would execute the logic. +-- Note that you can also delay the goal check by using the delayed event trigger syntax `:__Goal( Delay )`. +-- +-- +-- # 4) Score task completion. +-- +-- Upon reaching a certain task status in a task, additional scoring can be given. If the Mission has a scoring system attached, the scores will be added to the mission scoring. +-- Use the method @{#TASK.AddScore}() to add scores when a status is reached. +-- +-- # 5) Task briefing. +-- +-- A task briefing is a text that is shown to the player when he is assigned to the task. +-- The briefing is broadcasted by the command center owning the mission. +-- +-- The briefing is part of the parameters in the @{#TASK.New}() constructor, +-- but can separately be modified later in your mission using the +-- @{#TASK.SetBriefing}() method. +-- +-- +-- @field #TASK TASK +-- +TASK = { + ClassName = "TASK", + TaskScheduler = nil, + ProcessClasses = {}, -- The container of the Process classes that will be used to create and assign new processes for the task to ProcessUnits. + Processes = {}, -- The container of actual process objects instantiated and assigned to ProcessUnits. + Players = nil, + Scores = {}, + Menu = {}, + SetGroup = nil, + FsmTemplate = nil, + Mission = nil, + CommandCenter = nil, + TimeOut = 0, + AssignedGroups = {}, +} + +--- FSM PlayerAborted event handler prototype for TASK. +-- @function [parent=#TASK] OnAfterPlayerAborted +-- @param #TASK self +-- @param Wrapper.Unit#UNIT PlayerUnit The Unit of the Player when he went back to spectators or left the mission. +-- @param #string PlayerName The name of the Player. + +--- FSM PlayerCrashed event handler prototype for TASK. +-- @function [parent=#TASK] OnAfterPlayerCrashed +-- @param #TASK self +-- @param Wrapper.Unit#UNIT PlayerUnit The Unit of the Player when he crashed in the mission. +-- @param #string PlayerName The name of the Player. + +--- FSM PlayerDead event handler prototype for TASK. +-- @function [parent=#TASK] OnAfterPlayerDead +-- @param #TASK self +-- @param Wrapper.Unit#UNIT PlayerUnit The Unit of the Player when he died in the mission. +-- @param #string PlayerName The name of the Player. + +--- FSM Fail synchronous event function for TASK. +-- Use this event to Fail the Task. +-- @function [parent=#TASK] Fail +-- @param #TASK self + +--- FSM Fail asynchronous event function for TASK. +-- Use this event to Fail the Task. +-- @function [parent=#TASK] __Fail +-- @param #TASK self + +--- FSM Abort synchronous event function for TASK. +-- Use this event to Abort the Task. +-- @function [parent=#TASK] Abort +-- @param #TASK self + +--- FSM Abort asynchronous event function for TASK. +-- Use this event to Abort the Task. +-- @function [parent=#TASK] __Abort +-- @param #TASK self + +--- FSM Success synchronous event function for TASK. +-- Use this event to make the Task a Success. +-- @function [parent=#TASK] Success +-- @param #TASK self + +--- FSM Success asynchronous event function for TASK. +-- Use this event to make the Task a Success. +-- @function [parent=#TASK] __Success +-- @param #TASK self + +--- FSM Cancel synchronous event function for TASK. +-- Use this event to Cancel the Task. +-- @function [parent=#TASK] Cancel +-- @param #TASK self + +--- FSM Cancel asynchronous event function for TASK. +-- Use this event to Cancel the Task. +-- @function [parent=#TASK] __Cancel +-- @param #TASK self + +--- FSM Replan synchronous event function for TASK. +-- Use this event to Replan the Task. +-- @function [parent=#TASK] Replan +-- @param #TASK self + +--- FSM Replan asynchronous event function for TASK. +-- Use this event to Replan the Task. +-- @function [parent=#TASK] __Replan +-- @param #TASK self + + +--- Instantiates a new TASK. Should never be used. Interface Class. +-- @param #TASK self +-- @param Tasking.Mission#MISSION Mission The mission wherein the Task is registered. +-- @param Core.Set#SET_GROUP SetGroupAssign The set of groups for which the Task can be assigned. +-- @param #string TaskName The name of the Task +-- @param #string TaskType The type of the Task +-- @return #TASK self +function TASK:New( Mission, SetGroupAssign, TaskName, TaskType, TaskBriefing ) + + local self = BASE:Inherit( self, FSM_TASK:New( TaskName ) ) -- Tasking.Task#TASK + + self:SetStartState( "Planned" ) + self:AddTransition( "Planned", "Assign", "Assigned" ) + self:AddTransition( "Assigned", "AssignUnit", "Assigned" ) + self:AddTransition( "Assigned", "Success", "Success" ) + self:AddTransition( "Assigned", "Hold", "Hold" ) + self:AddTransition( "Assigned", "Fail", "Failed" ) + self:AddTransition( { "Planned", "Assigned" }, "Abort", "Aborted" ) + self:AddTransition( "Assigned", "Cancel", "Cancelled" ) + self:AddTransition( "Assigned", "Goal", "*" ) + + self.Fsm = {} + + local Fsm = self:GetUnitProcess() + Fsm:SetStartState( "Planned" ) + Fsm:AddProcess ( "Planned", "Accept", ACT_ASSIGN_ACCEPT:New( self.TaskBriefing ), { Assigned = "Assigned", Rejected = "Reject" } ) + Fsm:AddTransition( "Assigned", "Assigned", "*" ) + + --- Goal Handler OnBefore for TASK + -- @function [parent=#TASK] OnBeforeGoal + -- @param #TASK self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Wrapper.Unit#UNIT PlayerUnit The @{Wrapper.Unit} of the player. + -- @param #string PlayerName The name of the player. + -- @return #boolean + + --- Goal Handler OnAfter for TASK + -- @function [parent=#TASK] OnAfterGoal + -- @param #TASK self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Wrapper.Unit#UNIT PlayerUnit The @{Wrapper.Unit} of the player. + -- @param #string PlayerName The name of the player. + + --- Goal Trigger for TASK + -- @function [parent=#TASK] Goal + -- @param #TASK self + -- @param Wrapper.Unit#UNIT PlayerUnit The @{Wrapper.Unit} of the player. + -- @param #string PlayerName The name of the player. + + --- Goal Asynchronous Trigger for TASK + -- @function [parent=#TASK] __Goal + -- @param #TASK self + -- @param #number Delay + -- @param Wrapper.Unit#UNIT PlayerUnit The @{Wrapper.Unit} of the player. + -- @param #string PlayerName The name of the player. + + + + self:AddTransition( "*", "PlayerCrashed", "*" ) + self:AddTransition( "*", "PlayerAborted", "*" ) + self:AddTransition( "*", "PlayerRejected", "*" ) + self:AddTransition( "*", "PlayerDead", "*" ) + self:AddTransition( { "Failed", "Aborted", "Cancelled" }, "Replan", "Planned" ) + self:AddTransition( "*", "TimeOut", "Cancelled" ) + + self:F( "New TASK " .. TaskName ) + + self.Processes = {} + + self.Mission = Mission + self.CommandCenter = Mission:GetCommandCenter() + + self.SetGroup = SetGroupAssign + + self:SetType( TaskType ) + self:SetName( TaskName ) + self:SetID( Mission:GetNextTaskID( self ) ) -- The Mission orchestrates the task sequences .. + + self:SetBriefing( TaskBriefing ) + + + self.TaskInfo = TASKINFO:New( self ) + + self.TaskProgress = {} + + return self +end + +--- Get the Task FSM Process Template +-- @param #TASK self +-- @return Core.Fsm#FSM_PROCESS +function TASK:GetUnitProcess( TaskUnit ) + + if TaskUnit then + return self:GetStateMachine( TaskUnit ) + else + self.FsmTemplate = self.FsmTemplate or FSM_PROCESS:New() + return self.FsmTemplate + end +end + +--- Sets the Task FSM Process Template +-- @param #TASK self +-- @param Core.Fsm#FSM_PROCESS +function TASK:SetUnitProcess( FsmTemplate ) + + self.FsmTemplate = FsmTemplate +end + +--- Add a PlayerUnit to join the Task. +-- For each Group within the Task, the Unit is checked if it can join the Task. +-- If the Unit was not part of the Task, false is returned. +-- If the Unit is part of the Task, true is returned. +-- @param #TASK self +-- @param Wrapper.Unit#UNIT PlayerUnit The CLIENT or UNIT of the Player joining the Mission. +-- @param Wrapper.Group#GROUP PlayerGroup The GROUP of the player joining the Mission. +-- @return #boolean true if Unit is part of the Task. +function TASK:JoinUnit( PlayerUnit, PlayerGroup ) + self:F( { PlayerUnit = PlayerUnit, PlayerGroup = PlayerGroup } ) + + local PlayerUnitAdded = false + + local PlayerGroups = self:GetGroups() + + -- Is the PlayerGroup part of the PlayerGroups? + if PlayerGroups:IsIncludeObject( PlayerGroup ) then + + -- Check if the PlayerGroup is already assigned to the Task. If yes, the PlayerGroup is added to the Task. + -- If the PlayerGroup is not assigned to the Task, the menu needs to be set. In that case, the PlayerUnit will become the GroupPlayer leader. + if self:IsStatePlanned() or self:IsStateReplanned() then + --self:SetMenuForGroup( PlayerGroup ) + --self:MessageToGroups( PlayerUnit:GetPlayerName() .. " is planning to join Task " .. self:GetName() ) + end + if self:IsStateAssigned() then + local IsGroupAssigned = self:IsGroupAssigned( PlayerGroup ) + self:F( { IsGroupAssigned = IsGroupAssigned } ) + if IsGroupAssigned then + self:AssignToUnit( PlayerUnit ) + self:MessageToGroups( PlayerUnit:GetPlayerName() .. " joined Task " .. self:GetName() ) + end + end + end + + return PlayerUnitAdded +end + +--- A group rejecting a planned task. +-- @param #TASK self +-- @param Wrapper.Group#GROUP PlayerGroup The group rejecting the task. +-- @return #TASK +function TASK:RejectGroup( PlayerGroup ) + + local PlayerGroups = self:GetGroups() + + -- Is the PlayerGroup part of the PlayerGroups? + if PlayerGroups:IsIncludeObject( PlayerGroup ) then + + -- Check if the PlayerGroup is already assigned or is planned to be assigned to the Task. + -- If yes, the PlayerGroup is aborted from the Task. + -- If the PlayerUnit was the last unit of the PlayerGroup, the menu needs to be removed from the Group. + if self:IsStatePlanned() then + + local IsGroupAssigned = self:IsGroupAssigned( PlayerGroup ) + if IsGroupAssigned then + local PlayerName = PlayerGroup:GetUnit(1):GetPlayerName() + self:GetMission():GetCommandCenter():MessageToGroup( "Task " .. self:GetName() .. " has been rejected! We will select another task.", PlayerGroup ) + self:UnAssignFromGroup( PlayerGroup ) + + self:PlayerRejected( PlayerGroup:GetUnit(1) ) + end + + end + end + + return self +end + + +--- A group aborting the task. +-- @param #TASK self +-- @param Wrapper.Group#GROUP PlayerGroup The group aborting the task. +-- @return #TASK +function TASK:AbortGroup( PlayerGroup ) + + local PlayerGroups = self:GetGroups() + + -- Is the PlayerGroup part of the PlayerGroups? + if PlayerGroups:IsIncludeObject( PlayerGroup ) then + + -- Check if the PlayerGroup is already assigned or is planned to be assigned to the Task. + -- If yes, the PlayerGroup is aborted from the Task. + -- If the PlayerUnit was the last unit of the PlayerGroup, the menu needs to be removed from the Group. + if self:IsStateAssigned() then + + local IsGroupAssigned = self:IsGroupAssigned( PlayerGroup ) + if IsGroupAssigned then + local PlayerName = PlayerGroup:GetUnit(1):GetPlayerName() + self:UnAssignFromGroup( PlayerGroup ) + + -- Now check if the task needs to go to hold... + -- It will go to hold, if there are no players in the mission... + PlayerGroups:Flush( self ) + local IsRemaining = false + for GroupName, AssignedGroup in pairs( PlayerGroups:GetSet() or {} ) do + if self:IsGroupAssigned( AssignedGroup ) == true then + IsRemaining = true + self:F( { Task = self:GetName(), IsRemaining = IsRemaining } ) + break + end + end + + self:F( { Task = self:GetName(), IsRemaining = IsRemaining } ) + if IsRemaining == false then + self:Abort() + end + + self:PlayerAborted( PlayerGroup:GetUnit(1) ) + end + + end + end + + return self +end + + +--- A group crashing and thus aborting from the task. +-- @param #TASK self +-- @param Wrapper.Group#GROUP PlayerGroup The group aborting the task. +-- @return #TASK +function TASK:CrashGroup( PlayerGroup ) + self:F( { PlayerGroup = PlayerGroup } ) + + local PlayerGroups = self:GetGroups() + + -- Is the PlayerGroup part of the PlayerGroups? + if PlayerGroups:IsIncludeObject( PlayerGroup ) then + + -- Check if the PlayerGroup is already assigned to the Task. If yes, the PlayerGroup is aborted from the Task. + -- If the PlayerUnit was the last unit of the PlayerGroup, the menu needs to be removed from the Group. + if self:IsStateAssigned() then + local IsGroupAssigned = self:IsGroupAssigned( PlayerGroup ) + self:F( { IsGroupAssigned = IsGroupAssigned } ) + if IsGroupAssigned then + local PlayerName = PlayerGroup:GetUnit(1):GetPlayerName() + self:MessageToGroups( PlayerName .. " crashed! " ) + self:UnAssignFromGroup( PlayerGroup ) + + -- Now check if the task needs to go to hold... + -- It will go to hold, if there are no players in the mission... + + PlayerGroups:Flush( self ) + local IsRemaining = false + for GroupName, AssignedGroup in pairs( PlayerGroups:GetSet() or {} ) do + if self:IsGroupAssigned( AssignedGroup ) == true then + IsRemaining = true + self:F( { Task = self:GetName(), IsRemaining = IsRemaining } ) + break + end + end + + self:F( { Task = self:GetName(), IsRemaining = IsRemaining } ) + if IsRemaining == false then + self:Abort() + end + + self:PlayerCrashed( PlayerGroup:GetUnit(1) ) + end + + end + end + + return self +end + + + +--- Gets the Mission to where the TASK belongs. +-- @param #TASK self +-- @return Tasking.Mission#MISSION +function TASK:GetMission() + + return self.Mission +end + + +--- Gets the SET_GROUP assigned to the TASK. +-- @param #TASK self +-- @return Core.Set#SET_GROUP +function TASK:GetGroups() + + return self.SetGroup +end + + +--- Gets the SET_GROUP assigned to the TASK. +-- @param #TASK self +-- @param Core.Set#SET_GROUP GroupSet +-- @return Core.Set#SET_GROUP +function TASK:AddGroups( GroupSet ) + + GroupSet = GroupSet or SET_GROUP:New() + + self.SetGroup:ForEachGroup( + --- @param Wrapper.Group#GROUP GroupSet + function( GroupItem ) + GroupSet:Add( GroupItem:GetName(), GroupItem) + end + ) + + return GroupSet +end + +do -- Group Assignment + + --- Returns if the @{Task} is assigned to the Group. + -- @param #TASK self + -- @param Wrapper.Group#GROUP TaskGroup + -- @return #boolean + function TASK:IsGroupAssigned( TaskGroup ) + + local TaskGroupName = TaskGroup:GetName() + + if self.AssignedGroups[TaskGroupName] then + --self:T( { "Task is assigned to:", TaskGroup:GetName() } ) + return true + end + + --self:T( { "Task is not assigned to:", TaskGroup:GetName() } ) + return false + end + + + --- Set @{Wrapper.Group} assigned to the @{Task}. + -- @param #TASK self + -- @param Wrapper.Group#GROUP TaskGroup + -- @return #TASK + function TASK:SetGroupAssigned( TaskGroup ) + + local TaskName = self:GetName() + local TaskGroupName = TaskGroup:GetName() + + self.AssignedGroups[TaskGroupName] = TaskGroup + self:F( string.format( "Task %s is assigned to %s", TaskName, TaskGroupName ) ) + + -- Set the group to be assigned at mission level. This allows to decide the menu options on mission level for this group. + self:GetMission():SetGroupAssigned( TaskGroup ) + + local SetAssignedGroups = self:GetGroups() + +-- SetAssignedGroups:ForEachGroup( +-- function( AssignedGroup ) +-- if self:IsGroupAssigned(AssignedGroup) then +-- self:GetMission():GetCommandCenter():MessageToGroup( string.format( "Task %s is assigned to group %s.", TaskName, TaskGroupName ), AssignedGroup ) +-- else +-- self:GetMission():GetCommandCenter():MessageToGroup( string.format( "Task %s is assigned to your group.", TaskName ), AssignedGroup ) +-- end +-- end +-- ) + + return self + end + + --- Clear the @{Wrapper.Group} assignment from the @{Task}. + -- @param #TASK self + -- @param Wrapper.Group#GROUP TaskGroup + -- @return #TASK + function TASK:ClearGroupAssignment( TaskGroup ) + + local TaskName = self:GetName() + local TaskGroupName = TaskGroup:GetName() + + self.AssignedGroups[TaskGroupName] = nil + --self:F( string.format( "Task %s is unassigned to %s", TaskName, TaskGroupName ) ) + + -- Set the group to be assigned at mission level. This allows to decide the menu options on mission level for this group. + self:GetMission():ClearGroupAssignment( TaskGroup ) + + local SetAssignedGroups = self:GetGroups() + + SetAssignedGroups:ForEachGroup( + function( AssignedGroup ) + if self:IsGroupAssigned(AssignedGroup) then + --self:GetMission():GetCommandCenter():MessageToGroup( string.format( "Task %s is unassigned from group %s.", TaskName, TaskGroupName ), AssignedGroup ) + else + --self:GetMission():GetCommandCenter():MessageToGroup( string.format( "Task %s is unassigned from your group.", TaskName ), AssignedGroup ) + end + end + ) + + return self + end + +end + +do -- Group Assignment + + --- @param #TASK self + -- @param Actions.Act_Assign#ACT_ASSIGN AcceptClass + function TASK:SetAssignMethod( AcceptClass ) + + local ProcessTemplate = self:GetUnitProcess() + + ProcessTemplate:SetProcess( "Planned", "Accept", AcceptClass ) -- Actions.Act_Assign#ACT_ASSIGN + end + + + --- Assign the @{Task} to a @{Wrapper.Group}. + -- @param #TASK self + -- @param Wrapper.Group#GROUP TaskGroup + -- @return #TASK + function TASK:AssignToGroup( TaskGroup ) + self:F( TaskGroup:GetName() ) + + local TaskGroupName = TaskGroup:GetName() + local Mission = self:GetMission() + local CommandCenter = Mission:GetCommandCenter() + + self:SetGroupAssigned( TaskGroup ) + + local TaskUnits = TaskGroup:GetUnits() + for UnitID, UnitData in pairs( TaskUnits ) do + local TaskUnit = UnitData -- Wrapper.Unit#UNIT + local PlayerName = TaskUnit:GetPlayerName() + self:F(PlayerName) + if PlayerName ~= nil and PlayerName ~= "" then + self:AssignToUnit( TaskUnit ) + CommandCenter:MessageToGroup( + string.format( 'Task "%s": Briefing for player (%s):\n%s', + self:GetName(), + PlayerName, + self:GetBriefing() + ), TaskGroup + ) + end + end + + CommandCenter:SetMenu() + + self:MenuFlashTaskStatus( TaskGroup, self:GetMission():GetCommandCenter().FlashStatus ) + + return self + end + + --- UnAssign the @{Task} from a @{Wrapper.Group}. + -- @param #TASK self + -- @param Wrapper.Group#GROUP TaskGroup + function TASK:UnAssignFromGroup( TaskGroup ) + self:F2( { TaskGroup = TaskGroup:GetName() } ) + + self:ClearGroupAssignment( TaskGroup ) + + local TaskUnits = TaskGroup:GetUnits() + for UnitID, UnitData in pairs( TaskUnits ) do + local TaskUnit = UnitData -- Wrapper.Unit#UNIT + local PlayerName = TaskUnit:GetPlayerName() + if PlayerName ~= nil and PlayerName ~= "" then -- Only remove units that have players! + self:UnAssignFromUnit( TaskUnit ) + end + end + + local Mission = self:GetMission() + local CommandCenter = Mission:GetCommandCenter() + CommandCenter:SetMenu() + + self:MenuFlashTaskStatus( TaskGroup, false ) -- stop message flashing, if any #1383 & #1312 + + end +end + + +--- +-- @param #TASK self +-- @param Wrapper.Group#GROUP FindGroup +-- @return #boolean +function TASK:HasGroup( FindGroup ) + + local SetAttackGroup = self:GetGroups() + return SetAttackGroup:FindGroup( FindGroup:GetName() ) + +end + +--- Assign the @{Task} to an alive @{Wrapper.Unit}. +-- @param #TASK self +-- @param Wrapper.Unit#UNIT TaskUnit +-- @return #TASK self +function TASK:AssignToUnit( TaskUnit ) + self:F( TaskUnit:GetName() ) + + local FsmTemplate = self:GetUnitProcess() + + -- Assign a new FsmUnit to TaskUnit. + local FsmUnit = self:SetStateMachine( TaskUnit, FsmTemplate:Copy( TaskUnit, self ) ) -- Core.Fsm#FSM_PROCESS + + FsmUnit:SetStartState( "Planned" ) + + FsmUnit:Accept() -- Each Task needs to start with an Accept event to start the flow. + + return self +end + +--- UnAssign the @{Task} from an alive @{Wrapper.Unit}. +-- @param #TASK self +-- @param Wrapper.Unit#UNIT TaskUnit +-- @return #TASK self +function TASK:UnAssignFromUnit( TaskUnit ) + self:F( TaskUnit:GetName() ) + + self:RemoveStateMachine( TaskUnit ) + + -- If a Task Control Menu had been set, then this will be removed. + self:RemoveTaskControlMenu( TaskUnit ) + return self +end + +--- Sets the TimeOut for the @{Task}. If @{Task} stayed planned for longer than TimeOut, it gets into Cancelled status. +-- @param #TASK self +-- @param #integer Timer in seconds +-- @return #TASK self +function TASK:SetTimeOut ( Timer ) + self:F( Timer ) + self.TimeOut = Timer + self:__TimeOut( self.TimeOut ) + return self +end + +--- Send a message of the @{Task} to the assigned @{Wrapper.Group}s. +-- @param #TASK self +function TASK:MessageToGroups( Message ) + self:F( { Message = Message } ) + + local Mission = self:GetMission() + local CC = Mission:GetCommandCenter() + + for TaskGroupName, TaskGroup in pairs( self.SetGroup:GetSet() ) do + TaskGroup = TaskGroup -- Wrapper.Group#GROUP + if TaskGroup:IsAlive() == true then + CC:MessageToGroup( Message, TaskGroup, TaskGroup:GetName() ) + end + end +end + + +--- Send the briefng message of the @{Task} to the assigned @{Wrapper.Group}s. +-- @param #TASK self +function TASK:SendBriefingToAssignedGroups() + self:F2() + + for TaskGroupName, TaskGroup in pairs( self.SetGroup:GetSet() ) do + if TaskGroup:IsAlive() then + if self:IsGroupAssigned( TaskGroup ) then + TaskGroup:Message( self.TaskBriefing, 60 ) + end + end + end +end + + +--- UnAssign the @{Task} from the @{Wrapper.Group}s. +-- @param #TASK self +function TASK:UnAssignFromGroups() + self:F2() + + for TaskGroupName, TaskGroup in pairs( self.SetGroup:GetSet() ) do + if TaskGroup:IsAlive() == true then + if self:IsGroupAssigned(TaskGroup) then + self:UnAssignFromGroup( TaskGroup ) + end + end + end +end + + + +--- Returns if the @{Task} has still alive and assigned Units. +-- @param #TASK self +-- @return #boolean +function TASK:HasAliveUnits() + self:F() + + for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do + if TaskGroup:IsAlive() == true then + if self:IsStateAssigned() then + if self:IsGroupAssigned( TaskGroup ) then + for TaskUnitID, TaskUnit in pairs( TaskGroup:GetUnits() ) do + if TaskUnit:IsAlive() then + self:T( { HasAliveUnits = true } ) + return true + end + end + end + end + end + end + + self:T( { HasAliveUnits = false } ) + return false +end + +--- Set the menu options of the @{Task} to all the groups in the SetGroup. +-- @param #TASK self +-- @param #number MenuTime +-- @return #TASK +function TASK:SetMenu( MenuTime ) --R2.1 Mission Reports and Task Reports added. Fixes issue #424. + self:F( { self:GetName(), MenuTime } ) + + --self.SetGroup:Flush() + --for TaskGroupID, TaskGroupData in pairs( self.SetGroup:GetAliveSet() ) do + for TaskGroupID, TaskGroupData in pairs( self.SetGroup:GetSet() ) do + local TaskGroup = TaskGroupData -- Wrapper.Group#GROUP + if TaskGroup:IsAlive() == true and TaskGroup:GetPlayerNames() then + + -- Set Mission Menus + + local Mission = self:GetMission() + local MissionMenu = Mission:GetMenu( TaskGroup ) + if MissionMenu then + self:SetMenuForGroup( TaskGroup, MenuTime ) + end + end + end +end + + + +--- Set the Menu for a Group +-- @param #TASK self +-- @param #number MenuTime +-- @return #TASK +function TASK:SetMenuForGroup( TaskGroup, MenuTime ) + + if self:IsStatePlanned() or self:IsStateAssigned() then + self:SetPlannedMenuForGroup( TaskGroup, MenuTime ) + if self:IsGroupAssigned( TaskGroup ) then + self:SetAssignedMenuForGroup( TaskGroup, MenuTime ) + end + end +end + + +--- Set the planned menu option of the @{Task}. +-- @param #TASK self +-- @param Wrapper.Group#GROUP TaskGroup +-- @param #string MenuText The menu text. +-- @param #number MenuTime +-- @return #TASK self +function TASK:SetPlannedMenuForGroup( TaskGroup, MenuTime ) + self:F( TaskGroup:GetName() ) + + local Mission = self:GetMission() + local MissionName = Mission:GetName() + local MissionMenu = Mission:GetMenu( TaskGroup ) + + local TaskType = self:GetType() + local TaskPlayerCount = self:GetPlayerCount() + local TaskPlayerString = string.format( " (%dp)", TaskPlayerCount ) + local TaskText = string.format( "%s", self:GetName() ) + local TaskName = string.format( "%s", self:GetName() ) + + self.MenuPlanned = self.MenuPlanned or {} + self.MenuPlanned[TaskGroup] = MENU_GROUP_DELAYED:New( TaskGroup, "Join Planned Task", MissionMenu, Mission.MenuReportTasksPerStatus, Mission, TaskGroup, "Planned" ):SetTime( MenuTime ):SetTag( "Tasking" ) + local TaskTypeMenu = MENU_GROUP_DELAYED:New( TaskGroup, TaskType, self.MenuPlanned[TaskGroup] ):SetTime( MenuTime ):SetTag( "Tasking" ) + local TaskTypeMenu = MENU_GROUP_DELAYED:New( TaskGroup, TaskText, TaskTypeMenu ):SetTime( MenuTime ):SetTag( "Tasking" ) + + if not Mission:IsGroupAssigned( TaskGroup ) then + --self:F( { "Replacing Join Task menu" } ) + local JoinTaskMenu = MENU_GROUP_COMMAND_DELAYED:New( TaskGroup, string.format( "Join Task" ), TaskTypeMenu, self.MenuAssignToGroup, self, TaskGroup ):SetTime( MenuTime ):SetTag( "Tasking" ) + local MarkTaskMenu = MENU_GROUP_COMMAND_DELAYED:New( TaskGroup, string.format( "Mark Task Location on Map" ), TaskTypeMenu, self.MenuMarkToGroup, self, TaskGroup ):SetTime( MenuTime ):SetTag( "Tasking" ) + end + + local ReportTaskMenu = MENU_GROUP_COMMAND_DELAYED:New( TaskGroup, string.format( "Report Task Details" ), TaskTypeMenu, self.MenuTaskStatus, self, TaskGroup ):SetTime( MenuTime ):SetTag( "Tasking" ) + + return self +end + +--- Set the assigned menu options of the @{Task}. +-- @param #TASK self +-- @param Wrapper.Group#GROUP TaskGroup +-- @param #number MenuTime +-- @return #TASK self +function TASK:SetAssignedMenuForGroup( TaskGroup, MenuTime ) + self:F( { TaskGroup:GetName(), MenuTime } ) + + local TaskType = self:GetType() + local TaskPlayerCount = self:GetPlayerCount() + local TaskPlayerString = string.format( " (%dp)", TaskPlayerCount ) + local TaskText = string.format( "%s%s", self:GetName(), TaskPlayerString ) --, TaskThreatLevelString ) + local TaskName = string.format( "%s", self:GetName() ) + + for UnitName, TaskUnit in pairs( TaskGroup:GetPlayerUnits() ) do + local TaskUnit = TaskUnit -- Wrapper.Unit#UNIT + if TaskUnit then + local MenuControl = self:GetTaskControlMenu( TaskUnit ) + local TaskControl = MENU_GROUP:New( TaskGroup, "Control Task", MenuControl ):SetTime( MenuTime ):SetTag( "Tasking" ) + if self:IsStateAssigned() then + local TaskMenu = MENU_GROUP_COMMAND:New( TaskGroup, string.format( "Abort Task" ), TaskControl, self.MenuTaskAbort, self, TaskGroup ):SetTime( MenuTime ):SetTag( "Tasking" ) + end + local MarkMenu = MENU_GROUP_COMMAND:New( TaskGroup, string.format( "Mark Task Location on Map" ), TaskControl, self.MenuMarkToGroup, self, TaskGroup ):SetTime( MenuTime ):SetTag( "Tasking" ) + local TaskTypeMenu = MENU_GROUP_COMMAND:New( TaskGroup, string.format( "Report Task Details" ), TaskControl, self.MenuTaskStatus, self, TaskGroup ):SetTime( MenuTime ):SetTag( "Tasking" ) + if not self.FlashTaskStatus then + local TaskFlashStatusMenu = MENU_GROUP_COMMAND:New( TaskGroup, string.format( "Flash Task Details" ), TaskControl, self.MenuFlashTaskStatus, self, TaskGroup, true ):SetTime( MenuTime ):SetTag( "Tasking" ) + else + local TaskFlashStatusMenu = MENU_GROUP_COMMAND:New( TaskGroup, string.format( "Stop Flash Task Details" ), TaskControl, self.MenuFlashTaskStatus, self, TaskGroup, nil ):SetTime( MenuTime ):SetTag( "Tasking" ) + end + end + end + + return self +end + +--- Remove the menu options of the @{Task} to all the groups in the SetGroup. +-- @param #TASK self +-- @param #number MenuTime +-- @return #TASK +function TASK:RemoveMenu( MenuTime ) + self:F( { self:GetName(), MenuTime } ) + + for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do + if TaskGroup:IsAlive() == true then + local TaskGroup = TaskGroup -- Wrapper.Group#GROUP + if TaskGroup:IsAlive() == true and TaskGroup:GetPlayerNames() then + self:RefreshMenus( TaskGroup, MenuTime ) + end + end + end +end + + +--- Remove the menu option of the @{Task} for a @{Wrapper.Group}. +-- @param #TASK self +-- @param Wrapper.Group#GROUP TaskGroup +-- @param #number MenuTime +-- @return #TASK self +function TASK:RefreshMenus( TaskGroup, MenuTime ) + self:F( { TaskGroup:GetName(), MenuTime } ) + + local Mission = self:GetMission() + local MissionName = Mission:GetName() + local MissionMenu = Mission:GetMenu( TaskGroup ) + + local TaskName = self:GetName() + self.MenuPlanned = self.MenuPlanned or {} + local PlannedMenu = self.MenuPlanned[TaskGroup] + + self.MenuAssigned = self.MenuAssigned or {} + local AssignedMenu = self.MenuAssigned[TaskGroup] + + if PlannedMenu then + self.MenuPlanned[TaskGroup] = PlannedMenu:Remove( MenuTime , "Tasking" ) + PlannedMenu:Set() + end + + if AssignedMenu then + self.MenuAssigned[TaskGroup] = AssignedMenu:Remove( MenuTime, "Tasking" ) + AssignedMenu:Set() + end + +end + +--- Remove the assigned menu option of the @{Task} for a @{Wrapper.Group}. +-- @param #TASK self +-- @param Wrapper.Group#GROUP TaskGroup +-- @param #number MenuTime +-- @return #TASK self +function TASK:RemoveAssignedMenuForGroup( TaskGroup ) + self:F() + + local Mission = self:GetMission() + local MissionName = Mission:GetName() + local MissionMenu = Mission:GetMenu( TaskGroup ) + + if MissionMenu then + MissionMenu:RemoveSubMenus() + end + +end + +--- @param #TASK self +-- @param Wrapper.Group#GROUP TaskGroup +function TASK:MenuAssignToGroup( TaskGroup ) + + self:F( "Join Task menu selected") + + self:AssignToGroup( TaskGroup ) +end + +--- @param #TASK self +-- @param Wrapper.Group#GROUP TaskGroup +function TASK:MenuMarkToGroup( TaskGroup ) + self:F() + + self:UpdateTaskInfo( self.DetectedItem ) + + local TargetCoordinates = self.TaskInfo:GetData( "Coordinates" ) -- Core.Point#COORDINATE + if TargetCoordinates then + for TargetCoordinateID, TargetCoordinate in pairs( TargetCoordinates ) do + local Report = REPORT:New():SetIndent( 0 ) + self.TaskInfo:Report( Report, "M", TaskGroup, self ) + local MarkText = Report:Text( ", " ) + self:F( { Coordinate = TargetCoordinate, MarkText = MarkText } ) + TargetCoordinate:MarkToGroup( MarkText, TaskGroup ) + --Coordinate:MarkToAll( Briefing ) + end + else + local TargetCoordinate = self.TaskInfo:GetData( "Coordinate" ) -- Core.Point#COORDINATE + if TargetCoordinate then + local Report = REPORT:New():SetIndent( 0 ) + self.TaskInfo:Report( Report, "M", TaskGroup, self ) + local MarkText = Report:Text( ", " ) + self:F( { Coordinate = TargetCoordinate, MarkText = MarkText } ) + TargetCoordinate:MarkToGroup( MarkText, TaskGroup ) + end + end + +end + +--- Report the task status. +-- @param #TASK self +-- @param Wrapper.Group#GROUP TaskGroup +function TASK:MenuTaskStatus( TaskGroup ) + + if TaskGroup:IsAlive() then + + local ReportText = self:ReportDetails( TaskGroup ) + + self:T( ReportText ) + self:GetMission():GetCommandCenter():MessageTypeToGroup( ReportText, TaskGroup, MESSAGE.Type.Detailed ) + end + +end + +--- Report the task status. +-- @param #TASK self +function TASK:MenuFlashTaskStatus( TaskGroup, Flash ) + + self.FlashTaskStatus = Flash + + if self.FlashTaskStatus then + self.FlashTaskScheduler, self.FlashTaskScheduleID = SCHEDULER:New( self, self.MenuTaskStatus, { TaskGroup }, 0, 60) --Issue #1383 never ending flash messages + else + if self.FlashTaskScheduler then + self.FlashTaskScheduler:Stop( self.FlashTaskScheduleID ) + self.FlashTaskScheduler = nil + self.FlashTaskScheduleID = nil + end + end + +end + +--- Report the task status. +-- @param #TASK self +function TASK:MenuTaskAbort( TaskGroup ) + + self:AbortGroup( TaskGroup ) +end + + + +--- Returns the @{Task} name. +-- @param #TASK self +-- @return #string TaskName +function TASK:GetTaskName() + return self.TaskName +end + +--- Returns the @{Task} briefing. +-- @param #TASK self +-- @return #string Task briefing. +function TASK:GetTaskBriefing() + return self.TaskBriefing +end + + + + +--- Get the default or currently assigned @{Process} template with key ProcessName. +-- @param #TASK self +-- @param #string ProcessName +-- @return Core.Fsm#FSM_PROCESS +function TASK:GetProcessTemplate( ProcessName ) + + local ProcessTemplate = self.ProcessClasses[ProcessName] + + return ProcessTemplate +end + + + +-- TODO: Obscolete? +--- Fail processes from @{Task} with key @{Wrapper.Unit} +-- @param #TASK self +-- @param #string TaskUnitName +-- @return #TASK self +function TASK:FailProcesses( TaskUnitName ) + + for ProcessID, ProcessData in pairs( self.Processes[TaskUnitName] ) do + local Process = ProcessData + Process.Fsm:Fail() + end +end + +--- Add a FiniteStateMachine to @{Task} with key Task@{Wrapper.Unit} +-- @param #TASK self +-- @param Wrapper.Unit#UNIT TaskUnit +-- @param Core.Fsm#FSM_PROCESS Fsm +-- @return #TASK self +function TASK:SetStateMachine( TaskUnit, Fsm ) + self:F2( { TaskUnit, self.Fsm[TaskUnit] ~= nil, Fsm:GetClassNameAndID() } ) + + self.Fsm[TaskUnit] = Fsm + + return Fsm +end + +--- Gets the FiniteStateMachine of @{Task} with key Task@{Wrapper.Unit} +-- @param #TASK self +-- @param Wrapper.Unit#UNIT TaskUnit +-- @return Core.Fsm#FSM_PROCESS +function TASK:GetStateMachine( TaskUnit ) + self:F2( { TaskUnit, self.Fsm[TaskUnit] ~= nil } ) + + return self.Fsm[TaskUnit] +end + +--- Remove FiniteStateMachines from @{Task} with key Task@{Wrapper.Unit} +-- @param #TASK self +-- @param Wrapper.Unit#UNIT TaskUnit +-- @return #TASK self +function TASK:RemoveStateMachine( TaskUnit ) + self:F( { TaskUnit = TaskUnit:GetName(), HasFsm = ( self.Fsm[TaskUnit] ~= nil ) } ) + + --self:F( self.Fsm ) + --for TaskUnitT, Fsm in pairs( self.Fsm ) do + --local Fsm = Fsm -- Core.Fsm#FSM_PROCESS + --self:F( TaskUnitT ) + --self.Fsm[TaskUnit] = nil + --end + + if self.Fsm[TaskUnit] then + self.Fsm[TaskUnit]:Remove() + self.Fsm[TaskUnit] = nil + end + + collectgarbage() + self:F( "Garbage Collected, Processes should be finalized now ...") +end + + +--- Checks if there is a FiniteStateMachine assigned to Task@{Wrapper.Unit} for @{Task} +-- @param #TASK self +-- @param Wrapper.Unit#UNIT TaskUnit +-- @return #TASK self +function TASK:HasStateMachine( TaskUnit ) + self:F( { TaskUnit, self.Fsm[TaskUnit] ~= nil } ) + + return ( self.Fsm[TaskUnit] ~= nil ) +end + + +--- Gets the Scoring of the task +-- @param #TASK self +-- @return Functional.Scoring#SCORING Scoring +function TASK:GetScoring() + return self.Mission:GetScoring() +end + + +--- Gets the Task Index, which is a combination of the Task type, the Task name. +-- @param #TASK self +-- @return #string The Task ID +function TASK:GetTaskIndex() + + local TaskType = self:GetType() + local TaskName = self:GetName() + + return TaskType .. "." .. TaskName +end + +--- Sets the Name of the Task +-- @param #TASK self +-- @param #string TaskName +function TASK:SetName( TaskName ) + self.TaskName = TaskName +end + +--- Gets the Name of the Task +-- @param #TASK self +-- @return #string The Task Name +function TASK:GetName() + return self.TaskName +end + +--- Sets the Type of the Task +-- @param #TASK self +-- @param #string TaskType +function TASK:SetType( TaskType ) + self.TaskType = TaskType +end + +--- Gets the Type of the Task +-- @param #TASK self +-- @return #string TaskType +function TASK:GetType() + return self.TaskType +end + +--- Sets the ID of the Task +-- @param #TASK self +-- @param #string TaskID +function TASK:SetID( TaskID ) + self.TaskID = TaskID +end + +--- Gets the ID of the Task +-- @param #TASK self +-- @return #string TaskID +function TASK:GetID() + return self.TaskID +end + + +--- Sets a @{Task} to status **Success**. +-- @param #TASK self +function TASK:StateSuccess() + self:SetState( self, "State", "Success" ) + return self +end + +--- Is the @{Task} status **Success**. +-- @param #TASK self +function TASK:IsStateSuccess() + return self:Is( "Success" ) +end + +--- Sets a @{Task} to status **Failed**. +-- @param #TASK self +function TASK:StateFailed() + self:SetState( self, "State", "Failed" ) + return self +end + +--- Is the @{Task} status **Failed**. +-- @param #TASK self +function TASK:IsStateFailed() + return self:Is( "Failed" ) +end + +--- Sets a @{Task} to status **Planned**. +-- @param #TASK self +function TASK:StatePlanned() + self:SetState( self, "State", "Planned" ) + return self +end + +--- Is the @{Task} status **Planned**. +-- @param #TASK self +function TASK:IsStatePlanned() + return self:Is( "Planned" ) +end + +--- Sets a @{Task} to status **Aborted**. +-- @param #TASK self +function TASK:StateAborted() + self:SetState( self, "State", "Aborted" ) + return self +end + +--- Is the @{Task} status **Aborted**. +-- @param #TASK self +function TASK:IsStateAborted() + return self:Is( "Aborted" ) +end + +--- Sets a @{Task} to status **Cancelled**. +-- @param #TASK self +function TASK:StateCancelled() + self:SetState( self, "State", "Cancelled" ) + return self +end + +--- Is the @{Task} status **Cancelled**. +-- @param #TASK self +function TASK:IsStateCancelled() + return self:Is( "Cancelled" ) +end + +--- Sets a @{Task} to status **Assigned**. +-- @param #TASK self +function TASK:StateAssigned() + self:SetState( self, "State", "Assigned" ) + return self +end + +--- Is the @{Task} status **Assigned**. +-- @param #TASK self +function TASK:IsStateAssigned() + return self:Is( "Assigned" ) +end + +--- Sets a @{Task} to status **Hold**. +-- @param #TASK self +function TASK:StateHold() + self:SetState( self, "State", "Hold" ) + return self +end + +--- Is the @{Task} status **Hold**. +-- @param #TASK self +function TASK:IsStateHold() + return self:Is( "Hold" ) +end + +--- Sets a @{Task} to status **Replanned**. +-- @param #TASK self +function TASK:StateReplanned() + self:SetState( self, "State", "Replanned" ) + return self +end + +--- Is the @{Task} status **Replanned**. +-- @param #TASK self +function TASK:IsStateReplanned() + return self:Is( "Replanned" ) +end + +--- Gets the @{Task} status. +-- @param #TASK self +function TASK:GetStateString() + return self:GetState( self, "State" ) +end + +--- Sets a @{Task} briefing. +-- @param #TASK self +-- @param #string TaskBriefing +-- @return #TASK self +function TASK:SetBriefing( TaskBriefing ) + self:F(TaskBriefing) + self.TaskBriefing = TaskBriefing + return self +end + +--- Gets the @{Task} briefing. +-- @param #TASK self +-- @return #string The briefing text. +function TASK:GetBriefing() + return self.TaskBriefing +end + + + + +--- FSM function for a TASK +-- @param #TASK self +-- @param #string Event +-- @param #string From +-- @param #string To +function TASK:onenterAssigned( From, Event, To, PlayerUnit, PlayerName ) + + --- This test is required, because the state transition will be fired also when the state does not change in case of an event. + if From ~= "Assigned" then + + local PlayerNames = self:GetPlayerNames() + local PlayerText = REPORT:New() + for PlayerName, TaskName in pairs( PlayerNames ) do + PlayerText:Add( PlayerName ) + end + + self:GetMission():GetCommandCenter():MessageToCoalition( "Task " .. self:GetName() .. " is assigned to players " .. PlayerText:Text(",") .. ". Good Luck!" ) + + -- Set the total Progress to be achieved. + self:SetGoalTotal() -- Polymorphic to set the initial goal total! + + if self.Dispatcher then + self:F( "Firing Assign event " ) + self.Dispatcher:Assign( self, PlayerUnit, PlayerName ) + end + + self:GetMission():__Start( 1 ) + + -- When the task is assigned, the task goal needs to be checked of the derived classes. + self:__Goal( -10, PlayerUnit, PlayerName ) -- Polymorphic + + self:SetMenu() + + self:F( { "--> Task Assigned", TaskName = self:GetName(), Mission = self:GetMission():GetName() } ) + self:F( { "--> Task Player Names", PlayerNames = PlayerNames } ) + + end +end + + +--- FSM function for a TASK +-- @param #TASK self +-- @param #string Event +-- @param #string From +-- @param #string To +function TASK:onenterSuccess( From, Event, To ) + + self:F( { "<-> Task Replanned", TaskName = self:GetName(), Mission = self:GetMission():GetName() } ) + self:F( { "<-> Task Player Names", PlayerNames = self:GetPlayerNames() } ) + + self:GetMission():GetCommandCenter():MessageToCoalition( "Task " .. self:GetName() .. " is successful! Good job!" ) + self:UnAssignFromGroups() + + self:GetMission():__MissionGoals( 1 ) + +end + + +--- FSM function for a TASK +-- @param #TASK self +-- @param #string From +-- @param #string Event +-- @param #string To +function TASK:onenterAborted( From, Event, To ) + + self:F( { "<-- Task Aborted", TaskName = self:GetName(), Mission = self:GetMission():GetName() } ) + self:F( { "<-- Task Player Names", PlayerNames = self:GetPlayerNames() } ) + + if From ~= "Aborted" then + self:GetMission():GetCommandCenter():MessageToCoalition( "Task " .. self:GetName() .. " has been aborted! Task may be replanned." ) + self:__Replan( 5 ) + self:SetMenu() + end + +end + + +--- FSM function for a TASK +-- @param #TASK self +-- @param #string From +-- @param #string Event +-- @param #string To +function TASK:onenterCancelled( From, Event, To ) + + self:F( { "<-- Task Cancelled", TaskName = self:GetName(), Mission = self:GetMission():GetName() } ) + self:F( { "<-- Player Names", PlayerNames = self:GetPlayerNames() } ) + + if From ~= "Cancelled" then + self:GetMission():GetCommandCenter():MessageToCoalition( "Task " .. self:GetName() .. " has been cancelled! The tactical situation has changed." ) + self:UnAssignFromGroups() + self:SetMenu() + end + +end + +--- FSM function for a TASK +-- @param #TASK self +-- @param #string From +-- @param #string Event +-- @param #string To +function TASK:onafterReplan( From, Event, To ) + + self:F( { "Task Replanned", TaskName = self:GetName(), Mission = self:GetMission():GetName() } ) + self:F( { "Task Player Names", PlayerNames = self:GetPlayerNames() } ) + + self:GetMission():GetCommandCenter():MessageToCoalition( "Replanning Task " .. self:GetName() .. "." ) + + self:SetMenu() + +end + +--- FSM function for a TASK +-- @param #TASK self +-- @param #string From +-- @param #string Event +-- @param #string To +function TASK:onenterFailed( From, Event, To ) + + self:F( { "Task Failed", TaskName = self:GetName(), Mission = self:GetMission():GetName() } ) + self:F( { "Task Player Names", PlayerNames = self:GetPlayerNames() } ) + + self:GetMission():GetCommandCenter():MessageToCoalition( "Task " .. self:GetName() .. " has failed!" ) + + self:UnAssignFromGroups() +end + +--- FSM function for a TASK +-- @param #TASK self +-- @param #string Event +-- @param #string From +-- @param #string To +function TASK:onstatechange( From, Event, To ) + + if self:IsTrace() then + --MESSAGE:New( "@ Task " .. self.TaskName .. " : " .. From .. " changed to " .. To .. " by " .. Event, 2 ):ToAll() + end + + if self.Scores[To] then + local Scoring = self:GetScoring() + if Scoring then + self:F( { self.Scores[To].ScoreText, self.Scores[To].Score } ) + Scoring:_AddMissionScore( self.Mission, self.Scores[To].ScoreText, self.Scores[To].Score ) + end + end + +end + +--- FSM function for a TASK +-- @param #TASK self +-- @param #string Event +-- @param #string From +-- @param #string To +function TASK:onenterPlanned( From, Event, To) + if not self.TimeOut == 0 then + self.__TimeOut( self.TimeOut ) + end +end + +--- FSM function for a TASK +-- @param #TASK self +-- @param #string Event +-- @param #string From +-- @param #string To +function TASK:onbeforeTimeOut( From, Event, To ) + if From == "Planned" then + self:RemoveMenu() + return true + end + return false +end + +do -- Links + + --- Set goal of a task + -- @param #TASK self + -- @param Core.Goal#GOAL Goal + -- @return #TASK + function TASK:SetGoal( Goal ) + self.Goal = Goal + end + + + --- Get goal of a task + -- @param #TASK self + -- @return Core.Goal#GOAL The Goal + function TASK:GetGoal() + return self.Goal + end + + + --- Set dispatcher of a task + -- @param #TASK self + -- @param Tasking.DetectionManager#DETECTION_MANAGER Dispatcher + -- @return #TASK + function TASK:SetDispatcher( Dispatcher ) + self.Dispatcher = Dispatcher + end + + --- Set detection of a task + -- @param #TASK self + -- @param Function.Detection#DETECTION_BASE Detection + -- @param DetectedItem + -- @return #TASK + function TASK:SetDetection( Detection, DetectedItem ) + + self:F( { DetectedItem, Detection } ) + + self.Detection = Detection + self.DetectedItem = DetectedItem + end + +end + +do -- Reporting + +--- Create a summary report of the Task. +-- List the Task Name and Status +-- @param #TASK self +-- @param Wrapper.Group#GROUP ReportGroup +-- @return #string +function TASK:ReportSummary( ReportGroup ) + + self:UpdateTaskInfo( self.DetectedItem ) + + local Report = REPORT:New() + + -- List the name of the Task. + Report:Add( "Task " .. self:GetName() ) + + -- Determine the status of the Task. + Report:Add( "State: <" .. self:GetState() .. ">" ) + + self.TaskInfo:Report( Report, "S", ReportGroup, self ) + + return Report:Text( ', ' ) +end + +--- Create an overiew report of the Task. +-- List the Task Name and Status +-- @param #TASK self +-- @return #string +function TASK:ReportOverview( ReportGroup ) + + self:UpdateTaskInfo( self.DetectedItem ) + + -- List the name of the Task. + local TaskName = self:GetName() + local Report = REPORT:New() + + self.TaskInfo:Report( Report, "O", ReportGroup, self ) + + return Report:Text() +end + +--- Create a count of the players in the Task. +-- @param #TASK self +-- @return #number The total number of players in the task. +function TASK:GetPlayerCount() --R2.1 Get a count of the players. + + local PlayerCount = 0 + + -- Loop each Unit active in the Task, and find Player Names. + for TaskGroupID, PlayerGroup in pairs( self:GetGroups():GetSet() ) do + local PlayerGroup = PlayerGroup -- Wrapper.Group#GROUP + if PlayerGroup:IsAlive() == true then + if self:IsGroupAssigned( PlayerGroup ) then + local PlayerNames = PlayerGroup:GetPlayerNames() + PlayerCount = PlayerCount + ((PlayerNames) and #PlayerNames or 0) -- PlayerNames can be nil when there are no players. + end + end + end + + return PlayerCount +end + + +--- Create a list of the players in the Task. +-- @param #TASK self +-- @return #map<#string,Wrapper.Group#GROUP> A map of the players +function TASK:GetPlayerNames() --R2.1 Get a map of the players. + + local PlayerNameMap = {} + + -- Loop each Unit active in the Task, and find Player Names. + for TaskGroupID, PlayerGroup in pairs( self:GetGroups():GetSet() ) do + local PlayerGroup = PlayerGroup -- Wrapper.Group#GROUP + if PlayerGroup:IsAlive() == true then + if self:IsGroupAssigned( PlayerGroup ) then + local PlayerNames = PlayerGroup:GetPlayerNames() + for PlayerNameID, PlayerName in pairs( PlayerNames or {} ) do + PlayerNameMap[PlayerName] = PlayerGroup + end + end + end + end + + return PlayerNameMap +end + + +--- Create a detailed report of the Task. +-- List the Task Status, and the Players assigned to the Task. +-- @param #TASK self +-- @param Wrapper.Group#GROUP TaskGroup +-- @return #string +function TASK:ReportDetails( ReportGroup ) + + self:UpdateTaskInfo( self.DetectedItem ) + + local Report = REPORT:New():SetIndent( 3 ) + + -- List the name of the Task. + local Name = self:GetName() + + -- Determine the status of the Task. + local Status = "<" .. self:GetState() .. ">" + + Report:Add( "Task " .. Name .. " - " .. Status .. " - Detailed Report" ) + + -- Loop each Unit active in the Task, and find Player Names. + local PlayerNames = self:GetPlayerNames() + + local PlayerReport = REPORT:New() + for PlayerName, PlayerGroup in pairs( PlayerNames ) do + PlayerReport:Add( "Players group " .. PlayerGroup:GetCallsign() .. ": " .. PlayerName ) + end + local Players = PlayerReport:Text() + + if Players ~= "" then + Report:AddIndent( "Players assigned:", "-" ) + Report:AddIndent( Players ) + end + + self.TaskInfo:Report( Report, "D", ReportGroup, self ) + + return Report:Text() +end + + +end -- Reporting + + +do -- Additional Task Scoring and Task Progress + + --- Add Task Progress for a Player Name + -- @param #TASK self + -- @param #string PlayerName The name of the player. + -- @param #string ProgressText The text that explains the Progress achieved. + -- @param #number ProgressTime The time the progress was achieved. + -- @oaram #number ProgressPoints The amount of points of magnitude granted. This will determine the shared Mission Success scoring. + -- @return #TASK + function TASK:AddProgress( PlayerName, ProgressText, ProgressTime, ProgressPoints ) + self.TaskProgress = self.TaskProgress or {} + self.TaskProgress[ProgressTime] = self.TaskProgress[ProgressTime] or {} + self.TaskProgress[ProgressTime].PlayerName = PlayerName + self.TaskProgress[ProgressTime].ProgressText = ProgressText + self.TaskProgress[ProgressTime].ProgressPoints = ProgressPoints + self:GetMission():AddPlayerName( PlayerName ) + return self + end + + function TASK:GetPlayerProgress( PlayerName ) + local ProgressPlayer = 0 + for ProgressTime, ProgressData in pairs( self.TaskProgress ) do + if PlayerName == ProgressData.PlayerName then + ProgressPlayer = ProgressPlayer + ProgressData.ProgressPoints + end + end + return ProgressPlayer + end + + --- Set a score when progress has been made by the player. + -- @param #TASK self + -- @param #string PlayerName The name of the player. + -- @param #number Score The score in points to be granted when task process has been achieved. + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return #TASK + function TASK:SetScoreOnProgress( PlayerName, Score, TaskUnit ) + self:F( { PlayerName, Score, TaskUnit } ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + ProcessUnit:AddScoreProcess( "Engaging", "Account", "AccountPlayer", "Player " .. PlayerName .. " has achieved progress.", Score ) + + return self + end + + --- Set a score when all the targets in scope of the A2A attack, have been destroyed. + -- @param #TASK self + -- @param #string PlayerName The name of the player. + -- @param #number Score The score in points. + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return #TASK + function TASK:SetScoreOnSuccess( PlayerName, Score, TaskUnit ) + self:F( { PlayerName, Score, TaskUnit } ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + ProcessUnit:AddScore( "Success", "The task is a success!", Score ) + + return self + end + + --- Set a penalty when the A2A attack has failed. + -- @param #TASK self + -- @param #string PlayerName The name of the player. + -- @param #number Penalty The penalty in points, must be a negative value! + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return #TASK + function TASK:SetScoreOnFail( PlayerName, Penalty, TaskUnit ) + self:F( { PlayerName, Penalty, TaskUnit } ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + ProcessUnit:AddScore( "Failed", "The task is a failure!", Penalty ) + + return self + end + +end + +do -- Task Control Menu + + -- The Task Control Menu is a menu attached to the task at the main menu to quickly be able to do actions in the task. + -- The Task Control Menu can only be shown when the task is assigned to the player. + -- The Task Control Menu is linked to the process executing the task, so no task menu can be set to the main static task definition. + + --- Init Task Control Menu + -- @param #TASK self + -- @param Wrapper.Unit#UNIT TaskUnit The @{Wrapper.Unit} that contains a player. + -- @return Task Control Menu Refresh ID + function TASK:InitTaskControlMenu( TaskUnit ) + + self.TaskControlMenuTime = timer.getTime() + + return self.TaskControlMenuTime + end + + --- Get Task Control Menu + -- @param #TASK self + -- @param Wrapper.Unit#UNIT TaskUnit The @{Wrapper.Unit} that contains a player. + -- @return Core.Menu#MENU_GROUP TaskControlMenu The Task Control Menu + function TASK:GetTaskControlMenu( TaskUnit, TaskName ) + + TaskName = TaskName or "" + + local TaskGroup = TaskUnit:GetGroup() + local TaskPlayerCount = TaskGroup:GetPlayerCount() + + if TaskPlayerCount <= 1 then + self.TaskControlMenu = MENU_GROUP:New( TaskUnit:GetGroup(), "Task " .. self:GetName() .. " control" ):SetTime( self.TaskControlMenuTime ) + else + self.TaskControlMenu = MENU_GROUP:New( TaskUnit:GetGroup(), "Task " .. self:GetName() .. " control for " .. TaskUnit:GetPlayerName() ):SetTime( self.TaskControlMenuTime ) + end + + return self.TaskControlMenu + end + + --- Remove Task Control Menu + -- @param #TASK self + -- @param Wrapper.Unit#UNIT TaskUnit The @{Wrapper.Unit} that contains a player. + function TASK:RemoveTaskControlMenu( TaskUnit ) + + if self.TaskControlMenu then + self.TaskControlMenu:Remove() + self.TaskControlMenu = nil + end + end + + --- Refresh Task Control Menu + -- @param #TASK self + -- @param Wrapper.Unit#UNIT TaskUnit The @{Wrapper.Unit} that contains a player. + -- @param MenuTime The refresh time that was used to refresh the Task Control Menu items. + -- @param MenuTag The tag. + function TASK:RefreshTaskControlMenu( TaskUnit, MenuTime, MenuTag ) + + if self.TaskControlMenu then + self.TaskControlMenu:Remove( MenuTime, MenuTag ) + end + end + +end +--- **Tasking** -- Controls the information of a Task. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- ### Contributions: +-- +-- === +-- +-- @module Tasking.TaskInfo +-- @image MOOSE.JPG + +--- @type TASKINFO +-- @extends Core.Base#BASE + +--- +-- # TASKINFO class, extends @{Core.Base#BASE} +-- +-- ## The TASKINFO class implements the methods to contain information and display information of a task. +-- +-- @field #TASKINFO +TASKINFO = { + ClassName = "TASKINFO", +} + +--- @type TASKINFO.Detail #string A string that flags to document which level of detail needs to be shown in the report. +-- +-- - "M" for Markings on the Map (F10). +-- - "S" for Summary Reports. +-- - "O" for Overview Reports. +-- - "D" for Detailed Reports. +TASKINFO.Detail = "" + +--- Instantiates a new TASKINFO. +-- @param #TASKINFO self +-- @param Tasking.Task#TASK Task The task owning the information. +-- @return #TASKINFO self +function TASKINFO:New( Task ) + + local self = BASE:Inherit( self, BASE:New() ) -- Core.Base#BASE + + self.Task = Task + self.VolatileInfo = SET_BASE:New() + self.PersistentInfo = SET_BASE:New() + + self.Info = self.VolatileInfo + + return self +end + + +--- Add taskinfo. +-- @param #TASKINFO self +-- @param #string Key The info key. +-- @param Data The data of the info. +-- @param #number Order The display order, which is a number from 0 to 100. +-- @param #TASKINFO.Detail Detail The detail Level. +-- @param #boolean Keep (optional) If true, this would indicate that the planned taskinfo would be persistent when the task is completed, so that the original planned task info is used at the completed reports. +-- @return #TASKINFO self +function TASKINFO:AddInfo( Key, Data, Order, Detail, Keep, ShowKey, Type ) + self.VolatileInfo:Add( Key, { Data = Data, Order = Order, Detail = Detail, ShowKey = ShowKey, Type = Type } ) + if Keep == true then + self.PersistentInfo:Add( Key, { Data = Data, Order = Order, Detail = Detail, ShowKey = ShowKey, Type = Type } ) + end + return self +end + + +--- Get taskinfo. +-- @param #TASKINFO self +-- @param #string The info key. +-- @return Data The data of the info. +-- @return #number Order The display order, which is a number from 0 to 100. +-- @return #TASKINFO.Detail Detail The detail Level. +function TASKINFO:GetInfo( Key ) + local Object = self:Get( Key ) + return Object.Data, Object.Order, Object.Detail +end + + +--- Get data. +-- @param #TASKINFO self +-- @param #string The info key. +-- @return Data The data of the info. +function TASKINFO:GetData( Key ) + local Object = self.Info:Get( Key ) + return Object and Object.Data +end + + +--- Add Text. +-- @param #TASKINFO self +-- @param #string Key The key. +-- @param #string Text The text. +-- @param #number Order The display order, which is a number from 0 to 100. +-- @param #TASKINFO.Detail Detail The detail Level. +-- @param #boolean Keep (optional) If true, this would indicate that the planned taskinfo would be persistent when the task is completed, so that the original planned task info is used at the completed reports. +-- @return #TASKINFO self +function TASKINFO:AddText( Key, Text, Order, Detail, Keep ) + self:AddInfo( Key, Text, Order, Detail, Keep ) + return self +end + + +--- Add the task name. +-- @param #TASKINFO self +-- @param #number Order The display order, which is a number from 0 to 100. +-- @param #TASKINFO.Detail Detail The detail Level. +-- @param #boolean Keep (optional) If true, this would indicate that the planned taskinfo would be persistent when the task is completed, so that the original planned task info is used at the completed reports. +-- @return #TASKINFO self +function TASKINFO:AddTaskName( Order, Detail, Keep ) + self:AddInfo( "TaskName", self.Task:GetName(), Order, Detail, Keep ) + return self +end + + + + +--- Add a Coordinate. +-- @param #TASKINFO self +-- @param Core.Point#COORDINATE Coordinate +-- @param #number Order The display order, which is a number from 0 to 100. +-- @param #TASKINFO.Detail Detail The detail Level. +-- @param #boolean Keep (optional) If true, this would indicate that the planned taskinfo would be persistent when the task is completed, so that the original planned task info is used at the completed reports. +-- @return #TASKINFO self +function TASKINFO:AddCoordinate( Coordinate, Order, Detail, Keep, ShowKey, Name ) + self:AddInfo( Name or "Coordinate", Coordinate, Order, Detail, Keep, ShowKey, "Coordinate" ) + return self +end + + +--- Get the Coordinate. +-- @param #TASKINFO self +-- @return Core.Point#COORDINATE Coordinate +function TASKINFO:GetCoordinate( Name ) + return self:GetData( Name or "Coordinate" ) +end + + + +--- Add Coordinates. +-- @param #TASKINFO self +-- @param #list Coordinates +-- @param #number Order The display order, which is a number from 0 to 100. +-- @param #TASKINFO.Detail Detail The detail Level. +-- @param #boolean Keep (optional) If true, this would indicate that the planned taskinfo would be persistent when the task is completed, so that the original planned task info is used at the completed reports. +-- @return #TASKINFO self +function TASKINFO:AddCoordinates( Coordinates, Order, Detail, Keep ) + self:AddInfo( "Coordinates", Coordinates, Order, Detail, Keep ) + return self +end + + + +--- Add Threat. +-- @param #TASKINFO self +-- @param #string ThreatText The text of the Threat. +-- @param #string ThreatLevel The level of the Threat. +-- @param #number Order The display order, which is a number from 0 to 100. +-- @param #TASKINFO.Detail Detail The detail Level. +-- @param #boolean Keep (optional) If true, this would indicate that the planned taskinfo would be persistent when the task is completed, so that the original planned task info is used at the completed reports. +-- @return #TASKINFO self +function TASKINFO:AddThreat( ThreatText, ThreatLevel, Order, Detail, Keep ) + self:AddInfo( "Threat", " [" .. string.rep( "■", ThreatLevel ) .. string.rep( "□", 10 - ThreatLevel ) .. "]:" .. ThreatText, Order, Detail, Keep ) + return self +end + + +--- Get Threat. +-- @param #TASKINFO self +-- @return #string The threat +function TASKINFO:GetThreat() + self:GetInfo( "Threat" ) + return self +end + + + +--- Add the Target count. +-- @param #TASKINFO self +-- @param #number TargetCount The amount of targets. +-- @param #number Order The display order, which is a number from 0 to 100. +-- @param #TASKINFO.Detail Detail The detail Level. +-- @param #boolean Keep (optional) If true, this would indicate that the planned taskinfo would be persistent when the task is completed, so that the original planned task info is used at the completed reports. +-- @return #TASKINFO self +function TASKINFO:AddTargetCount( TargetCount, Order, Detail, Keep ) + self:AddInfo( "Counting", string.format( "%d", TargetCount ), Order, Detail, Keep ) + return self +end + +--- Add the Targets. +-- @param #TASKINFO self +-- @param #number TargetCount The amount of targets. +-- @param #string TargetTypes The text containing the target types. +-- @param #number Order The display order, which is a number from 0 to 100. +-- @param #TASKINFO.Detail Detail The detail Level. +-- @param #boolean Keep (optional) If true, this would indicate that the planned taskinfo would be persistent when the task is completed, so that the original planned task info is used at the completed reports. +-- @return #TASKINFO self +function TASKINFO:AddTargets( TargetCount, TargetTypes, Order, Detail, Keep ) + self:AddInfo( "Targets", string.format( "%d of %s", TargetCount, TargetTypes ), Order, Detail, Keep ) + return self +end + +--- Get Targets. +-- @param #TASKINFO self +-- @return #string The targets +function TASKINFO:GetTargets() + self:GetInfo( "Targets" ) + return self +end + + + + +--- Add the QFE at a Coordinate. +-- @param #TASKINFO self +-- @param Core.Point#COORDINATE Coordinate +-- @param #number Order The display order, which is a number from 0 to 100. +-- @param #TASKINFO.Detail Detail The detail Level. +-- @param #boolean Keep (optional) If true, this would indicate that the planned taskinfo would be persistent when the task is completed, so that the original planned task info is used at the completed reports. +-- @return #TASKINFO self +function TASKINFO:AddQFEAtCoordinate( Coordinate, Order, Detail, Keep ) + self:AddInfo( "QFE", Coordinate, Order, Detail, Keep ) + return self +end + +--- Add the Temperature at a Coordinate. +-- @param #TASKINFO self +-- @param Core.Point#COORDINATE Coordinate +-- @param #number Order The display order, which is a number from 0 to 100. +-- @param #TASKINFO.Detail Detail The detail Level. +-- @param #boolean Keep (optional) If true, this would indicate that the planned taskinfo would be persistent when the task is completed, so that the original planned task info is used at the completed reports. +-- @return #TASKINFO self +function TASKINFO:AddTemperatureAtCoordinate( Coordinate, Order, Detail, Keep ) + self:AddInfo( "Temperature", Coordinate, Order, Detail, Keep ) + return self +end + +--- Add the Wind at a Coordinate. +-- @param #TASKINFO self +-- @param Core.Point#COORDINATE Coordinate +-- @param #number Order The display order, which is a number from 0 to 100. +-- @param #TASKINFO.Detail Detail The detail Level. +-- @param #boolean Keep (optional) If true, this would indicate that the planned taskinfo would be persistent when the task is completed, so that the original planned task info is used at the completed reports. +-- @return #TASKINFO self +function TASKINFO:AddWindAtCoordinate( Coordinate, Order, Detail, Keep ) + self:AddInfo( "Wind", Coordinate, Order, Detail, Keep ) + return self +end + +--- Add Cargo. +-- @param #TASKINFO self +-- @param Core.Cargo#CARGO Cargo +-- @param #number Order The display order, which is a number from 0 to 100. +-- @param #TASKINFO.Detail Detail The detail Level. +-- @param #boolean Keep (optional) If true, this would indicate that the planned taskinfo would be persistent when the task is completed, so that the original planned task info is used at the completed reports. +-- @return #TASKINFO self +function TASKINFO:AddCargo( Cargo, Order, Detail, Keep ) + self:AddInfo( "Cargo", Cargo, Order, Detail, Keep ) + return self +end + + +--- Add Cargo set. +-- @param #TASKINFO self +-- @param Core.Set#SET_CARGO SetCargo +-- @param #number Order The display order, which is a number from 0 to 100. +-- @param #TASKINFO.Detail Detail The detail Level. +-- @param #boolean Keep (optional) If true, this would indicate that the planned taskinfo would be persistent when the task is completed, so that the original planned task info is used at the completed reports. +-- @return #TASKINFO self +function TASKINFO:AddCargoSet( SetCargo, Order, Detail, Keep ) + + local CargoReport = REPORT:New() + CargoReport:Add( "" ) + SetCargo:ForEachCargo( + --- @param Cargo.Cargo#CARGO Cargo + function( Cargo ) + CargoReport:Add( string.format( ' - %s (%s) %s - status %s ', Cargo:GetName(), Cargo:GetType(), Cargo:GetTransportationMethod(), Cargo:GetCurrentState() ) ) + end + ) + + self:AddInfo( "Cargo", CargoReport:Text(), Order, Detail, Keep ) + + + return self +end + + + +--- Create the taskinfo Report +-- @param #TASKINFO self +-- @param Core.Report#REPORT Report +-- @param #TASKINFO.Detail Detail The detail Level. +-- @param Wrapper.Group#GROUP ReportGroup +-- @param Tasking.Task#TASK Task +-- @return #TASKINFO self +function TASKINFO:Report( Report, Detail, ReportGroup, Task ) + + local Line = 0 + local LineReport = REPORT:New() + + if not self.Task:IsStatePlanned() and not self.Task:IsStateAssigned() then + self.Info = self.PersistentInfo + end + + for Key, Data in UTILS.spairs( self.Info.Set, function( t, a, b ) return t[a].Order < t[b].Order end ) do + + if Data.Detail:find( Detail ) then + local Text = "" + local ShowKey = ( Data.ShowKey == nil or Data.ShowKey == true ) + if Key == "TaskName" then + Key = nil + Text = Data.Data + elseif Data.Type and Data.Type == "Coordinate" then + local Coordinate = Data.Data -- Core.Point#COORDINATE + Text = Coordinate:ToString( ReportGroup:GetUnit(1), nil, Task ) + elseif Key == "Threat" then + local DataText = Data.Data -- #string + Text = DataText + elseif Key == "Counting" then + local DataText = Data.Data -- #string + Text = DataText + elseif Key == "Targets" then + local DataText = Data.Data -- #string + Text = DataText + elseif Key == "QFE" then + local Coordinate = Data.Data -- Core.Point#COORDINATE + Text = Coordinate:ToStringPressure( ReportGroup:GetUnit(1), nil, Task ) + elseif Key == "Temperature" then + local Coordinate = Data.Data -- Core.Point#COORDINATE + Text = Coordinate:ToStringTemperature( ReportGroup:GetUnit(1), nil, Task ) + elseif Key == "Wind" then + local Coordinate = Data.Data -- Core.Point#COORDINATE + Text = Coordinate:ToStringWind( ReportGroup:GetUnit(1), nil, Task ) + elseif Key == "Cargo" then + local DataText = Data.Data -- #string + Text = DataText + elseif Key == "Friendlies" then + local DataText = Data.Data -- #string + Text = DataText + elseif Key == "Players" then + local DataText = Data.Data -- #string + Text = DataText + else + local DataText = Data.Data -- #string + if type(DataText) == "string" then --Issue #1388 - don't just assume this is a string + Text = DataText + end + end + + if Line < math.floor( Data.Order / 10 ) then + if Line == 0 then + Report:AddIndent( LineReport:Text( ", " ), "-" ) + else + Report:AddIndent( LineReport:Text( ", " ) ) + end + LineReport = REPORT:New() + Line = math.floor( Data.Order / 10 ) + end + + if Text ~= "" then + LineReport:Add( ( ( Key and ShowKey == true ) and ( Key .. ": " ) or "" ) .. Text ) + end + + end + end + + Report:AddIndent( LineReport:Text( ", " ) ) +end +--- This module contains the TASK_MANAGER class and derived classes. +-- +-- === +-- +-- 1) @{Tasking.Task_Manager#TASK_MANAGER} class, extends @{Core.Fsm#FSM} +-- === +-- The @{Tasking.Task_Manager#TASK_MANAGER} class defines the core functions to report tasks to groups. +-- Reportings can be done in several manners, and it is up to the derived classes if TASK_MANAGER to model the reporting behaviour. +-- +-- 1.1) TASK_MANAGER constructor: +-- ----------------------------------- +-- * @{Tasking.Task_Manager#TASK_MANAGER.New}(): Create a new TASK_MANAGER instance. +-- +-- 1.2) TASK_MANAGER reporting: +-- --------------------------------- +-- Derived TASK_MANAGER classes will manage tasks using the method @{Tasking.Task_Manager#TASK_MANAGER.ManageTasks}(). This method implements polymorphic behaviour. +-- +-- The time interval in seconds of the task management can be changed using the methods @{Tasking.Task_Manager#TASK_MANAGER.SetRefreshTimeInterval}(). +-- To control how long a reporting message is displayed, use @{Tasking.Task_Manager#TASK_MANAGER.SetReportDisplayTime}(). +-- Derived classes need to implement the method @{Tasking.Task_Manager#TASK_MANAGER.GetReportDisplayTime}() to use the correct display time for displayed messages during a report. +-- +-- Task management can be started and stopped using the methods @{Tasking.Task_Manager#TASK_MANAGER.StartTasks}() and @{Tasking.Task_Manager#TASK_MANAGER.StopTasks}() respectively. +-- If an ad-hoc report is requested, use the method @{Tasking.Task_Manager#TASK_MANAGER#ManageTasks}(). +-- +-- The default task management interval is every 60 seconds. +-- +-- === +-- +-- ### Contributions: Mechanist, Prof_Hilactic, FlightControl - Concept & Testing +-- ### Author: FlightControl - Framework Design & Programming +-- +-- @module Tasking.Task_Manager +-- @image MOOSE.JPG + +do -- TASK_MANAGER + + --- TASK_MANAGER class. + -- @type TASK_MANAGER + -- @field Core.Set#SET_GROUP SetGroup The set of group objects containing players for which tasks are managed. + -- @extends Core.Fsm#FSM + TASK_MANAGER = { + ClassName = "TASK_MANAGER", + SetGroup = nil, + } + + --- TASK\_MANAGER constructor. + -- @param #TASK_MANAGER self + -- @param Core.Set#SET_GROUP SetGroup The set of group objects containing players for which tasks are managed. + -- @return #TASK_MANAGER self + function TASK_MANAGER:New( SetGroup ) + + -- Inherits from BASE + local self = BASE:Inherit( self, FSM:New() ) -- #TASK_MANAGER + + self.SetGroup = SetGroup + + self:SetStartState( "Stopped" ) + self:AddTransition( "Stopped", "StartTasks", "Started" ) + + --- StartTasks Handler OnBefore for TASK_MANAGER + -- @function [parent=#TASK_MANAGER] OnBeforeStartTasks + -- @param #TASK_MANAGER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- StartTasks Handler OnAfter for TASK_MANAGER + -- @function [parent=#TASK_MANAGER] OnAfterStartTasks + -- @param #TASK_MANAGER self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- StartTasks Trigger for TASK_MANAGER + -- @function [parent=#TASK_MANAGER] StartTasks + -- @param #TASK_MANAGER self + + --- StartTasks Asynchronous Trigger for TASK_MANAGER + -- @function [parent=#TASK_MANAGER] __StartTasks + -- @param #TASK_MANAGER self + -- @param #number Delay + + + + self:AddTransition( "Started", "StopTasks", "Stopped" ) + + --- StopTasks Handler OnBefore for TASK_MANAGER + -- @function [parent=#TASK_MANAGER] OnBeforeStopTasks + -- @param #TASK_MANAGER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- StopTasks Handler OnAfter for TASK_MANAGER + -- @function [parent=#TASK_MANAGER] OnAfterStopTasks + -- @param #TASK_MANAGER self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- StopTasks Trigger for TASK_MANAGER + -- @function [parent=#TASK_MANAGER] StopTasks + -- @param #TASK_MANAGER self + + --- StopTasks Asynchronous Trigger for TASK_MANAGER + -- @function [parent=#TASK_MANAGER] __StopTasks + -- @param #TASK_MANAGER self + -- @param #number Delay + + + self:AddTransition( "Started", "Manage", "Started" ) + + self:AddTransition( "Started", "Success", "Started" ) + + --- Success Handler OnAfter for TASK_MANAGER + -- @function [parent=#TASK_MANAGER] OnAfterSuccess + -- @param #TASK_MANAGER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Tasking.Task#TASK Task + + + self:AddTransition( "Started", "Failed", "Started" ) + + --- Failed Handler OnAfter for TASK_MANAGER + -- @function [parent=#TASK_MANAGER] OnAfterFailed + -- @param #TASK_MANAGER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Tasking.Task#TASK Task + + + self:AddTransition( "Started", "Aborted", "Started" ) + + --- Aborted Handler OnAfter for TASK_MANAGER + -- @function [parent=#TASK_MANAGER] OnAfterAborted + -- @param #TASK_MANAGER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Tasking.Task#TASK Task + + self:AddTransition( "Started", "Cancelled", "Started" ) + + --- Cancelled Handler OnAfter for TASK_MANAGER + -- @function [parent=#TASK_MANAGER] OnAfterCancelled + -- @param #TASK_MANAGER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Tasking.Task#TASK Task + + self:SetRefreshTimeInterval( 30 ) + + return self + end + + function TASK_MANAGER:onafterStartTasks( From, Event, To ) + self:Manage() + end + + function TASK_MANAGER:onafterManage( From, Event, To ) + + self:__Manage( -self._RefreshTimeInterval ) + + self:ManageTasks() + end + + --- Set the refresh time interval in seconds when a new task management action needs to be done. + -- @param #TASK_MANAGER self + -- @param #number RefreshTimeInterval The refresh time interval in seconds when a new task management action needs to be done. + -- @return #TASK_MANAGER self + function TASK_MANAGER:SetRefreshTimeInterval( RefreshTimeInterval ) + self:F2() + + self._RefreshTimeInterval = RefreshTimeInterval + end + + + --- Manages the tasks for the @{Core.Set#SET_GROUP}. + -- @param #TASK_MANAGER self + -- @return #TASK_MANAGER self + function TASK_MANAGER:ManageTasks() + + end + +end + +--- **Tasking** - This module contains the DETECTION_MANAGER class and derived classes. +-- +-- === +-- +-- The @{#DETECTION_MANAGER} class defines the core functions to report detected objects to groups. +-- Reportings can be done in several manners, and it is up to the derived classes if DETECTION_MANAGER to model the reporting behaviour. +-- +-- 1.1) DETECTION_MANAGER constructor: +-- ----------------------------------- +-- * @{#DETECTION_MANAGER.New}(): Create a new DETECTION_MANAGER instance. +-- +-- 1.2) DETECTION_MANAGER reporting: +-- --------------------------------- +-- Derived DETECTION_MANAGER classes will reports detected units using the method @{#DETECTION_MANAGER.ReportDetected}(). This method implements polymorphic behaviour. +-- +-- The time interval in seconds of the reporting can be changed using the methods @{#DETECTION_MANAGER.SetRefreshTimeInterval}(). +-- To control how long a reporting message is displayed, use @{#DETECTION_MANAGER.SetReportDisplayTime}(). +-- Derived classes need to implement the method @{#DETECTION_MANAGER.GetReportDisplayTime}() to use the correct display time for displayed messages during a report. +-- +-- Reporting can be started and stopped using the methods @{#DETECTION_MANAGER.StartReporting}() and @{#DETECTION_MANAGER.StopReporting}() respectively. +-- If an ad-hoc report is requested, use the method @{#DETECTION_MANAGER#ReportNow}(). +-- +-- The default reporting interval is every 60 seconds. The reporting messages are displayed 15 seconds. +-- +-- === +-- +-- 2) @{#DETECTION_REPORTING} class, extends @{#DETECTION_MANAGER} +-- === +-- The @{#DETECTION_REPORTING} class implements detected units reporting. Reporting can be controlled using the reporting methods available in the @{Tasking.DetectionManager#DETECTION_MANAGER} class. +-- +-- 2.1) DETECTION_REPORTING constructor: +-- ------------------------------- +-- The @{#DETECTION_REPORTING.New}() method creates a new DETECTION_REPORTING instance. +-- +-- +-- === +-- +-- ### Contributions: Mechanist, Prof_Hilactic, FlightControl - Concept & Testing +-- ### Author: FlightControl - Framework Design & Programming +-- +-- @module Tasking.DetectionManager +-- @image Task_Detection_Manager.JPG + +do -- DETECTION MANAGER + + --- @type DETECTION_MANAGER + -- @field Core.Set#SET_GROUP SetGroup The groups to which the FAC will report to. + -- @field Functional.Detection#DETECTION_BASE Detection The DETECTION_BASE object that is used to report the detected objects. + -- @field Tasking.CommandCenter#COMMANDCENTER CC The command center that is used to communicate with the players. + -- @extends Core.Fsm#FSM + + --- DETECTION_MANAGER class. + -- @field #DETECTION_MANAGER + DETECTION_MANAGER = { + ClassName = "DETECTION_MANAGER", + SetGroup = nil, + Detection = nil, + } + + --- @field Tasking.CommandCenter#COMMANDCENTER + DETECTION_MANAGER.CC = nil + + --- FAC constructor. + -- @param #DETECTION_MANAGER self + -- @param Core.Set#SET_GROUP SetGroup + -- @param Functional.Detection#DETECTION_BASE Detection + -- @return #DETECTION_MANAGER self + function DETECTION_MANAGER:New( SetGroup, Detection ) + + -- Inherits from BASE + local self = BASE:Inherit( self, FSM:New() ) -- #DETECTION_MANAGER + + self.SetGroup = SetGroup + self.Detection = Detection + + self:SetStartState( "Stopped" ) + self:AddTransition( "Stopped", "Start", "Started" ) + + --- Start Handler OnBefore for DETECTION_MANAGER + -- @function [parent=#DETECTION_MANAGER] OnBeforeStart + -- @param #DETECTION_MANAGER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Start Handler OnAfter for DETECTION_MANAGER + -- @function [parent=#DETECTION_MANAGER] OnAfterStart + -- @param #DETECTION_MANAGER self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Start Trigger for DETECTION_MANAGER + -- @function [parent=#DETECTION_MANAGER] Start + -- @param #DETECTION_MANAGER self + + --- Start Asynchronous Trigger for DETECTION_MANAGER + -- @function [parent=#DETECTION_MANAGER] __Start + -- @param #DETECTION_MANAGER self + -- @param #number Delay + + + + self:AddTransition( "Started", "Stop", "Stopped" ) + + --- Stop Handler OnBefore for DETECTION_MANAGER + -- @function [parent=#DETECTION_MANAGER] OnBeforeStop + -- @param #DETECTION_MANAGER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Stop Handler OnAfter for DETECTION_MANAGER + -- @function [parent=#DETECTION_MANAGER] OnAfterStop + -- @param #DETECTION_MANAGER self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Stop Trigger for DETECTION_MANAGER + -- @function [parent=#DETECTION_MANAGER] Stop + -- @param #DETECTION_MANAGER self + + --- Stop Asynchronous Trigger for DETECTION_MANAGER + -- @function [parent=#DETECTION_MANAGER] __Stop + -- @param #DETECTION_MANAGER self + -- @param #number Delay + + self:AddTransition( "Started", "Success", "Started" ) + + --- Success Handler OnAfter for DETECTION_MANAGER + -- @function [parent=#DETECTION_MANAGER] OnAfterSuccess + -- @param #DETECTION_MANAGER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Tasking.Task#TASK Task + + + self:AddTransition( "Started", "Failed", "Started" ) + + --- Failed Handler OnAfter for DETECTION_MANAGER + -- @function [parent=#DETECTION_MANAGER] OnAfterFailed + -- @param #DETECTION_MANAGER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Tasking.Task#TASK Task + + + self:AddTransition( "Started", "Aborted", "Started" ) + + --- Aborted Handler OnAfter for DETECTION_MANAGER + -- @function [parent=#DETECTION_MANAGER] OnAfterAborted + -- @param #DETECTION_MANAGER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Tasking.Task#TASK Task + + self:AddTransition( "Started", "Cancelled", "Started" ) + + --- Cancelled Handler OnAfter for DETECTION_MANAGER + -- @function [parent=#DETECTION_MANAGER] OnAfterCancelled + -- @param #DETECTION_MANAGER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Tasking.Task#TASK Task + + + self:AddTransition( "Started", "Report", "Started" ) + + self:SetRefreshTimeInterval( 30 ) + self:SetReportDisplayTime( 25 ) + + Detection:__Start( 3 ) + + return self + end + + function DETECTION_MANAGER:onafterStart( From, Event, To ) + self:Report() + end + + function DETECTION_MANAGER:onafterReport( From, Event, To ) + + self:__Report( -self._RefreshTimeInterval ) + + self:ProcessDetected( self.Detection ) + end + + --- Set the reporting time interval. + -- @param #DETECTION_MANAGER self + -- @param #number RefreshTimeInterval The interval in seconds when a report needs to be done. + -- @return #DETECTION_MANAGER self + function DETECTION_MANAGER:SetRefreshTimeInterval( RefreshTimeInterval ) + self:F2() + + self._RefreshTimeInterval = RefreshTimeInterval + end + + + --- Set the reporting message display time. + -- @param #DETECTION_MANAGER self + -- @param #number ReportDisplayTime The display time in seconds when a report needs to be done. + -- @return #DETECTION_MANAGER self + function DETECTION_MANAGER:SetReportDisplayTime( ReportDisplayTime ) + self:F2() + + self._ReportDisplayTime = ReportDisplayTime + end + + --- Get the reporting message display time. + -- @param #DETECTION_MANAGER self + -- @return #number ReportDisplayTime The display time in seconds when a report needs to be done. + function DETECTION_MANAGER:GetReportDisplayTime() + self:F2() + + return self._ReportDisplayTime + end + + + --- Set a command center to communicate actions to the players reporting to the command center. + -- @param #DETECTION_MANAGER self + -- @return #DETECTION_MANGER self + function DETECTION_MANAGER:SetTacticalMenu( DispatcherMainMenuText, DispatcherMenuText ) + + local DispatcherMainMenu = MENU_MISSION:New( DispatcherMainMenuText, nil ) + local DispatcherMenu = MENU_MISSION_COMMAND:New( DispatcherMenuText, DispatcherMainMenu, + function() + self:ShowTacticalDisplay( self.Detection ) + end + ) + + return self + end + + + + + --- Set a command center to communicate actions to the players reporting to the command center. + -- @param #DETECTION_MANAGER self + -- @param Tasking.CommandCenter#COMMANDCENTER CommandCenter The command center. + -- @return #DETECTION_MANGER self + function DETECTION_MANAGER:SetCommandCenter( CommandCenter ) + + self.CC = CommandCenter + + return self + end + + + --- Get the command center to communicate actions to the players. + -- @param #DETECTION_MANAGER self + -- @return Tasking.CommandCenter#COMMANDCENTER The command center. + function DETECTION_MANAGER:GetCommandCenter() + + return self.CC + end + + + --- Send an information message to the players reporting to the command center. + -- @param #DETECTION_MANAGER self + -- @param #table Squadron The squadron table. + -- @param #string Message The message to be sent. + -- @param #string SoundFile The name of the sound file .wav or .ogg. + -- @param #number SoundDuration The duration of the sound. + -- @param #string SoundPath The path pointing to the folder in the mission file. + -- @param Wrapper.Group#GROUP DefenderGroup The defender group sending the message. + -- @return #DETECTION_MANGER self + function DETECTION_MANAGER:MessageToPlayers( Squadron, Message, DefenderGroup ) + + self:F( { Message = Message } ) + +-- if not self.PreviousMessage or self.PreviousMessage ~= Message then +-- self.PreviousMessage = Message +-- if self.CC then +-- self.CC:MessageToCoalition( Message ) +-- end +-- end + + if self.CC then + self.CC:MessageToCoalition( Message ) + end + + Message = Message:gsub( "°", " degrees " ) + Message = Message:gsub( "(%d)%.(%d)", "%1 dot %2" ) + + -- Here we handle the transmission of the voice over. + -- If for a certain reason the Defender does not exist, we use the coordinate of the airbase to send the message from. + local RadioQueue = Squadron.RadioQueue -- Core.RadioSpeech#RADIOSPEECH + if RadioQueue then + local DefenderUnit = DefenderGroup:GetUnit(1) + if DefenderUnit and DefenderUnit:IsAlive() then + RadioQueue:SetSenderUnitName( DefenderUnit:GetName() ) + end + RadioQueue:Speak( Message, Squadron.Language ) + end + + return self + end + + + + --- Reports the detected items to the @{Core.Set#SET_GROUP}. + -- @param #DETECTION_MANAGER self + -- @param Functional.Detection#DETECTION_BASE Detection + -- @return #DETECTION_MANAGER self + function DETECTION_MANAGER:ProcessDetected( Detection ) + + end + +end + + +do -- DETECTION_REPORTING + + --- DETECTION_REPORTING class. + -- @type DETECTION_REPORTING + -- @field Core.Set#SET_GROUP SetGroup The groups to which the FAC will report to. + -- @field Functional.Detection#DETECTION_BASE Detection The DETECTION_BASE object that is used to report the detected objects. + -- @extends #DETECTION_MANAGER + DETECTION_REPORTING = { + ClassName = "DETECTION_REPORTING", + } + + + --- DETECTION_REPORTING constructor. + -- @param #DETECTION_REPORTING self + -- @param Core.Set#SET_GROUP SetGroup + -- @param Functional.Detection#DETECTION_AREAS Detection + -- @return #DETECTION_REPORTING self + function DETECTION_REPORTING:New( SetGroup, Detection ) + + -- Inherits from DETECTION_MANAGER + local self = BASE:Inherit( self, DETECTION_MANAGER:New( SetGroup, Detection ) ) -- #DETECTION_REPORTING + + self:Schedule( 1, 30 ) + return self + end + + --- Creates a string of the detected items in a @{Detection}. + -- @param #DETECTION_MANAGER self + -- @param Core.Set#SET_UNIT DetectedSet The detected Set created by the @{Functional.Detection#DETECTION_BASE} object. + -- @return #DETECTION_MANAGER self + function DETECTION_REPORTING:GetDetectedItemsText( DetectedSet ) + self:F2() + + local MT = {} -- Message Text + local UnitTypes = {} + + for DetectedUnitID, DetectedUnitData in pairs( DetectedSet:GetSet() ) do + local DetectedUnit = DetectedUnitData -- Wrapper.Unit#UNIT + if DetectedUnit:IsAlive() then + local UnitType = DetectedUnit:GetTypeName() + + if not UnitTypes[UnitType] then + UnitTypes[UnitType] = 1 + else + UnitTypes[UnitType] = UnitTypes[UnitType] + 1 + end + end + end + + for UnitTypeID, UnitType in pairs( UnitTypes ) do + MT[#MT+1] = UnitType .. " of " .. UnitTypeID + end + + return table.concat( MT, ", " ) + end + + + + --- Reports the detected items to the @{Core.Set#SET_GROUP}. + -- @param #DETECTION_REPORTING self + -- @param Wrapper.Group#GROUP Group The @{Wrapper.Group} object to where the report needs to go. + -- @param Functional.Detection#DETECTION_AREAS Detection The detection created by the @{Functional.Detection#DETECTION_BASE} object. + -- @return #boolean Return true if you want the reporting to continue... false will cancel the reporting loop. + function DETECTION_REPORTING:ProcessDetected( Group, Detection ) + self:F2( Group ) + + local DetectedMsg = {} + for DetectedAreaID, DetectedAreaData in pairs( Detection:GetDetectedAreas() ) do + local DetectedArea = DetectedAreaData -- Functional.Detection#DETECTION_AREAS.DetectedArea + DetectedMsg[#DetectedMsg+1] = " - Group #" .. DetectedAreaID .. ": " .. self:GetDetectedItemsText( DetectedArea.Set ) + end + local FACGroup = Detection:GetDetectionGroups() + FACGroup:MessageToGroup( "Reporting detected target groups:\n" .. table.concat( DetectedMsg, "\n" ), self:GetReportDisplayTime(), Group ) + + return true + end + +end + +--- **Tasking** -- Dynamically allocates A2G tasks to human players, based on detected ground targets through reconnaissance. +-- +-- **Features:** +-- +-- * Dynamically assign tasks to human players based on detected targets. +-- * Dynamically change the tasks as the tactical situation evolves during the mission. +-- * Dynamically assign (CAS) Close Air Support tasks for human players. +-- * Dynamically assign (BAI) Battlefield Air Interdiction tasks for human players. +-- * Dynamically assign (SEAD) Supression of Enemy Air Defense tasks for human players to eliminate G2A missile threats. +-- * Define and use an EWR (Early Warning Radar) network. +-- * Define different ranges to engage upon intruders. +-- * Keep task achievements. +-- * Score task achievements.-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- ### Contributions: +-- +-- === +-- +-- @module Tasking.Task_A2G_Dispatcher +-- @image Task_A2G_Dispatcher.JPG + +do -- TASK_A2G_DISPATCHER + + --- TASK\_A2G\_DISPATCHER class. + -- @type TASK_A2G_DISPATCHER + -- @field Core.Set#SET_GROUP SetGroup The groups to which the FAC will report to. + -- @field Functional.Detection#DETECTION_BASE Detection The DETECTION_BASE object that is used to report the detected objects. + -- @field Tasking.Mission#MISSION Mission + -- @extends Tasking.DetectionManager#DETECTION_MANAGER + + --- Orchestrates dynamic **A2G Task Dispatching** based on the detection results of a linked @{Detection} object. + -- + -- It uses the Tasking System within the MOOSE framework, which is a multi-player Tasking Orchestration system. + -- It provides a truly dynamic battle environment for pilots and ground commanders to engage upon, + -- in a true co-operation environment wherein **Multiple Teams** will collaborate in Missions to **achieve a common Mission Goal**. + -- + -- The A2G dispatcher will dispatch the A2G Tasks to a defined @{Set} of @{Wrapper.Group}s that will be manned by **Players**. + -- We call this the **AttackSet** of the A2G dispatcher. So, the Players are seated in the @{Client}s of the @{Wrapper.Group} @{Set}. + -- + -- Depending on the actions of the enemy, preventive tasks are dispatched to the players to orchestrate the engagement in a true co-operation. + -- The detection object will group the detected targets by its grouping method, and integrates a @{Set} of @{Wrapper.Group}s that are Recce vehicles or air units. + -- We call this the **RecceSet** of the A2G dispatcher. + -- + -- Depending on the current detected tactical situation, different task types will be dispatched to the Players seated in the AttackSet.. + -- There are currently 3 **Task Types** implemented in the TASK\_A2G\_DISPATCHER: + -- + -- - **SEAD Task**: Dispatched when there are ground based Radar Emitters detected within an area. + -- - **CAS Task**: Dispatched when there are no ground based Radar Emitters within the area, but there are friendly ground Units within 6 km from the enemy. + -- - **BAI Task**: Dispatched when there are no ground based Radar Emitters within the area, and there aren't friendly ground Units within 6 km from the enemy. + -- + -- # 0. Tactical Situations + -- + -- This chapters provides some insights in the tactical situations when certain Task Types are created. + -- The Task Types are depending on the enemy positions that were detected, and the current location of friendly units. + -- + -- ![](..\Presentations\TASK_A2G_DISPATCHER\Dia3.JPG) + -- + -- In the demonstration mission [TAD-A2G-000 - AREAS - Detection test], + -- the tactical situation is a demonstration how the A2G detection works. + -- This example will be taken further in the explanation in the following chapters. + -- + -- ![](..\Presentations\TASK_A2G_DISPATCHER\Dia4.JPG) + -- + -- The red coalition are the players, the blue coalition is the enemy. + -- + -- Red reconnaissance vehicles and airborne units are detecting the targets. + -- We call this the RecceSet as explained above, which is a Set of Groups that + -- have a group name starting with `Recce` (configured in the mission script). + -- + -- Red attack units are responsible for executing the mission for the command center. + -- We call this the AttackSet, which is a Set of Groups with a group name starting with `Attack` (configured in the mission script). + -- These units are setup in this demonstration mission to be ground vehicles and airplanes. + -- For demonstration purposes, the attack airplane is stationed on the ground to explain + -- the messages and the menus properly. + -- Further test missions demonstrate the A2G task dispatcher from within air. + -- + -- Depending upon the detection results, the A2G dispatcher will create different tasks. + -- + -- # 0.1. SEAD Task + -- + -- A SEAD Task is dispatched when there are ground based Radar Emitters detected within an area. + -- + -- ![](..\Presentations\TASK_A2G_DISPATCHER\Dia9.JPG) + -- + -- - Once all Radar Emitting Units have been destroyed, the Task will convert into a BAI or CAS task! + -- - A CAS and BAI task may be converted into a SEAD task, once a radar has been detected within the area! + -- + -- # 0.2. CAS Task + -- + -- A CAS Task is dispatched when there are no ground based Radar Emitters within the area, but there are friendly ground Units within 6 km from the enemy. + -- + -- ![](..\Presentations\TASK_A2G_DISPATCHER\Dia10.JPG) + -- + -- - After the detection of the CAS task, if the friendly Units are destroyed, the CAS task will convert into a BAI task! + -- - Only ground Units are taken into account. Airborne units are ships are not considered friendlies that require Close Air Support. + -- + -- # 0.3. BAI Task + -- + -- A BAI Task is dispatched when there are no ground based Radar Emitters within the area, and there aren't friendly ground Units within 6 km from the enemy. + -- + -- ![](..\Presentations\TASK_A2G_DISPATCHER\Dia11.JPG) + -- + -- - A BAI task may be converted into a CAS task if friendly Ground Units approach within 6 km range! + -- + -- # 1. Player Experience + -- + -- The A2G dispatcher is residing under a @{CommandCenter}, which is orchestrating a @{Mission}. + -- As a result, you'll find for DCS World missions that implement the A2G dispatcher a **Command Center Menu** and under this one or more **Mission Menus**. + -- + -- For example, if there are 2 Command Centers (CC). + -- Each CC is controlling a couple of Missions, the Radio Menu Structure could look like this: + -- + -- Radio MENU Structure (F10. Other) + -- + -- F1. Command Center [Gori] + -- F1. Mission "Alpha (Primary)" + -- F2. Mission "Beta (Secondary)" + -- F3. Mission "Gamma (Tactical)" + -- F1. Command Center [Lima] + -- F1. Mission "Overlord (High)" + -- + -- Command Center [Gori] is controlling Mission "Alpha", "Beta", "Gamma". Alpha is the Primary mission, Beta the Secondary and there is a Tacical mission Gamma. + -- Command Center [Lima] is controlling Missions "Overlord", which needs to be executed with High priority. + -- + -- ## 1.1. Mission Menu (Under the Command Center Menu) + -- + -- The Mission Menu controls the information of the mission, including the: + -- + -- - **Mission Briefing**: A briefing of the Mission in text, which will be shown as a message. + -- - **Mark Task Locations**: A summary of each Task will be shown on the map as a marker. + -- - **Create Task Reports**: A menu to create various reports of the current tasks dispatched by the A2G dispatcher. + -- - **Create Mission Reports**: A menu to create various reports on the current mission. + -- + -- For CC [Lima], Mission "Overlord", the menu structure could look like this: + -- + -- Radio MENU Structure (F10. Other) + -- + -- F1. Command Center [Lima] + -- F1. Mission "Overlord" + -- F1. Mission Briefing + -- F2. Mark Task Locations on Map + -- F3. Task Reports + -- F4. Mission Reports + -- + -- ![](..\Presentations\TASK_A2G_DISPATCHER\Dia5.JPG) + -- + -- ### 1.1.1. Mission Briefing Menu + -- + -- The Mission Briefing Menu will show in text a summary description of the overall mission objectives and expectations. + -- Note that the Mission Briefing is not the briefing of a specific task, but rather provides an overall strategy and tactical situation, + -- and explains the mission goals. + -- + -- + -- ### 1.1.2. Mark Task Locations Menu + -- + -- The Mark Task Locations Menu will mark the location indications of the Tasks on the map, if this intelligence is known by the Command Center. + -- For A2G tasks this information will always be know, but it can be that for other tasks a location intelligence will be less relevant. + -- Note that each Planned task and each Engaged task will be marked. Completed, Failed and Cancelled tasks are not marked. + -- Depending on the task type, a summary information is shown to bring to the player the relevant information for situational awareness. + -- + -- ### 1.1.3. Task Reports Menu + -- + -- The Task Reports Menu is a sub menu, that allows to create various reports: + -- + -- - **Tasks Summary**: This report will list all the Tasks that are or were active within the mission, indicating its status. + -- - **Planned Tasks**: This report will list all the Tasks that are in status Planned, which are Tasks not assigned to any player, and are ready to be executed. + -- - **Assigned Tasks**: This report will list all the Tasks that are in status Assigned, which are Tasks assigned to (a) player(s) and are currently executed. + -- - **Successful Tasks**: This report will list all the Tasks that are in status Success, which are Tasks executed by (a) player(s) and are completed successfully. + -- - **Failed Tasks**: This report will list all the Tasks that are in status Success, which are Tasks executed by (a) player(s) and that have failed. + -- + -- The information shown of the tasks will vary according the underlying task type, but are self explanatory. + -- + -- For CC [Gori], Mission "Alpha", the Task Reports menu structure could look like this: + -- + -- Radio MENU Structure (F10. Other) + -- + -- F1. Command Center [Gori] + -- F1. Mission "Alpha" + -- F1. Mission Briefing + -- F2. Mark Task Locations on Map + -- F3. Task Reports + -- F1. Tasks Summary + -- F2. Planned Tasks + -- F3. Assigned Tasks + -- F4. Successful Tasks + -- F5. Failed Tasks + -- F4. Mission Reports + -- + -- Note that these reports provide an "overview" of the tasks. Detailed information of the task can be retrieved using the Detailed Report on the Task Menu. + -- (See later). + -- + -- ### 1.1.4. Mission Reports Menu + -- + -- The Mission Reports Menu is a sub menu, that provides options to retrieve further information on the current Mission: + -- + -- - **Report Mission Progress**: Shows the progress of the current Mission. Each Task has a %-tage of completion. + -- - **Report Players per Task**: Show which players are engaged on which Task within the Mission. + -- + -- For CC |Gori|, Mission "Alpha", the Mission Reports menu structure could look like this: + -- + -- Radio MENU Structure (F10. Other) + -- + -- F1. Command Center [Gori] + -- F1. Mission "Alpha" + -- F1. Mission Briefing + -- F2. Mark Task Locations on Map + -- F3. Task Reports + -- F4. Mission Reports + -- F1. Report Mission Progress + -- F2. Report Players per Task + -- + -- + -- ## 1.2. Task Management Menus + -- + -- Very important to remember is: **Multiple Players can be assigned to the same Task, but from the player perspective, the Player can only be assigned to one Task per Mission at the same time!** + -- Consider this like the two major modes in which a player can be in. He can be free of tasks or he can be assigned to a Task. + -- Depending on whether a Task has been Planned or Assigned to a Player (Group), + -- **the Mission Menu will contain extra Menus to control specific Tasks.** + -- + -- #### 1.2.1. Join a Planned Task + -- + -- If the Player has not yet been assigned to a Task within the Mission, the Mission Menu will contain additionally a: + -- + -- - Join Planned Task Menu: This menu structure allows the player to join a planned task (a Task with status Planned). + -- + -- For CC |Gori|, Mission "Alpha", the menu structure could look like this: + -- + -- Radio MENU Structure (F10. Other) + -- + -- F1. Command Center [Gori] + -- F1. Mission "Alpha" + -- F1. Mission Briefing + -- F2. Mark Task Locations on Map + -- F3. Task Reports + -- F4. Mission Reports + -- F5. Join Planned Task + -- + -- **The F5. Join Planned Task allows the player to join a Planned Task and take an engagement in the running Mission.** + -- + -- #### 1.2.2. Manage an Assigned Task + -- + -- If the Player has been assigned to one Task within the Mission, the Mission Menu will contain an extra: + -- + -- - Assigned Task __TaskName__ Menu: This menu structure allows the player to take actions on the currently engaged task. + -- + -- In this example, the Group currently seated by the player is not assigned yet to a Task. + -- The Player has the option to assign itself to a Planned Task using menu option F5 under the Mission Menu "Alpha". + -- + -- This would be an example menu structure, + -- for CC |Gori|, Mission "Alpha", when a player would have joined Task CAS.001: + -- + -- Radio MENU Structure (F10. Other) + -- + -- F1. Command Center [Gori] + -- F1. Mission "Alpha" + -- F1. Mission Briefing + -- F2. Mark Task Locations on Map + -- F3. Task Reports + -- F4. Mission Reports + -- F5. Assigned Task CAS.001 + -- + -- **The F5. Assigned Task __TaskName__ allows the player to control the current Assigned Task and take further actions.** + -- + -- + -- ## 1.3. Join Planned Task Menu + -- + -- The Join Planned Task Menu contains the different Planned A2G Tasks **in a structured Menu Hierarchy**. + -- The Menu Hierarchy is structuring the Tasks per **Task Type**, and then by **Task Name (ID)**. + -- + -- For example, for CC [Gori], Mission "Alpha", + -- if a Mission "ALpha" contains 5 Planned Tasks, which would be: + -- + -- - 2 CAS Tasks + -- - 1 BAI Task + -- - 2 SEAD Tasks + -- + -- the Join Planned Task Menu Hierarchy could look like this: + -- + -- Radio MENU Structure (F10. Other) + -- + -- F1. Command Center [Gori] + -- F1. Mission "Alpha" + -- F1. Mission Briefing + -- F2. Mark Task Locations on Map + -- F3. Task Reports + -- F4. Mission Reports + -- F5. Join Planned Task + -- F2. BAI + -- F1. BAI.001 + -- F1. CAS + -- F1. CAS.002 + -- F3. SEAD + -- F1. SEAD.003 + -- F2. SEAD.004 + -- F3. SEAD.005 + -- + -- An example from within a running simulation: + -- + -- ![](..\Presentations\TASK_A2G_DISPATCHER\Dia6.JPG) + -- + -- Each Task Type Menu would have a list of the Task Menus underneath. + -- Each Task Menu (eg. `CAS.001`) has a **detailed Task Menu structure to control the specific task**! + -- + -- ### 1.3.1. Planned Task Menu + -- + -- Each Planned Task Menu will allow for the following actions: + -- + -- - Report Task Details: Provides a detailed report on the Planned Task. + -- - Mark Task Location on Map: Mark the approximate location of the Task on the Map, if relevant. + -- - Join Task: Join the Task. This is THE menu option to let a Player join the Task, and to engage within the Mission. + -- + -- The Join Planned Task Menu could look like this for for CC |Gori|, Mission "Alpha": + -- + -- Radio MENU Structure (F10. Other) + -- + -- F1. Command Center |Gori| + -- F1. Mission "Alpha" + -- F1. Mission Briefing + -- F2. Mark Task Locations on Map + -- F3. Task Reports + -- F4. Mission Reports + -- F5. Join Planned Task + -- F1. CAS + -- F1. CAS.001 + -- F1. Report Task Details + -- F2. Mark Task Location on Map + -- F3. Join Task + -- + -- **The Join Task is THE menu option to let a Player join the Task, and to engage within the Mission.** + -- + -- + -- ## 1.4. Assigned Task Menu + -- + -- The Assigned Task Menu allows to control the **current assigned task** within the Mission. + -- + -- Depending on the Type of Task, the following menu options will be available: + -- + -- - **Report Task Details**: Provides a detailed report on the Planned Task. + -- - **Mark Task Location on Map**: Mark the approximate location of the Task on the Map, if relevant. + -- - **Abort Task: Abort the current assigned Task:** This menu option lets the player abort the Task. + -- + -- For example, for CC |Gori|, Mission "Alpha", the Assigned Menu could be: + -- + -- F1. Command Center |Gori| + -- F1. Mission "Alpha" + -- F1. Mission Briefing + -- F2. Mark Task Locations on Map + -- F3. Task Reports + -- F4. Mission Reports + -- F5. Assigned Task + -- F1. Report Task Details + -- F2. Mark Task Location on Map + -- F3. Abort Task + -- + -- Task abortion will result in the Task to be Cancelled, and the Task **may** be **Replanned**. + -- However, this will depend on the setup of each Mission. + -- + -- ## 1.5. Messages + -- + -- During game play, different messages are displayed. + -- These messages provide an update of the achievements made, and the state wherein the task is. + -- + -- The various reports can be used also to retrieve the current status of the mission and its tasks. + -- + -- ![](..\Presentations\TASK_A2G_DISPATCHER\Dia7.JPG) + -- + -- The @{Settings} menu provides additional options to control the timing of the messages. + -- There are: + -- + -- - Status messages, which are quick status updates. The settings menu allows to switch off these messages. + -- - Information messages, which are shown a bit longer, as they contain important information. + -- - Summary reports, which are quick reports showing a high level summary. + -- - Overview reports, which are providing the essential information. It provides an overview of a greater thing, and may take a bit of time to read. + -- - Detailed reports, which provide with very detailed information. It takes a bit longer to read those reports, so the display of those could be a bit longer. + -- + -- # 2. TASK\_A2G\_DISPATCHER constructor + -- + -- The @{#TASK_A2G_DISPATCHER.New}() method creates a new TASK\_A2G\_DISPATCHER instance. + -- + -- # 3. Usage + -- + -- To use the TASK\_A2G\_DISPATCHER class, you need: + -- + -- - A @{CommandCenter} object. The master communication channel. + -- - A @{Mission} object. Each task belongs to a Mission. + -- - A @{Detection} object. There are several detection grouping methods to choose from. + -- - A @{Task_A2G_Dispatcher} object. The master A2G task dispatcher. + -- - A @{Set} of @{Wrapper.Group} objects that will detect the emeny, the RecceSet. This is attached to the @{Detection} object. + -- - A @{Set} ob @{Wrapper.Group} objects that will attack the enemy, the AttackSet. This is attached to the @{Task_A2G_Dispatcher} object. + -- + -- Below an example mission declaration that is defines a Task A2G Dispatcher object. + -- + -- -- Declare the Command Center + -- local HQ = GROUP + -- :FindByName( "HQ", "Bravo HQ" ) + -- + -- local CommandCenter = COMMANDCENTER + -- :New( HQ, "Lima" ) + -- + -- -- Declare the Mission for the Command Center. + -- local Mission = MISSION + -- :New( CommandCenter, "Overlord", "High", "Attack Detect Mission Briefing", coalition.side.RED ) + -- + -- -- Define the RecceSet that will detect the enemy. + -- local RecceSet = SET_GROUP + -- :New() + -- :FilterPrefixes( "FAC" ) + -- :FilterCoalitions("red") + -- :FilterStart() + -- + -- -- Setup the detection. We use DETECTION_AREAS to detect and group the enemies within areas of 3 km radius. + -- local DetectionAreas = DETECTION_AREAS + -- :New( RecceSet, 3000 ) -- The RecceSet will detect the enemies. + -- + -- -- Setup the AttackSet, which is a SET_GROUP. + -- -- The SET_GROUP is a dynamic collection of GROUP objects. + -- local AttackSet = SET_GROUP + -- :New() -- Create the SET_GROUP object. + -- :FilterCoalitions( "red" ) -- Only incorporate the RED coalitions. + -- :FilterPrefixes( "Attack" ) -- Only incorporate groups that start with the name Attack. + -- :FilterStart() -- Enable the dynamic filtering. From this moment the AttackSet will contain all groups that are red and start with the name Attack. + -- + -- -- Now we have everything to setup the main A2G TaskDispatcher. + -- TaskDispatcher = TASK_A2G_DISPATCHER + -- :New( Mission, AttackSet, DetectionAreas ) -- We assign the TaskDispatcher under Mission. The AttackSet will engage the enemy and will recieve the dispatched Tasks. The DetectionAreas will report any detected enemies to the TaskDispatcher. + -- + -- + -- + -- @field #TASK_A2G_DISPATCHER + TASK_A2G_DISPATCHER = { + ClassName = "TASK_A2G_DISPATCHER", + Mission = nil, + Detection = nil, + Tasks = {}, + } + + + --- TASK_A2G_DISPATCHER constructor. + -- @param #TASK_A2G_DISPATCHER self + -- @param Tasking.Mission#MISSION Mission The mission for which the task dispatching is done. + -- @param Core.Set#SET_GROUP SetGroup The set of groups that can join the tasks within the mission. + -- @param Functional.Detection#DETECTION_BASE Detection The detection results that are used to dynamically assign new tasks to human players. + -- @return #TASK_A2G_DISPATCHER self + function TASK_A2G_DISPATCHER:New( Mission, SetGroup, Detection ) + + -- Inherits from DETECTION_MANAGER + local self = BASE:Inherit( self, DETECTION_MANAGER:New( SetGroup, Detection ) ) -- #TASK_A2G_DISPATCHER + + self.Detection = Detection + self.Mission = Mission + self.FlashNewTask = true --set to false to suppress flash messages + + self.Detection:FilterCategories( { Unit.Category.GROUND_UNIT } ) + + self:AddTransition( "Started", "Assign", "Started" ) + + --- OnAfter Transition Handler for Event Assign. + -- @function [parent=#TASK_A2G_DISPATCHER] OnAfterAssign + -- @param #TASK_A2G_DISPATCHER self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @param Tasking.Task_A2G#TASK_A2G Task + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param #string PlayerName + + self:__Start( 5 ) + + return self + end + + --- Set flashing player messages on or off + -- @param #TASK_A2G_DISPATCHER self + -- @param #boolean onoff Set messages on (true) or off (false) + function TASK_A2G_DISPATCHER:SetSendMessages( onoff ) + self.FlashNewTask = onoff + end + + --- Creates a SEAD task when there are targets for it. + -- @param #TASK_A2G_DISPATCHER self + -- @param Functional.Detection#DETECTION_AREAS.DetectedItem DetectedItem + -- @return Core.Set#SET_UNIT TargetSetUnit: The target set of units. + -- @return #nil If there are no targets to be set. + function TASK_A2G_DISPATCHER:EvaluateSEAD( DetectedItem ) + self:F( { DetectedItem.ItemID } ) + + local DetectedSet = DetectedItem.Set + local DetectedZone = DetectedItem.Zone + + -- Determine if the set has radar targets. If it does, construct a SEAD task. + local RadarCount = DetectedSet:HasSEAD() + + if RadarCount > 0 then + + -- Here we're doing something advanced... We're copying the DetectedSet, but making a new Set only with SEADable Radar units in it. + local TargetSetUnit = SET_UNIT:New() + TargetSetUnit:SetDatabase( DetectedSet ) + TargetSetUnit:FilterHasSEAD() + TargetSetUnit:FilterOnce() -- Filter but don't do any events!!! Elements are added manually upon each detection. + + return TargetSetUnit + end + + return nil + end + + --- Creates a CAS task when there are targets for it. + -- @param #TASK_A2G_DISPATCHER self + -- @param Functional.Detection#DETECTION_AREAS.DetectedItem DetectedItem + -- @return Core.Set#SET_UNIT TargetSetUnit: The target set of units. + -- @return #nil If there are no targets to be set. + function TASK_A2G_DISPATCHER:EvaluateCAS( DetectedItem ) + self:F( { DetectedItem.ItemID } ) + + local DetectedSet = DetectedItem.Set + local DetectedZone = DetectedItem.Zone + + + -- Determine if the set has ground units. + -- There should be ground unit friendlies nearby. Airborne units are valid friendlies types. + -- And there shouldn't be any radar. + local GroundUnitCount = DetectedSet:HasGroundUnits() + local FriendliesNearBy = self.Detection:IsFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT ) -- Are there friendlies nearby of type GROUND_UNIT? + local RadarCount = DetectedSet:HasSEAD() + + if RadarCount == 0 and GroundUnitCount > 0 and FriendliesNearBy == true then + + -- Copy the Set + local TargetSetUnit = SET_UNIT:New() + TargetSetUnit:SetDatabase( DetectedSet ) + TargetSetUnit:FilterOnce() -- Filter but don't do any events!!! Elements are added manually upon each detection. + + return TargetSetUnit + end + + return nil + end + + --- Creates a BAI task when there are targets for it. + -- @param #TASK_A2G_DISPATCHER self + -- @param Functional.Detection#DETECTION_AREAS.DetectedItem DetectedItem + -- @return Core.Set#SET_UNIT TargetSetUnit: The target set of units. + -- @return #nil If there are no targets to be set. + function TASK_A2G_DISPATCHER:EvaluateBAI( DetectedItem, FriendlyCoalition ) + self:F( { DetectedItem.ItemID } ) + + local DetectedSet = DetectedItem.Set + local DetectedZone = DetectedItem.Zone + + + -- Determine if the set has ground units. + -- There shouldn't be any ground unit friendlies nearby. + -- And there shouldn't be any radar. + local GroundUnitCount = DetectedSet:HasGroundUnits() + local FriendliesNearBy = self.Detection:IsFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT ) -- Are there friendlies nearby of type GROUND_UNIT? + local RadarCount = DetectedSet:HasSEAD() + + if RadarCount == 0 and GroundUnitCount > 0 and FriendliesNearBy == false then + + -- Copy the Set + local TargetSetUnit = SET_UNIT:New() + TargetSetUnit:SetDatabase( DetectedSet ) + TargetSetUnit:FilterOnce() -- Filter but don't do any events!!! Elements are added manually upon each detection. + + return TargetSetUnit + end + + return nil + end + + + function TASK_A2G_DISPATCHER:RemoveTask( TaskIndex ) + self.Mission:RemoveTask( self.Tasks[TaskIndex] ) + self.Tasks[TaskIndex] = nil + end + + --- Evaluates the removal of the Task from the Mission. + -- Can only occur when the DetectedItem is Changed AND the state of the Task is "Planned". + -- @param #TASK_A2G_DISPATCHER self + -- @param Tasking.Mission#MISSION Mission + -- @param Tasking.Task#TASK Task + -- @param #boolean DetectedItemID + -- @param #boolean DetectedItemChange + -- @return Tasking.Task#TASK + function TASK_A2G_DISPATCHER:EvaluateRemoveTask( Mission, Task, TaskIndex, DetectedItemChanged ) + + if Task then + if ( Task:IsStatePlanned() and DetectedItemChanged == true ) or Task:IsStateCancelled() then + --self:F( "Removing Tasking: " .. Task:GetTaskName() ) + self:RemoveTask( TaskIndex ) + end + end + + return Task + end + + + --- Assigns tasks in relation to the detected items to the @{Core.Set#SET_GROUP}. + -- @param #TASK_A2G_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE Detection The detection created by the @{Functional.Detection#DETECTION_BASE} derived object. + -- @return #boolean Return true if you want the task assigning to continue... false will cancel the loop. + function TASK_A2G_DISPATCHER:ProcessDetected( Detection ) + self:F() + + local AreaMsg = {} + local TaskMsg = {} + local ChangeMsg = {} + + local Mission = self.Mission + + if Mission:IsIDLE() or Mission:IsENGAGED() then + + local TaskReport = REPORT:New() + + -- Checking the task queue for the dispatcher, and removing any obsolete task! + for TaskIndex, TaskData in pairs( self.Tasks ) do + local Task = TaskData -- Tasking.Task#TASK + if Task:IsStatePlanned() then + local DetectedItem = Detection:GetDetectedItemByIndex( TaskIndex ) + if not DetectedItem then + local TaskText = Task:GetName() + for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do + if self.FlashNewTask then + Mission:GetCommandCenter():MessageToGroup( string.format( "Obsolete A2G task %s for %s removed.", TaskText, Mission:GetShortText() ), TaskGroup ) + end + end + Task = self:RemoveTask( TaskIndex ) + end + end + end + + --- First we need to the detected targets. + for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do + + local DetectedItem = DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem + local DetectedSet = DetectedItem.Set -- Core.Set#SET_UNIT + local DetectedZone = DetectedItem.Zone + --self:F( { "Targets in DetectedItem", DetectedItem.ItemID, DetectedSet:Count(), tostring( DetectedItem ) } ) + --DetectedSet:Flush( self ) + + local DetectedItemID = DetectedItem.ID + local TaskIndex = DetectedItem.Index + local DetectedItemChanged = DetectedItem.Changed + + self:F( { DetectedItemChanged = DetectedItemChanged, DetectedItemID = DetectedItemID, TaskIndex = TaskIndex } ) + + local Task = self.Tasks[TaskIndex] -- Tasking.Task_A2G#TASK_A2G + + if Task then + -- If there is a Task and the task was assigned, then we check if the task was changed ... If it was, we need to reevaluate the targets. + if Task:IsStateAssigned() then + if DetectedItemChanged == true then -- The detection has changed, thus a new TargetSet is to be evaluated and set + local TargetsReport = REPORT:New() + local TargetSetUnit = self:EvaluateSEAD( DetectedItem ) -- Returns a SetUnit if there are targets to be SEADed... + if TargetSetUnit then + if Task:IsInstanceOf( TASK_A2G_SEAD ) then + Task:SetTargetSetUnit( TargetSetUnit ) + Task:SetDetection( Detection, DetectedItem ) + Task:UpdateTaskInfo( DetectedItem ) + TargetsReport:Add( Detection:GetChangeText( DetectedItem ) ) + else + Task:Cancel() + end + else + local TargetSetUnit = self:EvaluateCAS( DetectedItem ) -- Returns a SetUnit if there are targets to be CASed... + if TargetSetUnit then + if Task:IsInstanceOf( TASK_A2G_CAS ) then + Task:SetTargetSetUnit( TargetSetUnit ) + Task:SetDetection( Detection, DetectedItem ) + Task:UpdateTaskInfo( DetectedItem ) + TargetsReport:Add( Detection:GetChangeText( DetectedItem ) ) + else + Task:Cancel() + Task = self:RemoveTask( TaskIndex ) + end + else + local TargetSetUnit = self:EvaluateBAI( DetectedItem ) -- Returns a SetUnit if there are targets to be BAIed... + if TargetSetUnit then + if Task:IsInstanceOf( TASK_A2G_BAI ) then + Task:SetTargetSetUnit( TargetSetUnit ) + Task:SetDetection( Detection, DetectedItem ) + Task:UpdateTaskInfo( DetectedItem ) + TargetsReport:Add( Detection:GetChangeText( DetectedItem ) ) + else + Task:Cancel() + Task = self:RemoveTask( TaskIndex ) + end + end + end + end + + -- Now we send to each group the changes, if any. + for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do + local TargetsText = TargetsReport:Text(", ") + if ( Mission:IsGroupAssigned(TaskGroup) ) and TargetsText ~= "" and self.FlashNewTask then + Mission:GetCommandCenter():MessageToGroup( string.format( "Task %s has change of targets:\n %s", Task:GetName(), TargetsText ), TaskGroup ) + end + end + end + end + end + + if Task then + if Task:IsStatePlanned() then + if DetectedItemChanged == true then -- The detection has changed, thus a new TargetSet is to be evaluated and set + if Task:IsInstanceOf( TASK_A2G_SEAD ) then + local TargetSetUnit = self:EvaluateSEAD( DetectedItem ) -- Returns a SetUnit if there are targets to be SEADed... + if TargetSetUnit then + Task:SetTargetSetUnit( TargetSetUnit ) + Task:SetDetection( Detection, DetectedItem ) + Task:UpdateTaskInfo( DetectedItem ) + else + Task:Cancel() + Task = self:RemoveTask( TaskIndex ) + end + else + if Task:IsInstanceOf( TASK_A2G_CAS ) then + local TargetSetUnit = self:EvaluateCAS( DetectedItem ) -- Returns a SetUnit if there are targets to be CASed... + if TargetSetUnit then + Task:SetTargetSetUnit( TargetSetUnit ) + Task:SetDetection( Detection, DetectedItem ) + Task:UpdateTaskInfo( DetectedItem ) + else + Task:Cancel() + Task = self:RemoveTask( TaskIndex ) + end + else + if Task:IsInstanceOf( TASK_A2G_BAI ) then + local TargetSetUnit = self:EvaluateBAI( DetectedItem ) -- Returns a SetUnit if there are targets to be BAIed... + if TargetSetUnit then + Task:SetTargetSetUnit( TargetSetUnit ) + Task:SetDetection( Detection, DetectedItem ) + Task:UpdateTaskInfo( DetectedItem ) + else + Task:Cancel() + Task = self:RemoveTask( TaskIndex ) + end + else + Task:Cancel() + Task = self:RemoveTask( TaskIndex ) + end + end + end + end + end + end + + -- Evaluate SEAD + if not Task then + local TargetSetUnit = self:EvaluateSEAD( DetectedItem ) -- Returns a SetUnit if there are targets to be SEADed... + if TargetSetUnit then + Task = TASK_A2G_SEAD:New( Mission, self.SetGroup, string.format( "SEAD.%03d", DetectedItemID ), TargetSetUnit ) + DetectedItem.DesignateMenuName = string.format( "SEAD.%03d", DetectedItemID ) --inject a name for DESIGNATE, if using same DETECTION object + Task:SetDetection( Detection, DetectedItem ) + end + + -- Evaluate CAS + if not Task then + local TargetSetUnit = self:EvaluateCAS( DetectedItem ) -- Returns a SetUnit if there are targets to be CASed... + if TargetSetUnit then + Task = TASK_A2G_CAS:New( Mission, self.SetGroup, string.format( "CAS.%03d", DetectedItemID ), TargetSetUnit ) + DetectedItem.DesignateMenuName = string.format( "CAS.%03d", DetectedItemID ) --inject a name for DESIGNATE, if using same DETECTION object + Task:SetDetection( Detection, DetectedItem ) + end + + -- Evaluate BAI + if not Task then + local TargetSetUnit = self:EvaluateBAI( DetectedItem, self.Mission:GetCommandCenter():GetPositionable():GetCoalition() ) -- Returns a SetUnit if there are targets to be BAIed... + if TargetSetUnit then + Task = TASK_A2G_BAI:New( Mission, self.SetGroup, string.format( "BAI.%03d", DetectedItemID ), TargetSetUnit ) + DetectedItem.DesignateMenuName = string.format( "BAI.%03d", DetectedItemID ) --inject a name for DESIGNATE, if using same DETECTION object + Task:SetDetection( Detection, DetectedItem ) + end + end + end + + if Task then + self.Tasks[TaskIndex] = Task + Task:SetTargetZone( DetectedZone ) + Task:SetDispatcher( self ) + Task:UpdateTaskInfo( DetectedItem ) + Mission:AddTask( Task ) + + function Task.OnEnterSuccess( Task, From, Event, To ) + self:Success( Task ) + end + + function Task.OnEnterCancelled( Task, From, Event, To ) + self:Cancelled( Task ) + end + + function Task.OnEnterFailed( Task, From, Event, To ) + self:Failed( Task ) + end + + function Task.OnEnterAborted( Task, From, Event, To ) + self:Aborted( Task ) + end + + + TaskReport:Add( Task:GetName() ) + else + self:F("This should not happen") + end + end + + + -- OK, so the tasking has been done, now delete the changes reported for the area. + Detection:AcceptChanges( DetectedItem ) + end + + -- TODO set menus using the HQ coordinator + Mission:GetCommandCenter():SetMenu() + + local TaskText = TaskReport:Text(", ") + for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do + if ( not Mission:IsGroupAssigned(TaskGroup) ) and TaskText ~= "" and self.FlashNewTask then + Mission:GetCommandCenter():MessageToGroup( string.format( "%s has tasks %s. Subscribe to a task using the radio menu.", Mission:GetShortText(), TaskText ), TaskGroup ) + end + end + + end + + return true + end + +end +--- **Tasking** - The TASK_A2G models tasks for players in Air to Ground engagements. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- ### Contributions: +-- +-- === +-- +-- @module Tasking.Task_A2G +-- @image MOOSE.JPG + +do -- TASK_A2G + + --- The TASK_A2G class + -- @type TASK_A2G + -- @field Core.Set#SET_UNIT TargetSetUnit + -- @extends Tasking.Task#TASK + + --- The TASK_A2G class defines Air To Ground tasks for a @{Set} of Target Units, + -- based on the tasking capabilities defined in @{Tasking.Task#TASK}. + -- The TASK_A2G is implemented using a @{Core.Fsm#FSM_TASK}, and has the following statuses: + -- + -- * **None**: Start of the process + -- * **Planned**: The A2G task is planned. + -- * **Assigned**: The A2G task is assigned to a @{Wrapper.Group#GROUP}. + -- * **Success**: The A2G task is successfully completed. + -- * **Failed**: The A2G task has failed. This will happen if the player exists the task early, without communicating a possible cancellation to HQ. + -- + -- ## 1) Set the scoring of achievements in an A2G attack. + -- + -- Scoring or penalties can be given in the following circumstances: + -- + -- * @{#TASK_A2G.SetScoreOnDestroy}(): Set a score when a target in scope of the A2G attack, has been destroyed. + -- * @{#TASK_A2G.SetScoreOnSuccess}(): Set a score when all the targets in scope of the A2G attack, have been destroyed. + -- * @{#TASK_A2G.SetPenaltyOnFailed}(): Set a penalty when the A2G attack has failed. + -- + -- @field #TASK_A2G + TASK_A2G = { + ClassName = "TASK_A2G", + } + + --- Instantiates a new TASK_A2G. + -- @param #TASK_A2G self + -- @param Tasking.Mission#MISSION Mission + -- @param Core.Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. + -- @param #string TaskName The name of the Task. + -- @param Core.Set#SET_UNIT UnitSetTargets + -- @param #number TargetDistance The distance to Target when the Player is considered to have "arrived" at the engagement range. + -- @param Core.Zone#ZONE_BASE TargetZone The target zone, if known. + -- If the TargetZone parameter is specified, the player will be routed to the center of the zone where all the targets are assumed to be. + -- @return #TASK_A2G self + function TASK_A2G:New( Mission, SetGroup, TaskName, TargetSetUnit, TaskType, TaskBriefing ) + local self = BASE:Inherit( self, TASK:New( Mission, SetGroup, TaskName, TaskType, TaskBriefing ) ) -- Tasking.Task#TASK_A2G + self:F() + + self.TargetSetUnit = TargetSetUnit + self.TaskType = TaskType + + local Fsm = self:GetUnitProcess() + + Fsm:AddTransition( "Assigned", "RouteToRendezVous", "RoutingToRendezVous" ) + Fsm:AddProcess ( "RoutingToRendezVous", "RouteToRendezVousPoint", ACT_ROUTE_POINT:New(), { Arrived = "ArriveAtRendezVous" } ) + Fsm:AddProcess ( "RoutingToRendezVous", "RouteToRendezVousZone", ACT_ROUTE_ZONE:New(), { Arrived = "ArriveAtRendezVous" } ) + + Fsm:AddTransition( { "Arrived", "RoutingToRendezVous" }, "ArriveAtRendezVous", "ArrivedAtRendezVous" ) + + Fsm:AddTransition( { "ArrivedAtRendezVous", "HoldingAtRendezVous" }, "Engage", "Engaging" ) + Fsm:AddTransition( { "ArrivedAtRendezVous", "HoldingAtRendezVous" }, "HoldAtRendezVous", "HoldingAtRendezVous" ) + + Fsm:AddProcess ( "Engaging", "Account", ACT_ACCOUNT_DEADS:New(), {} ) + Fsm:AddTransition( "Engaging", "RouteToTarget", "Engaging" ) + Fsm:AddProcess( "Engaging", "RouteToTargetZone", ACT_ROUTE_ZONE:New(), {} ) + Fsm:AddProcess( "Engaging", "RouteToTargetPoint", ACT_ROUTE_POINT:New(), {} ) + Fsm:AddTransition( "Engaging", "RouteToTargets", "Engaging" ) + + --Fsm:AddTransition( "Accounted", "DestroyedAll", "Accounted" ) + --Fsm:AddTransition( "Accounted", "Success", "Success" ) + Fsm:AddTransition( "Rejected", "Reject", "Aborted" ) + Fsm:AddTransition( "Failed", "Fail", "Failed" ) + + + + --- Test + -- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task_A2G#TASK_A2G Task + function Fsm:onafterAssigned( TaskUnit, Task ) + self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + -- Determine the first Unit from the self.RendezVousSetUnit + + self:RouteToRendezVous() + end + + --- Test + -- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task_A2G#TASK_A2G Task + function Fsm:onafterRouteToRendezVous( TaskUnit, Task ) + self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + -- Determine the first Unit from the self.RendezVousSetUnit + + if Task:GetRendezVousZone( TaskUnit ) then + self:__RouteToRendezVousZone( 0.1 ) + else + if Task:GetRendezVousCoordinate( TaskUnit ) then + self:__RouteToRendezVousPoint( 0.1 ) + else + self:__ArriveAtRendezVous( 0.1 ) + end + end + end + + --- Test + -- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task#TASK_A2G Task + function Fsm:OnAfterArriveAtRendezVous( TaskUnit, Task ) + self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + -- Determine the first Unit from the self.TargetSetUnit + + self:__Engage( 0.1 ) + end + + --- Test + -- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task#TASK_A2G Task + function Fsm:onafterEngage( TaskUnit, Task ) + self:F( { self } ) + self:__Account( 0.1 ) + self:__RouteToTarget(0.1 ) + self:__RouteToTargets( -10 ) + end + + --- Test + -- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task_A2G#TASK_A2G Task + function Fsm:onafterRouteToTarget( TaskUnit, Task ) + self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + -- Determine the first Unit from the self.TargetSetUnit + + if Task:GetTargetZone( TaskUnit ) then + self:__RouteToTargetZone( 0.1 ) + else + local TargetUnit = Task.TargetSetUnit:GetFirst() -- Wrapper.Unit#UNIT + if TargetUnit then + local Coordinate = TargetUnit:GetPointVec3() + self:T( { TargetCoordinate = Coordinate, Coordinate:GetX(), Coordinate:GetY(), Coordinate:GetZ() } ) + Task:SetTargetCoordinate( Coordinate, TaskUnit ) + end + self:__RouteToTargetPoint( 0.1 ) + end + end + + --- Test + -- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task_A2G#TASK_A2G Task + function Fsm:onafterRouteToTargets( TaskUnit, Task ) + self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + local TargetUnit = Task.TargetSetUnit:GetFirst() -- Wrapper.Unit#UNIT + if TargetUnit then + Task:SetTargetCoordinate( TargetUnit:GetCoordinate(), TaskUnit ) + end + self:__RouteToTargets( -10 ) + end + + return self + + end + + --- @param #TASK_A2G self + -- @param Core.Set#SET_UNIT TargetSetUnit The set of targets. + function TASK_A2G:SetTargetSetUnit( TargetSetUnit ) + + self.TargetSetUnit = TargetSetUnit + end + + + + --- @param #TASK_A2G self + function TASK_A2G:GetPlannedMenuText() + return self:GetStateString() .. " - " .. self:GetTaskName() .. " ( " .. self.TargetSetUnit:GetUnitTypesText() .. " )" + end + + --- @param #TASK_A2G self + -- @param Core.Point#COORDINATE RendezVousCoordinate The Coordinate object referencing to the 2D point where the RendezVous point is located on the map. + -- @param #number RendezVousRange The RendezVousRange that defines when the player is considered to have arrived at the RendezVous point. + -- @param Wrapper.Unit#UNIT TaskUnit + function TASK_A2G:SetRendezVousCoordinate( RendezVousCoordinate, RendezVousRange, TaskUnit ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + local ActRouteRendezVous = ProcessUnit:GetProcess( "RoutingToRendezVous", "RouteToRendezVousPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT + ActRouteRendezVous:SetCoordinate( RendezVousCoordinate ) + ActRouteRendezVous:SetRange( RendezVousRange ) + end + + --- @param #TASK_A2G self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return Core.Point#COORDINATE The Coordinate object referencing to the 2D point where the RendezVous point is located on the map. + -- @return #number The RendezVousRange that defines when the player is considered to have arrived at the RendezVous point. + function TASK_A2G:GetRendezVousCoordinate( TaskUnit ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + local ActRouteRendezVous = ProcessUnit:GetProcess( "RoutingToRendezVous", "RouteToRendezVousPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT + return ActRouteRendezVous:GetCoordinate(), ActRouteRendezVous:GetRange() + end + + + + --- @param #TASK_A2G self + -- @param Core.Zone#ZONE_BASE RendezVousZone The Zone object where the RendezVous is located on the map. + -- @param Wrapper.Unit#UNIT TaskUnit + function TASK_A2G:SetRendezVousZone( RendezVousZone, TaskUnit ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + local ActRouteRendezVous = ProcessUnit:GetProcess( "RoutingToRendezVous", "RouteToRendezVousZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE + ActRouteRendezVous:SetZone( RendezVousZone ) + end + + --- @param #TASK_A2G self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return Core.Zone#ZONE_BASE The Zone object where the RendezVous is located on the map. + function TASK_A2G:GetRendezVousZone( TaskUnit ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + local ActRouteRendezVous = ProcessUnit:GetProcess( "RoutingToRendezVous", "RouteToRendezVousZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE + return ActRouteRendezVous:GetZone() + end + + --- @param #TASK_A2G self + -- @param Core.Point#COORDINATE TargetCoordinate The Coordinate object where the Target is located on the map. + -- @param Wrapper.Unit#UNIT TaskUnit + function TASK_A2G:SetTargetCoordinate( TargetCoordinate, TaskUnit ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT + ActRouteTarget:SetCoordinate( TargetCoordinate ) + end + + + --- @param #TASK_A2G self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return Core.Point#COORDINATE The Coordinate object where the Target is located on the map. + function TASK_A2G:GetTargetCoordinate( TaskUnit ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT + return ActRouteTarget:GetCoordinate() + end + + + --- @param #TASK_A2G self + -- @param Core.Zone#ZONE_BASE TargetZone The Zone object where the Target is located on the map. + -- @param Wrapper.Unit#UNIT TaskUnit + function TASK_A2G:SetTargetZone( TargetZone, TaskUnit ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE + ActRouteTarget:SetZone( TargetZone ) + end + + + --- @param #TASK_A2G self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return Core.Zone#ZONE_BASE The Zone object where the Target is located on the map. + function TASK_A2G:GetTargetZone( TaskUnit ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE + return ActRouteTarget:GetZone() + end + + function TASK_A2G:SetGoalTotal() + + self.GoalTotal = self.TargetSetUnit:Count() + end + + function TASK_A2G:GetGoalTotal() + + return self.GoalTotal + end + + --- Return the relative distance to the target vicinity from the player, in order to sort the targets in the reports per distance from the threats. + -- @param #TASK_A2G self + function TASK_A2G:ReportOrder( ReportGroup ) + self:UpdateTaskInfo( self.DetectedItem ) + + local Coordinate = self.TaskInfo:GetData( "Coordinate" ) + local Distance = ReportGroup:GetCoordinate():Get2DDistance( Coordinate ) + + return Distance + end + + + --- This method checks every 10 seconds if the goal has been reached of the task. + -- @param #TASK_A2G self + function TASK_A2G:onafterGoal( TaskUnit, From, Event, To ) + local TargetSetUnit = self.TargetSetUnit -- Core.Set#SET_UNIT + + if TargetSetUnit:Count() == 0 then + self:Success() + end + + self:__Goal( -10 ) + end + + --- @param #TASK_A2G self + function TASK_A2G:UpdateTaskInfo( DetectedItem ) + + if self:IsStatePlanned() or self:IsStateAssigned() then + local TargetCoordinate = DetectedItem and self.Detection:GetDetectedItemCoordinate( DetectedItem ) or self.TargetSetUnit:GetFirst():GetCoordinate() + self.TaskInfo:AddTaskName( 0, "MSOD" ) + self.TaskInfo:AddCoordinate( TargetCoordinate, 1, "SOD" ) + + local ThreatLevel, ThreatText + if DetectedItem then + ThreatLevel, ThreatText = self.Detection:GetDetectedItemThreatLevel( DetectedItem ) + else + ThreatLevel, ThreatText = self.TargetSetUnit:CalculateThreatLevelA2G() + end + self.TaskInfo:AddThreat( ThreatText, ThreatLevel, 10, "MOD", true ) + + if self.Detection then + local DetectedItemsCount = self.TargetSetUnit:Count() + local ReportTypes = REPORT:New() + local TargetTypes = {} + for TargetUnitName, TargetUnit in pairs( self.TargetSetUnit:GetSet() ) do + local TargetType = self.Detection:GetDetectedUnitTypeName( TargetUnit ) + if not TargetTypes[TargetType] then + TargetTypes[TargetType] = TargetType + ReportTypes:Add( TargetType ) + end + end + self.TaskInfo:AddTargetCount( DetectedItemsCount, 11, "O", true ) + self.TaskInfo:AddTargets( DetectedItemsCount, ReportTypes:Text( ", " ), 20, "D", true ) + else + local DetectedItemsCount = self.TargetSetUnit:Count() + local DetectedItemsTypes = self.TargetSetUnit:GetTypeNames() + self.TaskInfo:AddTargetCount( DetectedItemsCount, 11, "O", true ) + self.TaskInfo:AddTargets( DetectedItemsCount, DetectedItemsTypes, 20, "D", true ) + end + self.TaskInfo:AddQFEAtCoordinate( TargetCoordinate, 30, "MOD" ) + self.TaskInfo:AddTemperatureAtCoordinate( TargetCoordinate, 31, "MD" ) + self.TaskInfo:AddWindAtCoordinate( TargetCoordinate, 32, "MD" ) + end + + end + + --- This function is called from the @{Tasking.CommandCenter#COMMANDCENTER} to determine the method of automatic task selection. + -- @param #TASK_A2G self + -- @param #number AutoAssignMethod The method to be applied to the task. + -- @param Tasking.CommandCenter#COMMANDCENTER CommandCenter The command center. + -- @param Wrapper.Group#GROUP TaskGroup The player group. + function TASK_A2G:GetAutoAssignPriority( AutoAssignMethod, CommandCenter, TaskGroup ) + + if AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Random then + return math.random( 1, 9 ) + elseif AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Distance then + local Coordinate = self.TaskInfo:GetData( "Coordinate" ) + local Distance = Coordinate:Get2DDistance( CommandCenter:GetPositionable():GetCoordinate() ) + self:F({Distance=Distance}) + return math.floor( Distance ) + elseif AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Priority then + return 1 + end + + return 0 + end + +end + + +do -- TASK_A2G_SEAD + + --- The TASK_A2G_SEAD class + -- @type TASK_A2G_SEAD + -- @field Core.Set#SET_UNIT TargetSetUnit + -- @extends Tasking.Task#TASK + + --- Defines an Suppression or Extermination of Air Defenses task for a human player to be executed. + -- These tasks are important to be executed as they will help to achieve air superiority at the vicinity. + -- + -- The TASK_A2G_SEAD is used by the @{Tasking.Task_A2G_Dispatcher#TASK_A2G_DISPATCHER} to automatically create SEAD tasks + -- based on detected enemy ground targets. + -- + -- @field #TASK_A2G_SEAD + TASK_A2G_SEAD = { + ClassName = "TASK_A2G_SEAD", + } + + --- Instantiates a new TASK_A2G_SEAD. + -- @param #TASK_A2G_SEAD self + -- @param Tasking.Mission#MISSION Mission + -- @param Core.Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. + -- @param #string TaskName The name of the Task. + -- @param Core.Set#SET_UNIT TargetSetUnit + -- @param #string TaskBriefing The briefing of the task. + -- @return #TASK_A2G_SEAD self + function TASK_A2G_SEAD:New( Mission, SetGroup, TaskName, TargetSetUnit, TaskBriefing) + local self = BASE:Inherit( self, TASK_A2G:New( Mission, SetGroup, TaskName, TargetSetUnit, "SEAD", TaskBriefing ) ) -- #TASK_A2G_SEAD + self:F() + + Mission:AddTask( self ) + + self:SetBriefing( + TaskBriefing or + "Execute a Suppression of Enemy Air Defenses." + ) + + return self + end + + --- Set a score when a target in scope of the A2G attack, has been destroyed . + -- @param #TASK_A2G_SEAD self + -- @param #string PlayerName The name of the player. + -- @param #number Score The score in points to be granted when task process has been achieved. + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return #TASK_A2G_SEAD + function TASK_A2G_SEAD:SetScoreOnProgress( PlayerName, Score, TaskUnit ) + self:F( { PlayerName, Score, TaskUnit } ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + ProcessUnit:AddScoreProcess( "Engaging", "Account", "AccountForPlayer", "Player " .. PlayerName .. " has SEADed a target.", Score ) + + return self + end + + --- Set a score when all the targets in scope of the A2G attack, have been destroyed. + -- @param #TASK_A2G_SEAD self + -- @param #string PlayerName The name of the player. + -- @param #number Score The score in points. + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return #TASK_A2G_SEAD + function TASK_A2G_SEAD:SetScoreOnSuccess( PlayerName, Score, TaskUnit ) + self:F( { PlayerName, Score, TaskUnit } ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + ProcessUnit:AddScore( "Success", "All radar emitting targets have been successfully SEADed!", Score ) + + return self + end + + --- Set a penalty when the A2G attack has failed. + -- @param #TASK_A2G_SEAD self + -- @param #string PlayerName The name of the player. + -- @param #number Penalty The penalty in points, must be a negative value! + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return #TASK_A2G_SEAD + function TASK_A2G_SEAD:SetScoreOnFail( PlayerName, Penalty, TaskUnit ) + self:F( { PlayerName, Penalty, TaskUnit } ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + ProcessUnit:AddScore( "Failed", "The SEADing has failed!", Penalty ) + + return self + end + + +end + +do -- TASK_A2G_BAI + + --- The TASK_A2G_BAI class + -- @type TASK_A2G_BAI + -- @field Core.Set#SET_UNIT TargetSetUnit + -- @extends Tasking.Task#TASK + + --- Defines a Battlefield Air Interdiction task for a human player to be executed. + -- These tasks are more strategic in nature and are most of the time further away from friendly forces. + -- BAI tasks can also be used to express the abscence of friendly forces near the vicinity. + -- + -- The TASK_A2G_BAI is used by the @{Tasking.Task_A2G_Dispatcher#TASK_A2G_DISPATCHER} to automatically create BAI tasks + -- based on detected enemy ground targets. + -- + -- @field #TASK_A2G_BAI + TASK_A2G_BAI = { + ClassName = "TASK_A2G_BAI", + } + + --- Instantiates a new TASK_A2G_BAI. + -- @param #TASK_A2G_BAI self + -- @param Tasking.Mission#MISSION Mission + -- @param Core.Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. + -- @param #string TaskName The name of the Task. + -- @param Core.Set#SET_UNIT TargetSetUnit + -- @param #string TaskBriefing The briefing of the task. + -- @return #TASK_A2G_BAI self + function TASK_A2G_BAI:New( Mission, SetGroup, TaskName, TargetSetUnit, TaskBriefing ) + local self = BASE:Inherit( self, TASK_A2G:New( Mission, SetGroup, TaskName, TargetSetUnit, "BAI", TaskBriefing ) ) -- #TASK_A2G_BAI + self:F() + + Mission:AddTask( self ) + + self:SetBriefing( + TaskBriefing or + "Execute a Battlefield Air Interdiction of a group of enemy targets." + ) + + return self + end + + --- Set a score when a target in scope of the A2G attack, has been destroyed . + -- @param #TASK_A2G_BAI self + -- @param #string PlayerName The name of the player. + -- @param #number Score The score in points to be granted when task process has been achieved. + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return #TASK_A2G_BAI + function TASK_A2G_BAI:SetScoreOnProgress( PlayerName, Score, TaskUnit ) + self:F( { PlayerName, Score, TaskUnit } ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + ProcessUnit:AddScoreProcess( "Engaging", "Account", "AccountForPlayer", "Player " .. PlayerName .. " has destroyed a target in Battlefield Air Interdiction (BAI).", Score ) + + return self + end + + --- Set a score when all the targets in scope of the A2G attack, have been destroyed. + -- @param #TASK_A2G_BAI self + -- @param #string PlayerName The name of the player. + -- @param #number Score The score in points. + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return #TASK_A2G_BAI + function TASK_A2G_BAI:SetScoreOnSuccess( PlayerName, Score, TaskUnit ) + self:F( { PlayerName, Score, TaskUnit } ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + ProcessUnit:AddScore( "Success", "All targets have been successfully destroyed! The Battlefield Air Interdiction (BAI) is a success!", Score ) + + return self + end + + --- Set a penalty when the A2G attack has failed. + -- @param #TASK_A2G_BAI self + -- @param #string PlayerName The name of the player. + -- @param #number Penalty The penalty in points, must be a negative value! + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return #TASK_A2G_BAI + function TASK_A2G_BAI:SetScoreOnFail( PlayerName, Penalty, TaskUnit ) + self:F( { PlayerName, Penalty, TaskUnit } ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + ProcessUnit:AddScore( "Failed", "The Battlefield Air Interdiction (BAI) has failed!", Penalty ) + + return self + end + +end + + + + +do -- TASK_A2G_CAS + + --- The TASK_A2G_CAS class + -- @type TASK_A2G_CAS + -- @field Core.Set#SET_UNIT TargetSetUnit + -- @extends Tasking.Task#TASK + + --- Defines an Close Air Support task for a human player to be executed. + -- Friendly forces will be in the vicinity within 6km from the enemy. + -- + -- The TASK_A2G_CAS is used by the @{Tasking.Task_A2G_Dispatcher#TASK_A2G_DISPATCHER} to automatically create CAS tasks + -- based on detected enemy ground targets. + -- + -- @field #TASK_A2G_CAS + TASK_A2G_CAS = { + ClassName = "TASK_A2G_CAS", + } + + --- Instantiates a new TASK_A2G_CAS. + -- @param #TASK_A2G_CAS self + -- @param Tasking.Mission#MISSION Mission + -- @param Core.Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. + -- @param #string TaskName The name of the Task. + -- @param Core.Set#SET_UNIT TargetSetUnit + -- @param #string TaskBriefing The briefing of the task. + -- @return #TASK_A2G_CAS self + function TASK_A2G_CAS:New( Mission, SetGroup, TaskName, TargetSetUnit, TaskBriefing ) + local self = BASE:Inherit( self, TASK_A2G:New( Mission, SetGroup, TaskName, TargetSetUnit, "CAS", TaskBriefing ) ) -- #TASK_A2G_CAS + self:F() + + Mission:AddTask( self ) + + self:SetBriefing( + TaskBriefing or + "Execute a Close Air Support for a group of enemy targets. " .. + "Beware of friendlies at the vicinity! " + ) + + + return self + end + + + --- Set a score when a target in scope of the A2G attack, has been destroyed . + -- @param #TASK_A2G_CAS self + -- @param #string PlayerName The name of the player. + -- @param #number Score The score in points to be granted when task process has been achieved. + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return #TASK_A2G_CAS + function TASK_A2G_CAS:SetScoreOnProgress( PlayerName, Score, TaskUnit ) + self:F( { PlayerName, Score, TaskUnit } ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + ProcessUnit:AddScoreProcess( "Engaging", "Account", "AccountForPlayer", "Player " .. PlayerName .. " has destroyed a target in Close Air Support (CAS).", Score ) + + return self + end + + --- Set a score when all the targets in scope of the A2G attack, have been destroyed. + -- @param #TASK_A2G_CAS self + -- @param #string PlayerName The name of the player. + -- @param #number Score The score in points. + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return #TASK_A2G_CAS + function TASK_A2G_CAS:SetScoreOnSuccess( PlayerName, Score, TaskUnit ) + self:F( { PlayerName, Score, TaskUnit } ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + ProcessUnit:AddScore( "Success", "All targets have been successfully destroyed! The Close Air Support (CAS) was a success!", Score ) + + return self + end + + --- Set a penalty when the A2G attack has failed. + -- @param #TASK_A2G_CAS self + -- @param #string PlayerName The name of the player. + -- @param #number Penalty The penalty in points, must be a negative value! + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return #TASK_A2G_CAS + function TASK_A2G_CAS:SetScoreOnFail( PlayerName, Penalty, TaskUnit ) + self:F( { PlayerName, Penalty, TaskUnit } ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + ProcessUnit:AddScore( "Failed", "The Close Air Support (CAS) has failed!", Penalty ) + + return self + end + + +end +--- **Tasking** - Dynamically allocates A2A tasks to human players, based on detected airborne targets through an EWR network. +-- +-- **Features:** +-- +-- * Dynamically assign tasks to human players based on detected targets. +-- * Dynamically change the tasks as the tactical situation evolves during the mission. +-- * Dynamically assign (CAP) Control Air Patrols tasks for human players to perform CAP. +-- * Dynamically assign (GCI) Ground Control Intercept tasks for human players to perform GCI. +-- * Dynamically assign Engage tasks for human players to engage on close-by airborne bogeys. +-- * Define and use an EWR (Early Warning Radar) network. +-- * Define different ranges to engage upon intruders. +-- * Keep task achievements. +-- * Score task achievements. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- ### Contributions: +-- +-- === +-- +-- @module Tasking.Task_A2A_Dispatcher +-- @image Task_A2A_Dispatcher.JPG + +do -- TASK_A2A_DISPATCHER + + --- TASK_A2A_DISPATCHER class. + -- @type TASK_A2A_DISPATCHER + -- @extends Tasking.DetectionManager#DETECTION_MANAGER + + --- Orchestrates the dynamic dispatching of tasks upon groups of detected units determined a @{Set} of EWR installation groups. + -- + -- ![Banner Image](..\Presentations\TASK_A2A_DISPATCHER\Dia3.JPG) + -- + -- The EWR will detect units, will group them, and will dispatch @{Task}s to groups. Depending on the type of target detected, different tasks will be dispatched. + -- Find a summary below describing for which situation a task type is created: + -- + -- ![Banner Image](..\Presentations\TASK_A2A_DISPATCHER\Dia9.JPG) + -- + -- * **INTERCEPT Task**: Is created when the target is known, is detected and within a danger zone, and there is no friendly airborne in range. + -- * **SWEEP Task**: Is created when the target is unknown, was detected and the last position is only known, and within a danger zone, and there is no friendly airborne in range. + -- * **ENGAGE Task**: Is created when the target is known, is detected and within a danger zone, and there is a friendly airborne in range, that will receive this task. + -- + -- ## 1. TASK\_A2A\_DISPATCHER constructor: + -- + -- The @{#TASK_A2A_DISPATCHER.New}() method creates a new TASK\_A2A\_DISPATCHER instance. + -- + -- ### 1.1. Define or set the **Mission**: + -- + -- Tasking is executed to accomplish missions. Therefore, a MISSION object needs to be given as the first parameter. + -- + -- local HQ = GROUP:FindByName( "HQ", "Bravo" ) + -- local CommandCenter = COMMANDCENTER:New( HQ, "Lima" ) + -- local Mission = MISSION:New( CommandCenter, "A2A Mission", "High", "Watch the air enemy units being detected.", coalition.side.RED ) + -- + -- Missions are governed by COMMANDCENTERS, so, ensure you have a COMMANDCENTER object installed and setup within your mission. + -- Create the MISSION object, and hook it under the command center. + -- + -- ### 1.2. Build a set of the groups seated by human players: + -- + -- ![Banner Image](..\Presentations\TASK_A2A_DISPATCHER\Dia6.JPG) + -- + -- A set or collection of the groups wherein human players can be seated, these can be clients or units that can be joined as a slot or jumping into. + -- + -- local AttackGroups = SET_GROUP:New():FilterCoalitions( "red" ):FilterPrefixes( "Defender" ):FilterStart() + -- + -- The set is built using the SET_GROUP class. Apply any filter criteria to identify the correct groups for your mission. + -- Only these slots or units will be able to execute the mission and will receive tasks for this mission, once available. + -- + -- ### 1.3. Define the **EWR network**: + -- + -- As part of the TASK\_A2A\_DISPATCHER constructor, an EWR network must be given as the third parameter. + -- An EWR network, or, Early Warning Radar network, is used to early detect potential airborne targets and to understand the position of patrolling targets of the enemy. + -- + -- ![Banner Image](..\Presentations\TASK_A2A_DISPATCHER\Dia5.JPG) + -- + -- Typically EWR networks are setup using 55G6 EWR, 1L13 EWR, Hawk sr and Patriot str ground based radar units. + -- These radars have different ranges and 55G6 EWR and 1L13 EWR radars are Eastern Bloc units (eg Russia, Ukraine, Georgia) while the Hawk and Patriot radars are Western (eg US). + -- Additionally, ANY other radar capable unit can be part of the EWR network! Also AWACS airborne units, planes, helicopters can help to detect targets, as long as they have radar. + -- The position of these units is very important as they need to provide enough coverage + -- to pick up enemy aircraft as they approach so that CAP and GCI flights can be tasked to intercept them. + -- + -- ![Banner Image](..\Presentations\TASK_A2A_DISPATCHER\Dia7.JPG) + -- + -- Additionally in a hot war situation where the border is no longer respected the placement of radars has a big effect on how fast the war escalates. + -- For example if they are a long way forward and can detect enemy planes on the ground and taking off + -- they will start to vector CAP and GCI flights to attack them straight away which will immediately draw a response from the other coalition. + -- Having the radars further back will mean a slower escalation because fewer targets will be detected and + -- therefore less CAP and GCI flights will spawn and this will tend to make just the border area active rather than a melee over the whole map. + -- It all depends on what the desired effect is. + -- + -- EWR networks are **dynamically constructed**, that is, they form part of the @{Functional.Detection#DETECTION_BASE} object that is given as the input parameter of the TASK\_A2A\_DISPATCHER class. + -- By defining in a **smart way the names or name prefixes of the groups** with EWR capable units, these groups will be **automatically added or deleted** from the EWR network, + -- increasing or decreasing the radar coverage of the Early Warning System. + -- + -- See the following example to setup an EWR network containing EWR stations and AWACS. + -- + -- local EWRSet = SET_GROUP:New():FilterPrefixes( "EWR" ):FilterCoalitions("red"):FilterStart() + -- + -- local EWRDetection = DETECTION_AREAS:New( EWRSet, 6000 ) + -- EWRDetection:SetFriendliesRange( 10000 ) + -- EWRDetection:SetRefreshTimeInterval(30) + -- + -- -- Setup the A2A dispatcher, and initialize it. + -- A2ADispatcher = TASK_A2A_DISPATCHER:New( Mission, AttackGroups, EWRDetection ) + -- + -- The above example creates a SET_GROUP instance, and stores this in the variable (object) **EWRSet**. + -- **EWRSet** is then being configured to filter all active groups with a group name starting with **EWR** to be included in the Set. + -- **EWRSet** is then being ordered to start the dynamic filtering. Note that any destroy or new spawn of a group with the above names will be removed or added to the Set. + -- Then a new **EWRDetection** object is created from the class DETECTION_AREAS. A grouping radius of 6000 is choosen, which is 6km. + -- The **EWRDetection** object is then passed to the @{#TASK_A2A_DISPATCHER.New}() method to indicate the EWR network configuration and setup the A2A tasking and detection mechanism. + -- + -- ### 2. Define the detected **target grouping radius**: + -- + -- ![Banner Image](..\Presentations\TASK_A2A_DISPATCHER\Dia8.JPG) + -- + -- The target grouping radius is a property of the Detection object, that was passed to the AI\_A2A\_DISPATCHER object, but can be changed. + -- The grouping radius should not be too small, but also depends on the types of planes and the era of the simulation. + -- Fast planes like in the 80s, need a larger radius than WWII planes. + -- Typically I suggest to use 30000 for new generation planes and 10000 for older era aircraft. + -- + -- Note that detected targets are constantly re-grouped, that is, when certain detected aircraft are moving further than the group radius, then these aircraft will become a separate + -- group being detected. This may result in additional GCI being started by the dispatcher! So don't make this value too small! + -- + -- ## 3. Set the **Engage radius**: + -- + -- Define the radius to engage any target by airborne friendlies, which are executing cap or returning from an intercept mission. + -- + -- ![Banner Image](..\Presentations\TASK_A2A_DISPATCHER\Dia11.JPG) + -- + -- So, if there is a target area detected and reported, + -- then any friendlies that are airborne near this target area, + -- will be commanded to (re-)engage that target when available (if no other tasks were commanded). + -- For example, if 100000 is given as a value, then any friendly that is airborne within 100km from the detected target, + -- will be considered to receive the command to engage that target area. + -- You need to evaluate the value of this parameter carefully. + -- If too small, more intercept missions may be triggered upon detected target areas. + -- If too large, any airborne cap may not be able to reach the detected target area in time, because it is too far. + -- + -- ## 4. Set **Scoring** and **Messages**: + -- + -- The TASK\_A2A\_DISPATCHER is a state machine. It triggers the event Assign when a new player joins a @{Task} dispatched by the TASK\_A2A\_DISPATCHER. + -- An _event handler_ can be defined to catch the **Assign** event, and add **additional processing** to set _scoring_ and to _define messages_, + -- when the player reaches certain achievements in the task. + -- + -- The prototype to handle the **Assign** event needs to be developed as follows: + -- + -- TaskDispatcher = TASK_A2A_DISPATCHER:New( ... ) + -- + -- --- @param #TaskDispatcher self + -- -- @param #string From Contains the name of the state from where the Event was triggered. + -- -- @param #string Event Contains the name of the event that was triggered. In this case Assign. + -- -- @param #string To Contains the name of the state that will be transitioned to. + -- -- @param Tasking.Task_A2A#TASK_A2A Task The Task object, which is any derived object from TASK_A2A. + -- -- @param Wrapper.Unit#UNIT TaskUnit The Unit or Client that contains the Player. + -- -- @param #string PlayerName The name of the Player that joined the TaskUnit. + -- function TaskDispatcher:OnAfterAssign( From, Event, To, Task, TaskUnit, PlayerName ) + -- Task:SetScoreOnProgress( PlayerName, 20, TaskUnit ) + -- Task:SetScoreOnSuccess( PlayerName, 200, TaskUnit ) + -- Task:SetScoreOnFail( PlayerName, -100, TaskUnit ) + -- end + -- + -- The **OnAfterAssign** method (function) is added to the TaskDispatcher object. + -- This method will be called when a new player joins a unit in the set of groups in scope of the dispatcher. + -- So, this method will be called only **ONCE** when a player joins a unit in scope of the task. + -- + -- The TASK class implements various methods to additional **set scoring** for player achievements: + -- + -- * @{Tasking.Task#TASK.SetScoreOnProgress}() will add additional scores when a player achieves **Progress** while executing the task. + -- Examples of **task progress** can be destroying units, arriving at zones etc. + -- + -- * @{Tasking.Task#TASK.SetScoreOnSuccess}() will add additional scores when the task goes into **Success** state. + -- This means the **task has been successfully completed**. + -- + -- * @{Tasking.Task#TASK.SetScoreOnSuccess}() will add additional (negative) scores when the task goes into **Failed** state. + -- This means the **task has not been successfully completed**, and the scores must be given with a negative value! + -- + -- @field #TASK_A2A_DISPATCHER + TASK_A2A_DISPATCHER = { + ClassName = "TASK_A2A_DISPATCHER", + Mission = nil, + Detection = nil, + Tasks = {}, + SweepZones = {}, + } + + + --- TASK_A2A_DISPATCHER constructor. + -- @param #TASK_A2A_DISPATCHER self + -- @param Tasking.Mission#MISSION Mission The mission for which the task dispatching is done. + -- @param Core.Set#SET_GROUP SetGroup The set of groups that can join the tasks within the mission. + -- @param Functional.Detection#DETECTION_BASE Detection The detection results that are used to dynamically assign new tasks to human players. + -- @return #TASK_A2A_DISPATCHER self + function TASK_A2A_DISPATCHER:New( Mission, SetGroup, Detection ) + + -- Inherits from DETECTION_MANAGER + local self = BASE:Inherit( self, DETECTION_MANAGER:New( SetGroup, Detection ) ) -- #TASK_A2A_DISPATCHER + + self.Detection = Detection + self.Mission = Mission + self.FlashNewTask = false + + -- TODO: Check detection through radar. + self.Detection:FilterCategories( Unit.Category.AIRPLANE, Unit.Category.HELICOPTER ) + self.Detection:InitDetectRadar( true ) + self.Detection:SetRefreshTimeInterval( 30 ) + + self:AddTransition( "Started", "Assign", "Started" ) + + + --- OnAfter Transition Handler for Event Assign. + -- @function [parent=#TASK_A2A_DISPATCHER] OnAfterAssign + -- @param #TASK_A2A_DISPATCHER self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @param Tasking.Task_A2A#TASK_A2A Task + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param #string PlayerName + + self:__Start( 5 ) + + return self + end + + + --- Define the radius to when an ENGAGE task will be generated for any nearby by airborne friendlies, which are executing cap or returning from an intercept mission. + -- So, if there is a target area detected and reported, + -- then any friendlies that are airborne near this target area, + -- will be commanded to (re-)engage that target when available (if no other tasks were commanded). + -- An ENGAGE task will be created for those pilots. + -- For example, if 100000 is given as a value, then any friendly that is airborne within 100km from the detected target, + -- will be considered to receive the command to engage that target area. + -- You need to evaluate the value of this parameter carefully. + -- If too small, more intercept missions may be triggered upon detected target areas. + -- If too large, any airborne cap may not be able to reach the detected target area in time, because it is too far. + -- @param #TASK_A2A_DISPATCHER self + -- @param #number EngageRadius (Optional, Default = 100000) The radius to report friendlies near the target. + -- @return #TASK_A2A_DISPATCHER + -- @usage + -- + -- -- Set 50km as the radius to engage any target by airborne friendlies. + -- TaskA2ADispatcher:SetEngageRadius( 50000 ) + -- + -- -- Set 100km as the radius to engage any target by airborne friendlies. + -- TaskA2ADispatcher:SetEngageRadius() -- 100000 is the default value. + -- + function TASK_A2A_DISPATCHER:SetEngageRadius( EngageRadius ) + + self.Detection:SetFriendliesRange( EngageRadius or 100000 ) + + return self + end + + --- Set flashing player messages on or off + -- @param #TASK_A2A_DISPATCHER self + -- @param #boolean onoff Set messages on (true) or off (false) + function TASK_A2A_DISPATCHER:SetSendMessages( onoff ) + self.FlashNewTask = onoff + end + + --- Creates an INTERCEPT task when there are targets for it. + -- @param #TASK_A2A_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem + -- @return Core.Set#SET_UNIT TargetSetUnit: The target set of units. + -- @return #nil If there are no targets to be set. + function TASK_A2A_DISPATCHER:EvaluateINTERCEPT( DetectedItem ) + self:F( { DetectedItem.ItemID } ) + + local DetectedSet = DetectedItem.Set + local DetectedZone = DetectedItem.Zone + + -- Check if there is at least one UNIT in the DetectedSet is visible. + + if DetectedItem.IsDetected == true then + + -- Here we're doing something advanced... We're copying the DetectedSet. + local TargetSetUnit = SET_UNIT:New() + TargetSetUnit:SetDatabase( DetectedSet ) + TargetSetUnit:FilterOnce() -- Filter but don't do any events!!! Elements are added manually upon each detection. + + return TargetSetUnit + end + + return nil + end + + + --- Creates an SWEEP task when there are targets for it. + -- @param #TASK_A2A_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem + -- @return Core.Set#SET_UNIT TargetSetUnit: The target set of units. + -- @return #nil If there are no targets to be set. + function TASK_A2A_DISPATCHER:EvaluateSWEEP( DetectedItem ) + self:F( { DetectedItem.ItemID } ) + + local DetectedSet = DetectedItem.Set + local DetectedZone = DetectedItem.Zone + + + if DetectedItem.IsDetected == false then + + -- Here we're doing something advanced... We're copying the DetectedSet. + local TargetSetUnit = SET_UNIT:New() + TargetSetUnit:SetDatabase( DetectedSet ) + TargetSetUnit:FilterOnce() -- Filter but don't do any events!!! Elements are added manually upon each detection. + + return TargetSetUnit + end + + return nil + end + + + --- Creates an ENGAGE task when there are human friendlies airborne near the targets. + -- @param #TASK_A2A_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem + -- @return Core.Set#SET_UNIT TargetSetUnit: The target set of units. + -- @return #nil If there are no targets to be set. + function TASK_A2A_DISPATCHER:EvaluateENGAGE( DetectedItem ) + self:F( { DetectedItem.ItemID } ) + + local DetectedSet = DetectedItem.Set + local DetectedZone = DetectedItem.Zone + + local PlayersCount, PlayersReport = self:GetPlayerFriendliesNearBy( DetectedItem ) + + + -- Only allow ENGAGE when there are Players near the zone, and when the Area has detected items since the last run in a 60 seconds time zone. + if PlayersCount > 0 and DetectedItem.IsDetected == true then + + -- Here we're doing something advanced... We're copying the DetectedSet. + local TargetSetUnit = SET_UNIT:New() + TargetSetUnit:SetDatabase( DetectedSet ) + TargetSetUnit:FilterOnce() -- Filter but don't do any events!!! Elements are added manually upon each detection. + + return TargetSetUnit + end + + return nil + end + + + + + --- Evaluates the removal of the Task from the Mission. + -- Can only occur when the DetectedItem is Changed AND the state of the Task is "Planned". + -- @param #TASK_A2A_DISPATCHER self + -- @param Tasking.Mission#MISSION Mission + -- @param Tasking.Task#TASK Task + -- @param Functional.Detection#DETECTION_BASE Detection The detection created by the @{Functional.Detection#DETECTION_BASE} derived object. + -- @param #boolean DetectedItemID + -- @param #boolean DetectedItemChange + -- @return Tasking.Task#TASK + function TASK_A2A_DISPATCHER:EvaluateRemoveTask( Mission, Task, Detection, DetectedItem, DetectedItemIndex, DetectedItemChanged ) + + if Task then + + if Task:IsStatePlanned() then + local TaskName = Task:GetName() + local TaskType = TaskName:match( "(%u+)%.%d+" ) + + self:T2( { TaskType = TaskType } ) + + local Remove = false + + local IsPlayers = Detection:IsPlayersNearBy( DetectedItem ) + if TaskType == "ENGAGE" then + if IsPlayers == false then + Remove = true + end + end + + if TaskType == "INTERCEPT" then + if IsPlayers == true then + Remove = true + end + if DetectedItem.IsDetected == false then + Remove = true + end + end + + if TaskType == "SWEEP" then + if DetectedItem.IsDetected == true then + Remove = true + end + end + + local DetectedSet = DetectedItem.Set -- Core.Set#SET_UNIT + --DetectedSet:Flush( self ) + --self:F( { DetectedSetCount = DetectedSet:Count() } ) + if DetectedSet:Count() == 0 then + Remove = true + end + + if DetectedItemChanged == true or Remove then + Task = self:RemoveTask( DetectedItemIndex ) + end + end + end + + return Task + end + + --- Calculates which friendlies are nearby the area + -- @param #TASK_A2A_DISPATCHER self + -- @param DetectedItem + -- @return #number, Core.CommandCenter#REPORT + function TASK_A2A_DISPATCHER:GetFriendliesNearBy( DetectedItem ) + + local DetectedSet = DetectedItem.Set + local FriendlyUnitsNearBy = self.Detection:GetFriendliesNearBy( DetectedItem, Unit.Category.AIRPLANE ) + + local FriendlyTypes = {} + local FriendliesCount = 0 + + if FriendlyUnitsNearBy then + local DetectedTreatLevel = DetectedSet:CalculateThreatLevelA2G() + for FriendlyUnitName, FriendlyUnitData in pairs( FriendlyUnitsNearBy ) do + local FriendlyUnit = FriendlyUnitData -- Wrapper.Unit#UNIT + if FriendlyUnit:IsAirPlane() then + local FriendlyUnitThreatLevel = FriendlyUnit:GetThreatLevel() + FriendliesCount = FriendliesCount + 1 + local FriendlyType = FriendlyUnit:GetTypeName() + FriendlyTypes[FriendlyType] = FriendlyTypes[FriendlyType] and ( FriendlyTypes[FriendlyType] + 1 ) or 1 + if DetectedTreatLevel < FriendlyUnitThreatLevel + 2 then + end + end + end + + end + + --self:F( { FriendliesCount = FriendliesCount } ) + + local FriendlyTypesReport = REPORT:New() + + if FriendliesCount > 0 then + for FriendlyType, FriendlyTypeCount in pairs( FriendlyTypes ) do + FriendlyTypesReport:Add( string.format("%d of %s", FriendlyTypeCount, FriendlyType ) ) + end + else + FriendlyTypesReport:Add( "-" ) + end + + + return FriendliesCount, FriendlyTypesReport + end + + --- Calculates which HUMAN friendlies are nearby the area + -- @param #TASK_A2A_DISPATCHER self + -- @param DetectedItem + -- @return #number, Core.CommandCenter#REPORT + function TASK_A2A_DISPATCHER:GetPlayerFriendliesNearBy( DetectedItem ) + + local DetectedSet = DetectedItem.Set + local PlayersNearBy = self.Detection:GetPlayersNearBy( DetectedItem ) + + local PlayerTypes = {} + local PlayersCount = 0 + + if PlayersNearBy then + local DetectedTreatLevel = DetectedSet:CalculateThreatLevelA2G() + for PlayerUnitName, PlayerUnitData in pairs( PlayersNearBy ) do + local PlayerUnit = PlayerUnitData -- Wrapper.Unit#UNIT + local PlayerName = PlayerUnit:GetPlayerName() + --self:F( { PlayerName = PlayerName, PlayerUnit = PlayerUnit } ) + if PlayerUnit:IsAirPlane() and PlayerName ~= nil then + local FriendlyUnitThreatLevel = PlayerUnit:GetThreatLevel() + PlayersCount = PlayersCount + 1 + local PlayerType = PlayerUnit:GetTypeName() + PlayerTypes[PlayerName] = PlayerType + if DetectedTreatLevel < FriendlyUnitThreatLevel + 2 then + end + end + end + + end + + local PlayerTypesReport = REPORT:New() + + if PlayersCount > 0 then + for PlayerName, PlayerType in pairs( PlayerTypes ) do + PlayerTypesReport:Add( string.format('"%s" in %s', PlayerName, PlayerType ) ) + end + else + PlayerTypesReport:Add( "-" ) + end + + + return PlayersCount, PlayerTypesReport + end + + function TASK_A2A_DISPATCHER:RemoveTask( TaskIndex ) + self.Mission:RemoveTask( self.Tasks[TaskIndex] ) + self.Tasks[TaskIndex] = nil + end + + + --- Assigns tasks in relation to the detected items to the @{Core.Set#SET_GROUP}. + -- @param #TASK_A2A_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE Detection The detection created by the @{Functional.Detection#DETECTION_BASE} derived object. + -- @return #boolean Return true if you want the task assigning to continue... false will cancel the loop. + function TASK_A2A_DISPATCHER:ProcessDetected( Detection ) + self:F() + + local AreaMsg = {} + local TaskMsg = {} + local ChangeMsg = {} + + local Mission = self.Mission + + if Mission:IsIDLE() or Mission:IsENGAGED() then + + local TaskReport = REPORT:New() + + -- Checking the task queue for the dispatcher, and removing any obsolete task! + for TaskIndex, TaskData in pairs( self.Tasks ) do + local Task = TaskData -- Tasking.Task#TASK + if Task:IsStatePlanned() then + local DetectedItem = Detection:GetDetectedItemByIndex( TaskIndex ) + if not DetectedItem then + local TaskText = Task:GetName() + for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do + Mission:GetCommandCenter():MessageToGroup( string.format( "Obsolete A2A task %s for %s removed.", TaskText, Mission:GetShortText() ), TaskGroup ) + end + Task = self:RemoveTask( TaskIndex ) + end + end + end + + -- Now that all obsolete tasks are removed, loop through the detected targets. + for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do + + local DetectedItem = DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem + local DetectedSet = DetectedItem.Set -- Core.Set#SET_UNIT + local DetectedCount = DetectedSet:Count() + local DetectedZone = DetectedItem.Zone + --self:F( { "Targets in DetectedItem", DetectedItem.ItemID, DetectedSet:Count(), tostring( DetectedItem ) } ) + --DetectedSet:Flush( self ) + + local DetectedID = DetectedItem.ID + local TaskIndex = DetectedItem.Index + local DetectedItemChanged = DetectedItem.Changed + + local Task = self.Tasks[TaskIndex] + Task = self:EvaluateRemoveTask( Mission, Task, Detection, DetectedItem, TaskIndex, DetectedItemChanged ) -- Task will be removed if it is planned and changed. + + -- Evaluate INTERCEPT + if not Task and DetectedCount > 0 then + local TargetSetUnit = self:EvaluateENGAGE( DetectedItem ) -- Returns a SetUnit if there are targets to be INTERCEPTed... + if TargetSetUnit then + Task = TASK_A2A_ENGAGE:New( Mission, self.SetGroup, string.format( "ENGAGE.%03d", DetectedID ), TargetSetUnit ) + Task:SetDetection( Detection, DetectedItem ) + Task:UpdateTaskInfo( DetectedItem ) + else + local TargetSetUnit = self:EvaluateINTERCEPT( DetectedItem ) -- Returns a SetUnit if there are targets to be INTERCEPTed... + if TargetSetUnit then + Task = TASK_A2A_INTERCEPT:New( Mission, self.SetGroup, string.format( "INTERCEPT.%03d", DetectedID ), TargetSetUnit ) + Task:SetDetection( Detection, DetectedItem ) + Task:UpdateTaskInfo( DetectedItem ) + else + local TargetSetUnit = self:EvaluateSWEEP( DetectedItem ) -- Returns a SetUnit + if TargetSetUnit then + Task = TASK_A2A_SWEEP:New( Mission, self.SetGroup, string.format( "SWEEP.%03d", DetectedID ), TargetSetUnit ) + Task:SetDetection( Detection, DetectedItem ) + Task:UpdateTaskInfo( DetectedItem ) + end + end + end + + if Task then + self.Tasks[TaskIndex] = Task + Task:SetTargetZone( DetectedZone, DetectedItem.Coordinate.y, DetectedItem.Coordinate.Heading ) + Task:SetDispatcher( self ) + Mission:AddTask( Task ) + + function Task.OnEnterSuccess( Task, From, Event, To ) + self:Success( Task ) + end + + function Task.OnEnterCancelled( Task, From, Event, To ) + self:Cancelled( Task ) + end + + function Task.OnEnterFailed( Task, From, Event, To ) + self:Failed( Task ) + end + + function Task.OnEnterAborted( Task, From, Event, To ) + self:Aborted( Task ) + end + + TaskReport:Add( Task:GetName() ) + else + self:F("This should not happen") + end + + end + + if Task then + local FriendliesCount, FriendliesReport = self:GetFriendliesNearBy( DetectedItem, Unit.Category.AIRPLANE ) + Task.TaskInfo:AddText( "Friendlies", string.format( "%d ( %s )", FriendliesCount, FriendliesReport:Text( "," ) ), 40, "MOD" ) + local PlayersCount, PlayersReport = self:GetPlayerFriendliesNearBy( DetectedItem ) + Task.TaskInfo:AddText( "Players", string.format( "%d ( %s )", PlayersCount, PlayersReport:Text( "," ) ), 40, "MOD" ) + end + + -- OK, so the tasking has been done, now delete the changes reported for the area. + Detection:AcceptChanges( DetectedItem ) + end + + -- TODO set menus using the HQ coordinator + Mission:GetCommandCenter():SetMenu() + + local TaskText = TaskReport:Text(", ") + + for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do + if ( not Mission:IsGroupAssigned(TaskGroup) ) and TaskText ~= "" and (self.FlashNewTask) then + Mission:GetCommandCenter():MessageToGroup( string.format( "%s has tasks %s. Subscribe to a task using the radio menu.", Mission:GetShortText(), TaskText ), TaskGroup ) + end + end + + end + + return true + end + +end +--- **Tasking** - The TASK_A2A models tasks for players in Air to Air engagements. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- ### Contributions: +-- +-- === +-- +-- @module Tasking.Task_A2A +-- @image MOOSE.JPG + +do -- TASK_A2A + + --- The TASK_A2A class + -- @type TASK_A2A + -- @field Core.Set#SET_UNIT TargetSetUnit + -- @extends Tasking.Task#TASK + + --- Defines Air To Air tasks for a @{Set} of Target Units, + -- based on the tasking capabilities defined in @{Tasking.Task#TASK}. + -- The TASK_A2A is implemented using a @{Core.Fsm#FSM_TASK}, and has the following statuses: + -- + -- * **None**: Start of the process + -- * **Planned**: The A2A task is planned. + -- * **Assigned**: The A2A task is assigned to a @{Wrapper.Group#GROUP}. + -- * **Success**: The A2A task is successfully completed. + -- * **Failed**: The A2A task has failed. This will happen if the player exists the task early, without communicating a possible cancellation to HQ. + -- + -- # 1) Set the scoring of achievements in an A2A attack. + -- + -- Scoring or penalties can be given in the following circumstances: + -- + -- * @{#TASK_A2A.SetScoreOnDestroy}(): Set a score when a target in scope of the A2A attack, has been destroyed. + -- * @{#TASK_A2A.SetScoreOnSuccess}(): Set a score when all the targets in scope of the A2A attack, have been destroyed. + -- * @{#TASK_A2A.SetPenaltyOnFailed}(): Set a penalty when the A2A attack has failed. + -- + -- @field #TASK_A2A + TASK_A2A = { + ClassName = "TASK_A2A", + } + + --- Instantiates a new TASK_A2A. + -- @param #TASK_A2A self + -- @param Tasking.Mission#MISSION Mission + -- @param Core.Set#SET_GROUP SetAttack The set of groups for which the Task can be assigned. + -- @param #string TaskName The name of the Task. + -- @param Core.Set#SET_UNIT UnitSetTargets + -- @param #number TargetDistance The distance to Target when the Player is considered to have "arrived" at the engagement range. + -- @param Core.Zone#ZONE_BASE TargetZone The target zone, if known. + -- If the TargetZone parameter is specified, the player will be routed to the center of the zone where all the targets are assumed to be. + -- @return #TASK_A2A self + function TASK_A2A:New( Mission, SetAttack, TaskName, TargetSetUnit, TaskType, TaskBriefing ) + local self = BASE:Inherit( self, TASK:New( Mission, SetAttack, TaskName, TaskType, TaskBriefing ) ) -- Tasking.Task#TASK_A2A + self:F() + + self.TargetSetUnit = TargetSetUnit + self.TaskType = TaskType + + local Fsm = self:GetUnitProcess() + + + Fsm:AddTransition( "Assigned", "RouteToRendezVous", "RoutingToRendezVous" ) + Fsm:AddProcess ( "RoutingToRendezVous", "RouteToRendezVousPoint", ACT_ROUTE_POINT:New(), { Arrived = "ArriveAtRendezVous" } ) + Fsm:AddProcess ( "RoutingToRendezVous", "RouteToRendezVousZone", ACT_ROUTE_ZONE:New(), { Arrived = "ArriveAtRendezVous" } ) + + Fsm:AddTransition( { "Arrived", "RoutingToRendezVous" }, "ArriveAtRendezVous", "ArrivedAtRendezVous" ) + + Fsm:AddTransition( { "ArrivedAtRendezVous", "HoldingAtRendezVous" }, "Engage", "Engaging" ) + Fsm:AddTransition( { "ArrivedAtRendezVous", "HoldingAtRendezVous" }, "HoldAtRendezVous", "HoldingAtRendezVous" ) + + Fsm:AddProcess ( "Engaging", "Account", ACT_ACCOUNT_DEADS:New(), {} ) + Fsm:AddTransition( "Engaging", "RouteToTarget", "Engaging" ) + Fsm:AddProcess( "Engaging", "RouteToTargetZone", ACT_ROUTE_ZONE:New(), {} ) + Fsm:AddProcess( "Engaging", "RouteToTargetPoint", ACT_ROUTE_POINT:New(), {} ) + Fsm:AddTransition( "Engaging", "RouteToTargets", "Engaging" ) + +-- Fsm:AddTransition( "Accounted", "DestroyedAll", "Accounted" ) +-- Fsm:AddTransition( "Accounted", "Success", "Success" ) + Fsm:AddTransition( "Rejected", "Reject", "Aborted" ) + Fsm:AddTransition( "Failed", "Fail", "Failed" ) + + + ---- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param #TASK_CARGO Task + function Fsm:OnLeaveAssigned( TaskUnit, Task ) + self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + + self:SelectAction() + end + + --- Test + -- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task_A2A#TASK_A2A Task + function Fsm:onafterRouteToRendezVous( TaskUnit, Task ) + self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + -- Determine the first Unit from the self.RendezVousSetUnit + + if Task:GetRendezVousZone( TaskUnit ) then + self:__RouteToRendezVousZone( 0.1 ) + else + if Task:GetRendezVousCoordinate( TaskUnit ) then + self:__RouteToRendezVousPoint( 0.1 ) + else + self:__ArriveAtRendezVous( 0.1 ) + end + end + end + + --- Test + -- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task#TASK_A2A Task + function Fsm:OnAfterArriveAtRendezVous( TaskUnit, Task ) + self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + -- Determine the first Unit from the self.TargetSetUnit + + self:__Engage( 0.1 ) + end + + --- Test + -- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task#TASK_A2A Task + function Fsm:onafterEngage( TaskUnit, Task ) + self:F( { self } ) + self:__Account( 0.1 ) + self:__RouteToTarget(0.1 ) + self:__RouteToTargets( -10 ) + end + + --- Test + -- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task_A2A#TASK_A2A Task + function Fsm:onafterRouteToTarget( TaskUnit, Task ) + self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + -- Determine the first Unit from the self.TargetSetUnit + + if Task:GetTargetZone( TaskUnit ) then + self:__RouteToTargetZone( 0.1 ) + else + local TargetUnit = Task.TargetSetUnit:GetFirst() -- Wrapper.Unit#UNIT + if TargetUnit then + local Coordinate = TargetUnit:GetPointVec3() + self:T( { TargetCoordinate = Coordinate, Coordinate:GetX(), Coordinate:GetAlt(), Coordinate:GetZ() } ) + Task:SetTargetCoordinate( Coordinate, TaskUnit ) + end + self:__RouteToTargetPoint( 0.1 ) + end + end + + --- Test + -- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task_A2A#TASK_A2A Task + function Fsm:onafterRouteToTargets( TaskUnit, Task ) + self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + local TargetUnit = Task.TargetSetUnit:GetFirst() -- Wrapper.Unit#UNIT + if TargetUnit then + Task:SetTargetCoordinate( TargetUnit:GetCoordinate(), TaskUnit ) + end + self:__RouteToTargets( -10 ) + end + + return self + + end + + --- @param #TASK_A2A self + -- @param Core.Set#SET_UNIT TargetSetUnit The set of targets. + function TASK_A2A:SetTargetSetUnit( TargetSetUnit ) + + self.TargetSetUnit = TargetSetUnit + end + + + + --- @param #TASK_A2A self + function TASK_A2A:GetPlannedMenuText() + return self:GetStateString() .. " - " .. self:GetTaskName() .. " ( " .. self.TargetSetUnit:GetUnitTypesText() .. " )" + end + + --- @param #TASK_A2A self + -- @param Core.Point#COORDINATE RendezVousCoordinate The Coordinate object referencing to the 2D point where the RendezVous point is located on the map. + -- @param #number RendezVousRange The RendezVousRange that defines when the player is considered to have arrived at the RendezVous point. + -- @param Wrapper.Unit#UNIT TaskUnit + function TASK_A2A:SetRendezVousCoordinate( RendezVousCoordinate, RendezVousRange, TaskUnit ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + local ActRouteRendezVous = ProcessUnit:GetProcess( "RoutingToRendezVous", "RouteToRendezVousPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT + ActRouteRendezVous:SetCoordinate( RendezVousCoordinate ) + ActRouteRendezVous:SetRange( RendezVousRange ) + end + + --- @param #TASK_A2A self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return Core.Point#COORDINATE The Coordinate object referencing to the 2D point where the RendezVous point is located on the map. + -- @return #number The RendezVousRange that defines when the player is considered to have arrived at the RendezVous point. + function TASK_A2A:GetRendezVousCoordinate( TaskUnit ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + local ActRouteRendezVous = ProcessUnit:GetProcess( "RoutingToRendezVous", "RouteToRendezVousPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT + return ActRouteRendezVous:GetCoordinate(), ActRouteRendezVous:GetRange() + end + + + + --- @param #TASK_A2A self + -- @param Core.Zone#ZONE_BASE RendezVousZone The Zone object where the RendezVous is located on the map. + -- @param Wrapper.Unit#UNIT TaskUnit + function TASK_A2A:SetRendezVousZone( RendezVousZone, TaskUnit ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + local ActRouteRendezVous = ProcessUnit:GetProcess( "RoutingToRendezVous", "RouteToRendezVousZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE + ActRouteRendezVous:SetZone( RendezVousZone ) + end + + --- @param #TASK_A2A self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return Core.Zone#ZONE_BASE The Zone object where the RendezVous is located on the map. + function TASK_A2A:GetRendezVousZone( TaskUnit ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + local ActRouteRendezVous = ProcessUnit:GetProcess( "RoutingToRendezVous", "RouteToRendezVousZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE + return ActRouteRendezVous:GetZone() + end + + --- @param #TASK_A2A self + -- @param Core.Point#COORDINATE TargetCoordinate The Coordinate object where the Target is located on the map. + -- @param Wrapper.Unit#UNIT TaskUnit + function TASK_A2A:SetTargetCoordinate( TargetCoordinate, TaskUnit ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT + ActRouteTarget:SetCoordinate( TargetCoordinate ) + end + + + --- @param #TASK_A2A self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return Core.Point#COORDINATE The Coordinate object where the Target is located on the map. + function TASK_A2A:GetTargetCoordinate( TaskUnit ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT + return ActRouteTarget:GetCoordinate() + end + + + --- @param #TASK_A2A self + -- @param Core.Zone#ZONE_BASE TargetZone The Zone object where the Target is located on the map. + -- @param Wrapper.Unit#UNIT TaskUnit + function TASK_A2A:SetTargetZone( TargetZone, Altitude, Heading, TaskUnit ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE + ActRouteTarget:SetZone( TargetZone, Altitude, Heading ) + end + + + --- @param #TASK_A2A self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return Core.Zone#ZONE_BASE The Zone object where the Target is located on the map. + function TASK_A2A:GetTargetZone( TaskUnit ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE + return ActRouteTarget:GetZone() + end + + function TASK_A2A:SetGoalTotal() + + self.GoalTotal = self.TargetSetUnit:Count() + end + + function TASK_A2A:GetGoalTotal() + + return self.GoalTotal + end + + --- Return the relative distance to the target vicinity from the player, in order to sort the targets in the reports per distance from the threats. + -- @param #TASK_A2A self + function TASK_A2A:ReportOrder( ReportGroup ) + self:UpdateTaskInfo( self.DetectedItem ) + + local Coordinate = self.TaskInfo:GetData( "Coordinate" ) + local Distance = ReportGroup:GetCoordinate():Get2DDistance( Coordinate ) + + return Distance + end + + + --- This method checks every 10 seconds if the goal has been reached of the task. + -- @param #TASK_A2A self + function TASK_A2A:onafterGoal( TaskUnit, From, Event, To ) + local TargetSetUnit = self.TargetSetUnit -- Core.Set#SET_UNIT + + if TargetSetUnit:Count() == 0 then + self:Success() + end + + self:__Goal( -10 ) + end + + + --- @param #TASK_A2A self + function TASK_A2A:UpdateTaskInfo( DetectedItem ) + + if self:IsStatePlanned() or self:IsStateAssigned() then + local TargetCoordinate = DetectedItem and self.Detection:GetDetectedItemCoordinate( DetectedItem ) or self.TargetSetUnit:GetFirst():GetCoordinate() + self.TaskInfo:AddTaskName( 0, "MSOD" ) + self.TaskInfo:AddCoordinate( TargetCoordinate, 1, "SOD" ) + + local ThreatLevel, ThreatText + if DetectedItem then + ThreatLevel, ThreatText = self.Detection:GetDetectedItemThreatLevel( DetectedItem ) + else + ThreatLevel, ThreatText = self.TargetSetUnit:CalculateThreatLevelA2G() + end + self.TaskInfo:AddThreat( ThreatText, ThreatLevel, 10, "MOD", true ) + + if self.Detection then + local DetectedItemsCount = self.TargetSetUnit:Count() + local ReportTypes = REPORT:New() + local TargetTypes = {} + for TargetUnitName, TargetUnit in pairs( self.TargetSetUnit:GetSet() ) do + local TargetType = self.Detection:GetDetectedUnitTypeName( TargetUnit ) + if not TargetTypes[TargetType] then + TargetTypes[TargetType] = TargetType + ReportTypes:Add( TargetType ) + end + end + self.TaskInfo:AddTargetCount( DetectedItemsCount, 11, "O", true ) + self.TaskInfo:AddTargets( DetectedItemsCount, ReportTypes:Text( ", " ), 20, "D", true ) + else + local DetectedItemsCount = self.TargetSetUnit:Count() + local DetectedItemsTypes = self.TargetSetUnit:GetTypeNames() + self.TaskInfo:AddTargetCount( DetectedItemsCount, 11, "O", true ) + self.TaskInfo:AddTargets( DetectedItemsCount, DetectedItemsTypes, 20, "D", true ) + end + end + end + + --- This function is called from the @{Tasking.CommandCenter#COMMANDCENTER} to determine the method of automatic task selection. + -- @param #TASK_A2A self + -- @param #number AutoAssignMethod The method to be applied to the task. + -- @param Tasking.CommandCenter#COMMANDCENTER CommandCenter The command center. + -- @param Wrapper.Group#GROUP TaskGroup The player group. + function TASK_A2A:GetAutoAssignPriority( AutoAssignMethod, CommandCenter, TaskGroup ) + + if AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Random then + return math.random( 1, 9 ) + elseif AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Distance then + local Coordinate = self.TaskInfo:GetData( "Coordinate" ) + local Distance = Coordinate:Get2DDistance( CommandCenter:GetPositionable():GetCoordinate() ) + return math.floor( Distance ) + elseif AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Priority then + return 1 + end + + return 0 + end + +end + + +do -- TASK_A2A_INTERCEPT + + --- The TASK_A2A_INTERCEPT class + -- @type TASK_A2A_INTERCEPT + -- @field Core.Set#SET_UNIT TargetSetUnit + -- @extends Tasking.Task#TASK + + --- Defines an intercept task for a human player to be executed. + -- When enemy planes need to be intercepted by human players, use this task type to urgen the players to get out there! + -- + -- The TASK_A2A_INTERCEPT is used by the @{Tasking.Task_A2A_Dispatcher#TASK_A2A_DISPATCHER} to automatically create intercept tasks + -- based on detected airborne enemy targets intruding friendly airspace. + -- + -- The task is defined for a @{Tasking.Mission#MISSION}, where a friendly @{Core.Set#SET_GROUP} consisting of GROUPs with one human players each, is intercepting the targets. + -- The task is given a name and a briefing, that is used in the menu structure and in the reporting. + -- + -- @field #TASK_A2A_INTERCEPT + TASK_A2A_INTERCEPT = { + ClassName = "TASK_A2A_INTERCEPT", + } + + + + --- Instantiates a new TASK_A2A_INTERCEPT. + -- @param #TASK_A2A_INTERCEPT self + -- @param Tasking.Mission#MISSION Mission + -- @param Core.Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. + -- @param #string TaskName The name of the Task. + -- @param Core.Set#SET_UNIT TargetSetUnit + -- @param #string TaskBriefing The briefing of the task. + -- @return #TASK_A2A_INTERCEPT + function TASK_A2A_INTERCEPT:New( Mission, SetGroup, TaskName, TargetSetUnit, TaskBriefing ) + local self = BASE:Inherit( self, TASK_A2A:New( Mission, SetGroup, TaskName, TargetSetUnit, "INTERCEPT", TaskBriefing ) ) -- #TASK_A2A_INTERCEPT + self:F() + + Mission:AddTask( self ) + + self:SetBriefing( + TaskBriefing or + "Intercept incoming intruders.\n" + ) + + return self + end + + --- Set a score when a target in scope of the A2A attack, has been destroyed . + -- @param #TASK_A2A_INTERCEPT self + -- @param #string PlayerName The name of the player. + -- @param #number Score The score in points to be granted when task process has been achieved. + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return #TASK_A2A_INTERCEPT + function TASK_A2A_INTERCEPT:SetScoreOnProgress( PlayerName, Score, TaskUnit ) + self:F( { PlayerName, Score, TaskUnit } ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + ProcessUnit:AddScoreProcess( "Engaging", "Account", "AccountForPlayer", "Player " .. PlayerName .. " has intercepted a target.", Score ) + + return self + end + + --- Set a score when all the targets in scope of the A2A attack, have been destroyed. + -- @param #TASK_A2A_INTERCEPT self + -- @param #string PlayerName The name of the player. + -- @param #number Score The score in points. + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return #TASK_A2A_INTERCEPT + function TASK_A2A_INTERCEPT:SetScoreOnSuccess( PlayerName, Score, TaskUnit ) + self:F( { PlayerName, Score, TaskUnit } ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + ProcessUnit:AddScore( "Success", "All targets have been successfully intercepted!", Score ) + + return self + end + + --- Set a penalty when the A2A attack has failed. + -- @param #TASK_A2A_INTERCEPT self + -- @param #string PlayerName The name of the player. + -- @param #number Penalty The penalty in points, must be a negative value! + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return #TASK_A2A_INTERCEPT + function TASK_A2A_INTERCEPT:SetScoreOnFail( PlayerName, Penalty, TaskUnit ) + self:F( { PlayerName, Penalty, TaskUnit } ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + ProcessUnit:AddScore( "Failed", "The intercept has failed!", Penalty ) + + return self + end + + +end + + +do -- TASK_A2A_SWEEP + + --- The TASK_A2A_SWEEP class + -- @type TASK_A2A_SWEEP + -- @field Core.Set#SET_UNIT TargetSetUnit + -- @extends Tasking.Task#TASK + + --- Defines a sweep task for a human player to be executed. + -- A sweep task needs to be given when targets were detected but somehow the detection was lost. + -- Most likely, these enemy planes are hidden in the mountains or are flying under radar. + -- These enemy planes need to be sweeped by human players, and use this task type to urge the players to get out there and find those enemy fighters. + -- + -- The TASK_A2A_SWEEP is used by the @{Tasking.Task_A2A_Dispatcher#TASK_A2A_DISPATCHER} to automatically create sweep tasks + -- based on detected airborne enemy targets intruding friendly airspace, for which the detection has been lost for more than 60 seconds. + -- + -- The task is defined for a @{Tasking.Mission#MISSION}, where a friendly @{Core.Set#SET_GROUP} consisting of GROUPs with one human players each, is sweeping the targets. + -- The task is given a name and a briefing, that is used in the menu structure and in the reporting. + -- + -- @field #TASK_A2A_SWEEP + TASK_A2A_SWEEP = { + ClassName = "TASK_A2A_SWEEP", + } + + + + --- Instantiates a new TASK_A2A_SWEEP. + -- @param #TASK_A2A_SWEEP self + -- @param Tasking.Mission#MISSION Mission + -- @param Core.Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. + -- @param #string TaskName The name of the Task. + -- @param Core.Set#SET_UNIT TargetSetUnit + -- @param #string TaskBriefing The briefing of the task. + -- @return #TASK_A2A_SWEEP self + function TASK_A2A_SWEEP:New( Mission, SetGroup, TaskName, TargetSetUnit, TaskBriefing ) + local self = BASE:Inherit( self, TASK_A2A:New( Mission, SetGroup, TaskName, TargetSetUnit, "SWEEP", TaskBriefing ) ) -- #TASK_A2A_SWEEP + self:F() + + Mission:AddTask( self ) + + self:SetBriefing( + TaskBriefing or + "Perform a fighter sweep. Incoming intruders were detected and could be hiding at the location.\n" + ) + + return self + end + + --- @param #TASK_A2A_SWEEP self + function TASK_A2A_SWEEP:onafterGoal( TaskUnit, From, Event, To ) + local TargetSetUnit = self.TargetSetUnit -- Core.Set#SET_UNIT + + if TargetSetUnit:Count() == 0 then + self:Success() + end + + self:__Goal( -10 ) + end + + --- Set a score when a target in scope of the A2A attack, has been destroyed . + -- @param #TASK_A2A_SWEEP self + -- @param #string PlayerName The name of the player. + -- @param #number Score The score in points to be granted when task process has been achieved. + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return #TASK_A2A_SWEEP + function TASK_A2A_SWEEP:SetScoreOnProgress( PlayerName, Score, TaskUnit ) + self:F( { PlayerName, Score, TaskUnit } ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + ProcessUnit:AddScoreProcess( "Engaging", "Account", "AccountForPlayer", "Player " .. PlayerName .. " has sweeped a target.", Score ) + + return self + end + + --- Set a score when all the targets in scope of the A2A attack, have been destroyed. + -- @param #TASK_A2A_SWEEP self + -- @param #string PlayerName The name of the player. + -- @param #number Score The score in points. + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return #TASK_A2A_SWEEP + function TASK_A2A_SWEEP:SetScoreOnSuccess( PlayerName, Score, TaskUnit ) + self:F( { PlayerName, Score, TaskUnit } ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + ProcessUnit:AddScore( "Success", "All targets have been successfully sweeped!", Score ) + + return self + end + + --- Set a penalty when the A2A attack has failed. + -- @param #TASK_A2A_SWEEP self + -- @param #string PlayerName The name of the player. + -- @param #number Penalty The penalty in points, must be a negative value! + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return #TASK_A2A_SWEEP + function TASK_A2A_SWEEP:SetScoreOnFail( PlayerName, Penalty, TaskUnit ) + self:F( { PlayerName, Penalty, TaskUnit } ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + ProcessUnit:AddScore( "Failed", "The sweep has failed!", Penalty ) + + return self + end + +end + + +do -- TASK_A2A_ENGAGE + + --- The TASK_A2A_ENGAGE class + -- @type TASK_A2A_ENGAGE + -- @field Core.Set#SET_UNIT TargetSetUnit + -- @extends Tasking.Task#TASK + + --- Defines an engage task for a human player to be executed. + -- When enemy planes are close to human players, use this task type is used urge the players to get out there! + -- + -- The TASK_A2A_ENGAGE is used by the @{Tasking.Task_A2A_Dispatcher#TASK_A2A_DISPATCHER} to automatically create engage tasks + -- based on detected airborne enemy targets intruding friendly airspace. + -- + -- The task is defined for a @{Tasking.Mission#MISSION}, where a friendly @{Core.Set#SET_GROUP} consisting of GROUPs with one human players each, is engaging the targets. + -- The task is given a name and a briefing, that is used in the menu structure and in the reporting. + -- + -- @field #TASK_A2A_ENGAGE + TASK_A2A_ENGAGE = { + ClassName = "TASK_A2A_ENGAGE", + } + + + + --- Instantiates a new TASK_A2A_ENGAGE. + -- @param #TASK_A2A_ENGAGE self + -- @param Tasking.Mission#MISSION Mission + -- @param Core.Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. + -- @param #string TaskName The name of the Task. + -- @param Core.Set#SET_UNIT TargetSetUnit + -- @param #string TaskBriefing The briefing of the task. + -- @return #TASK_A2A_ENGAGE self + function TASK_A2A_ENGAGE:New( Mission, SetGroup, TaskName, TargetSetUnit, TaskBriefing ) + local self = BASE:Inherit( self, TASK_A2A:New( Mission, SetGroup, TaskName, TargetSetUnit, "ENGAGE", TaskBriefing ) ) -- #TASK_A2A_ENGAGE + self:F() + + Mission:AddTask( self ) + + self:SetBriefing( + TaskBriefing or + "Bogeys are nearby! Players close by are ordered to ENGAGE the intruders!\n" + ) + + return self + end + + --- Set a score when a target in scope of the A2A attack, has been destroyed . + -- @param #TASK_A2A_ENGAGE self + -- @param #string PlayerName The name of the player. + -- @param #number Score The score in points to be granted when task process has been achieved. + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return #TASK_A2A_ENGAGE + function TASK_A2A_ENGAGE:SetScoreOnProgress( PlayerName, Score, TaskUnit ) + self:F( { PlayerName, Score, TaskUnit } ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + ProcessUnit:AddScoreProcess( "Engaging", "Account", "AccountForPlayer", "Player " .. PlayerName .. " has engaged and destroyed a target.", Score ) + + return self + end + + --- Set a score when all the targets in scope of the A2A attack, have been destroyed. + -- @param #TASK_A2A_ENGAGE self + -- @param #string PlayerName The name of the player. + -- @param #number Score The score in points. + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return #TASK_A2A_ENGAGE + function TASK_A2A_ENGAGE:SetScoreOnSuccess( PlayerName, Score, TaskUnit ) + self:F( { PlayerName, Score, TaskUnit } ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + ProcessUnit:AddScore( "Success", "All targets have been successfully engaged!", Score ) + + return self + end + + --- Set a penalty when the A2A attack has failed. + -- @param #TASK_A2A_ENGAGE self + -- @param #string PlayerName The name of the player. + -- @param #number Penalty The penalty in points, must be a negative value! + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return #TASK_A2A_ENGAGE + function TASK_A2A_ENGAGE:SetScoreOnFail( PlayerName, Penalty, TaskUnit ) + self:F( { PlayerName, Penalty, TaskUnit } ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + ProcessUnit:AddScore( "Failed", "The target engagement has failed!", Penalty ) + + return self + end + +end + +--- **Tasking** -- Base class to model tasks for players to transport cargo. +-- +-- ## Features: +-- +-- * TASK_CARGO is the **base class** for: +-- +-- * @{Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT} +-- * @{Tasking.Task_Cargo_CSAR#TASK_CARGO_CSAR} +-- +-- +-- === +-- +-- ## Test Missions: +-- +-- Test missions can be located on the main GITHUB site. +-- +-- [FlightControl-Master/MOOSE_MISSIONS/TAD - Task Dispatching/CGO - Cargo Dispatching/](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/TAD%20-%20Task%20Dispatching/CGO%20-%20Cargo%20Dispatching) +-- +-- === +-- +-- ## Tasking system. +-- +-- #### If you are not yet aware what the MOOSE tasking system is about, read FIRST the explanation on the @{Tasking.Task} module. +-- +-- === +-- +-- ## Context of cargo tasking. +-- +-- The Moose framework provides various CARGO classes that allow DCS physical or logical objects to be transported or sling loaded by Carriers. +-- The CARGO_ classes, as part of the MOOSE core, are able to Board, Load, UnBoard and UnLoad cargo between Carrier units. +-- +-- The TASK_CARGO class is not meant to use within your missions as a mission designer. It is a base class, and other classes are derived from it. +-- +-- The following TASK_CARGO_ classes are important, as they implement the CONCRETE tasks: +-- +-- * @{Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT}: Defines a task for a human player to transport a set of cargo between various zones. +-- * @{Tasking.Task_Cargo_CSAR#TASK_CARGO_CSAR}: Defines a task for a human player to Search and Rescue wounded pilots. +-- +-- However! The menu system and basic usage of the TASK_CARGO classes is explained in the @{#TASK_CARGO} class description. +-- So please browse further below to understand how to use it from a player perspective! +-- +-- === +-- +-- ## Cargo tasking from a player perspective. +-- +-- A human player can join the battle field in a client airborne slot or a ground vehicle within the CA module (ALT-J). +-- The player needs to accept the task from the task overview list within the mission, using the menus. +-- +-- Once the task is assigned to the player and accepted by the player, the player will obtain +-- an extra **Cargo (Radio) Menu** that contains the CARGO objects that need to be transported. +-- +-- Each @{Cargo.Cargo} object has a certain state: +-- +-- * **UnLoaded**: The cargo is located within the battlefield. It may still need to be transported. +-- * **Loaded**: The cargo is loaded within a Carrier. This can be your air unit, or another air unit, or even a vehicle. +-- * **Boarding**: The cargo is running or moving towards your Carrier for loading. +-- * **UnBoarding**: The cargo is driving or jumping out of your Carrier and moves to a location in the Deployment Zone. +-- +-- Cargo must be transported towards different Deployment @{Core.Zone}s. +-- +-- The Cargo Menu system allows to execute **various actions** to transport the cargo. +-- In the menu, you'll find for each CARGO, that is part of the scope of the task, various actions that can be completed. +-- Depending on the location of your Carrier unit, the menu options will vary. +-- +-- ### Joining a Cargo Transport Task +-- +-- Once you've joined a task, using the **Join Planned Task Menu**, +-- you can Pickup cargo from a pickup location and Deploy cargo in deployment zones, using the **Task Action Menu**. +-- +-- ### Task Action Menu. +-- +-- When a player has joined a **`CARGO`** task (type), for that player only, +-- it's **Task Action Menu** will show an additional menu options. +-- +-- From within this menu, you will be able to route to a cargo location, deploy zone, and load/unload cargo. +-- +-- ### Pickup cargo by Boarding, Loading and Sling Loading. +-- +-- There are three different ways how cargo can be picked up: +-- +-- - **Boarding**: Moveable cargo (like infantry or vehicles), can be boarded, that means, the cargo will move towards your carrier to board. +-- However, it can only execute the boarding actions if it is within the foreseen **Reporting Range**. +-- Therefore, it is important that you steer your Carrier within the Reporting Range around the cargo, +-- so that boarding actions can be executed on the cargo. The reporting range is set by the mission designer. +-- Fortunately, the cargo is reporting to you when it is within reporting range. +-- +-- - **Loading**: Stationary cargo (like crates), which are heavy, can only be loaded or sling loaded, meaning, +-- your carrier must be close enough to the cargo to be able to load the cargo within the carrier bays. +-- Moose provides you with an additional menu system to load stationary cargo into your carrier bays using the menu. +-- These menu options will become available, when the carrier is within loading range. +-- The Moose cargo will report to the carrier when the range is close enough. The load range is set by the mission designer. +-- +-- - **Sling Loading**: Stationary cargo (like crates), which are heavy, can only be loaded or sling loaded, meaning, +-- your carrier must be close enough to the cargo to be able to load the cargo within the carrier bays. +-- Sling loading cargo is done using the default DCS menu system. However, Moose cargo will report to the carrier that +-- it is within sling loading range. +-- +-- In order to be able to pickup cargo, you'll need to know where the cargo is located, right? +-- +-- Fortunately, if your Carrier is not within the reporting range of the cargo, +-- **the HQ can help to route you to the locations of cargo**. +-- +-- ![Task_Types](../Tasking/Task_Cargo_Main_Menu.JPG) +-- +-- Use the task action menu to receive HQ help for this. +-- +-- ![Task_Types](../Tasking/Task_Cargo_Action_Menu.JPG) +-- +-- Depending on the location within the battlefield, the task action menu will contain **Route options** that can be selected +-- to start the HQ sending you routing messages. +-- The **route options will vary**, depending on the position of your carrier, and the location of the cargo and the deploy zones. +-- Note that the route options will **only be created** for cargo that is **in scope of your cargo transportation task**, +-- so there may be other cargo objects within the DCS simulation, but if those belong to other cargo transportations tasks, +-- then no routing options will be shown for these cargo. +-- This is done to ensure that **different teams** have a **defined scope** for defined cargo, and that **multiple teams** can join +-- **multiple tasks**, transporting cargo **simultaneously** in a **cooperation**. +-- +-- In this example, there is a menu option to **Route to pickup cargo...**. +-- Use this menu to route towards cargo locations for pickup into your carrier. +-- +-- ![Task_Types](../Tasking/Task_Cargo_Types_Menu.JPG) +-- +-- When you select this menu, you'll see a new menu listing the different cargo types that are out there in the dcs simulator. +-- These cargo types are symbolic names that are assigned by the mission designer, like oil, liquid, engineers, food, workers etc. +-- MOOSE has introduced this concept to allow mission designers to make different cargo types for different purposes. +-- Only the creativity of the mission designer limits now the things that can be done with cargo ... +-- Okay, let's continue ..., and let's select Oil ... +-- +-- When selected, the HQ will send you routing messages. +-- +-- ![Task_Types](../Tasking/Task_Cargo_Routing_BR.JPG) +-- +-- An example of routing in BR mode. +-- +-- Note that the coordinate display format in the message can be switched between LL DMS, LL DDM, MGRS and BR. +-- +-- ![Task_Types](../Tasking/Main_Settings.JPG) +-- +-- Use the @{Core.Settings} menu to change your display format preferences. +-- +-- ![Task_Types](../Tasking/Settings_A2G_Coordinate.JPG) +-- +-- There you can change the display format to another format that suits your need. +-- Because cargo transportation is Air 2 Ground oriented, you need to select the A2G coordinate format display options. +-- Note that the main settings menu contains much more +-- options to control your display formats, like switch to metric and imperial, or change the duration of the display messages. +-- +-- ![Task_Types](../Tasking/Task_Cargo_Routing_LL.JPG) +-- +-- Here I changed the routing display format to LL DMS. +-- +-- One important thing to know, is that the routing messages will flash at regular time intervals. +-- When using BR coordinate display format, the **distance and angle will change accordingly** from your carrier position and the location of the cargo. +-- +-- Another important note is the routing towards deploy zones. +-- These routing options will only be shown, when your carrier bays have cargo loaded. +-- So, only when there is something to be deployed from your carrier, the deploy options will be shown. +-- +-- #### Pickup Cargo. +-- +-- In order to pickup cargo, use the **task action menu** to **route to a specific cargo**. +-- When a cargo route is selected, the HQ will send you routing messages indicating the location of the cargo. +-- +-- Upon arrival at the cargo, and when the cargo is within **reporting range**, the cargo will contact you and **further instructions will be given**. +-- +-- - When your Carrier is airborne, you will receive instructions to land your Carrier. +-- The action will not be completed until you've landed your Carrier. +-- +-- - For ground carriers, you can just drive to the optimal cargo board or load position. +-- +-- It takes a bit of skill to land a helicopter near a cargo to be loaded, but that is part of the game, isn't it? +-- Expecially when you are landing in a "hot" zone, so when cargo is under immediate threat of fire. +-- +-- #### Board Cargo (infantry). +-- +-- ![](../Tasking/Boarding_Ready.png) +-- +-- If your Carrier is within the **Reporting Range of the cargo**, and the cargo is **moveable**, the **cargo can be boarded**! +-- This type of cargo will be most of the time be infantry. +-- +-- ![](../Tasking/Boarding_Menu.png) +-- +-- A **Board cargo...** sub menu has appeared, because your carrier is in boarding range of the cargo (infantry). +-- Select the **Board cargo...** menu. +-- +-- ![](../Tasking/Boarding_Menu_Engineers.png) +-- +-- Any cargo that can be boarded (thus movable cargo), within boarding range of the carrier, will be listed here! +-- In this example, the cargo **Engineers** can be boarded, by selecting the menu option. +-- +-- ![](../Tasking/Boarding_Started.png) +-- +-- After the menu option to board the cargo has been selected, the boarding process is started. +-- A message from the cargo is communicated to the pilot, that boarding is started. +-- +-- ![](../Tasking/Boarding_Ongoing.png) +-- +-- **The pilot must wait at the exact position until all cargo has been boarded!** +-- +-- The moveable cargo will run in formation to your carrier, and will board one by one, depending on the near range set by the mission designer. +-- The near range as added because carriers can be large or small, depending on the object size of the carrier. +-- +-- ![](../Tasking/Boarding_In_Progress.png) +-- +-- ![](../Tasking/Boarding_Almost_Done.png) +-- +-- Note that multiple units may need to board your Carrier, so it is required to await the full boarding process. +-- +-- ![](../Tasking/Boarding_Done.png) +-- +-- Once the cargo is fully boarded within your Carrier, you will be notified of this. +-- +-- **Remarks:** +-- +-- * For airborne Carriers, it is required to land first before the Boarding process can be initiated. +-- If during boarding the Carrier gets airborne, the boarding process will be cancelled. +-- * The carrier must remain stationary when the boarding sequence has started until further notified. +-- +-- #### Load Cargo. +-- +-- Cargo can be loaded into vehicles or helicopters or airplanes, as long as the carrier is sufficiently near to the cargo object. +-- +-- ![](../Tasking/Loading_Ready.png) +-- +-- If your Carrier is within the **Loading Range of the cargo**, thus, sufficiently near to the cargo, and the cargo is **stationary**, the **cargo can be loaded**, but not boarded! +-- +-- ![](../Tasking/Loading_Menu.png) +-- +-- Select the task action menu and now a **Load cargo...** sub menu will be listed. +-- Select the **Load cargo...** sub menu, and a further detailed menu will be shown. +-- +-- ![](../Tasking/Loading_Menu_Crate.png) +-- +-- For each non-moveable cargo object (crates etc), **within loading range of the carrier**, the cargo will be listed and can be loaded into the carrier! +-- +-- ![](../Tasking/Loading_Cargo_Loaded.png) +-- +-- Once the cargo is loaded within your Carrier, you will be notified of this. +-- +-- **Remarks:** +-- +-- * For airborne Carriers, it is required to **land first right near the cargo**, before the loading process can be initiated. +-- As stated, this requires some pilot skills :-) +-- +-- #### Sling Load Cargo (helicopters only). +-- +-- If your Carrier is within the **Loading Range of the cargo**, and the cargo is **stationary**, the **cargo can also be sling loaded**! +-- Note that this is only possible for helicopters. +-- +-- To sling load cargo, there is no task action menu required. Just follow the normal sling loading procedure and the cargo will report. +-- Use the normal DCS sling loading menu system to hook the cargo you the cable attached on your helicopter. +-- +-- Again note that you may land firstly right next to the cargo, before the loading process can be initiated. +-- As stated, this requires some pilot skills :-) +-- +-- +-- ### Deploy cargo by Unboarding, Unloading and Sling Deploying. +-- +-- #### **Deploying the relevant cargo within deploy zones, will make you achieve cargo transportation tasks!!!** +-- +-- There are two different ways how cargo can be deployed: +-- +-- - **Unboarding**: Moveable cargo (like infantry or vehicles), can be unboarded, that means, +-- the cargo will step out of the carrier and will run to a group location. +-- Moose provides you with an additional menu system to unload stationary cargo from the carrier bays, +-- using the menu. These menu options will become available, when the carrier is within the deploy zone. +-- +-- - **Unloading**: Stationary cargo (like crates), which are heavy, can only be unloaded or sling loaded. +-- Moose provides you with an additional menu system to unload stationary cargo from the carrier bays, +-- using the menu. These menu options will become available, when the carrier is within the deploy zone. +-- +-- - **Sling Deploying**: Stationary cargo (like crates), which are heavy, can also be sling deployed. +-- Once the cargo is within the deploy zone, the cargo can be deployed from the sling onto the ground. +-- +-- In order to be able to deploy cargo, you'll need to know where the deploy zone is located, right? +-- Fortunately, the HQ can help to route you to the locations of deploy zone. +-- Use the task action menu to receive HQ help for this. +-- +-- ![](../Tasking/Routing_Deploy_Zone_Menu.png) +-- +-- Depending on the location within the battlefield, the task action menu will contain **Route options** that can be selected +-- to start the HQ sending you routing messages. Also, if the carrier cargo bays contain cargo, +-- then beside **Route options** there will also be **Deploy options** listed. +-- These **Deploy options** are meant to route you to the deploy zone locations. +-- +-- ![](../Tasking/Routing_Deploy_Zone_Menu_Workplace.png) +-- +-- Depending on the task that you have selected, the deploy zones will be listed. +-- **There may be multiple deploy zones within the mission, but only the deploy zones relevant for your task will be available in the menu!** +-- +-- ![](../Tasking/Routing_Deploy_Zone_Message.png) +-- +-- When a routing option is selected, you are sent routing messages in a selected coordinate format. +-- Possible routing coordinate formats are: Bearing Range (BR), Lattitude Longitude (LL) or Military Grid System (MGRS). +-- Note that for LL, there are two sub formats. (See pickup). +-- +-- ![](../Tasking/Routing_Deploy_Zone_Arrived.png) +-- +-- When you are within the range of the deploy zone (can be also a polygon!), a message is communicated by HQ that you have arrived within the zone! +-- +-- The routing messages are formulated in the coordinate format that is currently active as configured in your settings profile. +-- ![Task_Types](../Tasking/Task_Cargo_Settings.JPG) +-- Use the **Settings Menu** to select the coordinate format that you would like to use for location determination. +-- +-- #### Unboard Cargo. +-- +-- If your carrier contains cargo, and the cargo is **moveable**, the **cargo can be unboarded**! +-- You can only unload cargo if there is cargo within your cargo bays within the carrier. +-- +-- ![](../Tasking/Unboarding_Menu.png) +-- +-- Select the task action menu and now an **Unboard cargo...** sub menu will be listed! +-- Again, this option will only be listed if there is a non moveable cargo within your cargo bays. +-- +-- ![](../Tasking/Unboarding_Menu_Engineers.png) +-- +-- Now you will see a menu option to unload the non-moveable cargo. +-- In this example, you can unload the **Engineers** that was loaded within your carrier cargo bays. +-- Depending on the cargo loaded within your cargo bays, you will see other options here! +-- Select the relevant menu option from the cargo unload menu, and the cargo will unloaded from your carrier. +-- +-- ![](../Tasking/Unboarding_Started.png) +-- +-- **The cargo will step out of your carrier and will move towards a grouping point.** +-- When the unboarding process has started, you will be notified by a message to your carrier. +-- +-- ![](../Tasking/Unboarding_In_Progress.png) +-- +-- The moveable cargo will unboard one by one, so note that multiple units may need to unboard your Carrier, +-- so it is required to await the full completion of the unboarding process. +-- +-- ![](../Tasking/Unboarding_Done.png) +-- +-- Once the cargo is fully unboarded from your carrier, you will be notified of this. +-- +-- **Remarks:** +-- +-- * For airborne carriers, it is required to land first before the unboarding process can be initiated. +-- If during unboarding the Carrier gets airborne, the unboarding process will be cancelled. +-- * Once the moveable cargo is unboarded, they will start moving towards a specified gathering point. +-- * The moveable cargo will send a message to your carrier with unboarding status updates. +-- +-- **Deploying a cargo within a deployment zone, may complete a deployment task! So ensure that you deploy the right cargo at the right deployment zone!** +-- +-- #### Unload Cargo. +-- +-- If your carrier contains cargo, and the cargo is **stationary**, the **cargo can be unloaded**, but not unboarded! +-- You can only unload cargo if there is cargo within your cargo bays within the carrier. +-- +-- ![](../Tasking/Unloading_Menu.png) +-- +-- Select the task action menu and now an **Unload cargo...** sub menu will be listed! +-- Again, this option will only be listed if there is a non moveable cargo within your cargo bays. +-- +-- ![](../Tasking/Unloading_Menu_Crate.png) +-- +-- Now you will see a menu option to unload the non-moveable cargo. +-- In this example, you can unload the **Crate** that was loaded within your carrier cargo bays. +-- Depending on the cargo loaded within your cargo bays, you will see other options here! +-- Select the relevant menu option from the cargo unload menu, and the cargo will unloaded from your carrier. +-- +-- ![](../Tasking/Unloading_Done.png) +-- +-- Once the cargo is unloaded fom your Carrier, you may be notified of this, when there is a truck near to the cargo. +-- If there is no truck near to the unload area, no message will be sent to your carrier! +-- +-- **Remarks:** +-- +-- * For airborne Carriers, it is required to land first, before the unloading process can be initiated. +-- * A truck must be near the unload area to get messages to your carrier of the unload event! +-- * Unloading is only for non-moveable cargo. +-- * The non-moveable cargo must be within your cargo bays, or no unload option will be available. +-- +-- **Deploying a cargo within a deployment zone, may complete a deployment task! So ensure that you deploy the right cargo at the right deployment zone!** +-- +-- +-- #### Sling Deploy Cargo (helicopters only). +-- +-- If your Carrier is within the **deploy zone**, and the cargo is **stationary**, the **cargo can also be sling deploying**! +-- Note that this is only possible for helicopters. +-- +-- To sling deploy cargo, there is no task action menu required. Just follow the normal sling deploying procedure. +-- +-- **Deploying a cargo within a deployment zone, may complete a deployment task! So ensure that you deploy the right cargo at the right deployment zone!** +-- +-- ## Cargo tasking from a mission designer perspective. +-- +-- Please consult the documentation how to implement the derived classes of SET_CARGO in: +-- +-- - @{Tasking.Task_Cargo#TASK_CARGO}: Documents the main methods how to handle the cargo tasking from a mission designer perspective. +-- - @{Tasking.Task_Cargo#TASK_CARGO_TRANSPORT}: Documents the specific methods how to handle the cargo transportation tasking from a mission designer perspective. +-- - @{Tasking.Task_Cargo#TASK_CARGO_CSAR}: Documents the specific methods how to handle the cargo CSAR tasking from a mission designer perspective. +-- +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- ### Contributions: +-- +-- === +-- +-- @module Tasking.Task_Cargo +-- @image MOOSE.JPG + +do -- TASK_CARGO + + --- @type TASK_CARGO + -- @extends Tasking.Task#TASK + + --- Model tasks for players to transport Cargo. + -- + -- This models the process of a flexible transporation tasking system of cargo. + -- + -- # 1) A flexible tasking system. + -- + -- The TASK_CARGO classes provide you with a flexible tasking sytem, + -- that allows you to transport cargo of various types between various locations + -- and various dedicated deployment zones. + -- + -- The cargo in scope of the TASK\_CARGO classes must be explicitly given, and is of type SET\_CARGO. + -- The SET_CARGO contains a collection of CARGO objects that must be handled by the players in the mission. + -- + -- # 2) Cargo Tasking from a mission designer perspective. + -- + -- A cargo task is governed by a @{Tasking.Mission} object. Tasks are of different types. + -- The @{#TASK} object is used or derived by more detailed tasking classes that will implement the task execution mechanisms + -- and goals. + -- + -- ## 2.1) Derived cargo task classes. + -- + -- The following TASK_CARGO classes are derived from @{#TASK}. + -- + -- TASK + -- TASK_CARGO + -- TASK_CARGO_TRANSPORT + -- TASK_CARGO_CSAR + -- + -- ### 2.1.1) Cargo Tasks + -- + -- - @{Tasking.Task_Cargo#TASK_CARGO_TRANSPORT} - Models the transportation of cargo to deployment zones. + -- - @{Tasking.Task_Cargo#TASK_CARGO_CSAR} - Models the rescue of downed friendly pilots from behind enemy lines. + -- + -- ## 2.2) Handle TASK_CARGO Events ... + -- + -- The TASK_CARGO classes define @{Cargo} transport tasks, + -- based on the tasking capabilities defined in @{Tasking.Task#TASK}. + -- + -- ### 2.2.1) Boarding events. + -- + -- Specific Cargo event can be captured, that allow to trigger specific actions! + -- + -- * **Boarded**: Triggered when the Cargo has been Boarded into your Carrier. + -- * **UnBoarded**: Triggered when the cargo has been Unboarded from your Carrier and has arrived at the Deployment Zone. + -- + -- ### 2.2.2) Loading events. + -- + -- Specific Cargo event can be captured, that allow to trigger specific actions! + -- + -- * **Loaded**: Triggered when the Cargo has been Loaded into your Carrier. + -- * **UnLoaded**: Triggered when the cargo has been Unloaded from your Carrier and has arrived at the Deployment Zone. + -- + -- ### 2.2.2) Standard TASK_CARGO Events + -- + -- The TASK_CARGO is implemented using a @{Core.Fsm#FSM_TASK}, and has the following standard statuses: + -- + -- * **None**: Start of the process. + -- * **Planned**: The cargo task is planned. + -- * **Assigned**: The cargo task is assigned to a @{Wrapper.Group#GROUP}. + -- * **Success**: The cargo task is successfully completed. + -- * **Failed**: The cargo task has failed. This will happen if the player exists the task early, without communicating a possible cancellation to HQ. + -- + -- + -- + -- === + -- + -- @field #TASK_CARGO + TASK_CARGO = { + ClassName = "TASK_CARGO", + } + + --- Instantiates a new TASK_CARGO. + -- @param #TASK_CARGO self + -- @param Tasking.Mission#MISSION Mission + -- @param Core.Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. + -- @param #string TaskName The name of the Task. + -- @param Core.Set#SET_CARGO SetCargo The scope of the cargo to be transported. + -- @param #string TaskType The type of Cargo task. + -- @param #string TaskBriefing The Cargo Task briefing. + -- @return #TASK_CARGO self + function TASK_CARGO:New( Mission, SetGroup, TaskName, SetCargo, TaskType, TaskBriefing ) + local self = BASE:Inherit( self, TASK:New( Mission, SetGroup, TaskName, TaskType, TaskBriefing ) ) -- #TASK_CARGO + self:F( {Mission, SetGroup, TaskName, SetCargo, TaskType}) + + self.SetCargo = SetCargo + self.TaskType = TaskType + self.SmokeColor = SMOKECOLOR.Red + + self.CargoItemCount = {} -- Map of Carriers having a cargo item count to check the cargo loading limits. + self.CargoLimit = 10 + + self.DeployZones = {} -- setmetatable( {}, { __mode = "v" } ) -- weak table on value + + self:AddTransition( "*", "CargoDeployed", "*" ) + + --- CargoDeployed Handler OnBefore for TASK_CARGO + -- @function [parent=#TASK_CARGO] OnBeforeCargoDeployed + -- @param #TASK_CARGO self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Wrapper.Unit#UNIT TaskUnit The Unit (Client) that Deployed the cargo. You can use this to retrieve the PlayerName etc. + -- @param Core.Cargo#CARGO Cargo The Cargo that got PickedUp by the TaskUnit. You can use this to check Cargo Status. + -- @param Core.Zone#ZONE DeployZone The zone where the Cargo got Deployed or UnBoarded. + -- @return #boolean + + --- CargoDeployed Handler OnAfter for TASK_CARGO + -- @function [parent=#TASK_CARGO] OnAfterCargoDeployed + -- @param #TASK_CARGO self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Wrapper.Unit#UNIT TaskUnit The Unit (Client) that Deployed the cargo. You can use this to retrieve the PlayerName etc. + -- @param Core.Cargo#CARGO Cargo The Cargo that got PickedUp by the TaskUnit. You can use this to check Cargo Status. + -- @param Core.Zone#ZONE DeployZone The zone where the Cargo got Deployed or UnBoarded. + -- @usage + -- + -- -- Add a Transport task to transport cargo of different types to a Transport Deployment Zone. + -- TaskDispatcher = TASK_CARGO_DISPATCHER:New( Mission, TransportGroups ) + -- + -- local CargoSetWorkmaterials = SET_CARGO:New():FilterTypes( "Workmaterials" ):FilterStart() + -- local EngineerCargoGroup = CARGO_GROUP:New( GROUP:FindByName( "Engineers" ), "Workmaterials", "Engineers", 250 ) + -- local ConcreteCargo = CARGO_SLINGLOAD:New( STATIC:FindByName( "Concrete" ), "Workmaterials", "Concrete", 150, 50 ) + -- local CrateCargo = CARGO_CRATE:New( STATIC:FindByName( "Crate" ), "Workmaterials", "Crate", 150, 50 ) + -- local EnginesCargo = CARGO_CRATE:New( STATIC:FindByName( "Engines" ), "Workmaterials", "Engines", 150, 50 ) + -- local MetalCargo = CARGO_CRATE:New( STATIC:FindByName( "Metal" ), "Workmaterials", "Metal", 150, 50 ) + -- + -- -- Here we add the task. We name the task "Build a Workplace". + -- -- We provide the CargoSetWorkmaterials, and a briefing as the 2nd and 3rd parameter. + -- -- The :AddTransportTask() returns a Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT object, which we keep as a reference for further actions. + -- -- The WorkplaceTask holds the created and returned Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT object. + -- local WorkplaceTask = TaskDispatcher:AddTransportTask( "Build a Workplace", CargoSetWorkmaterials, "Transport the workers, engineers and the equipment near the Workplace." ) + -- + -- -- Here we set a TransportDeployZone. We use the WorkplaceTask as the reference, and provide a ZONE object. + -- TaskDispatcher:SetTransportDeployZone( WorkplaceTask, ZONE:New( "Workplace" ) ) + -- + -- Helos = { SPAWN:New( "Helicopters 1" ), SPAWN:New( "Helicopters 2" ), SPAWN:New( "Helicopters 3" ), SPAWN:New( "Helicopters 4" ), SPAWN:New( "Helicopters 5" ) } + -- EnemyHelos = { SPAWN:New( "Enemy Helicopters 1" ), SPAWN:New( "Enemy Helicopters 2" ), SPAWN:New( "Enemy Helicopters 3" ) } + -- + -- -- This is our worker method! So when a cargo is deployed within a deployment zone, this method will be called. + -- -- By example we are spawning here a random friendly helicopter and a random enemy helicopter. + -- function WorkplaceTask:OnAfterCargoDeployed( From, Event, To, TaskUnit, Cargo, DeployZone ) + -- Helos[ math.random(1,#Helos) ]:Spawn() + -- EnemyHelos[ math.random(1,#EnemyHelos) ]:Spawn() + -- end + + self:AddTransition( "*", "CargoPickedUp", "*" ) + + --- CargoPickedUp Handler OnBefore for TASK_CARGO + -- @function [parent=#TASK_CARGO] OnBeforeCargoPickedUp + -- @param #TASK_CARGO self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Wrapper.Unit#UNIT TaskUnit The Unit (Client) that PickedUp the cargo. You can use this to retrieve the PlayerName etc. + -- @param Core.Cargo#CARGO Cargo The Cargo that got PickedUp by the TaskUnit. You can use this to check Cargo Status. + -- @return #boolean + + --- CargoPickedUp Handler OnAfter for TASK_CARGO + -- @function [parent=#TASK_CARGO] OnAfterCargoPickedUp + -- @param #TASK_CARGO self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Wrapper.Unit#UNIT TaskUnit The Unit (Client) that PickedUp the cargo. You can use this to retrieve the PlayerName etc. + -- @param Core.Cargo#CARGO Cargo The Cargo that got PickedUp by the TaskUnit. You can use this to check Cargo Status. + + + local Fsm = self:GetUnitProcess() + +-- Fsm:SetStartState( "Planned" ) +-- +-- Fsm:AddProcess ( "Planned", "Accept", ACT_ASSIGN_ACCEPT:New( self.TaskBriefing ), { Assigned = "SelectAction", Rejected = "Reject" } ) + + Fsm:AddTransition( { "Planned", "Assigned", "Cancelled", "WaitingForCommand", "ArrivedAtPickup", "ArrivedAtDeploy", "Boarded", "UnBoarded", "Loaded", "UnLoaded", "Landed", "Boarding" }, "SelectAction", "*" ) + + Fsm:AddTransition( "*", "RouteToPickup", "RoutingToPickup" ) + Fsm:AddProcess ( "RoutingToPickup", "RouteToPickupPoint", ACT_ROUTE_POINT:New(), { Arrived = "ArriveAtPickup", Cancelled = "CancelRouteToPickup" } ) + Fsm:AddTransition( "Arrived", "ArriveAtPickup", "ArrivedAtPickup" ) + Fsm:AddTransition( "Cancelled", "CancelRouteToPickup", "Cancelled" ) + + Fsm:AddTransition( "*", "RouteToDeploy", "RoutingToDeploy" ) + Fsm:AddProcess ( "RoutingToDeploy", "RouteToDeployZone", ACT_ROUTE_ZONE:New(), { Arrived = "ArriveAtDeploy", Cancelled = "CancelRouteToDeploy" } ) + Fsm:AddTransition( "Arrived", "ArriveAtDeploy", "ArrivedAtDeploy" ) + Fsm:AddTransition( "Cancelled", "CancelRouteToDeploy", "Cancelled" ) + + Fsm:AddTransition( { "ArrivedAtPickup", "ArrivedAtDeploy", "Landing" }, "Land", "Landing" ) + Fsm:AddTransition( "Landing", "Landed", "Landed" ) + + Fsm:AddTransition( "*", "PrepareBoarding", "AwaitBoarding" ) + Fsm:AddTransition( "AwaitBoarding", "Board", "Boarding" ) + Fsm:AddTransition( "Boarding", "Boarded", "Boarded" ) + + Fsm:AddTransition( "*", "Load", "Loaded" ) + + Fsm:AddTransition( "*", "PrepareUnBoarding", "AwaitUnBoarding" ) + Fsm:AddTransition( "AwaitUnBoarding", "UnBoard", "UnBoarding" ) + Fsm:AddTransition( "UnBoarding", "UnBoarded", "UnBoarded" ) + + Fsm:AddTransition( "*", "Unload", "Unloaded" ) + + Fsm:AddTransition( "*", "Planned", "Planned" ) + + + Fsm:AddTransition( "Deployed", "Success", "Success" ) + Fsm:AddTransition( "Rejected", "Reject", "Aborted" ) + Fsm:AddTransition( "Failed", "Fail", "Failed" ) + + ---- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param #TASK_CARGO Task + function Fsm:OnAfterAssigned( TaskUnit, Task ) + self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + + self:SelectAction() + end + + + + --- + -- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param #TASK_CARGO Task + function Fsm:onafterSelectAction( TaskUnit, Task ) + + local TaskUnitName = TaskUnit:GetName() + local MenuTime = Task:InitTaskControlMenu( TaskUnit ) + local MenuControl = Task:GetTaskControlMenu( TaskUnit ) + + Task.SetCargo:ForEachCargo( + + --- @param Cargo.Cargo#CARGO Cargo + function( Cargo ) + + if Cargo:IsAlive() then + +-- if Task:is( "RoutingToPickup" ) then +-- MENU_GROUP_COMMAND:New( +-- TaskUnit:GetGroup(), +-- "Cancel Route " .. Cargo.Name, +-- MenuControl, +-- self.MenuRouteToPickupCancel, +-- self, +-- Cargo +-- ):SetTime(MenuTime) +-- end + + --self:F( { CargoUnloaded = Cargo:IsUnLoaded(), CargoLoaded = Cargo:IsLoaded(), CargoItemCount = CargoItemCount } ) + + local TaskGroup = TaskUnit:GetGroup() + + if Cargo:IsUnLoaded() then + local CargoBayFreeWeight = TaskUnit:GetCargoBayFreeWeight() + local CargoWeight = Cargo:GetWeight() + + self:F({CargoBayFreeWeight=CargoBayFreeWeight}) + + -- Only when there is space within the bay to load the next cargo item! + if CargoBayFreeWeight > CargoWeight then + if Cargo:IsInReportRadius( TaskUnit:GetPointVec2() ) then + local NotInDeployZones = true + for DeployZoneName, DeployZone in pairs( Task.DeployZones ) do + if Cargo:IsInZone( DeployZone ) then + NotInDeployZones = false + end + end + if NotInDeployZones then + if not TaskUnit:InAir() then + if Cargo:CanBoard() == true then + if Cargo:IsInLoadRadius( TaskUnit:GetPointVec2() ) then + Cargo:Report( "Ready for boarding.", "board", TaskUnit:GetGroup() ) + local BoardMenu = MENU_GROUP:New( TaskGroup, "Board cargo", MenuControl ):SetTime( MenuTime ):SetTag( "Cargo" ) + MENU_GROUP_COMMAND:New( TaskUnit:GetGroup(), Cargo.Name, BoardMenu, self.MenuBoardCargo, self, Cargo ):SetTime(MenuTime):SetTag("Cargo"):SetRemoveParent() + else + Cargo:Report( "Board at " .. Cargo:GetCoordinate():ToString( TaskUnit:GetGroup() .. "." ), "reporting", TaskUnit:GetGroup() ) + end + else + if Cargo:CanLoad() == true then + if Cargo:IsInLoadRadius( TaskUnit:GetPointVec2() ) then + Cargo:Report( "Ready for loading.", "load", TaskUnit:GetGroup() ) + local LoadMenu = MENU_GROUP:New( TaskGroup, "Load cargo", MenuControl ):SetTime( MenuTime ):SetTag( "Cargo" ) + MENU_GROUP_COMMAND:New( TaskUnit:GetGroup(), Cargo.Name, LoadMenu, self.MenuLoadCargo, self, Cargo ):SetTime(MenuTime):SetTag("Cargo"):SetRemoveParent() + else + Cargo:Report( "Load at " .. Cargo:GetCoordinate():ToString( TaskUnit:GetGroup() ) .. " within " .. Cargo.NearRadius .. ".", "reporting", TaskUnit:GetGroup() ) + end + else + --local Cargo = Cargo -- Cargo.CargoSlingload#CARGO_SLINGLOAD + if Cargo:CanSlingload() == true then + if Cargo:IsInLoadRadius( TaskUnit:GetPointVec2() ) then + Cargo:Report( "Ready for sling loading.", "slingload", TaskUnit:GetGroup() ) + local SlingloadMenu = MENU_GROUP:New( TaskGroup, "Slingload cargo", MenuControl ):SetTime( MenuTime ):SetTag( "Cargo" ) + MENU_GROUP_COMMAND:New( TaskUnit:GetGroup(), Cargo.Name, SlingloadMenu, self.MenuLoadCargo, self, Cargo ):SetTime(MenuTime):SetTag("Cargo"):SetRemoveParent() + else + Cargo:Report( "Slingload at " .. Cargo:GetCoordinate():ToString( TaskUnit:GetGroup() ) .. ".", "reporting", TaskUnit:GetGroup() ) + end + end + end + end + else + Cargo:ReportResetAll( TaskUnit:GetGroup() ) + end + end + else + if not Cargo:IsDeployed() == true then + local RouteToPickupMenu = MENU_GROUP:New( TaskGroup, "Route to pickup cargo", MenuControl ):SetTime( MenuTime ):SetTag( "Cargo" ) + --MENU_GROUP_COMMAND:New( TaskUnit:GetGroup(), Cargo.Name, RouteToPickupMenu, self.MenuRouteToPickup, self, Cargo ):SetTime(MenuTime):SetTag("Cargo"):SetRemoveParent() + Cargo:ReportResetAll( TaskUnit:GetGroup() ) + if Cargo:CanBoard() == true then + if not Cargo:IsInLoadRadius( TaskUnit:GetPointVec2() ) then + local BoardMenu = MENU_GROUP:New( TaskGroup, "Board cargo", RouteToPickupMenu ):SetTime( MenuTime ):SetTag( "Cargo" ) + MENU_GROUP_COMMAND:New( TaskUnit:GetGroup(), Cargo.Name, BoardMenu, self.MenuRouteToPickup, self, Cargo ):SetTime(MenuTime):SetTag("Cargo"):SetRemoveParent() + end + else + if Cargo:CanLoad() == true then + if not Cargo:IsInLoadRadius( TaskUnit:GetPointVec2() ) then + local LoadMenu = MENU_GROUP:New( TaskGroup, "Load cargo", RouteToPickupMenu ):SetTime( MenuTime ):SetTag( "Cargo" ) + MENU_GROUP_COMMAND:New( TaskUnit:GetGroup(), Cargo.Name, LoadMenu, self.MenuRouteToPickup, self, Cargo ):SetTime(MenuTime):SetTag("Cargo"):SetRemoveParent() + end + else + --local Cargo = Cargo -- Cargo.CargoSlingload#CARGO_SLINGLOAD + if Cargo:CanSlingload() == true then + if not Cargo:IsInLoadRadius( TaskUnit:GetPointVec2() ) then + local SlingloadMenu = MENU_GROUP:New( TaskGroup, "Slingload cargo", RouteToPickupMenu ):SetTime( MenuTime ):SetTag( "Cargo" ) + MENU_GROUP_COMMAND:New( TaskUnit:GetGroup(), Cargo.Name, SlingloadMenu, self.MenuRouteToPickup, self, Cargo ):SetTime(MenuTime):SetTag("Cargo"):SetRemoveParent() + end + end + end + end + end + end + end + + -- Cargo in deployzones are flagged as deployed. + for DeployZoneName, DeployZone in pairs( Task.DeployZones ) do + if Cargo:IsInZone( DeployZone ) then + Task:I( { CargoIsDeployed = Task.CargoDeployed and "true" or "false" } ) + if Cargo:IsDeployed() == false then + Cargo:SetDeployed( true ) + -- Now we call a callback method to handle the CargoDeployed event. + Task:I( { CargoIsAlive = Cargo:IsAlive() and "true" or "false" } ) + if Cargo:IsAlive() then + Task:CargoDeployed( TaskUnit, Cargo, DeployZone ) + end + end + end + end + + end + + if Cargo:IsLoaded() == true and Cargo:IsLoadedInCarrier( TaskUnit ) == true then + if not TaskUnit:InAir() then + if Cargo:CanUnboard() == true then + local UnboardMenu = MENU_GROUP:New( TaskGroup, "Unboard cargo", MenuControl ):SetTime( MenuTime ):SetTag( "Cargo" ) + MENU_GROUP_COMMAND:New( TaskUnit:GetGroup(), Cargo.Name, UnboardMenu, self.MenuUnboardCargo, self, Cargo ):SetTime(MenuTime):SetTag("Cargo"):SetRemoveParent() + else + if Cargo:CanUnload() == true then + local UnloadMenu = MENU_GROUP:New( TaskGroup, "Unload cargo", MenuControl ):SetTime( MenuTime ):SetTag( "Cargo" ) + MENU_GROUP_COMMAND:New( TaskUnit:GetGroup(), Cargo.Name, UnloadMenu, self.MenuUnloadCargo, self, Cargo ):SetTime(MenuTime):SetTag("Cargo"):SetRemoveParent() + end + end + end + end + + -- Deployzones are optional zones that can be selected to request routing information. + for DeployZoneName, DeployZone in pairs( Task.DeployZones ) do + if not Cargo:IsInZone( DeployZone ) then + local RouteToDeployMenu = MENU_GROUP:New( TaskGroup, "Route to deploy cargo", MenuControl ):SetTime( MenuTime ):SetTag( "Cargo" ) + MENU_GROUP_COMMAND:New( TaskUnit:GetGroup(), "Zone " .. DeployZoneName, RouteToDeployMenu, self.MenuRouteToDeploy, self, DeployZone ):SetTime(MenuTime):SetTag("Cargo"):SetRemoveParent() + end + end + end + + end + ) + + Task:RefreshTaskControlMenu( TaskUnit, MenuTime, "Cargo" ) + + self:__SelectAction( -1 ) + + end + + + --- + -- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param #TASK_CARGO Task + function Fsm:OnLeaveWaitingForCommand( TaskUnit, Task ) + self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + + --local MenuControl = Task:GetTaskControlMenu( TaskUnit ) + + --MenuControl:Remove() + end + + function Fsm:MenuBoardCargo( Cargo ) + self:__PrepareBoarding( 1.0, Cargo ) + end + + function Fsm:MenuLoadCargo( Cargo ) + self:__Load( 1.0, Cargo ) + end + + function Fsm:MenuUnboardCargo( Cargo, DeployZone ) + self:__PrepareUnBoarding( 1.0, Cargo, DeployZone ) + end + + function Fsm:MenuUnloadCargo( Cargo, DeployZone ) + self:__Unload( 1.0, Cargo, DeployZone ) + end + + function Fsm:MenuRouteToPickup( Cargo ) + self:__RouteToPickup( 1.0, Cargo ) + end + + function Fsm:MenuRouteToDeploy( DeployZone ) + self:__RouteToDeploy( 1.0, DeployZone ) + end + + + + --- + --#TASK_CAROG_TRANSPORT self + --#Wrapper.Unit#UNIT + + + --- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task_Cargo#TASK_CARGO Task + -- @param From + -- @param Event + -- @param To + -- @param Core.Cargo#CARGO Cargo + function Fsm:onafterRouteToPickup( TaskUnit, Task, From, Event, To, Cargo ) + self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + + if Cargo:IsAlive() then + self.Cargo = Cargo -- Cargo.Cargo#CARGO + Task:SetCargoPickup( self.Cargo, TaskUnit ) + self:__RouteToPickupPoint( -0.1 ) + end + + end + + + + --- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task_Cargo#TASK_CARGO Task + function Fsm:onafterArriveAtPickup( TaskUnit, Task ) + self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + if self.Cargo:IsAlive() then + if TaskUnit:IsAir() then + Task:GetMission():GetCommandCenter():MessageToGroup( "Land", TaskUnit:GetGroup() ) + self:__Land( -0.1, "Pickup" ) + else + self:__SelectAction( -0.1 ) + end + end + end + + + --- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task_Cargo#TASK_CARGO Task + function Fsm:onafterCancelRouteToPickup( TaskUnit, Task ) + self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + + Task:GetMission():GetCommandCenter():MessageToGroup( "Cancelled routing to Cargo " .. self.Cargo:GetName(), TaskUnit:GetGroup() ) + self:__SelectAction( -0.1 ) + end + + + --- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + function Fsm:onafterRouteToDeploy( TaskUnit, Task, From, Event, To, DeployZone ) + self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + + self:F( DeployZone ) + self.DeployZone = DeployZone + Task:SetDeployZone( self.DeployZone, TaskUnit ) + self:__RouteToDeployZone( -0.1 ) + end + + + --- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task_Cargo#TASK_CARGO Task + function Fsm:onafterArriveAtDeploy( TaskUnit, Task ) + self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + + if TaskUnit:IsAir() then + Task:GetMission():GetCommandCenter():MessageToGroup( "Land", TaskUnit:GetGroup() ) + self:__Land( -0.1, "Deploy" ) + else + self:__SelectAction( -0.1 ) + end + end + + + --- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task_Cargo#TASK_CARGO Task + function Fsm:onafterCancelRouteToDeploy( TaskUnit, Task ) + self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + + Task:GetMission():GetCommandCenter():MessageToGroup( "Cancelled routing to deploy zone " .. self.DeployZone:GetName(), TaskUnit:GetGroup() ) + self:__SelectAction( -0.1 ) + end + + + + --- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task_Cargo#TASK_CARGO Task + function Fsm:onafterLand( TaskUnit, Task, From, Event, To, Action ) + self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + + if Action == "Pickup" then + if self.Cargo:IsAlive() then + if self.Cargo:IsInReportRadius( TaskUnit:GetPointVec2() ) then + if TaskUnit:InAir() then + self:__Land( -10, Action ) + else + Task:GetMission():GetCommandCenter():MessageToGroup( "Landed at pickup location...", TaskUnit:GetGroup() ) + self:__Landed( -0.1, Action ) + end + else + self:__RouteToPickup( -0.1, self.Cargo ) + end + end + else + if TaskUnit:IsAlive() then + if TaskUnit:IsInZone( self.DeployZone ) then + if TaskUnit:InAir() then + self:__Land( -10, Action ) + else + Task:GetMission():GetCommandCenter():MessageToGroup( "Landed at deploy zone " .. self.DeployZone:GetName(), TaskUnit:GetGroup() ) + self:__Landed( -0.1, Action ) + end + else + self:__RouteToDeploy( -0.1, self.Cargo ) + end + end + end + end + + --- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task_Cargo#TASK_CARGO Task + function Fsm:onafterLanded( TaskUnit, Task, From, Event, To, Action ) + self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + + if Action == "Pickup" then + if self.Cargo:IsAlive() then + if self.Cargo:IsInReportRadius( TaskUnit:GetPointVec2() ) then + if TaskUnit:InAir() then + self:__Land( -0.1, Action ) + else + self:__SelectAction( -0.1 ) + end + else + self:__RouteToPickup( -0.1, self.Cargo ) + end + end + else + if TaskUnit:IsAlive() then + if TaskUnit:IsInZone( self.DeployZone ) then + if TaskUnit:InAir() then + self:__Land( -10, Action ) + else + self:__SelectAction( -0.1 ) + end + else + self:__RouteToDeploy( -0.1, self.Cargo ) + end + end + end + end + + --- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task_Cargo#TASK_CARGO Task + function Fsm:onafterPrepareBoarding( TaskUnit, Task, From, Event, To, Cargo ) + self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + + if Cargo and Cargo:IsAlive() then + self:__Board( -0.1, Cargo ) + end + end + + + --- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task_Cargo#TASK_CARGO Task + function Fsm:onafterBoard( TaskUnit, Task, From, Event, To, Cargo ) + self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + + function Cargo:OnEnterLoaded( From, Event, To, TaskUnit, TaskProcess ) + self:F({From, Event, To, TaskUnit, TaskProcess }) + TaskProcess:__Boarded( 0.1, self ) + end + + if Cargo:IsAlive() then + if Cargo:IsInLoadRadius( TaskUnit:GetPointVec2() ) then + if TaskUnit:InAir() then + --- ABORT the boarding. Split group if any and go back to select action. + else + Cargo:MessageToGroup( "Boarding ...", TaskUnit:GetGroup() ) + if not Cargo:IsBoarding() then + Cargo:Board( TaskUnit, nil, self ) + end + end + else + --self:__ArriveAtCargo( -0.1 ) + end + end + end + + + --- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task_Cargo#TASK_CARGO Task + function Fsm:onafterBoarded( TaskUnit, Task, From, Event, To, Cargo ) + + local TaskUnitName = TaskUnit:GetName() + self:F( { TaskUnit = TaskUnitName, Task = Task and Task:GetClassNameAndID() } ) + + Cargo:MessageToGroup( "Boarded cargo " .. Cargo:GetName(), TaskUnit:GetGroup() ) + + self:__Load( -0.1, Cargo ) + + end + + + --- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task_Cargo#TASK_CARGO Task + function Fsm:onafterLoad( TaskUnit, Task, From, Event, To, Cargo ) + + local TaskUnitName = TaskUnit:GetName() + self:F( { TaskUnit = TaskUnitName, Task = Task and Task:GetClassNameAndID() } ) + + if not Cargo:IsLoaded() then + Cargo:Load( TaskUnit ) + end + + Cargo:MessageToGroup( "Loaded cargo " .. Cargo:GetName(), TaskUnit:GetGroup() ) + TaskUnit:AddCargo( Cargo ) + + Task:CargoPickedUp( TaskUnit, Cargo ) + + self:SelectAction( -1 ) + + end + + + --- + -- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task_Cargo#TASK_CARGO Task + -- @param From + -- @param Event + -- @param To + -- @param Cargo + -- @param Core.Zone#ZONE_BASE DeployZone + function Fsm:onafterPrepareUnBoarding( TaskUnit, Task, From, Event, To, Cargo ) + self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID(), From, Event, To, Cargo } ) + + self.Cargo = Cargo + self.DeployZone = nil + + -- Check if the Cargo is at a deployzone... If it is, provide it as a parameter! + if Cargo:IsAlive() then + for DeployZoneName, DeployZone in pairs( Task.DeployZones ) do + if Cargo:IsInZone( DeployZone ) then + self.DeployZone = DeployZone -- Core.Zone#ZONE_BASE + break + end + end + self:__UnBoard( -0.1, Cargo, self.DeployZone ) + end + end + + --- + -- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task_Cargo#TASK_CARGO Task + -- @param From + -- @param Event + -- @param To + -- @param Cargo + -- @param Core.Zone#ZONE_BASE DeployZone + function Fsm:onafterUnBoard( TaskUnit, Task, From, Event, To, Cargo, DeployZone ) + self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID(), From, Event, To, Cargo, DeployZone } ) + + function self.Cargo:OnEnterUnLoaded( From, Event, To, DeployZone, TaskProcess ) + self:F({From, Event, To, DeployZone, TaskProcess }) + TaskProcess:__UnBoarded( -0.1 ) + end + + if self.Cargo:IsAlive() then + self.Cargo:MessageToGroup( "UnBoarding ...", TaskUnit:GetGroup() ) + if DeployZone then + self.Cargo:UnBoard( DeployZone:GetCoordinate():GetRandomCoordinateInRadius( 25, 10 ), 400, self ) + else + self.Cargo:UnBoard( TaskUnit:GetCoordinate():GetRandomCoordinateInRadius( 25, 10 ), 400, self ) + end + end + end + + + --- + -- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task_Cargo#TASK_CARGO Task + function Fsm:onafterUnBoarded( TaskUnit, Task ) + + local TaskUnitName = TaskUnit:GetName() + self:F( { TaskUnit = TaskUnitName, Task = Task and Task:GetClassNameAndID() } ) + + self.Cargo:MessageToGroup( "UnBoarded cargo " .. self.Cargo:GetName(), TaskUnit:GetGroup() ) + + self:Unload( self.Cargo ) + end + + --- + -- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task_Cargo#TASK_CARGO Task + function Fsm:onafterUnload( TaskUnit, Task, From, Event, To, Cargo, DeployZone ) + + local TaskUnitName = TaskUnit:GetName() + self:F( { TaskUnit = TaskUnitName, Task = Task and Task:GetClassNameAndID() } ) + + if not Cargo:IsUnLoaded() then + if DeployZone then + Cargo:UnLoad( DeployZone:GetCoordinate():GetRandomCoordinateInRadius( 25, 10 ), 400, self ) + else + Cargo:UnLoad( TaskUnit:GetCoordinate():GetRandomCoordinateInRadius( 25, 10 ), 400, self ) + end + end + TaskUnit:RemoveCargo( Cargo ) + + Cargo:MessageToGroup( "Unloaded cargo " .. Cargo:GetName(), TaskUnit:GetGroup() ) + + self:Planned() + self:__SelectAction( 1 ) + end + + return self + + end + + + --- Set a limit on the amount of cargo items that can be loaded into the Carriers. + -- @param #TASK_CARGO self + -- @param CargoLimit Specifies a number of cargo items that can be loaded in the helicopter. + -- @return #TASK_CARGO + function TASK_CARGO:SetCargoLimit( CargoLimit ) + self.CargoLimit = CargoLimit + return self + end + + + ---@param Color Might be SMOKECOLOR.Blue, SMOKECOLOR.Red SMOKECOLOR.Orange, SMOKECOLOR.White or SMOKECOLOR.Green + function TASK_CARGO:SetSmokeColor(SmokeColor) + -- Makes sure Coloe is set + if SmokeColor == nil then + self.SmokeColor = SMOKECOLOR.Red -- Make sure a default color is exist + + elseif type(SmokeColor) == "number" then + self:F2(SmokeColor) + if SmokeColor > 0 and SmokeColor <=5 then -- Make sure number is within ragne, assuming first enum is one + self.SmokeColor = SMOKECOLOR.SmokeColor + end + end + end + + --@return SmokeColor + function TASK_CARGO:GetSmokeColor() + return self.SmokeColor + end + + --- @param #TASK_CARGO self + function TASK_CARGO:GetPlannedMenuText() + return self:GetStateString() .. " - " .. self:GetTaskName() .. " ( " .. self.TargetSetUnit:GetUnitTypesText() .. " )" + end + + --- @param #TASK_CARGO self + -- @return Core.Set#SET_CARGO The Cargo Set. + function TASK_CARGO:GetCargoSet() + + return self.SetCargo + end + + --- @param #TASK_CARGO self + -- @return #list The Deployment Zones. + function TASK_CARGO:GetDeployZones() + + return self.DeployZones + end + + --- @param #TASK_CARGO self + -- @param AI.AI_Cargo#AI_CARGO Cargo The cargo. + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return #TASK_CARGO + function TASK_CARGO:SetCargoPickup( Cargo, TaskUnit ) + + self:F({Cargo, TaskUnit}) + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + local MenuTime = self:InitTaskControlMenu( TaskUnit ) + local MenuControl = self:GetTaskControlMenu( TaskUnit ) + + local ActRouteCargo = ProcessUnit:GetProcess( "RoutingToPickup", "RouteToPickupPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT + ActRouteCargo:Reset() + ActRouteCargo:SetCoordinate( Cargo:GetCoordinate() ) + ActRouteCargo:SetRange( Cargo:GetLoadRadius() ) + ActRouteCargo:SetMenuCancel( TaskUnit:GetGroup(), "Cancel Routing to Cargo " .. Cargo:GetName(), MenuControl, MenuTime, "Cargo" ) + ActRouteCargo:Start() + + return self + end + + + --- @param #TASK_CARGO self + -- @param Core.Zone#ZONE DeployZone + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return #TASK_CARGO + function TASK_CARGO:SetDeployZone( DeployZone, TaskUnit ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + local MenuTime = self:InitTaskControlMenu( TaskUnit ) + local MenuControl = self:GetTaskControlMenu( TaskUnit ) + + local ActRouteDeployZone = ProcessUnit:GetProcess( "RoutingToDeploy", "RouteToDeployZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE + ActRouteDeployZone:Reset() + ActRouteDeployZone:SetZone( DeployZone ) + ActRouteDeployZone:SetMenuCancel( TaskUnit:GetGroup(), "Cancel Routing to Deploy Zone" .. DeployZone:GetName(), MenuControl, MenuTime, "Cargo" ) + ActRouteDeployZone:Start() + + return self + end + + + --- @param #TASK_CARGO self + -- @param Core.Zone#ZONE DeployZone + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return #TASK_CARGO + function TASK_CARGO:AddDeployZone( DeployZone, TaskUnit ) + + self.DeployZones[DeployZone:GetName()] = DeployZone + + return self + end + + --- @param #TASK_CARGO self + -- @param Core.Zone#ZONE DeployZone + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return #TASK_CARGO + function TASK_CARGO:RemoveDeployZone( DeployZone, TaskUnit ) + + self.DeployZones[DeployZone:GetName()] = nil + + return self + end + + --- @param #TASK_CARGO self + -- @param #list DeployZones + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return #TASK_CARGO + function TASK_CARGO:SetDeployZones( DeployZones, TaskUnit ) + + for DeployZoneID, DeployZone in pairs( DeployZones or {} ) do + self.DeployZones[DeployZone:GetName()] = DeployZone + end + + return self + end + + + + --- @param #TASK_CARGO self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return Core.Zone#ZONE_BASE The Zone object where the Target is located on the map. + function TASK_CARGO:GetTargetZone( TaskUnit ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE + return ActRouteTarget:GetZone() + end + + --- Set a score when progress is made. + -- @param #TASK_CARGO self + -- @param #string Text The text to display to the player, when there is progress on the task goals. + -- @param #number Score The score in points. + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return #TASK_CARGO + function TASK_CARGO:SetScoreOnProgress( Text, Score, TaskUnit ) + self:F( { Text, Score, TaskUnit } ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + ProcessUnit:AddScoreProcess( "Engaging", "Account", "Account", Text, Score ) + + return self + end + + --- Set a score when success is achieved. + -- @param #TASK_CARGO self + -- @param #string Text The text to display to the player, when the task goals have been achieved. + -- @param #number Score The score in points. + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return #TASK_CARGO + function TASK_CARGO:SetScoreOnSuccess( Text, Score, TaskUnit ) + self:F( { Text, Score, TaskUnit } ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + ProcessUnit:AddScore( "Success", Text, Score ) + + return self + end + + --- Set a penalty when the task goals have failed.. + -- @param #TASK_CARGO self + -- @param #string Text The text to display to the player, when the task goals has failed. + -- @param #number Penalty The penalty in points. + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return #TASK_CARGO + function TASK_CARGO:SetScoreOnFail( Text, Penalty, TaskUnit ) + self:F( { Text, Score, TaskUnit } ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + ProcessUnit:AddScore( "Failed", Text, Penalty ) + + return self + end + + function TASK_CARGO:SetGoalTotal() + + self.GoalTotal = self.SetCargo:Count() + end + + function TASK_CARGO:GetGoalTotal() + + return self.GoalTotal + end + + --- @param #TASK_CARGO self + function TASK_CARGO:UpdateTaskInfo() + + if self:IsStatePlanned() or self:IsStateAssigned() then + self.TaskInfo:AddTaskName( 0, "MSOD" ) + self.TaskInfo:AddCargoSet( self.SetCargo, 10, "SOD", true ) + local Coordinates = {} + for CargoName, Cargo in pairs( self.SetCargo:GetSet() ) do + local Cargo = Cargo -- Cargo.Cargo#CARGO + if not Cargo:IsLoaded() then + Coordinates[#Coordinates+1] = Cargo:GetCoordinate() + end + end + self.TaskInfo:AddCoordinates( Coordinates, 1, "M" ) + end + end + + function TASK_CARGO:ReportOrder( ReportGroup ) + + return 0 + end + + --- This function is called from the @{Tasking.CommandCenter#COMMANDCENTER} to determine the method of automatic task selection. + -- @param #TASK_CARGO self + -- @param #number AutoAssignMethod The method to be applied to the task. + -- @param Wrapper.Group#GROUP TaskGroup The player group. + function TASK_CARGO:GetAutoAssignPriority( AutoAssignMethod, TaskGroup ) + + if AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Random then + return math.random( 1, 9 ) + elseif AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Distance then + return 0 + elseif AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Priority then + return 1 + end + + return 0 + end + + + +end + + +--- **Tasking** -- Models tasks for players to transport cargo. +-- +-- **Specific features:** +-- +-- * Creates a task to transport #Cargo.Cargo to and between deployment zones. +-- * Derived from the TASK_CARGO class, which is derived from the TASK class. +-- * Orchestrate the task flow, so go from Planned to Assigned to Success, Failed or Cancelled. +-- * Co-operation tasking, so a player joins a group of players executing the same task. +-- +-- +-- **A complete task menu system to allow players to:** +-- +-- * Join the task, abort the task. +-- * Mark the task location on the map. +-- * Provide details of the target. +-- * Route to the cargo. +-- * Route to the deploy zones. +-- * Load/Unload cargo. +-- * Board/Unboard cargo. +-- * Slingload cargo. +-- * Display the task briefing. +-- +-- +-- **A complete mission menu system to allow players to:** +-- +-- * Join a task, abort the task. +-- * Display task reports. +-- * Display mission statistics. +-- * Mark the task locations on the map. +-- * Provide details of the targets. +-- * Display the mission briefing. +-- * Provide status updates as retrieved from the command center. +-- * Automatically assign a random task as part of a mission. +-- * Manually assign a specific task as part of a mission. +-- +-- +-- **A settings system, using the settings menu:** +-- +-- * Tweak the duration of the display of messages. +-- * Switch between metric and imperial measurement system. +-- * Switch between coordinate formats used in messages: BR, BRA, LL DMS, LL DDM, MGRS. +-- * Different settings modes for A2G and A2A operations. +-- * Various other options. +-- +-- === +-- +-- Please read through the #Tasking.Task_Cargo process to understand the mechanisms of tasking and cargo tasking and handling. +-- +-- Enjoy! +-- FC +-- +-- === +-- +-- @module Tasking.Task_Cargo_Transport +-- @image Task_Cargo_Transport.JPG + + +do -- TASK_CARGO_TRANSPORT + + -- @type TASK_CARGO_TRANSPORT + -- @extends Tasking.Task_CARGO#TASK_CARGO + + --- Orchestrates the task for players to transport cargo to or between deployment zones. + -- + -- Transport tasks are suited to govern the process of transporting cargo to specific deployment zones. + -- Typically, this task is executed by helicopter pilots, but it can also be executed by ground forces! + -- + -- === + -- + -- A transport task can be created manually. + -- + -- # 1) Create a transport task manually (code it). + -- + -- Although it is recommended to use the dispatcher, you can create a transport task yourself as a mission designer. + -- It is easy, as it works just like any other task setup. + -- + -- ## 1.1) Create a command center. + -- + -- First you need to create a command center using the Tasking.CommandCenter#COMMANDCENTER.New constructor. + -- + -- local CommandCenter = COMMANDCENTER + -- :New( HQ, "Lima" ) -- Create the CommandCenter. + -- + -- ## 1.2) Create a mission. + -- + -- Tasks work in a mission, which groups these tasks to achieve a joint mission goal. + -- A command center can govern multiple missions. + -- Create a new mission, using the Tasking.Mission#MISSION.New constructor. + -- + -- -- Declare the Mission for the Command Center. + -- local Mission = MISSION + -- :New( CommandCenter, + -- "Overlord", + -- "High", + -- "Transport the cargo to the deploy zones.", + -- coalition.side.RED + -- ) + -- + -- ## 1.3) Create the transport cargo task. + -- + -- So, now that we have a command center and a mission, we now create the transport task. + -- We create the transport task using the #TASK_CARGO_TRANSPORT.New constructor. + -- + -- Because a transport task will not generate the cargo itself, you'll need to create it first. + -- The cargo in this case will be the downed pilot! + -- + -- -- Here we define the "cargo set", which is a collection of cargo objects. + -- -- The cargo set will be the input for the cargo transportation task. + -- -- So a transportation object is handling a cargo set, which is automatically refreshed when new cargo is added/deleted. + -- local CargoSet = SET_CARGO:New():FilterTypes( "Cargo" ):FilterStart() + -- + -- -- Now we add cargo into the battle scene. + -- local PilotGroup = GROUP:FindByName( "Engineers" ) + -- + -- -- CARGO_GROUP can be used to setup cargo with a GROUP object underneath. + -- -- We name this group Engineers. + -- -- Note that the name of the cargo is "Engineers". + -- -- The cargoset "CargoSet" will embed all defined cargo of type "Pilots" (prefix) into its set. + -- local CargoGroup = CARGO_GROUP:New( PilotGroup, "Cargo", "Engineer Team 1", 500 ) + -- + -- What is also needed, is to have a set of Core.Groups defined that contains the clients of the players. + -- + -- -- Allocate the Transport, which are the helicopter to retrieve the pilot, that can be manned by players. + -- local GroupSet = SET_GROUP:New():FilterPrefixes( "Transport" ):FilterStart() + -- + -- Now that we have a CargoSet and a GroupSet, we can now create the TransportTask manually. + -- + -- -- Declare the transport task. + -- local TransportTask = TASK_CARGO_TRANSPORT + -- :New( Mission, + -- GroupSet, + -- "Transport Engineers", + -- CargoSet, + -- "Fly behind enemy lines, and retrieve the downed pilot." + -- ) + -- + -- So you can see, setting up a transport task manually is a lot of work. + -- It is better you use the cargo dispatcher to create transport tasks and it will work as it is intended. + -- By doing this, cargo transport tasking will become a dynamic experience. + -- + -- + -- # 2) Create a task using the Tasking.Task_Cargo_Dispatcher module. + -- + -- Actually, it is better to **GENERATE** these tasks using the Tasking.Task_Cargo_Dispatcher module. + -- Using the dispatcher module, transport tasks can be created easier. + -- + -- Find below an example how to use the TASK_CARGO_DISPATCHER class: + -- + -- + -- -- Find the HQ group. + -- HQ = GROUP:FindByName( "HQ", "Bravo" ) + -- + -- -- Create the command center with the name "Lima". + -- CommandCenter = COMMANDCENTER + -- :New( HQ, "Lima" ) + -- + -- -- Create the mission, for the command center, with the name "Operation Cargo Fun", a "Tactical" mission, with the mission briefing "Transport Cargo", for the BLUE coalition. + -- Mission = MISSION + -- :New( CommandCenter, "Operation Cargo Fun", "Tactical", "Transport Cargo", coalition.side.BLUE ) + -- + -- -- Create the SET of GROUPs containing clients (players) that will transport the cargo. + -- -- These are have a name that start with "Transport" and are of the "blue" coalition. + -- TransportGroups = SET_GROUP:New():FilterCoalitions( "blue" ):FilterPrefixes( "Transport" ):FilterStart() + -- + -- + -- -- Here we create the TASK_CARGO_DISPATCHER object! This is where we assign the dispatcher to generate tasks in the Mission for the TransportGroups. + -- TaskDispatcher = TASK_CARGO_DISPATCHER:New( Mission, TransportGroups ) + -- + -- + -- -- Here we declare the SET of CARGOs called "Workmaterials". + -- local CargoSetWorkmaterials = SET_CARGO:New():FilterTypes( "Workmaterials" ):FilterStart() + -- + -- -- Here we declare (add) CARGO_GROUP objects of various types, that are filtered and added in the CargoSetworkmaterials cargo set. + -- -- These cargo objects have the type "Workmaterials" which is exactly the type of cargo the CargoSetworkmaterials is filtering on. + -- local EngineerCargoGroup = CARGO_GROUP:New( GROUP:FindByName( "Engineers" ), "Workmaterials", "Engineers", 250 ) + -- local ConcreteCargo = CARGO_SLINGLOAD:New( STATIC:FindByName( "Concrete" ), "Workmaterials", "Concrete", 150, 50 ) + -- local CrateCargo = CARGO_CRATE:New( STATIC:FindByName( "Crate" ), "Workmaterials", "Crate", 150, 50 ) + -- local EnginesCargo = CARGO_CRATE:New( STATIC:FindByName( "Engines" ), "Workmaterials", "Engines", 150, 50 ) + -- local MetalCargo = CARGO_CRATE:New( STATIC:FindByName( "Metal" ), "Workmaterials", "Metal", 150, 50 ) + -- + -- -- And here we create a new WorkplaceTask, using the :AddTransportTask method of the TaskDispatcher. + -- local WorkplaceTask = TaskDispatcher:AddTransportTask( "Build a Workplace", CargoSetWorkmaterials, "Transport the workers, engineers and the equipment near the Workplace." ) + -- TaskDispatcher:SetTransportDeployZone( WorkplaceTask, ZONE:New( "Workplace" ) ) + -- + -- # 3) Handle cargo task events. + -- + -- When a player is picking up and deploying cargo using his carrier, events are generated by the tasks. These events can be captured and tailored with your own code. + -- + -- In order to properly capture the events and avoid mistakes using the documentation, it is advised that you execute the following actions: + -- + -- * **Copy / Paste** the code section into your script. + -- * **Change** the "myclass" literal to the task object name you have in your script. + -- * Within the function, you can now **write your own code**! + -- * **IntelliSense** will recognize the type of the variables provided by the function. Note: the From, Event and To variables can be safely ignored, + -- but you need to declare them as they are automatically provided by the event handling system of MOOSE. + -- + -- You can send messages or fire off any other events within the code section. The sky is the limit! + -- + -- + -- ## 3.1) Handle the CargoPickedUp event. + -- + -- Find below an example how to tailor the **CargoPickedUp** event, generated by the WorkplaceTask: + -- + -- function WorkplaceTask:OnAfterCargoPickedUp( From, Event, To, TaskUnit, Cargo ) + -- + -- MESSAGE:NewType( "Unit " .. TaskUnit:GetName().. " has picked up cargo.", MESSAGE.Type.Information ):ToAll() + -- + -- end + -- + -- If you want to code your own event handler, use this code fragment to tailor the event when a player carrier has picked up a cargo object in the CarrierGroup. + -- You can use this event handler to post messages to players, or provide status updates etc. + -- + -- --- CargoPickedUp event handler OnAfter for "myclass". + -- -- @param #string From A string that contains the "*from state name*" when the event was triggered. + -- -- @param #string Event A string that contains the "*event name*" when the event was triggered. + -- -- @param #string To A string that contains the "*to state name*" when the event was triggered. + -- -- @param Wrapper.Unit#UNIT TaskUnit The unit (client) of the player that has picked up the cargo. + -- -- @param Cargo.Cargo#CARGO Cargo The cargo object that has been picked up. Note that this can be a CARGO_GROUP, CARGO_CRATE or CARGO_SLINGLOAD object! + -- function myclass:OnAfterCargoPickedUp( From, Event, To, TaskUnit, Cargo ) + -- + -- -- Write here your own code. + -- + -- end + -- + -- + -- ## 3.2) Handle the CargoDeployed event. + -- + -- Find below an example how to tailor the **CargoDeployed** event, generated by the WorkplaceTask: + -- + -- function WorkplaceTask:OnAfterCargoDeployed( From, Event, To, TaskUnit, Cargo, DeployZone ) + -- + -- MESSAGE:NewType( "Unit " .. TaskUnit:GetName().. " has deployed cargo at zone " .. DeployZone:GetName(), MESSAGE.Type.Information ):ToAll() + -- + -- Helos[ math.random(1,#Helos) ]:Spawn() + -- EnemyHelos[ math.random(1,#EnemyHelos) ]:Spawn() + -- end + -- + -- If you want to code your own event handler, use this code fragment to tailor the event when a player carrier has deployed a cargo object from the CarrierGroup. + -- You can use this event handler to post messages to players, or provide status updates etc. + -- + -- + -- --- CargoDeployed event handler OnAfter foR "myclass". + -- -- @param #string From A string that contains the "*from state name*" when the event was triggered. + -- -- @param #string Event A string that contains the "*event name*" when the event was triggered. + -- -- @param #string To A string that contains the "*to state name*" when the event was triggered. + -- -- @param Wrapper.Unit#UNIT TaskUnit The unit (client) of the player that has deployed the cargo. + -- -- @param Cargo.Cargo#CARGO Cargo The cargo object that has been deployed. Note that this can be a CARGO_GROUP, CARGO_CRATE or CARGO_SLINGLOAD object! + -- -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. + -- function myclass:OnAfterCargoDeployed( From, Event, To, TaskUnit, Cargo, DeployZone ) + -- + -- -- Write here your own code. + -- + -- end + -- + -- + -- + -- === + -- + -- @field #TASK_CARGO_TRANSPORT + TASK_CARGO_TRANSPORT = { + ClassName = "TASK_CARGO_TRANSPORT", + } + + --- Instantiates a new TASK_CARGO_TRANSPORT. + -- @param #TASK_CARGO_TRANSPORT self + -- @param Tasking.Mission#MISSION Mission + -- @param Core.Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. + -- @param #string TaskName The name of the Task. + -- @param Core.Set#SET_CARGO SetCargo The scope of the cargo to be transported. + -- @param #string TaskBriefing The Cargo Task briefing. + -- @return #TASK_CARGO_TRANSPORT self + function TASK_CARGO_TRANSPORT:New( Mission, SetGroup, TaskName, SetCargo, TaskBriefing ) + local self = BASE:Inherit( self, TASK_CARGO:New( Mission, SetGroup, TaskName, SetCargo, "Transport", TaskBriefing ) ) -- #TASK_CARGO_TRANSPORT + self:F() + + Mission:AddTask( self ) + + local Fsm = self:GetUnitProcess() + + local CargoReport = REPORT:New( "Transport Cargo. The following cargo needs to be transported including initial positions:") + + SetCargo:ForEachCargo( + --- @param Core.Cargo#CARGO Cargo + function( Cargo ) + local CargoType = Cargo:GetType() + local CargoName = Cargo:GetName() + local CargoCoordinate = Cargo:GetCoordinate() + CargoReport:Add( string.format( '- "%s" (%s) at %s', CargoName, CargoType, CargoCoordinate:ToStringMGRS() ) ) + end + ) + + self:SetBriefing( + TaskBriefing or + CargoReport:Text() + ) + + + return self + end + + function TASK_CARGO_TRANSPORT:ReportOrder( ReportGroup ) + + return 0 + end + + + --- + -- @param #TASK_CARGO_TRANSPORT self + -- @return #boolean + function TASK_CARGO_TRANSPORT:IsAllCargoTransported() + + local CargoSet = self:GetCargoSet() + local Set = CargoSet:GetSet() + + local DeployZones = self:GetDeployZones() + + local CargoDeployed = true + + -- Loop the CargoSet (so evaluate each Cargo in the SET_CARGO ). + for CargoID, CargoData in pairs( Set ) do + local Cargo = CargoData -- Core.Cargo#CARGO + + self:F( { Cargo = Cargo:GetName(), CargoDeployed = Cargo:IsDeployed() } ) + + if Cargo:IsDeployed() then + +-- -- Loop the DeployZones set for the TASK_CARGO_TRANSPORT. +-- for DeployZoneID, DeployZone in pairs( DeployZones ) do +-- +-- -- If all cargo is in one of the deploy zones, then all is good. +-- self:T( { Cargo.CargoObject } ) +-- if Cargo:IsInZone( DeployZone ) == false then +-- CargoDeployed = false +-- end +-- end + else + CargoDeployed = false + end + end + + self:F( { CargoDeployed = CargoDeployed } ) + + return CargoDeployed + end + + --- @param #TASK_CARGO_TRANSPORT self + function TASK_CARGO_TRANSPORT:onafterGoal( TaskUnit, From, Event, To ) + local CargoSet = self.CargoSet + + if self:IsAllCargoTransported() then + self:Success() + end + + self:__Goal( -10 ) + end + +end + +--- **Tasking** -- Orchestrates the task for players to execute CSAR for downed pilots. +-- +-- **Specific features:** +-- +-- * Creates a task to retrieve a pilot @{Cargo.Cargo} from behind enemy lines. +-- * Derived from the TASK_CARGO class, which is derived from the TASK class. +-- * Orchestrate the task flow, so go from Planned to Assigned to Success, Failed or Cancelled. +-- * Co-operation tasking, so a player joins a group of players executing the same task. +-- +-- +-- **A complete task menu system to allow players to:** +-- +-- * Join the task, abort the task. +-- * Mark the task location on the map. +-- * Provide details of the target. +-- * Route to the cargo. +-- * Route to the deploy zones. +-- * Load/Unload cargo. +-- * Board/Unboard cargo. +-- * Slingload cargo. +-- * Display the task briefing. +-- +-- +-- **A complete mission menu system to allow players to:** +-- +-- * Join a task, abort the task. +-- * Display task reports. +-- * Display mission statistics. +-- * Mark the task locations on the map. +-- * Provide details of the targets. +-- * Display the mission briefing. +-- * Provide status updates as retrieved from the command center. +-- * Automatically assign a random task as part of a mission. +-- * Manually assign a specific task as part of a mission. +-- +-- +-- **A settings system, using the settings menu:** +-- +-- * Tweak the duration of the display of messages. +-- * Switch between metric and imperial measurement system. +-- * Switch between coordinate formats used in messages: BR, BRA, LL DMS, LL DDM, MGRS. +-- * Different settings modes for A2G and A2A operations. +-- * Various other options. +-- +-- === +-- +-- Please read through the @{Tasking.Task_Cargo} process to understand the mechanisms of tasking and cargo tasking and handling. +-- +-- The cargo will be a downed pilot, which is located somwhere on the battlefield. Use the menus system and facilities to +-- join the CSAR task, and retrieve the pilot from behind enemy lines. The menu system is generic, there is nothing +-- specific on a CSAR task that requires further explanation, than reading the generic TASK_CARGO explanations. +-- +-- Enjoy! +-- FC +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- ### Contributions: +-- +-- === +-- +-- @module Tasking.Task_Cargo_CSAR +-- @image Task_Cargo_CSAR.JPG + + +do -- TASK_CARGO_CSAR + + --- @type TASK_CARGO_CSAR + -- @extends Tasking.Task_Cargo#TASK_CARGO + + --- Orchestrates the task for players to execute CSAR for downed pilots. + -- + -- CSAR tasks are suited to govern the process of return downed pilots behind enemy lines back to safetly. + -- Typically, this task is executed by helicopter pilots, but it can also be executed by ground forces! + -- + -- === + -- + -- A CSAR task can be created manually, but actually, it is better to **GENERATE** these tasks using the + -- @{Tasking.Task_Cargo_Dispatcher} module. + -- + -- Using the dispatcher, CSAR tasks will be created **automatically** when a pilot ejects from a damaged AI aircraft. + -- When this happens, the pilot actually will survive, but needs to be retrieved from behind enemy lines. + -- + -- # 1) Create a CSAR task manually (code it). + -- + -- Although it is recommended to use the dispatcher, you can create a CSAR task yourself as a mission designer. + -- It is easy, as it works just like any other task setup. + -- + -- ## 1.1) Create a command center. + -- + -- First you need to create a command center using the @{Tasking.CommandCenter#COMMANDCENTER.New}() constructor. + -- + -- local CommandCenter = COMMANDCENTER + -- :New( HQ, "Lima" ) -- Create the CommandCenter. + -- + -- ## 1.2) Create a mission. + -- + -- Tasks work in a mission, which groups these tasks to achieve a joint mission goal. + -- A command center can govern multiple missions. + -- Create a new mission, using the @{Tasking.Mission#MISSION.New}() constructor. + -- + -- -- Declare the Mission for the Command Center. + -- local Mission = MISSION + -- :New( CommandCenter, + -- "Overlord", + -- "High", + -- "Retrieve the downed pilots.", + -- coalition.side.RED + -- ) + -- + -- ## 1.3) Create the CSAR cargo task. + -- + -- So, now that we have a command center and a mission, we now create the CSAR task. + -- We create the CSAR task using the @{#TASK_CARGO_CSAR.New}() constructor. + -- + -- Because a CSAR task will not generate the cargo itself, you'll need to create it first. + -- The cargo in this case will be the downed pilot! + -- + -- -- Here we define the "cargo set", which is a collection of cargo objects. + -- -- The cargo set will be the input for the cargo transportation task. + -- -- So a transportation object is handling a cargo set, which is automatically refreshed when new cargo is added/deleted. + -- local CargoSet = SET_CARGO:New():FilterTypes( "Pilots" ):FilterStart() + -- + -- -- Now we add cargo into the battle scene. + -- local PilotGroup = GROUP:FindByName( "Pilot" ) + -- + -- -- CARGO_GROUP can be used to setup cargo with a GROUP object underneath. + -- -- We name this group Engineers. + -- -- Note that the name of the cargo is "Engineers". + -- -- The cargoset "CargoSet" will embed all defined cargo of type "Pilots" (prefix) into its set. + -- local CargoGroup = CARGO_GROUP:New( PilotGroup, "Pilots", "Downed Pilot", 500 ) + -- + -- What is also needed, is to have a set of @{Core.Group}s defined that contains the clients of the players. + -- + -- -- Allocate the Transport, which are the helicopter to retrieve the pilot, that can be manned by players. + -- local GroupSet = SET_GROUP:New():FilterPrefixes( "Transport" ):FilterStart() + -- + -- Now that we have a CargoSet and a GroupSet, we can now create the CSARTask manually. + -- + -- -- Declare the CSAR task. + -- local CSARTask = TASK_CARGO_CSAR + -- :New( Mission, + -- GroupSet, + -- "CSAR Pilot", + -- CargoSet, + -- "Fly behind enemy lines, and retrieve the downed pilot." + -- ) + -- + -- So you can see, setting up a CSAR task manually is a lot of work. + -- It is better you use the cargo dispatcher to generate CSAR tasks and it will work as it is intended. + -- By doing this, CSAR tasking will become a dynamic experience. + -- + -- # 2) Create a task using the @{Tasking.Task_Cargo_Dispatcher} module. + -- + -- Actually, it is better to **GENERATE** these tasks using the @{Tasking.Task_Cargo_Dispatcher} module. + -- Using the dispatcher module, transport tasks can be created much more easy. + -- + -- Find below an example how to use the TASK_CARGO_DISPATCHER class: + -- + -- + -- -- Find the HQ group. + -- HQ = GROUP:FindByName( "HQ", "Bravo" ) + -- + -- -- Create the command center with the name "Lima". + -- CommandCenter = COMMANDCENTER + -- :New( HQ, "Lima" ) + -- + -- -- Create the mission, for the command center, with the name "CSAR Mission", a "Tactical" mission, with the mission briefing "Rescue downed pilots.", for the RED coalition. + -- Mission = MISSION + -- :New( CommandCenter, "CSAR Mission", "Tactical", "Rescue downed pilots.", coalition.side.RED ) + -- + -- -- Create the SET of GROUPs containing clients (players) that will transport the cargo. + -- -- These are have a name that start with "Rescue" and are of the "red" coalition. + -- AttackGroups = SET_GROUP:New():FilterCoalitions( "red" ):FilterPrefixes( "Rescue" ):FilterStart() + -- + -- + -- -- Here we create the TASK_CARGO_DISPATCHER object! This is where we assign the dispatcher to generate tasks in the Mission for the AttackGroups. + -- TaskDispatcher = TASK_CARGO_DISPATCHER:New( Mission, AttackGroups ) + -- + -- + -- -- Here the task dispatcher will generate automatically CSAR tasks once a pilot ejects. + -- TaskDispatcher:StartCSARTasks( + -- "CSAR", + -- { ZONE_UNIT:New( "Hospital", STATIC:FindByName( "Hospital" ), 100 ) }, + -- "One of our pilots has ejected. Go out to Search and Rescue our pilot!\n" .. + -- "Use the radio menu to let the command center assist you with the CSAR tasking." + -- ) + -- + -- # 3) Handle cargo task events. + -- + -- When a player is picking up and deploying cargo using his carrier, events are generated by the tasks. These events can be captured and tailored with your own code. + -- + -- In order to properly capture the events and avoid mistakes using the documentation, it is advised that you execute the following actions: + -- + -- * **Copy / Paste** the code section into your script. + -- * **Change** the CLASS literal to the task object name you have in your script. + -- * Within the function, you can now **write your own code**! + -- * **IntelliSense** will recognize the type of the variables provided by the function. Note: the From, Event and To variables can be safely ignored, + -- but you need to declare them as they are automatically provided by the event handling system of MOOSE. + -- + -- You can send messages or fire off any other events within the code section. The sky is the limit! + -- + -- NOTE: CSAR tasks are actually automatically created by the TASK_CARGO_DISPATCHER. So the underlying is not really applicable for mission designers as they will use the dispatcher instead + -- of capturing these events from manually created CSAR tasks! + -- + -- ## 3.1) Handle the **CargoPickedUp** event. + -- + -- Find below an example how to tailor the **CargoPickedUp** event, generated by the CSARTask: + -- + -- function CSARTask:OnAfterCargoPickedUp( From, Event, To, TaskUnit, Cargo ) + -- + -- MESSAGE:NewType( "Unit " .. TaskUnit:GetName().. " has picked up cargo.", MESSAGE.Type.Information ):ToAll() + -- + -- end + -- + -- If you want to code your own event handler, use this code fragment to tailor the event when a player carrier has picked up a cargo object in the CarrierGroup. + -- You can use this event handler to post messages to players, or provide status updates etc. + -- + -- --- CargoPickedUp event handler OnAfter for CLASS. + -- -- @param #CLASS self + -- -- @param #string From A string that contains the "*from state name*" when the event was triggered. + -- -- @param #string Event A string that contains the "*event name*" when the event was triggered. + -- -- @param #string To A string that contains the "*to state name*" when the event was triggered. + -- -- @param Wrapper.Unit#UNIT TaskUnit The unit (client) of the player that has picked up the cargo. + -- -- @param Cargo.Cargo#CARGO Cargo The cargo object that has been picked up. Note that this can be a CARGO_GROUP, CARGO_CRATE or CARGO_SLINGLOAD object! + -- function CLASS:OnAfterCargoPickedUp( From, Event, To, TaskUnit, Cargo ) + -- + -- -- Write here your own code. + -- + -- end + -- + -- + -- ## 3.2) Handle the **CargoDeployed** event. + -- + -- Find below an example how to tailor the **CargoDeployed** event, generated by the CSARTask: + -- + -- function CSARTask:OnAfterCargoDeployed( From, Event, To, TaskUnit, Cargo, DeployZone ) + -- + -- MESSAGE:NewType( "Unit " .. TaskUnit:GetName().. " has deployed cargo at zone " .. DeployZone:GetName(), MESSAGE.Type.Information ):ToAll() + -- + -- end + -- + -- If you want to code your own event handler, use this code fragment to tailor the event when a player carrier has deployed a cargo object from the CarrierGroup. + -- You can use this event handler to post messages to players, or provide status updates etc. + -- + -- + -- --- CargoDeployed event handler OnAfter for CLASS. + -- -- @param #CLASS self + -- -- @param #string From A string that contains the "*from state name*" when the event was triggered. + -- -- @param #string Event A string that contains the "*event name*" when the event was triggered. + -- -- @param #string To A string that contains the "*to state name*" when the event was triggered. + -- -- @param Wrapper.Unit#UNIT TaskUnit The unit (client) of the player that has deployed the cargo. + -- -- @param Cargo.Cargo#CARGO Cargo The cargo object that has been deployed. Note that this can be a CARGO_GROUP, CARGO_CRATE or CARGO_SLINGLOAD object! + -- -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. + -- function CLASS:OnAfterCargoDeployed( From, Event, To, TaskUnit, Cargo, DeployZone ) + -- + -- -- Write here your own code. + -- + -- end + -- + -- === + -- + -- @field #TASK_CARGO_CSAR + TASK_CARGO_CSAR = { + ClassName = "TASK_CARGO_CSAR", + } + + --- Instantiates a new TASK_CARGO_CSAR. + -- @param #TASK_CARGO_CSAR self + -- @param Tasking.Mission#MISSION Mission + -- @param Core.Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. + -- @param #string TaskName The name of the Task. + -- @param Core.Set#SET_CARGO SetCargo The scope of the cargo to be transported. + -- @param #string TaskBriefing The Cargo Task briefing. + -- @return #TASK_CARGO_CSAR self + function TASK_CARGO_CSAR:New( Mission, SetGroup, TaskName, SetCargo, TaskBriefing ) + local self = BASE:Inherit( self, TASK_CARGO:New( Mission, SetGroup, TaskName, SetCargo, "CSAR", TaskBriefing ) ) -- #TASK_CARGO_CSAR + self:F() + + Mission:AddTask( self ) + + + -- Events + + self:AddTransition( "*", "CargoPickedUp", "*" ) + self:AddTransition( "*", "CargoDeployed", "*" ) + + self:F( { CargoDeployed = self.CargoDeployed ~= nil and "true" or "false" } ) + + --- OnAfter Transition Handler for Event CargoPickedUp. + -- @function [parent=#TASK_CARGO_CSAR] OnAfterCargoPickedUp + -- @param #TASK_CARGO_CSAR self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @param Wrapper.Unit#UNIT TaskUnit The Unit (Client) that PickedUp the cargo. You can use this to retrieve the PlayerName etc. + -- @param Core.Cargo#CARGO Cargo The Cargo that got PickedUp by the TaskUnit. You can use this to check Cargo Status. + + --- OnAfter Transition Handler for Event CargoDeployed. + -- @function [parent=#TASK_CARGO_CSAR] OnAfterCargoDeployed + -- @param #TASK_CARGO_CSAR self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @param Wrapper.Unit#UNIT TaskUnit The Unit (Client) that Deployed the cargo. You can use this to retrieve the PlayerName etc. + -- @param Core.Cargo#CARGO Cargo The Cargo that got PickedUp by the TaskUnit. You can use this to check Cargo Status. + -- @param Core.Zone#ZONE DeployZone The zone where the Cargo got Deployed or UnBoarded. + + local Fsm = self:GetUnitProcess() + + local CargoReport = REPORT:New( "Rescue a downed pilot from the following position:") + + SetCargo:ForEachCargo( + --- @param Core.Cargo#CARGO Cargo + function( Cargo ) + local CargoType = Cargo:GetType() + local CargoName = Cargo:GetName() + local CargoCoordinate = Cargo:GetCoordinate() + CargoReport:Add( string.format( '- "%s" (%s) at %s', CargoName, CargoType, CargoCoordinate:ToStringMGRS() ) ) + end + ) + + self:SetBriefing( + TaskBriefing or + CargoReport:Text() + ) + + + return self + end + + + + function TASK_CARGO_CSAR:ReportOrder( ReportGroup ) + + return 0 + end + + + --- + -- @param #TASK_CARGO_CSAR self + -- @return #boolean + function TASK_CARGO_CSAR:IsAllCargoTransported() + + local CargoSet = self:GetCargoSet() + local Set = CargoSet:GetSet() + + local DeployZones = self:GetDeployZones() + + local CargoDeployed = true + + -- Loop the CargoSet (so evaluate each Cargo in the SET_CARGO ). + for CargoID, CargoData in pairs( Set ) do + local Cargo = CargoData -- Core.Cargo#CARGO + + self:F( { Cargo = Cargo:GetName(), CargoDeployed = Cargo:IsDeployed() } ) + + if Cargo:IsDeployed() then + +-- -- Loop the DeployZones set for the TASK_CARGO_CSAR. +-- for DeployZoneID, DeployZone in pairs( DeployZones ) do +-- +-- -- If all cargo is in one of the deploy zones, then all is good. +-- self:T( { Cargo.CargoObject } ) +-- if Cargo:IsInZone( DeployZone ) == false then +-- CargoDeployed = false +-- end +-- end + else + CargoDeployed = false + end + end + + self:F( { CargoDeployed = CargoDeployed } ) + + return CargoDeployed + end + + --- @param #TASK_CARGO_CSAR self + function TASK_CARGO_CSAR:onafterGoal( TaskUnit, From, Event, To ) + local CargoSet = self.CargoSet + + if self:IsAllCargoTransported() then + self:Success() + end + + self:__Goal( -10 ) + end + +end + +--- **Tasking** - Creates and manages player TASK_CARGO tasks. +-- +-- The **TASK_CARGO_DISPATCHER** allows you to setup various tasks for let human +-- players transport cargo as part of a task. +-- +-- The cargo dispatcher will implement for you mechanisms to create cargo transportation tasks: +-- +-- * As setup by the mission designer. +-- * Dynamically create CSAR missions (when a pilot is downed as part of a downed plane). +-- * Dynamically spawn new cargo and create cargo taskings! +-- +-- +-- +-- **Specific features:** +-- +-- * Creates a task to transport @{Cargo.Cargo} to and between deployment zones. +-- * Derived from the TASK_CARGO class, which is derived from the TASK class. +-- * Orchestrate the task flow, so go from Planned to Assigned to Success, Failed or Cancelled. +-- * Co-operation tasking, so a player joins a group of players executing the same task. +-- +-- +-- **A complete task menu system to allow players to:** +-- +-- * Join the task, abort the task. +-- * Mark the task location on the map. +-- * Provide details of the target. +-- * Route to the cargo. +-- * Route to the deploy zones. +-- * Load/Unload cargo. +-- * Board/Unboard cargo. +-- * Slingload cargo. +-- * Display the task briefing. +-- +-- +-- **A complete mission menu system to allow players to:** +-- +-- * Join a task, abort the task. +-- * Display task reports. +-- * Display mission statistics. +-- * Mark the task locations on the map. +-- * Provide details of the targets. +-- * Display the mission briefing. +-- * Provide status updates as retrieved from the command center. +-- * Automatically assign a random task as part of a mission. +-- * Manually assign a specific task as part of a mission. +-- +-- +-- **A settings system, using the settings menu:** +-- +-- * Tweak the duration of the display of messages. +-- * Switch between metric and imperial measurement system. +-- * Switch between coordinate formats used in messages: BR, BRA, LL DMS, LL DDM, MGRS. +-- * Different settings modes for A2G and A2A operations. +-- * Various other options. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- ### Contributions: +-- +-- === +-- +-- @module Tasking.Task_Cargo_Dispatcher +-- @image Task_Cargo_Dispatcher.JPG + +do -- TASK_CARGO_DISPATCHER + + --- TASK_CARGO_DISPATCHER class. + -- @type TASK_CARGO_DISPATCHER + -- @extends Tasking.Task_Manager#TASK_MANAGER + -- @field TASK_CARGO_DISPATCHER.CSAR CSAR + -- @field Core.Set#SET_ZONE SetZonesCSAR + + --- @type TASK_CARGO_DISPATCHER.CSAR + -- @field Wrapper.Unit#UNIT PilotUnit + -- @field Tasking.Task#TASK Task + + + --- Implements the dynamic dispatching of cargo tasks. + -- + -- The **TASK_CARGO_DISPATCHER** allows you to setup various tasks for let human + -- players transport cargo as part of a task. + -- + -- There are currently **two types of tasks** that can be constructed: + -- + -- * A **normal cargo transport** task, which tasks humans to transport cargo from a location towards a deploy zone. + -- * A **CSAR** cargo transport task. CSAR tasks are **automatically generated** when a friendly (AI) plane is downed and the friendly pilot ejects... + -- You as a player (the helo pilot) can go out in the battlefield, fly behind enemy lines, and rescue the pilot (back to a deploy zone). + -- + -- Let's explore **step by step** how to setup the task cargo dispatcher. + -- + -- # 1. Setup a mission environment. + -- + -- It is easy, as it works just like any other task setup, so setup a command center and a mission. + -- + -- ## 1.1. Create a command center. + -- + -- First you need to create a command center using the @{Tasking.CommandCenter#COMMANDCENTER.New}() constructor. + -- + -- local CommandCenter = COMMANDCENTER + -- :New( HQ, "Lima" ) -- Create the CommandCenter. + -- + -- ## 1.2. Create a mission. + -- + -- Tasks work in a mission, which groups these tasks to achieve a joint mission goal. + -- A command center can govern multiple missions. + -- Create a new mission, using the @{Tasking.Mission#MISSION.New}() constructor. + -- + -- -- Declare the Mission for the Command Center. + -- local Mission = MISSION + -- :New( CommandCenter, + -- "Overlord", + -- "High", + -- "Transport the cargo.", + -- coalition.side.RED + -- ) + -- + -- + -- # 2. Dispatch a **transport cargo** task. + -- + -- So, now that we have a command center and a mission, we now create the transport task. + -- We create the transport task using the @{#TASK_CARGO_DISPATCHER.AddTransportTask}() constructor. + -- + -- ## 2.1. Create the cargo in the mission. + -- + -- Because a transport task will not generate the cargo itself, you'll need to create it first. + -- + -- -- Here we define the "cargo set", which is a collection of cargo objects. + -- -- The cargo set will be the input for the cargo transportation task. + -- -- So a transportation object is handling a cargo set, which is automatically updated when new cargo is added/deleted. + -- local WorkmaterialsCargoSet = SET_CARGO:New():FilterTypes( "Workmaterials" ):FilterStart() + -- + -- -- Now we add cargo into the battle scene. + -- local PilotGroup = GROUP:FindByName( "Engineers" ) + -- + -- -- CARGO_GROUP can be used to setup cargo with a GROUP object underneath. + -- -- We name the type of this group "Workmaterials", so that this cargo group will be included within the WorkmaterialsCargoSet. + -- -- Note that the name of the cargo is "Engineer Team 1". + -- local CargoGroup = CARGO_GROUP:New( PilotGroup, "Workmaterials", "Engineer Team 1", 500 ) + -- + -- What is also needed, is to have a set of @{Core.Group}s defined that contains the clients of the players. + -- + -- -- Allocate the Transport, which are the helicopters to retrieve the pilot, that can be manned by players. + -- -- The name of these helicopter groups containing one client begins with "Transport", as modelled within the mission editor. + -- local PilotGroupSet = SET_GROUP:New():FilterPrefixes( "Transport" ):FilterStart() + -- + -- ## 2.2. Setup the cargo transport task. + -- + -- First, we need to create a TASK_CARGO_DISPATCHER object. + -- + -- TaskDispatcher = TASK_CARGO_DISPATCHER:New( Mission, PilotGroupSet ) + -- + -- So, the variable `TaskDispatcher` will contain the object of class TASK_CARGO_DISPATCHER, which will allow you to dispatch cargo transport tasks: + -- + -- * for mission `Mission`. + -- * for the group set `PilotGroupSet`. + -- + -- Now that we have `TaskDispatcher` object, we can now **create the TransportTask**, using the @{#TASK_CARGO_DISPATCHER.AddTransportTask}() method! + -- + -- local TransportTask = TaskDispatcher:AddTransportTask( + -- "Transport workmaterials", + -- WorkmaterialsCargoSet, + -- "Transport the workers, engineers and the equipment near the Workplace." ) + -- + -- As a result of this code, the `TransportTask` (returned) variable will contain an object of @{#TASK_CARGO_TRANSPORT}! + -- We pass to the method the title of the task, and the `WorkmaterialsCargoSet`, which is the set of cargo groups to be transported! + -- This object can also be used to setup additional things, or to control this specific task with special actions. + -- + -- And you're done! As you can see, it is a bit of work, but the reward is great. + -- And, because all this is done using program interfaces, you can build a mission with a **dynamic cargo transport task mechanism** yourself! + -- Based on events happening within your mission, you can use the above methods to create new cargo, and setup a new task for cargo transportation to a group of players! + -- + -- + -- # 3. Dispatch CSAR tasks. + -- + -- CSAR tasks can be dynamically created when a friendly pilot ejects, or can be created manually. + -- We'll explore both options. + -- + -- ## 3.1. CSAR task dynamic creation. + -- + -- Because there is an "event" in a running simulation that creates CSAR tasks, the method @{#TASK_CARGO_DISPATCHER.StartCSARTasks}() will create automatically: + -- + -- 1. a new downed pilot at the location where the plane was shot + -- 2. declare that pilot as cargo + -- 3. creates a CSAR task automatically to retrieve that pilot + -- 4. requires deploy zones to be specified where to transport the downed pilot to, in order to complete that task. + -- + -- You create a CSAR task dynamically in a very easy way: + -- + -- TaskDispatcher:StartCSARTasks( + -- "CSAR", + -- { ZONE_UNIT:New( "Hospital", STATIC:FindByName( "Hospital" ), 100 ) }, + -- "One of our pilots has ejected. Go out to Search and Rescue our pilot!\n" .. + -- "Use the radio menu to let the command center assist you with the CSAR tasking." + -- ) + -- + -- The method @{#TASK_CARGO_DISPATCHER.StopCSARTasks}() will automatically stop with the creation of CSAR tasks when friendly pilots eject. + -- + -- **Remarks:** + -- + -- * the ZONE_UNIT can also be a ZONE, or a ZONE_POLYGON object, or any other ZONE_ object! + -- * you can declare the array of zones in another variable, or course! + -- + -- + -- ## 3.2. CSAR task manual creation. + -- + -- We create the CSAR task using the @{#TASK_CARGO_DISPATCHER.AddCSARTask}() constructor. + -- + -- The method will create a new CSAR task, and will generate the pilots cargo itself, at the specified coordinate. + -- + -- What is first needed, is to have a set of @{Core.Group}s defined that contains the clients of the players. + -- + -- -- Allocate the Transport, which are the helicopter to retrieve the pilot, that can be manned by players. + -- local GroupSet = SET_GROUP:New():FilterPrefixes( "Transport" ):FilterStart() + -- + -- We need to create a TASK_CARGO_DISPATCHER object. + -- + -- TaskDispatcher = TASK_CARGO_DISPATCHER:New( Mission, GroupSet ) + -- + -- So, the variable `TaskDispatcher` will contain the object of class TASK_CARGO_DISPATCHER, which will allow you to dispatch cargo CSAR tasks: + -- + -- * for mission `Mission`. + -- * for the group of players (pilots) captured within the `GroupSet` (those groups with a name starting with `"Transport"`). + -- + -- Now that we have a PilotsCargoSet and a GroupSet, we can now create the CSAR task manually. + -- + -- -- Declare the CSAR task. + -- local CSARTask = TaskDispatcher:AddCSARTask( + -- "CSAR Task", + -- Coordinate, + -- 270, + -- "Bring the pilot back!" + -- ) + -- + -- As a result of this code, the `CSARTask` (returned) variable will contain an object of @{#TASK_CARGO_CSAR}! + -- We pass to the method the title of the task, and the `WorkmaterialsCargoSet`, which is the set of cargo groups to be transported! + -- This object can also be used to setup additional things, or to control this specific task with special actions. + -- Note that when you declare a CSAR task manually, you'll still need to specify a deployment zone! + -- + -- # 4. Setup the deploy zone(s). + -- + -- The task cargo dispatcher also foresees methods to setup the deployment zones to where the cargo needs to be transported! + -- + -- There are two levels on which deployment zones can be configured: + -- + -- * Default deploy zones: The TASK_CARGO_DISPATCHER object can have default deployment zones, which will apply over all tasks active in the task dispatcher. + -- * Task specific deploy zones: The TASK_CARGO_DISPATCHER object can have specific deployment zones which apply to a specific task only! + -- + -- Note that for Task specific deployment zones, there are separate deployment zone creation methods per task type! + -- + -- ## 4.1. Setup default deploy zones. + -- + -- Use the @{#TASK_CARGO_DISPATCHER.SetDefaultDeployZone}() to setup one deployment zone, and @{#TASK_CARGO_DISPATCHER.SetDefaultDeployZones}() to setup multiple default deployment zones in one call. + -- + -- ## 4.2. Setup task specific deploy zones for a **transport task**. + -- + -- Use the @{#TASK_CARGO_DISPATCHER.SetTransportDeployZone}() to setup one deployment zone, and @{#TASK_CARGO_DISPATCHER.SetTransportDeployZones}() to setup multiple default deployment zones in one call. + -- + -- ## 4.3. Setup task specific deploy zones for a **CSAR task**. + -- + -- Use the @{#TASK_CARGO_DISPATCHER.SetCSARDeployZone}() to setup one deployment zone, and @{#TASK_CARGO_DISPATCHER.SetCSARDeployZones}() to setup multiple default deployment zones in one call. + -- + -- ## 4.4. **CSAR ejection zones**. + -- + -- Setup a set of zones where the pilots will only eject and a task is created for CSAR. When such a set of zones is given, any ejection outside those zones will not result in a pilot created for CSAR! + -- + -- Use the @{#TASK_CARGO_DISPATCHER.SetCSARZones}() to setup the set of zones. + -- + -- ## 4.5. **CSAR ejection maximum**. + -- + -- Setup how many pilots will eject the maximum. This to avoid an overload of CSAR tasks being created :-) The default is endless CSAR tasks. + -- + -- Use the @{#TASK_CARGO_DISPATCHER.SetMaxCSAR}() to setup the maximum of pilots that will eject for CSAR. + -- + -- + -- # 5) Handle cargo task events. + -- + -- When a player is picking up and deploying cargo using his carrier, events are generated by the dispatcher. These events can be captured and tailored with your own code. + -- + -- In order to properly capture the events and avoid mistakes using the documentation, it is advised that you execute the following actions: + -- + -- * **Copy / Paste** the code section into your script. + -- * **Change** the CLASS literal to the task object name you have in your script. + -- * Within the function, you can now **write your own code**! + -- * **IntelliSense** will recognize the type of the variables provided by the function. Note: the From, Event and To variables can be safely ignored, + -- but you need to declare them as they are automatically provided by the event handling system of MOOSE. + -- + -- You can send messages or fire off any other events within the code section. The sky is the limit! + -- + -- First, we need to create a TASK_CARGO_DISPATCHER object. + -- + -- TaskDispatcher = TASK_CARGO_DISPATCHER:New( Mission, PilotGroupSet ) + -- + -- Second, we create a new cargo transport task for the transportation of workmaterials. + -- + -- TaskDispatcher:AddTransportTask( + -- "Transport workmaterials", + -- WorkmaterialsCargoSet, + -- "Transport the workers, engineers and the equipment near the Workplace." ) + -- + -- Note that we don't really need to keep the resulting task, it is kept internally also in the dispatcher. + -- + -- Using the `TaskDispatcher` object, we can now cpature the CargoPickedUp and CargoDeployed events. + -- + -- ## 5.1) Handle the **CargoPickedUp** event. + -- + -- Find below an example how to tailor the **CargoPickedUp** event, generated by the `TaskDispatcher`: + -- + -- function TaskDispatcher:OnAfterCargoPickedUp( From, Event, To, Task, TaskPrefix, TaskUnit, Cargo ) + -- + -- MESSAGE:NewType( "Unit " .. TaskUnit:GetName().. " has picked up cargo for task " .. Task:GetName() .. ".", MESSAGE.Type.Information ):ToAll() + -- + -- end + -- + -- If you want to code your own event handler, use this code fragment to tailor the event when a player carrier has picked up a cargo object in the CarrierGroup. + -- You can use this event handler to post messages to players, or provide status updates etc. + -- + -- --- CargoPickedUp event handler OnAfter for CLASS. + -- -- @param #CLASS self + -- -- @param #string From A string that contains the "*from state name*" when the event was triggered. + -- -- @param #string Event A string that contains the "*event name*" when the event was triggered. + -- -- @param #string To A string that contains the "*to state name*" when the event was triggered. + -- -- @param Tasking.Task_Cargo#TASK_CARGO Task The cargo task for which the cargo has been picked up. Note that this will be a derived TAKS_CARGO object! + -- -- @param #string TaskPrefix The prefix of the task that was provided when the task was created. + -- -- @param Wrapper.Unit#UNIT TaskUnit The unit (client) of the player that has picked up the cargo. + -- -- @param Cargo.Cargo#CARGO Cargo The cargo object that has been picked up. Note that this can be a CARGO_GROUP, CARGO_CRATE or CARGO_SLINGLOAD object! + -- function CLASS:OnAfterCargoPickedUp( From, Event, To, Task, TaskPrefix, TaskUnit, Cargo ) + -- + -- -- Write here your own code. + -- + -- end + -- + -- + -- ## 5.2) Handle the **CargoDeployed** event. + -- + -- Find below an example how to tailor the **CargoDeployed** event, generated by the `TaskDispatcher`: + -- + -- function WorkplaceTask:OnAfterCargoDeployed( From, Event, To, Task, TaskPrefix, TaskUnit, Cargo, DeployZone ) + -- + -- MESSAGE:NewType( "Unit " .. TaskUnit:GetName().. " has deployed cargo at zone " .. DeployZone:GetName() .. " for task " .. Task:GetName() .. ".", MESSAGE.Type.Information ):ToAll() + -- + -- Helos[ math.random(1,#Helos) ]:Spawn() + -- EnemyHelos[ math.random(1,#EnemyHelos) ]:Spawn() + -- end + -- + -- If you want to code your own event handler, use this code fragment to tailor the event when a player carrier has deployed a cargo object from the CarrierGroup. + -- You can use this event handler to post messages to players, or provide status updates etc. + -- + -- + -- --- CargoDeployed event handler OnAfter for CLASS. + -- -- @param #CLASS self + -- -- @param #string From A string that contains the "*from state name*" when the event was triggered. + -- -- @param #string Event A string that contains the "*event name*" when the event was triggered. + -- -- @param #string To A string that contains the "*to state name*" when the event was triggered. + -- -- @param Tasking.Task_Cargo#TASK_CARGO Task The cargo task for which the cargo has been deployed. Note that this will be a derived TAKS_CARGO object! + -- -- @param #string TaskPrefix The prefix of the task that was provided when the task was created. + -- -- @param Wrapper.Unit#UNIT TaskUnit The unit (client) of the player that has deployed the cargo. + -- -- @param Cargo.Cargo#CARGO Cargo The cargo object that has been deployed. Note that this can be a CARGO_GROUP, CARGO_CRATE or CARGO_SLINGLOAD object! + -- -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. + -- function CLASS:OnAfterCargoDeployed( From, Event, To, Task, TaskPrefix, TaskUnit, Cargo, DeployZone ) + -- + -- -- Write here your own code. + -- + -- end + -- + -- + -- + -- @field #TASK_CARGO_DISPATCHER + TASK_CARGO_DISPATCHER = { + ClassName = "TASK_CARGO_DISPATCHER", + Mission = nil, + Tasks = {}, + CSAR = {}, + CSARSpawned = 0, + + Transport = {}, + TransportCount = 0, + } + + + --- TASK_CARGO_DISPATCHER constructor. + -- @param #TASK_CARGO_DISPATCHER self + -- @param Tasking.Mission#MISSION Mission The mission for which the task dispatching is done. + -- @param Core.Set#SET_GROUP SetGroup The set of groups that can join the tasks within the mission. + -- @return #TASK_CARGO_DISPATCHER self + function TASK_CARGO_DISPATCHER:New( Mission, SetGroup ) + + -- Inherits from DETECTION_MANAGER + local self = BASE:Inherit( self, TASK_MANAGER:New( SetGroup ) ) -- #TASK_CARGO_DISPATCHER + + self.Mission = Mission + + self:AddTransition( "Started", "Assign", "Started" ) + self:AddTransition( "Started", "CargoPickedUp", "Started" ) + self:AddTransition( "Started", "CargoDeployed", "Started" ) + + --- OnAfter Transition Handler for Event Assign. + -- @function [parent=#TASK_CARGO_DISPATCHER] OnAfterAssign + -- @param #TASK_CARGO_DISPATCHER self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @param Tasking.Task_A2A#TASK_A2A Task + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param #string PlayerName + + self:SetCSARRadius() + self:__StartTasks( 5 ) + + self.MaxCSAR = nil + self.CountCSAR = 0 + + -- For CSAR missions, we process the event when a pilot ejects. + + self:HandleEvent( EVENTS.Ejection ) + + return self + end + + + --- Sets the set of zones were pilots will only be spawned (eject) when the planes crash. + -- Note that because this is a set of zones, the MD can create the zones dynamically within his mission! + -- Just provide a set of zones, see usage, but find the tactical situation here: + -- + -- ![CSAR Zones](../Tasking/CSAR_Zones.JPG) + -- + -- @param #TASK_CARGO_DISPATCHER self + -- @param Core.Set#SET_ZONE SetZonesCSAR The set of zones where pilots will only be spawned for CSAR when they eject. + -- @usage + -- + -- TaskDispatcher = TASK_CARGO_DISPATCHER:New( Mission, AttackGroups ) + -- + -- -- Use this call to pass the set of zones. + -- -- Note that you can create the set of zones inline, because the FilterOnce method (and other SET_ZONE methods return self). + -- -- So here the zones can be created as normal trigger zones (MOOSE creates a collection of ZONE objects when teh mission starts of all trigger zones). + -- -- Just name them as CSAR zones here. + -- TaskDispatcher:SetCSARZones( SET_ZONE:New():FilterPrefixes("CSAR"):FilterOnce() ) + -- + function TASK_CARGO_DISPATCHER:SetCSARZones( SetZonesCSAR ) + + self.SetZonesCSAR = SetZonesCSAR + + end + + + --- Sets the maximum of pilots that will be spawned (eject) when the planes crash. + -- @param #TASK_CARGO_DISPATCHER self + -- @param #number MaxCSAR The maximum of pilots that will eject for CSAR. + -- @usage + -- + -- TaskDispatcher = TASK_CARGO_DISPATCHER:New( Mission, AttackGroups ) + -- + -- -- Use this call to the maximum of CSAR to 10. + -- TaskDispatcher:SetMaxCSAR( 10 ) + -- + function TASK_CARGO_DISPATCHER:SetMaxCSAR( MaxCSAR ) + + self.MaxCSAR = MaxCSAR + + end + + + + --- Handle the event when a pilot ejects. + -- @param #TASK_CARGO_DISPATCHER self + -- @param Core.Event#EVENTDATA EventData + function TASK_CARGO_DISPATCHER:OnEventEjection( EventData ) + self:F( { EventData = EventData } ) + + if self.CSARTasks == true then + + local CSARCoordinate = EventData.IniUnit:GetCoordinate() + local CSARCoalition = EventData.IniUnit:GetCoalition() + local CSARCountry = EventData.IniUnit:GetCountry() + local CSARHeading = EventData.IniUnit:GetHeading() + + -- Only add a CSAR task if the coalition of the mission is equal to the coalition of the ejected unit. + if CSARCoalition == self.Mission:GetCommandCenter():GetCoalition() then + -- And only add if the eject is in one of the zones, if defined. + if not self.SetZonesCSAR or ( self.SetZonesCSAR and self.SetZonesCSAR:IsCoordinateInZone( CSARCoordinate ) ) then + -- And only if the maximum of pilots is not reached that ejected! + if not self.MaxCSAR or ( self.MaxCSAR and self.CountCSAR < self.MaxCSAR ) then + local CSARTaskName = self:AddCSARTask( self.CSARTaskName, CSARCoordinate, CSARHeading, CSARCountry, self.CSARBriefing ) + self:SetCSARDeployZones( CSARTaskName, self.CSARDeployZones ) + self.CountCSAR = self.CountCSAR + 1 + end + end + end + end + + return self + end + + + --- Define one default deploy zone for all the cargo tasks. + -- @param #TASK_CARGO_DISPATCHER self + -- @param DefaultDeployZone A default deploy zone. + -- @return #TASK_CARGO_DISPATCHER + function TASK_CARGO_DISPATCHER:SetDefaultDeployZone( DefaultDeployZone ) + + self.DefaultDeployZones = { DefaultDeployZone } + + return self + end + + + --- Define the deploy zones for all the cargo tasks. + -- @param #TASK_CARGO_DISPATCHER self + -- @param DefaultDeployZones A list of the deploy zones. + -- @return #TASK_CARGO_DISPATCHER + -- + function TASK_CARGO_DISPATCHER:SetDefaultDeployZones( DefaultDeployZones ) + + self.DefaultDeployZones = DefaultDeployZones + + return self + end + + + --- Start the generation of CSAR tasks to retrieve a downed pilots. + -- You need to specify a task briefing, a task name, default deployment zone(s). + -- This method can only be used once! + -- @param #TASK_CARGO_DISPATCHER self + -- @param #string CSARTaskName The CSAR task name. + -- @param #string CSARDeployZones The zones to where the CSAR deployment should be directed. + -- @param #string CSARBriefing The briefing of the CSAR tasks. + -- @return #TASK_CARGO_DISPATCHER + function TASK_CARGO_DISPATCHER:StartCSARTasks( CSARTaskName, CSARDeployZones, CSARBriefing) + + if not self.CSARTasks then + self.CSARTasks = true + self.CSARTaskName = CSARTaskName + self.CSARDeployZones = CSARDeployZones + self.CSARBriefing = CSARBriefing + else + error( "TASK_CARGO_DISPATCHER: The generation of CSAR tasks has already started." ) + end + + return self + end + + + --- Stop the generation of CSAR tasks to retrieve a downed pilots. + -- @param #TASK_CARGO_DISPATCHER self + -- @return #TASK_CARGO_DISPATCHER + function TASK_CARGO_DISPATCHER:StopCSARTasks() + + if self.CSARTasks then + self.CSARTasks = nil + self.CSARTaskName = nil + self.CSARDeployZones = nil + self.CSARBriefing = nil + else + error( "TASK_CARGO_DISPATCHER: The generation of CSAR tasks was not yet started." ) + end + + return self + end + + + --- Add a CSAR task to retrieve a downed pilot. + -- You need to specify a coordinate from where the pilot will be spawned to be rescued. + -- @param #TASK_CARGO_DISPATCHER self + -- @param #string CSARTaskPrefix (optional) The prefix of the CSAR task. + -- @param Core.Point#COORDINATE CSARCoordinate The coordinate where a downed pilot will be spawned. + -- @param #number CSARHeading The heading of the pilot in degrees. + -- @param DCSCountry#Country CSARCountry The country ID of the pilot that will be spawned. + -- @param #string CSARBriefing The briefing of the CSAR task. + -- @return #string The CSAR Task Name as a string. The Task Name is the main key and is shown in the task list of the Mission Tasking menu. + -- @usage + -- + -- -- Add a CSAR task to rescue a downed pilot from within a coordinate. + -- local Coordinate = PlaneUnit:GetPointVec2() + -- TaskA2ADispatcher:AddCSARTask( "CSAR Task", Coordinate ) + -- + -- -- Add a CSAR task to rescue a downed pilot from within a coordinate of country RUSSIA, which is pointing to the west (270°). + -- local Coordinate = PlaneUnit:GetPointVec2() + -- TaskA2ADispatcher:AddCSARTask( "CSAR Task", Coordinate, 270, Country.RUSSIA ) + -- + function TASK_CARGO_DISPATCHER:AddCSARTask( CSARTaskPrefix, CSARCoordinate, CSARHeading, CSARCountry, CSARBriefing ) + + local CSARCoalition = self.Mission:GetCommandCenter():GetCoalition() + + CSARHeading = CSARHeading or 0 + CSARCountry = CSARCountry or self.Mission:GetCommandCenter():GetCountry() + + self.CSARSpawned = self.CSARSpawned + 1 + + local CSARTaskName = string.format( ( CSARTaskPrefix or "CSAR" ) .. ".%03d", self.CSARSpawned ) + + -- Create the CSAR Pilot SPAWN object. + -- Let us create the Template for the replacement Pilot :-) + local Template = { + ["visible"] = false, + ["hidden"] = false, + ["task"] = "Ground Nothing", + ["name"] = string.format( "CSAR Pilot#%03d", self.CSARSpawned ), + ["x"] = CSARCoordinate.x, + ["y"] = CSARCoordinate.z, + ["units"] = + { + [1] = + { + ["type"] = ( CSARCoalition == coalition.side.BLUE ) and "Soldier M4" or "Infantry AK", + ["name"] = string.format( "CSAR Pilot#%03d-01", self.CSARSpawned ), + ["skill"] = "Excellent", + ["playerCanDrive"] = false, + ["x"] = CSARCoordinate.x, + ["y"] = CSARCoordinate.z, + ["heading"] = CSARHeading, + }, -- end of [1] + }, -- end of ["units"] + } + + local CSARGroup = GROUP:NewTemplate( Template, CSARCoalition, Group.Category.GROUND, CSARCountry ) + + self.CSAR[CSARTaskName] = {} + self.CSAR[CSARTaskName].PilotGroup = CSARGroup + self.CSAR[CSARTaskName].Briefing = CSARBriefing + self.CSAR[CSARTaskName].Task = nil + self.CSAR[CSARTaskName].TaskPrefix = CSARTaskPrefix + + return CSARTaskName + end + + + --- Define the radius to when a CSAR task will be generated for any downed pilot within range of the nearest CSAR airbase. + -- @param #TASK_CARGO_DISPATCHER self + -- @param #number CSARRadius (Optional, Default = 50000) The radius in meters to decide whether a CSAR needs to be created. + -- @return #TASK_CARGO_DISPATCHER + -- @usage + -- + -- -- Set 20km as the radius to CSAR any downed pilot within range of the nearest CSAR airbase. + -- TaskA2ADispatcher:SetEngageRadius( 20000 ) + -- + -- -- Set 50km as the radius to to CSAR any downed pilot within range of the nearest CSAR airbase. + -- TaskA2ADispatcher:SetEngageRadius() -- 50000 is the default value. + -- + function TASK_CARGO_DISPATCHER:SetCSARRadius( CSARRadius ) + + self.CSARRadius = CSARRadius or 50000 + + return self + end + + + --- Define one deploy zone for the CSAR tasks. + -- @param #TASK_CARGO_DISPATCHER self + -- @param #string CSARTaskName (optional) The name of the CSAR task. + -- @param CSARDeployZone A CSAR deploy zone. + -- @return #TASK_CARGO_DISPATCHER + function TASK_CARGO_DISPATCHER:SetCSARDeployZone( CSARTaskName, CSARDeployZone ) + + if CSARTaskName then + self.CSAR[CSARTaskName].DeployZones = { CSARDeployZone } + end + + return self + end + + + --- Define the deploy zones for the CSAR tasks. + -- @param #TASK_CARGO_DISPATCHER self + -- @param #string CSARTaskName (optional) The name of the CSAR task. + -- @param CSARDeployZones A list of the CSAR deploy zones. + -- @return #TASK_CARGO_DISPATCHER + -- + function TASK_CARGO_DISPATCHER:SetCSARDeployZones( CSARTaskName, CSARDeployZones ) + + if CSARTaskName and self.CSAR[CSARTaskName] then + self.CSAR[CSARTaskName].DeployZones = CSARDeployZones + end + + return self + end + + + --- Add a Transport task to transport cargo from fixed locations to a deployment zone. + -- @param #TASK_CARGO_DISPATCHER self + -- @param #string TaskPrefix (optional) The prefix of the transport task. + -- This prefix will be appended with a . + a number of 3 digits. + -- If no TaskPrefix is given, then "Transport" will be used as the prefix. + -- @param Core.SetCargo#SET_CARGO SetCargo The SetCargo to be transported. + -- @param #string Briefing The briefing of the task transport to be shown to the player. + -- @param #boolean Silent If true don't send a message that a new task is available. + -- @return Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT + -- @usage + -- + -- -- Add a Transport task to transport cargo of different types to a Transport Deployment Zone. + -- TaskDispatcher = TASK_CARGO_DISPATCHER:New( Mission, TransportGroups ) + -- + -- local CargoSetWorkmaterials = SET_CARGO:New():FilterTypes( "Workmaterials" ):FilterStart() + -- local EngineerCargoGroup = CARGO_GROUP:New( GROUP:FindByName( "Engineers" ), "Workmaterials", "Engineers", 250 ) + -- local ConcreteCargo = CARGO_SLINGLOAD:New( STATIC:FindByName( "Concrete" ), "Workmaterials", "Concrete", 150, 50 ) + -- local CrateCargo = CARGO_CRATE:New( STATIC:FindByName( "Crate" ), "Workmaterials", "Crate", 150, 50 ) + -- local EnginesCargo = CARGO_CRATE:New( STATIC:FindByName( "Engines" ), "Workmaterials", "Engines", 150, 50 ) + -- local MetalCargo = CARGO_CRATE:New( STATIC:FindByName( "Metal" ), "Workmaterials", "Metal", 150, 50 ) + -- + -- -- Here we add the task. We name the task "Build a Workplace". + -- -- We provide the CargoSetWorkmaterials, and a briefing as the 2nd and 3rd parameter. + -- -- The :AddTransportTask() returns a Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT object, which we keep as a reference for further actions. + -- -- The WorkplaceTask holds the created and returned Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT object. + -- local WorkplaceTask = TaskDispatcher:AddTransportTask( "Build a Workplace", CargoSetWorkmaterials, "Transport the workers, engineers and the equipment near the Workplace." ) + -- + -- -- Here we set a TransportDeployZone. We use the WorkplaceTask as the reference, and provide a ZONE object. + -- TaskDispatcher:SetTransportDeployZone( WorkplaceTask, ZONE:New( "Workplace" ) ) + -- + function TASK_CARGO_DISPATCHER:AddTransportTask( TaskPrefix, SetCargo, Briefing, Silent ) + + self.TransportCount = self.TransportCount + 1 + + local verbose = Silent or false + + local TaskName = string.format( ( TaskPrefix or "Transport" ) .. ".%03d", self.TransportCount ) + + self.Transport[TaskName] = {} + self.Transport[TaskName].SetCargo = SetCargo + self.Transport[TaskName].Briefing = Briefing + self.Transport[TaskName].Task = nil + self.Transport[TaskName].TaskPrefix = TaskPrefix + + self:ManageTasks(verbose) + + return self.Transport[TaskName] and self.Transport[TaskName].Task + end + + + --- Define one deploy zone for the Transport tasks. + -- @param #TASK_CARGO_DISPATCHER self + -- @param Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT Task The name of the Transport task. + -- @param TransportDeployZone A Transport deploy zone. + -- @return #TASK_CARGO_DISPATCHER + -- @usage + -- + -- + function TASK_CARGO_DISPATCHER:SetTransportDeployZone( Task, TransportDeployZone ) + + if self.Transport[Task.TaskName] then + self.Transport[Task.TaskName].DeployZones = { TransportDeployZone } + else + error( "Task does not exist" ) + end + + self:ManageTasks() + + return self + end + + + --- Define the deploy zones for the Transport tasks. + -- @param #TASK_CARGO_DISPATCHER self + -- @param Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT Task The name of the Transport task. + -- @param TransportDeployZones A list of the Transport deploy zones. + -- @return #TASK_CARGO_DISPATCHER + -- + function TASK_CARGO_DISPATCHER:SetTransportDeployZones( Task, TransportDeployZones ) + + if self.Transport[Task.TaskName] then + self.Transport[Task.TaskName].DeployZones = TransportDeployZones + else + error( "Task does not exist" ) + end + + self:ManageTasks() + + return self + end + + --- Evaluates of a CSAR task needs to be started. + -- @param #TASK_CARGO_DISPATCHER self + -- @return Core.Set#SET_CARGO The SetCargo to be rescued. + -- @return #nil If there is no CSAR task required. + function TASK_CARGO_DISPATCHER:EvaluateCSAR( CSARUnit ) + + local CSARCargo = CARGO_GROUP:New( CSARUnit, "Pilot", CSARUnit:GetName(), 80, 1500, 10 ) + + local SetCargo = SET_CARGO:New() + SetCargo:AddCargosByName( CSARUnit:GetName() ) + + SetCargo:Flush(self) + + return SetCargo + + end + + + + --- Assigns tasks to the @{Core.Set#SET_GROUP}. + -- @param #TASK_CARGO_DISPATCHER self + -- @param #boolean Silent Announce new task (nil/false) or not (true). + -- @return #boolean Return true if you want the task assigning to continue... false will cancel the loop. + function TASK_CARGO_DISPATCHER:ManageTasks(Silent) + self:F() + local verbose = Silent and true + local AreaMsg = {} + local TaskMsg = {} + local ChangeMsg = {} + + local Mission = self.Mission + + if Mission:IsIDLE() or Mission:IsENGAGED() then + + local TaskReport = REPORT:New() + + -- Checking the task queue for the dispatcher, and removing any obsolete task! + for TaskIndex, TaskData in pairs( self.Tasks ) do + local Task = TaskData -- Tasking.Task#TASK + if Task:IsStatePlanned() then + -- Here we need to check if the pilot is still existing. +-- local DetectedItem = Detection:GetDetectedItemByIndex( TaskIndex ) +-- if not DetectedItem then +-- local TaskText = Task:GetName() +-- for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do +-- Mission:GetCommandCenter():MessageToGroup( string.format( "Obsolete A2A task %s for %s removed.", TaskText, Mission:GetShortText() ), TaskGroup ) +-- end +-- Task = self:RemoveTask( TaskIndex ) +-- end + end + end + + -- Now that all obsolete tasks are removed, loop through the CSAR pilots. + for CSARName, CSAR in pairs( self.CSAR ) do + + if not CSAR.Task then + -- New CSAR Task + local SetCargo = self:EvaluateCSAR( CSAR.PilotGroup ) + CSAR.Task = TASK_CARGO_CSAR:New( Mission, self.SetGroup, CSARName, SetCargo, CSAR.Briefing ) + CSAR.Task.TaskPrefix = CSAR.TaskPrefix -- We keep the TaskPrefix for further reference! + Mission:AddTask( CSAR.Task ) + TaskReport:Add( CSARName ) + if CSAR.DeployZones then + CSAR.Task:SetDeployZones( CSAR.DeployZones or {} ) + else + CSAR.Task:SetDeployZones( self.DefaultDeployZones or {} ) + end + + -- Now broadcast the onafterCargoPickedUp event to the Task Cargo Dispatcher. + function CSAR.Task.OnAfterCargoPickedUp( Task, From, Event, To, TaskUnit, Cargo ) + self:CargoPickedUp( Task, Task.TaskPrefix, TaskUnit, Cargo ) + end + + -- Now broadcast the onafterCargoDeployed event to the Task Cargo Dispatcher. + function CSAR.Task.OnAfterCargoDeployed( Task, From, Event, To, TaskUnit, Cargo, DeployZone ) + self:CargoDeployed( Task, Task.TaskPrefix, TaskUnit, Cargo, DeployZone ) + end + + end + end + + + -- Now that all obsolete tasks are removed, loop through the Transport tasks. + for TransportName, Transport in pairs( self.Transport ) do + + if not Transport.Task then + -- New Transport Task + Transport.Task = TASK_CARGO_TRANSPORT:New( Mission, self.SetGroup, TransportName, Transport.SetCargo, Transport.Briefing ) + Transport.Task.TaskPrefix = Transport.TaskPrefix -- We keep the TaskPrefix for further reference! + Mission:AddTask( Transport.Task ) + TaskReport:Add( TransportName ) + function Transport.Task.OnEnterSuccess( Task, From, Event, To ) + self:Success( Task ) + end + + function Transport.Task.OnEnterCancelled( Task, From, Event, To ) + self:Cancelled( Task ) + end + + function Transport.Task.OnEnterFailed( Task, From, Event, To ) + self:Failed( Task ) + end + + function Transport.Task.OnEnterAborted( Task, From, Event, To ) + self:Aborted( Task ) + end + + -- Now broadcast the onafterCargoPickedUp event to the Task Cargo Dispatcher. + function Transport.Task.OnAfterCargoPickedUp( Task, From, Event, To, TaskUnit, Cargo ) + self:CargoPickedUp( Task, Task.TaskPrefix, TaskUnit, Cargo ) + end + + -- Now broadcast the onafterCargoDeployed event to the Task Cargo Dispatcher. + function Transport.Task.OnAfterCargoDeployed( Task, From, Event, To, TaskUnit, Cargo, DeployZone ) + self:CargoDeployed( Task, Task.TaskPrefix, TaskUnit, Cargo, DeployZone ) + end + + end + + if Transport.DeployZones then + Transport.Task:SetDeployZones( Transport.DeployZones or {} ) + else + Transport.Task:SetDeployZones( self.DefaultDeployZones or {} ) + end + + end + + + -- TODO set menus using the HQ coordinator + Mission:GetCommandCenter():SetMenu() + + local TaskText = TaskReport:Text(", ") + + for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do + if ( not Mission:IsGroupAssigned(TaskGroup) ) and TaskText ~= "" and not verbose then + Mission:GetCommandCenter():MessageToGroup( string.format( "%s has tasks %s. Subscribe to a task using the radio menu.", Mission:GetShortText(), TaskText ), TaskGroup ) + end + end + + end + + return true + end + +end +--- **Tasking** - The TASK_Protect models tasks for players to protect or capture specific zones. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- ### Contributions: MillerTime +-- +-- === +-- +-- @module Tasking.TaskZoneCapture +-- @image MOOSE.JPG + +do -- TASK_ZONE_GOAL + + --- The TASK_ZONE_GOAL class + -- @type TASK_ZONE_GOAL + -- @field Core.ZoneGoal#ZONE_GOAL ZoneGoal + -- @extends Tasking.Task#TASK + + --- # TASK_ZONE_GOAL class, extends @{Tasking.Task#TASK} + -- + -- The TASK_ZONE_GOAL class defines the task to protect or capture a protection zone. + -- The TASK_ZONE_GOAL is implemented using a @{Core.Fsm#FSM_TASK}, and has the following statuses: + -- + -- * **None**: Start of the process + -- * **Planned**: The A2G task is planned. + -- * **Assigned**: The A2G task is assigned to a @{Wrapper.Group#GROUP}. + -- * **Success**: The A2G task is successfully completed. + -- * **Failed**: The A2G task has failed. This will happen if the player exists the task early, without communicating a possible cancellation to HQ. + -- + -- ## Set the scoring of achievements in an A2G attack. + -- + -- Scoring or penalties can be given in the following circumstances: + -- + -- * @{#TASK_ZONE_GOAL.SetScoreOnDestroy}(): Set a score when a target in scope of the A2G attack, has been destroyed. + -- * @{#TASK_ZONE_GOAL.SetScoreOnSuccess}(): Set a score when all the targets in scope of the A2G attack, have been destroyed. + -- * @{#TASK_ZONE_GOAL.SetPenaltyOnFailed}(): Set a penalty when the A2G attack has failed. + -- + -- @field #TASK_ZONE_GOAL + TASK_ZONE_GOAL = { + ClassName = "TASK_ZONE_GOAL", + } + + --- Instantiates a new TASK_ZONE_GOAL. + -- @param #TASK_ZONE_GOAL self + -- @param Tasking.Mission#MISSION Mission + -- @param Core.Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. + -- @param #string TaskName The name of the Task. + -- @param Core.ZoneGoalCoalition#ZONE_GOAL_COALITION ZoneGoal + -- @return #TASK_ZONE_GOAL self + function TASK_ZONE_GOAL:New( Mission, SetGroup, TaskName, ZoneGoal, TaskType, TaskBriefing ) + local self = BASE:Inherit( self, TASK:New( Mission, SetGroup, TaskName, TaskType, TaskBriefing ) ) -- #TASK_ZONE_GOAL + self:F() + + self.ZoneGoal = ZoneGoal + self.TaskType = TaskType + + local Fsm = self:GetUnitProcess() + + + Fsm:AddTransition( "Assigned", "StartMonitoring", "Monitoring" ) + Fsm:AddTransition( "Monitoring", "Monitor", "Monitoring", {} ) + Fsm:AddProcess( "Monitoring", "RouteToZone", ACT_ROUTE_ZONE:New(), {} ) + + Fsm:AddTransition( "Rejected", "Reject", "Aborted" ) + Fsm:AddTransition( "Failed", "Fail", "Failed" ) + + self:SetTargetZone( self.ZoneGoal:GetZone() ) + + --- Test + -- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task#TASK Task + function Fsm:OnAfterAssigned( TaskUnit, Task ) + self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + + self:__StartMonitoring( 0.1 ) + self:__RouteToZone( 0.1 ) + end + + --- Test + -- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task#TASK_ZONE_GOAL Task + function Fsm:onafterStartMonitoring( TaskUnit, Task ) + self:F( { self } ) + self:__Monitor( 0.1 ) + end + + --- Monitor Loop + -- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task#TASK_ZONE_GOAL Task + function Fsm:onafterMonitor( TaskUnit, Task ) + self:F( { self } ) + self:__Monitor( 15 ) + end + + --- Test + -- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task_A2G#TASK_ZONE_GOAL Task + function Fsm:onafterRouteTo( TaskUnit, Task ) + self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + -- Determine the first Unit from the self.TargetSetUnit + + if Task:GetTargetZone( TaskUnit ) then + self:__RouteToZone( 0.1 ) + end + end + + return self + + end + + --- @param #TASK_ZONE_GOAL self + -- @param Core.ZoneGoal#ZONE_GOAL ZoneGoal The ZoneGoal Engine. + function TASK_ZONE_GOAL:SetProtect( ZoneGoal ) + + self.ZoneGoal = ZoneGoal -- Core.ZoneGoal#ZONE_GOAL + end + + + + --- @param #TASK_ZONE_GOAL self + function TASK_ZONE_GOAL:GetPlannedMenuText() + return self:GetStateString() .. " - " .. self:GetTaskName() .. " ( " .. self.ZoneGoal:GetZoneName() .. " )" + end + + + --- @param #TASK_ZONE_GOAL self + -- @param Core.Zone#ZONE_BASE TargetZone The Zone object where the Target is located on the map. + -- @param Wrapper.Unit#UNIT TaskUnit + function TASK_ZONE_GOAL:SetTargetZone( TargetZone, TaskUnit ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + local ActRouteZone = ProcessUnit:GetProcess( "Monitoring", "RouteToZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE + ActRouteZone:SetZone( TargetZone ) + end + + + --- @param #TASK_ZONE_GOAL self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return Core.Zone#ZONE_BASE The Zone object where the Target is located on the map. + function TASK_ZONE_GOAL:GetTargetZone( TaskUnit ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + local ActRouteZone = ProcessUnit:GetProcess( "Monitoring", "RouteToZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE + return ActRouteZone:GetZone() + end + + function TASK_ZONE_GOAL:SetGoalTotal( GoalTotal ) + + self.GoalTotal = GoalTotal + end + + function TASK_ZONE_GOAL:GetGoalTotal() + + return self.GoalTotal + end + +end + + +do -- TASK_CAPTURE_ZONE + + --- The TASK_CAPTURE_ZONE class + -- @type TASK_CAPTURE_ZONE + -- @field Core.ZoneGoalCoalition#ZONE_GOAL_COALITION ZoneGoal + -- @extends #TASK_ZONE_GOAL + + --- # TASK_CAPTURE_ZONE class, extends @{Tasking.TaskZoneGoal#TASK_ZONE_GOAL} + -- + -- The TASK_CAPTURE_ZONE class defines an Suppression or Extermination of Air Defenses task for a human player to be executed. + -- These tasks are important to be executed as they will help to achieve air superiority at the vicinity. + -- + -- The TASK_CAPTURE_ZONE is used by the @{Tasking.Task_A2G_Dispatcher#TASK_A2G_DISPATCHER} to automatically create SEAD tasks + -- based on detected enemy ground targets. + -- + -- @field #TASK_CAPTURE_ZONE + TASK_CAPTURE_ZONE = { + ClassName = "TASK_CAPTURE_ZONE", + } + + + --- Instantiates a new TASK_CAPTURE_ZONE. + -- @param #TASK_CAPTURE_ZONE self + -- @param Tasking.Mission#MISSION Mission + -- @param Core.Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. + -- @param #string TaskName The name of the Task. + -- @param Core.ZoneGoalCoalition#ZONE_GOAL_COALITION ZoneGoalCoalition + -- @param #string TaskBriefing The briefing of the task. + -- @return #TASK_CAPTURE_ZONE self + function TASK_CAPTURE_ZONE:New( Mission, SetGroup, TaskName, ZoneGoalCoalition, TaskBriefing) + local self = BASE:Inherit( self, TASK_ZONE_GOAL:New( Mission, SetGroup, TaskName, ZoneGoalCoalition, "CAPTURE", TaskBriefing ) ) -- #TASK_CAPTURE_ZONE + self:F() + + Mission:AddTask( self ) + + self.TaskCoalition = ZoneGoalCoalition:GetCoalition() + self.TaskCoalitionName = ZoneGoalCoalition:GetCoalitionName() + self.TaskZoneName = ZoneGoalCoalition:GetZoneName() + + ZoneGoalCoalition:MonitorDestroyedUnits() + + self:SetBriefing( + TaskBriefing or + "Capture Zone " .. self.TaskZoneName + ) + + self:UpdateTaskInfo( true ) + + self:SetGoal( self.ZoneGoal.Goal ) + + return self + end + + + --- Instantiates a new TASK_CAPTURE_ZONE. + -- @param #TASK_CAPTURE_ZONE self + function TASK_CAPTURE_ZONE:UpdateTaskInfo( Persist ) + + Persist = Persist or false + + local ZoneCoordinate = self.ZoneGoal:GetZone():GetCoordinate() + self.TaskInfo:AddTaskName( 0, "MSOD", Persist ) + self.TaskInfo:AddCoordinate( ZoneCoordinate, 1, "SOD", Persist ) +-- self.TaskInfo:AddText( "Zone Name", self.ZoneGoal:GetZoneName(), 10, "MOD", Persist ) +-- self.TaskInfo:AddText( "Zone Coalition", self.ZoneGoal:GetCoalitionName(), 11, "MOD", Persist ) + local SetUnit = self.ZoneGoal:GetScannedSetUnit() + local ThreatLevel, ThreatText = SetUnit:CalculateThreatLevelA2G() + local ThreatCount = SetUnit:Count() + self.TaskInfo:AddThreat( ThreatText, ThreatLevel, 20, "MOD", Persist ) + self.TaskInfo:AddInfo( "Remaining Units", ThreatCount, 21, "MOD", Persist, true) + + if self.Dispatcher then + local DefenseTaskCaptureDispatcher = self.Dispatcher:GetDefenseTaskCaptureDispatcher() -- Tasking.Task_Capture_Dispatcher#TASK_CAPTURE_DISPATCHER + + if DefenseTaskCaptureDispatcher then + -- Loop through all zones of the player Defenses, and check which zone has an assigned task! + -- The Zones collection contains a Task. This Task is checked if it is assigned. + -- If Assigned, then this task will be the task that is the closest to the defense zone. + for TaskName, CaptureZone in pairs( DefenseTaskCaptureDispatcher.Zones or {} ) do + local Task = CaptureZone.Task -- Tasking.Task_Capture_Zone#TASK_CAPTURE_ZONE + if Task and Task:IsStateAssigned() then -- We also check assigned. + -- Now we register the defense player zone information to the task report. + self.TaskInfo:AddInfo( "Defense Player Zone", Task.ZoneGoal:GetName(), 30, "MOD", Persist ) + self.TaskInfo:AddCoordinate( Task.ZoneGoal:GetZone():GetCoordinate(), 31, "MOD", Persist, false, "Defense Player Coordinate" ) + end + end + end + local DefenseAIA2GDispatcher = self.Dispatcher:GetDefenseAIA2GDispatcher() -- AI.AI_A2G_Dispatcher#AI_A2G_DISPATCHER + + if DefenseAIA2GDispatcher then + -- Loop through all the tasks of the AI Defenses, and check which zone is involved in the defenses and is active! + for Defender, Task in pairs( DefenseAIA2GDispatcher:GetDefenderTasks() or {} ) do + local DetectedItem = DefenseAIA2GDispatcher:GetDefenderTaskTarget( Defender ) + if DetectedItem then + local DetectedZone = DefenseAIA2GDispatcher.Detection:GetDetectedItemZone( DetectedItem ) + if DetectedZone then + self.TaskInfo:AddInfo( "Defense AI Zone", DetectedZone:GetName(), 40, "MOD", Persist ) + self.TaskInfo:AddCoordinate( DetectedZone:GetCoordinate(), 41, "MOD", Persist, false, "Defense AI Coordinate" ) + end + end + end + end + end + + end + + + function TASK_CAPTURE_ZONE:ReportOrder( ReportGroup ) + + local Coordinate = self.TaskInfo:GetCoordinate() + local Distance = ReportGroup:GetCoordinate():Get2DDistance( Coordinate ) + + return Distance + end + + + --- @param #TASK_CAPTURE_ZONE self + -- @param Wrapper.Unit#UNIT TaskUnit + function TASK_CAPTURE_ZONE:OnAfterGoal( From, Event, To, PlayerUnit, PlayerName ) + + self:F( { PlayerUnit = PlayerUnit, Achieved = self.ZoneGoal.Goal:IsAchieved() } ) + + if self.ZoneGoal then + if self.ZoneGoal.Goal:IsAchieved() then + local TotalContributions = self.ZoneGoal.Goal:GetTotalContributions() + local PlayerContributions = self.ZoneGoal.Goal:GetPlayerContributions() + self:F( { TotalContributions = TotalContributions, PlayerContributions = PlayerContributions } ) + for PlayerName, PlayerContribution in pairs( PlayerContributions ) do + local Scoring = self:GetScoring() + if Scoring then + Scoring:_AddMissionGoalScore( self.Mission, PlayerName, "Zone " .. self.ZoneGoal:GetZoneName() .." captured", PlayerContribution * 200 / TotalContributions ) + end + end + self:Success() + end + end + + self:__Goal( -10, PlayerUnit, PlayerName ) + end + + --- This function is called from the @{Tasking.CommandCenter#COMMANDCENTER} to determine the method of automatic task selection. + -- @param #TASK_CAPTURE_ZONE self + -- @param #number AutoAssignMethod The method to be applied to the task. + -- @param Tasking.CommandCenter#COMMANDCENTER CommandCenter The command center. + -- @param Wrapper.Group#GROUP TaskGroup The player group. + function TASK_CAPTURE_ZONE:GetAutoAssignPriority( AutoAssignMethod, CommandCenter, TaskGroup, AutoAssignReference ) + + if AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Random then + return math.random( 1, 9 ) + elseif AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Distance then + local Coordinate = self.TaskInfo:GetCoordinate() + local Distance = Coordinate:Get2DDistance( CommandCenter:GetPositionable():GetCoordinate() ) + return math.floor( Distance ) + elseif AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Priority then + return 1 + end + + return 0 + end + +end + +--- **Tasking** - Creates and manages player TASK_ZONE_CAPTURE tasks. +-- +-- The **TASK_CAPTURE_DISPATCHER** allows you to setup various tasks for let human +-- players capture zones in a co-operation effort. +-- +-- The dispatcher will implement for you mechanisms to create capture zone tasks: +-- +-- * As setup by the mission designer. +-- * Dynamically capture zone tasks. +-- +-- +-- +-- **Specific features:** +-- +-- * Creates a task to capture zones and achieve mission goals. +-- * Orchestrate the task flow, so go from Planned to Assigned to Success, Failed or Cancelled. +-- * Co-operation tasking, so a player joins a group of players executing the same task. +-- +-- +-- **A complete task menu system to allow players to:** +-- +-- * Join the task, abort the task. +-- * Mark the location of the zones to capture on the map. +-- * Provide details of the zones. +-- * Route to the zones. +-- * Display the task briefing. +-- +-- +-- **A complete mission menu system to allow players to:** +-- +-- * Join a task, abort the task. +-- * Display task reports. +-- * Display mission statistics. +-- * Mark the task locations on the map. +-- * Provide details of the zones. +-- * Display the mission briefing. +-- * Provide status updates as retrieved from the command center. +-- * Automatically assign a random task as part of a mission. +-- * Manually assign a specific task as part of a mission. +-- +-- +-- **A settings system, using the settings menu:** +-- +-- * Tweak the duration of the display of messages. +-- * Switch between metric and imperial measurement system. +-- * Switch between coordinate formats used in messages: BR, BRA, LL DMS, LL DDM, MGRS. +-- * Various other options. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- ### Contributions: +-- +-- === +-- +-- @module Tasking.Task_Zone_Capture_Dispatcher +-- @image MOOSE.JPG + +do -- TASK_CAPTURE_DISPATCHER + + --- TASK_CAPTURE_DISPATCHER class. + -- @type TASK_CAPTURE_DISPATCHER + -- @extends Tasking.Task_Manager#TASK_MANAGER + -- @field TASK_CAPTURE_DISPATCHER.ZONE ZONE + + --- @type TASK_CAPTURE_DISPATCHER.CSAR + -- @field Wrapper.Unit#UNIT PilotUnit + -- @field Tasking.Task#TASK Task + + + --- Implements the dynamic dispatching of capture zone tasks. + -- + -- The **TASK_CAPTURE_DISPATCHER** allows you to setup various tasks for let human + -- players capture zones in a co-operation effort. + -- + -- Let's explore **step by step** how to setup the task capture zone dispatcher. + -- + -- # 1. Setup a mission environment. + -- + -- It is easy, as it works just like any other task setup, so setup a command center and a mission. + -- + -- ## 1.1. Create a command center. + -- + -- First you need to create a command center using the @{Tasking.CommandCenter#COMMANDCENTER.New}() constructor. + -- The command assumes that you´ve setup a group in the mission editor with the name HQ. + -- This group will act as the command center object. + -- It is a good practice to mark this group as invisible and invulnerable. + -- + -- local CommandCenter = COMMANDCENTER + -- :New( GROUP:FindByName( "HQ" ), "HQ" ) -- Create the CommandCenter. + -- + -- ## 1.2. Create a mission. + -- + -- Tasks work in a **mission**, which groups these tasks to achieve a joint **mission goal**. A command center can **govern multiple missions**. + -- + -- Create a new mission, using the @{Tasking.Mission#MISSION.New}() constructor. + -- + -- -- Declare the Mission for the Command Center. + -- local Mission = MISSION + -- :New( CommandCenter, + -- "Overlord", + -- "High", + -- "Capture the blue zones.", + -- coalition.side.RED + -- ) + -- + -- + -- # 2. Dispatch a **capture zone** task. + -- + -- So, now that we have a command center and a mission, we now create the capture zone task. + -- We create the capture zone task using the @{#TASK_CAPTURE_DISPATCHER.AddCaptureZoneTask}() constructor. + -- + -- ## 2.1. Create the capture zones. + -- + -- Because a capture zone task will not generate the capture zones, you'll need to create them first. + -- + -- + -- -- We define here a capture zone; of the type ZONE_CAPTURE_COALITION. + -- -- The zone to be captured has the name Alpha, and was defined in the mission editor as a trigger zone. + -- CaptureZone = ZONE:New( "Alpha" ) + -- CaptureZoneCoalitionApha = ZONE_CAPTURE_COALITION:New( CaptureZone, coalition.side.RED ) + -- + -- ## 2.2. Create a set of player groups. + -- + -- What is also needed, is to have a set of @{Core.Group}s defined that contains the clients of the players. + -- + -- -- Allocate the player slots, which must be aircraft (airplanes or helicopters), that can be manned by players. + -- -- We use the method FilterPrefixes to filter those player groups that have client slots, as defined in the mission editor. + -- -- In this example, we filter the groups where the name starts with "Blue Player", which captures the blue player slots. + -- local PlayerGroupSet = SET_GROUP:New():FilterPrefixes( "Blue Player" ):FilterStart() + -- + -- ## 2.3. Setup the capture zone task. + -- + -- First, we need to create a TASK_CAPTURE_DISPATCHER object. + -- + -- TaskCaptureZoneDispatcher = TASK_CAPTURE_DISPATCHER:New( Mission, PilotGroupSet ) + -- + -- So, the variable `TaskCaptureZoneDispatcher` will contain the object of class TASK_CAPTURE_DISPATCHER, + -- which will allow you to dispatch capture zone tasks: + -- + -- * for mission `Mission`, as was defined in section 1.2. + -- * for the group set `PilotGroupSet`, as was defined in section 2.2. + -- + -- Now that we have `TaskDispatcher` object, we can now **create the TaskCaptureZone**, using the @{#TASK_CAPTURE_DISPATCHER.AddCaptureZoneTask}() method! + -- + -- local TaskCaptureZone = TaskCaptureZoneDispatcher:AddCaptureZoneTask( + -- "Capture zone Alpha", + -- CaptureZoneCoalitionAlpha, + -- "Fly to zone Alpha and eliminate all enemy forces to capture it." ) + -- + -- As a result of this code, the `TaskCaptureZone` (returned) variable will contain an object of @{#TASK_CAPTURE_ZONE}! + -- We pass to the method the title of the task, and the `CaptureZoneCoalitionAlpha`, which is the zone to be captured, as defined in section 2.1! + -- This returned `TaskCaptureZone` object can now be used to setup additional task configurations, or to control this specific task with special events. + -- + -- And you're done! As you can see, it is a small bit of work, but the reward is great. + -- And, because all this is done using program interfaces, you can easily build a mission to capture zones yourself! + -- Based on various events happening within your mission, you can use the above methods to create new capture zones, + -- and setup a new capture zone task and assign it to a group of players, while your mission is running! + -- + -- + -- + -- @field #TASK_CAPTURE_DISPATCHER + TASK_CAPTURE_DISPATCHER = { + ClassName = "TASK_CAPTURE_DISPATCHER", + Mission = nil, + Tasks = {}, + Zones = {}, + ZoneCount = 0, + } + + + + TASK_CAPTURE_DISPATCHER.AI_A2G_Dispatcher = nil -- AI.AI_A2G_Dispatcher#AI_A2G_DISPATCHER + + --- TASK_CAPTURE_DISPATCHER constructor. + -- @param #TASK_CAPTURE_DISPATCHER self + -- @param Tasking.Mission#MISSION Mission The mission for which the task dispatching is done. + -- @param Core.Set#SET_GROUP SetGroup The set of groups that can join the tasks within the mission. + -- @return #TASK_CAPTURE_DISPATCHER self + function TASK_CAPTURE_DISPATCHER:New( Mission, SetGroup ) + + -- Inherits from DETECTION_MANAGER + local self = BASE:Inherit( self, TASK_MANAGER:New( SetGroup ) ) -- #TASK_CAPTURE_DISPATCHER + + self.Mission = Mission + self.FlashNewTask = false + + self:AddTransition( "Started", "Assign", "Started" ) + self:AddTransition( "Started", "ZoneCaptured", "Started" ) + + self:__StartTasks( 5 ) + + return self + end + + + --- Link a task capture dispatcher from the other coalition to understand its plan for defenses. + -- This is used for the tactical overview, so the players also know the zones attacked by the other coalition! + -- @param #TASK_CAPTURE_DISPATCHER self + -- @param #TASK_CAPTURE_DISPATCHER DefenseTaskCaptureDispatcher + function TASK_CAPTURE_DISPATCHER:SetDefenseTaskCaptureDispatcher( DefenseTaskCaptureDispatcher ) + + self.DefenseTaskCaptureDispatcher = DefenseTaskCaptureDispatcher + end + + + --- Get the linked task capture dispatcher from the other coalition to understand its plan for defenses. + -- This is used for the tactical overview, so the players also know the zones attacked by the other coalition! + -- @param #TASK_CAPTURE_DISPATCHER self + -- @return #TASK_CAPTURE_DISPATCHER + function TASK_CAPTURE_DISPATCHER:GetDefenseTaskCaptureDispatcher() + + return self.DefenseTaskCaptureDispatcher + end + + + --- Link an AI A2G dispatcher from the other coalition to understand its plan for defenses. + -- This is used for the tactical overview, so the players also know the zones attacked by the other AI A2G dispatcher! + -- @param #TASK_CAPTURE_DISPATCHER self + -- @param AI.AI_A2G_Dispatcher#AI_A2G_DISPATCHER DefenseAIA2GDispatcher + function TASK_CAPTURE_DISPATCHER:SetDefenseAIA2GDispatcher( DefenseAIA2GDispatcher ) + + self.DefenseAIA2GDispatcher = DefenseAIA2GDispatcher + end + + + --- Get the linked AI A2G dispatcher from the other coalition to understand its plan for defenses. + -- This is used for the tactical overview, so the players also know the zones attacked by the AI A2G dispatcher! + -- @param #TASK_CAPTURE_DISPATCHER self + -- @return AI.AI_A2G_Dispatcher#AI_A2G_DISPATCHER + function TASK_CAPTURE_DISPATCHER:GetDefenseAIA2GDispatcher() + + return self.DefenseAIA2GDispatcher + end + + + --- Add a capture zone task. + -- @param #TASK_CAPTURE_DISPATCHER self + -- @param #string TaskPrefix (optional) The prefix of the capture zone task. + -- If no TaskPrefix is given, then "Capture" will be used as the TaskPrefix. + -- The TaskPrefix will be appended with a . + a number of 3 digits, if the TaskPrefix already exists in the task collection. + -- @param Functional.CaptureZoneCoalition#ZONE_CAPTURE_COALITION CaptureZone The zone of the coalition to be captured as the task goal. + -- @param #string Briefing The briefing of the task to be shown to the player. + -- @return Tasking.Task_Capture_Zone#TASK_CAPTURE_ZONE + -- @usage + -- + -- + function TASK_CAPTURE_DISPATCHER:AddCaptureZoneTask( TaskPrefix, CaptureZone, Briefing ) + + local TaskName = TaskPrefix or "Capture" + if self.Zones[TaskName] then + self.ZoneCount = self.ZoneCount + 1 + TaskName = string.format( "%s.%03d", TaskName, self.ZoneCount ) + end + + self.Zones[TaskName] = {} + self.Zones[TaskName].CaptureZone = CaptureZone + self.Zones[TaskName].Briefing = Briefing + self.Zones[TaskName].Task = nil + self.Zones[TaskName].TaskPrefix = TaskPrefix + + self:ManageTasks() + + return self.Zones[TaskName] and self.Zones[TaskName].Task + end + + + --- Link an AI_A2G_DISPATCHER to the TASK_CAPTURE_DISPATCHER. + -- @param #TASK_CAPTURE_DISPATCHER self + -- @param AI.AI_A2G_Dispatcher#AI_A2G_DISPATCHER AI_A2G_Dispatcher The AI Dispatcher to be linked to the tasking. + -- @return Tasking.Task_Capture_Zone#TASK_CAPTURE_ZONE + function TASK_CAPTURE_DISPATCHER:Link_AI_A2G_Dispatcher( AI_A2G_Dispatcher ) + + self.AI_A2G_Dispatcher = AI_A2G_Dispatcher -- AI.AI_A2G_Dispatcher#AI_A2G_DISPATCHER + AI_A2G_Dispatcher.Detection:LockDetectedItems() + + return self + end + + + --- Assigns tasks to the @{Core.Set#SET_GROUP}. + -- @param #TASK_CAPTURE_DISPATCHER self + -- @return #boolean Return true if you want the task assigning to continue... false will cancel the loop. + function TASK_CAPTURE_DISPATCHER:ManageTasks() + self:F() + + local AreaMsg = {} + local TaskMsg = {} + local ChangeMsg = {} + + local Mission = self.Mission + + if Mission:IsIDLE() or Mission:IsENGAGED() then + + local TaskReport = REPORT:New() + + -- Checking the task queue for the dispatcher, and removing any obsolete task! + for TaskIndex, TaskData in pairs( self.Tasks ) do + local Task = TaskData -- Tasking.Task#TASK + if Task:IsStatePlanned() then + -- Here we need to check if the pilot is still existing. +-- Task = self:RemoveTask( TaskIndex ) + end + + end + + -- Now that all obsolete tasks are removed, loop through the Zone tasks. + for TaskName, CaptureZone in pairs( self.Zones ) do + + if not CaptureZone.Task then + -- New Transport Task + CaptureZone.Task = TASK_CAPTURE_ZONE:New( Mission, self.SetGroup, TaskName, CaptureZone.CaptureZone, CaptureZone.Briefing ) + CaptureZone.Task.TaskPrefix = CaptureZone.TaskPrefix -- We keep the TaskPrefix for further reference! + Mission:AddTask( CaptureZone.Task ) + TaskReport:Add( TaskName ) + + -- Link the Task Dispatcher to the capture zone task, because it is used on the UpdateTaskInfo. + CaptureZone.Task:SetDispatcher( self ) + CaptureZone.Task:UpdateTaskInfo() + + function CaptureZone.Task.OnEnterAssigned( Task, From, Event, To ) + if self.AI_A2G_Dispatcher then + self.AI_A2G_Dispatcher:Unlock( Task.TaskZoneName ) -- This will unlock the zone to be defended by AI. + end + CaptureZone.Task:UpdateTaskInfo() + CaptureZone.Task.ZoneGoal.Attacked = true + end + + function CaptureZone.Task.OnEnterSuccess( Task, From, Event, To ) + --self:Success( Task ) + if self.AI_A2G_Dispatcher then + self.AI_A2G_Dispatcher:Lock( Task.TaskZoneName ) -- This will lock the zone from being defended by AI. + end + CaptureZone.Task:UpdateTaskInfo() + CaptureZone.Task.ZoneGoal.Attacked = false + end + + function CaptureZone.Task.OnEnterCancelled( Task, From, Event, To ) + self:Cancelled( Task ) + if self.AI_A2G_Dispatcher then + self.AI_A2G_Dispatcher:Lock( Task.TaskZoneName ) -- This will lock the zone from being defended by AI. + end + CaptureZone.Task:UpdateTaskInfo() + CaptureZone.Task.ZoneGoal.Attacked = false + end + + function CaptureZone.Task.OnEnterFailed( Task, From, Event, To ) + self:Failed( Task ) + if self.AI_A2G_Dispatcher then + self.AI_A2G_Dispatcher:Lock( Task.TaskZoneName ) -- This will lock the zone from being defended by AI. + end + CaptureZone.Task:UpdateTaskInfo() + CaptureZone.Task.ZoneGoal.Attacked = false + end + + function CaptureZone.Task.OnEnterAborted( Task, From, Event, To ) + self:Aborted( Task ) + if self.AI_A2G_Dispatcher then + self.AI_A2G_Dispatcher:Lock( Task.TaskZoneName ) -- This will lock the zone from being defended by AI. + end + CaptureZone.Task:UpdateTaskInfo() + CaptureZone.Task.ZoneGoal.Attacked = false + end + + -- Now broadcast the onafterCargoPickedUp event to the Task Cargo Dispatcher. + function CaptureZone.Task.OnAfterCaptured( Task, From, Event, To, TaskUnit ) + self:Captured( Task, Task.TaskPrefix, TaskUnit ) + if self.AI_A2G_Dispatcher then + self.AI_A2G_Dispatcher:Lock( Task.TaskZoneName ) -- This will lock the zone from being defended by AI. + end + CaptureZone.Task:UpdateTaskInfo() + CaptureZone.Task.ZoneGoal.Attacked = false + end + + end + + end + + + -- TODO set menus using the HQ coordinator + Mission:GetCommandCenter():SetMenu() + + local TaskText = TaskReport:Text(", ") + + for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do + if ( not Mission:IsGroupAssigned(TaskGroup) ) and TaskText ~= "" and ( not self.FlashNewTask ) then + Mission:GetCommandCenter():MessageToGroup( string.format( "%s has tasks %s. Subscribe to a task using the radio menu.", Mission:GetShortText(), TaskText ), TaskGroup ) + end + end + + end + + return true + end + +end +--- GLOBALS: The order of the declarations is important here. Don't touch it. + +--- Declare the event dispatcher based on the EVENT class +_EVENTDISPATCHER = EVENT:New() -- Core.Event#EVENT + +--- Declare the timer dispatcher based on the SCHEDULEDISPATCHER class +_SCHEDULEDISPATCHER = SCHEDULEDISPATCHER:New() -- Core.ScheduleDispatcher#SCHEDULEDISPATCHER + +--- Declare the main database object, which is used internally by the MOOSE classes. +_DATABASE = DATABASE:New() -- Core.Database#DATABASE + +--- Settings +_SETTINGS = SETTINGS:Set() +_SETTINGS:SetPlayerMenuOn() + +--- Register cargos. +_DATABASE:_RegisterCargos() + +--- Register zones. +_DATABASE:_RegisterZones() +_DATABASE:_RegisterAirbases() + +--- Check if os etc is available. +BASE:I("Checking de-sanitization of os, io and lfs:") +local __na=false +if os then + BASE:I("- os available") +else + BASE:I("- os NOT available! Some functions may not work.") + __na=true +end +if io then + BASE:I("- io available") +else + BASE:I("- io NOT available! Some functions may not work.") + __na=true +end +if lfs then + BASE:I("- lfs available") +else + BASE:I("- lfs NOT available! Some functions may not work.") + __na=true +end +if __na then + BASE:I("Check /Scripts/MissionScripting.lua and comment out the lines with sanitizeModule(''). Use at your own risk!)") +end +BASE:TraceOnOff( false ) +env.info( '*** MOOSE INCLUDE END *** ' ) diff --git a/Moose Setup/Eclipse/Moose Loader Dynamic.launch b/Moose Setup/Eclipse/Moose Loader Dynamic.launch index 5efaf2485..4b236719f 100644 --- a/Moose Setup/Eclipse/Moose Loader Dynamic.launch +++ b/Moose Setup/Eclipse/Moose Loader Dynamic.launch @@ -1,6 +1,6 @@ - - - + + + diff --git a/Moose Setup/Eclipse/Moose Loader Static.launch b/Moose Setup/Eclipse/Moose Loader Static.launch index 4ee25e04a..d4c572762 100644 --- a/Moose Setup/Eclipse/Moose Loader Static.launch +++ b/Moose Setup/Eclipse/Moose Loader Static.launch @@ -1,6 +1,6 @@ - - - + + + diff --git a/Moose Setup/Moose_Create.lua b/Moose Setup/Moose_Create.lua index 42a3213aa..41c41c22e 100644 --- a/Moose Setup/Moose_Create.lua +++ b/Moose Setup/Moose_Create.lua @@ -5,19 +5,32 @@ local MooseCommitHash = arg[2] local MooseDevelopmentPath = arg[3] local MooseSetupPath = arg[4] local MooseTargetPath = arg[5] +local isWindows = arg[6] +if not isWindows then + isWindows = 0 +end print( "Moose (D)ynamic (S)tatic : " .. MooseDynamicStatic ) print( "Commit Hash ID : " .. MooseCommitHash ) print( "Moose development path : " .. MooseDevelopmentPath ) print( "Moose setup path : " .. MooseSetupPath ) print( "Moose target path : " .. MooseTargetPath ) +print( "isWidows : " .. isWindows) + +function PathConvert(splatnixPath) + if isWindows == 0 then + return splatnixPath + end + return splatnixPath:gsub("/", "\\") +end + local MooseModulesFilePath = MooseDevelopmentPath .. "/Modules.lua" local LoaderFilePath = MooseTargetPath .. "/Moose.lua" print( "Reading Moose source list : " .. MooseModulesFilePath ) - -local LoaderFile = io.open( LoaderFilePath, "w" ) +print("Opening Loaderfile " .. PathConvert(LoaderFilePath)) +local LoaderFile = assert(io.open( PathConvert(LoaderFilePath), "w+" )) if MooseDynamicStatic == "S" then LoaderFile:write( "env.info( '*** MOOSE GITHUB Commit Hash ID: " .. MooseCommitHash .. " ***' )\n" ) @@ -31,13 +44,14 @@ if MooseDynamicStatic == "S" then MooseLoaderPath = MooseSetupPath .. "/Moose Templates/Moose_Static_Loader.lua" end -local MooseLoader = io.open( MooseLoaderPath, "r" ) + +local MooseLoader = assert(io.open( PathConvert(MooseLoaderPath), "r" )) local MooseLoaderText = MooseLoader:read( "*a" ) MooseLoader:close() LoaderFile:write( MooseLoaderText ) -local MooseSourcesFile = io.open( MooseModulesFilePath, "r" ) +local MooseSourcesFile = assert(io.open( PathConvert(MooseModulesFilePath), "r" )) local MooseSource = MooseSourcesFile:read("*l") while( MooseSource ) do @@ -50,7 +64,7 @@ while( MooseSource ) do end if MooseDynamicStatic == "S" then print( "Load static: " .. MooseFilePath ) - local MooseSourceFile = io.open( MooseFilePath, "r" ) + local MooseSourceFile = assert(io.open( PathConvert(MooseFilePath), "r" )) local MooseSourceFileText = MooseSourceFile:read( "*a" ) MooseSourceFile:close() @@ -72,3 +86,8 @@ LoaderFile:write( "env.info( '*** MOOSE INCLUDE END *** ' )\n" ) MooseSourcesFile:close() LoaderFile:close() + +print("Moose include generation complete.") +if MooseDynamicStatic == "D" then + print("To enable dynamic moose loading, add a soft or hard link from \"\\Scripts\\Moose\" to the \"Moose Development\\Moose\" subdirectory of the Moose_Framework repository.") +end \ No newline at end of file diff --git a/lua54/lua54.dll b/lua54/lua54.dll new file mode 100644 index 0000000000000000000000000000000000000000..003fc57d2e6126147e09c59a9fc2e67aa865036d GIT binary patch literal 356234 zcmeFad3;pW-S|I~1ttN!gMh{sb+oZ2RWYep6G51pOu`+QKmY|=t;I%Bt5qs9f`TyV zBqg`k(X`dpZclx>{p|K=pDH3<%pwU()c|#gwi@6Wk+CKI&Z=lA^k zd;NH6X6`-P=X}m*Kc91^;S24ae2>Rd!2hG89?y0@(X!}+2 zSAEZO;FiZi4-fKhkeN*IcaWvQzN${LAK5pI-Ne|QV1egBjnB0Oo;K;= zfqc*3%A}V2FJ9nzOpx_&{smSScyg;}s=Z%Z;OTGx%Dfu)IV*bAhA3~^PoR>3D062X zx&H#5#?`Z~ydru9F9)Tdf;uwoQ^t23^fY$Ny2+{7v4-*><=5~TIJVyE;H z>iG=y)WrF8{{=l2!C4!edaKXmblIGsQKk4IG8v$|u}RZf2j z=ipw=`MHnj?^>sxS`*er)jRx=dL6TBmt8F5u982E)Dygn99u8gF)Mn#f=|%lVXaiX z_m8ayd^fGTsfj8I70`l*|2!WaTd#t8S6zQ26%|dysU&NbCzZ$kfxnt_XFJc0^3hJY zz~?)*USrLSR9drTi2=i;u16uQ~U;N>%S%FCQIle`Oz2Z}$1B-sv)4ALUZz zGapm${0me)L;em>F7;;}TQ69nD*gX|{y&3(5i!WKZLb@$$-5)mp z+F%09)q5!of?<=Ms<(YG7+FY28fp`hXCW( zv=uPCTFc4QvnDdFmUn3CzC4katwUPNe(B9BoJ4iQ0*xKfDGjE*L-;CU9vtmU6mCJD zIM9Kof`D0 z^+^?G;bHN&=BG-t&k)8*z|kuxW1dsqxAHyqN-f`$dgH^Aw;iGq;SUP{$`v(ytXtn*K$R_lJle9}B%o)CeMuCNLheISGsoo_F@I?od}?Mljz z+P{e9d35u!*5{*ziTCnjr4*RYNx>!xG7nU+FZ*D06ne_9t9&_R4)ji7BBP3U73p-h z+wXzQWbLSG!y?ni`v0^)ty)F*WO}mCpxf3CI*gr?c+VUAJp~RR@4r7fn*K!&{ZSgR zCwy5!u|pvVsD`3$bFKAe0LZp}f%n;m&fJHe({Jizr>>$QncV%<00y-@8Tw7UZa{TU zeG}Bm!U_XKiD4)8J1pRBsvmKd`9OtSf1_jt-Mc< zZ}6QEeVP|_+BTy&RK07=yWM_+-R^uGQBq-;Pe;ry z!`y4{J;@8P_f~eqH~Z?MbrGx9ZiCiVpNwnkq#*~t6l{QtJWlmqrl zA=*({+ZG&73}|MTwyle4Rv!2C9UA5l`<0&vSS$SL{c4tDupIqiRPT!}i&&rchnqSS1C)2ywRS|z zXCvn4{hO=mlB>YgLO*?knRXi3b$>X~;fJonkWIw$H<(MInxIdIf*Pv3Bj!SXtRM1r z`?ta+hSe+z*f75(9~61U@bR|hFASFF%iFlHb13=sGwU^v*{3%T%a`?2BXFo;E%k@b z@*ABa1>U~yHSJ^VfrnDoPG>Q?{Dxp?xS$*5;*9TTYUx7+?cNRZji&u>Wv|hB2!@Aj z_8H}!f>@(<_vS`8*WVKyMpJgh{)tBP!eH>3(qP@~i7v0vw8yY`)4Dr4V3qt37LuuxRj@(-=Lg~FrLgYj2X$@Rs*<+P4>y>r{B>=O zC6VgRn+^z4BIfRJ^}d@wEA$qzPEvS+T_V;pXY`?YO*V#l+bC6&9uUZYXnr`^^0Zoj$98E_mu78bSSdlZ>K zXa#+A9L%A2fydtJ_8lKNLA$FXKC(u;tE;F_+a?TMHl9CJ{gu>5+2m7stdh%LiPx*_ z3n+&)lo*KL;+uxFN?wfi(=HN``&dXUGVyE=lckyj#flmvY9kD4api-_zb#HVQOXt(%UA=l2 zuI^xk%@M)0l5VL-bgw7NKVw4Au^(l6KGISGtP3$T3IWiW(a#U7a zRZ1-jGR^jnDY!)nGV<(W%DT{aYC+3Koa$gfH1ft>klr2|K{%#V*0M(5moQha1$ zv@kw0Jr2~M}Vs-i75giNNrNQj(gkx#Gv%Ylz^!F7BjFZw9}jD1RbxUpCWOLSGD zt87i{?neSpjiH718Q#8J@Urx(;Q1c?RqjWE8L@m()+jP(%pij^-g3!dU?0Kz-gX-bWOX{zoCqoXpQ}2DG_5NxweGh1=7x!*d1(J zh@B?QF^H7y;u3MceX{gy4hs6|7)wkntIDCC8lNZqSD{x&9w7CmbqKPS)5)!H0D8Dz z{ERaD`FrF&-{=FODfNR0Uy-V-BT4y#<8_U4!|W#?{rEnsIvSdG)tPj> z$lp-j8SXq-ASO-3Dwqni{s0zGXsR#N7VNrTI`RW>TldGNKx4{kQUt=o)Et7#l~?1Zm-i8Ra>_I0XsGFn_N;F6 zVM{v(L3a*)y7VDc#GNo$o2QjOZRZ{D#rGam3n`u<1d!T%1V$fcpE&rZt$k=vX4vC8 zq8B)5MRnqFMv}VpgEwkl|2ZMRkGP%L^-=KSekL0SZU40K{^xInt;Kh9{7&V9I!l|w zkJvXtOl9^m;BnkGL^Rb>oxE`UQ)UST_S&&ld-_pRSLThw-ZmvNQUynY!-LAljhNH zDMqwBp*CDRGcSF=U{)N=How=Ce#)T_;TKT2*G{|zdd1yK;_F&I`=3)YoGf1GA|OG2 zTb`j#4Si+S47M#qQY`0njkxz907VY}_S6h}(yxx;OZDF%N^GwkP(%14=6<^n5ClBw zHDYF(MM9L+0)F?sO~CK-5e|dN@Nl`a;PExBreenF5Ap0Qe)sTmqoAEYB`? zXYxOfh5Y!)SiH-Xr&)cz*L*s}3U4}2CL%PG;mo?$A;H&{^IJ!^us$O8MBE`MggeDQ zPMvv|INNbl6>>kLo1OXS=22ak0)+M3F9Lb`PviX4?0Ow|BcroXh=tKbqOm^Gk;>Qe+Fevad}c86K{?Fg(< zCFftx#qYnD7yD5y$I6R01quH~Ui|m#$H)r?&obV^!AP=Z0MTJv=eY}*NWljzq{H!J z_6YJ_$%_GdfVEbp^&ixVTOSDu{V@|1`fnD(2@2gO>v;7Ytc8R*e$Wm+UHNf5@Qsrn zr!kph^U0MTOOKTwYI^tlo9X=~ zGrgZ?r+1G`uPy0L@0-Xc-;6r}|TE*5!n2Shs z*>*owL6>-o$Lf9t!X6F=)7G3mAB(c{~diTV`veWgaftcWnw-X*4GA=eOjos3O9>sDZ=2UGOdO9 zn>?!k!6`%agbOhU23>+Iv!Al0m>DT^6nAXU{&EJZr@L;yu=!eH^F`XW{jxQJXfYpj z7@avgOg%=y4*ta`#RPEyDbZTj!*=QE6gu)o_`f$;0(6WIg+4^z>7S3uZ>L;2W9b)I zIVEpFe?wEB-Ix*#`1RyMBI+I5!!^a#ozls=g+d>WC}2yf1_M(c*4t_V5;;|1MyH2e zTudSqHt0l4O2@@aC1e(o4U_S2!I(0yrYnRk@`JOp(>-JGy7Hs91I>%MfXNXlT}=o` z+-GqU%KJpN<|7LTmrZ^*ai7ebOpNH~l-TXJpI;^0NgF0Y8)A}z<$z+l>PFwXv|+N! zPC-U~OSx%KM?P`pjS9PsF4BK=;CJ*zP){7iG{l478?jnS+ZZyoQizA?x#6kueHiJH zWOz{Q)rxVTT^AwV>KY6EX0#OYhfA>K=R<<_jxQ*HKMhN$^4HL`UZRfyiSQ`UG_zdgz(m35j%mo33m?afN?IiANE~zps z>t4!;>6U5R7ELnDggj~wFPN7Y_^@afXh`hxuQ3NnAbeEw>)qZ4InV z7Jp$vy$+no0&i*sMY;U$;IGk07Iq)cUPy9NX`rEXcZ7x2LeFCGsURdF?kDVJ*NT34 z)(Ln=fWIA}?6U(suUjWxqs8NetZ z{!XaU>zGt+x)0eHjP@!H?xa`|ZS-Mr#ZP2G)4y}iMm^b99Y6YRT#a~t1af&rl zCVR8OmPN>mp89J&3fBg>{$h8#R6Ande8eLqUZN2%JNtOE?-X87Kdn5^d_2%jyEwf| z?A`hj9?xLE+LPWc&oaktH(ZMa?=zZIF9x<=mFx=tOR&>9P;BHADKU>IE2`TrVlar% zwZpJWj@rNcrjjhJ&j;IXxK_L4ix8do6<7IiU{Xtj1jpnfeQVq5ud|9f2lA3jL5CK0 z=6+r33~Cx|;z@<;KU(4XQ~?zzyEgS+X1`UzFZef-h99vE8B|$8Uv!E>`y-Q>ffnk= z8v8Usvg4AC`OM4c>Kx8%tG_0@ow*fyGoMZUl1|%JT%CT(LF#GbD9Pk@gMXP$l7zxeh1QS^SSQcg7)y*xyTB;{|cPd#D}As zh@>nJY-j6tw0v-lSu+=gP$F35cK%X-`U&XC=}(0lHlJtTF-y2LV`38=5ffV+$(>-x zowR1a`j4c2Y*)>Lu(}P$Z!q@I}>#8IKnV9PJzU5HD&>pQZjts>8AN zPnpQO{R(suUr-UmtpY#`J|=qe$W{@~%ZZe!00e#%k+P2M0bv#`)Mq3Mrn#7AWU==# z_C>va?VwWc3O57u=T3+&pZn>UnDoVB^y|2vvk}W@O(DyH9jHVn`^yK}@!KcXeMI(X zWT&TQP}>&v;m&1^f8}LJfG-XJ?Elo1=*gNhGyVE|yhPGGJh$w05}Sa&%vucw!A z)2qE6f{w_#GW+DOgG5A9 znIpVURrZRQteN#@vy%BSG`slg4mC7&8~4c8Q$xPqh~(%BG+05VxX>R)94iB$@HhcZ z3w5Nffe{@3ll`cdUKCLx`#;2JCqyrZk9<0I2Dw6(fAc9azv}KSCos;~PQMcYg)K8~ zo#Kbq=QqDa8{PrfoX`8Wyc9|ofM&53>I)E~Xx9^Io}XWs-1K}P{Y&vL*uFWEX_EReMAV-9&w%i(V30jg zp_j>Rn?GUc?-SE_KVnT8yoQaifi3z_f!A+nb$hI-m86vHV|}+^pVdb5nYr25Y5Uph z9ffqFXQ2Cc)m86`&eS^xCN>}15=Wqluc9{(#38OwQI|QjTNQZ#&>k}P>)WOC2$D}UP1u?iCfCDS7PJb!-f2%fhj*UD@Mv^Weez-q)FF?NC7;GY##s zikKr)j2&-za>j^(0B5rv+&l6~#*`s{?e3ps^Miu2v28v>eDkcO$e+C-b7ykxsJXB6 z=&9ze%D(bliQz3a=g%&paUuT)!n{`kz1^5_IM2kX1|8i%|t5 zzF9=t4BSJ>7GbCg9s)jE*)3NfVw)C`HnU(g5^9%W?qLtptT30)Hm@n@sjbU&JX6fm zCcL4QtK3e1@^PKYs@dmCr{;oHp|;Wr^E%;Kv6x9=8{=2q{9F1LIr3ItsjtY%u`@(> z8WwS#Yp}wfY%m`a1JnvOlJJGOVK-nt%Fbxm?A2NxW6Y3sBktFVs&LakLe|aGsiyZV z@Z~di4`7DPQ|nAy%{-D^Jl#cwSFqq0fH$x9daT8?s;|F`tsW$%wC{y(bS5-i3L;C-$$fHN8|Nhj_7yW!w7VQrWjoEI8Y%4~0sxW~btb z2BMQ1tb$U98CQn@hvX#(E&3Pr%XP7kOES|4Rj0LNg)AhJOneJ(pM=%)&qhiKSvg>Kwn_mD%%vvAdsm)P`UX?unKO}}HsGOzl=y8eR!f2HedQ1c0b-Ki8wDjrK zx|0`c-FF2vTtyY_LqXZx$$UGxn35(w$4lb$F;M)J$l_Ow2cED;N{A{ z0+D}>N*+Y4!oeow-yxBI?7V7qotDqs6MuC}{FOM_1KPITod>m|J=i6=@?Yc^@CU=@ z{%}*D{x0UiB0nvm|v> zH&b=+rTLsa7YOyl2cvc7;n*JpwhU#v@dc;$QMdMS)K2$hJGTFe#02`11V#e0QrD@N z+fl0DF|HkY6nYLOE}RGzYg;u4hCR;Yj0+1fTbc^K4!evFAQ62+Sry86Md)R$Pl(jM z{XN;5yhsRh%W)3PE3+TMXF?*dgrfbjVStlIjN6F|Ab}*`G7vKP=v`Vq(em4T%8 zR7SIBNx{8paCzsIcb|}Vw`cGTY^hCPA@dBCy3k64X*7URf!Tfubf=b4Gcz40Y^T7H zs!%(iDA8s=EOkFk-8J}ssoC-+)4v-}4^_(!feNW_KHL6(Ui5gXdo~rsuM3e7+HZeP zo_pQrL?&P^wBxZi=9W?SI;ZWk(l)){-Ji5ROYe4LZnugQy4!PP`!z=LGTD!LTz2Z& zc6lyh_B5D#?2||Zg#u?X zcTKGBSMhVPDQ<{w0y$di0m?-b5!kuD6!j4*ZKTA=k1j*{U_wFH_FJD2$>Zb}q|a3A zsp@DgPtdxuV~zF}|IZ4Q1{IBHKj_fvxvyTHBT17p?)BzyuJot$zp*OZ#CXZ+L6Nt~0!#oL zLw2(y?J(%fiqXM;7`Vcwq)Ui{nOo?{3tcv59igx4%k$J@o?WG&%0=;;yHl-_F(v&k zN6$!{Ga3n5$BVHT1*9VK@Iaj!zo|>N$k5C&EH#~-LKNm`v`{A(@E=BUxxdcrM6z9) zngXYW%sko3bKpbyn0o1$eFQ#<*N=i9+2#(|XVU;9*Kv7(C#RgIA=*0i3bOUa$J5>T zs4}T!k6_lRkW_>WZ&}%N@TfaHC zUCd5w4Xk0GT}Ao{4Ml-E7GF;O*^Vkq&(zIXe!-q-(gSn!(#^LQ!bX-o7l6={-Qh%gErZ>6n;wV$?tqgD|*g-m6}V1 zTzf%$;m&~)z4M^Is7u6~cut)IT2WW1Xb+P~O`<`rJ%G_6Rov1yi2g9j7=KEc-rOli zC238!8Z}crRCDNC_?zv8kQWz9g=#0GN;Xew9{CWwY{A@>=xZ4|Q+BpzO!$!W``!5L zhyjPbr9BHRUEuI9ovfQ3-o8+E-{wgdsUFBA)hFw`YSTlxJ^kqedfMXjl$$@{*rVYj z&W=d9CopC&5;WhecKrg5%7$~Ccs`1p|M8bx=byrpbe%uLnMe<CeB_i<%1HdEGK(6((Xwzodx z+5d@qkT9obnhzJMDNVC_7TH$N8m z$@Xgq%Z~u>(7Uu4Ki5h2_K&OXu9w2s%BTMad>v;;KfVu7Z@#E|G(C3VL(oNUejd-M zUn>$p-uLFqc_yd)ZdER--Fa?R&vJAvb?aDtaiUUR$vnIJJ6d_PG$@{*NTXi+RrotG zXX^Ud7M}794`|Hd&aHt;zake_Cm%#H3sYGu-wySt;x#XH$kXt$#@)6cR@ z>y-UnhMBTPE9H}Qs|;p{jA=!c*{*D1dw_fv)xWUp z�f;07-EI#f3`j!PHf};!QR2fG_aX96_kl6(GUHA0mQNwY<*Kx4~JmCercXC-Jn* z5#?JoACl108N#OGW2O8~6Y#G)HYPh@N9-W;OT7bT$0j||{-F#pz)OdIoY^7L1q2PA z7k0_BV^V7ewZ=s|ForWLaxrCzIex8WlK3>s{UPg8yx!-umInks4BkZcf|#CyFDmMP z0mRX$U+GsVpT-Ef*<;lQ%nxMnS0XtI%IG*Rx zsUBvT2~5_9oUKkV7VL-628sE;=x{HSW$+zn`%rO3#?X5O{i zKZJ-Y_b09mcw3(*jb87xiy#bif*Z_4*QEbErjMQd7eX!KP|b(t6Z3rJ7@7SFJo|U7 zGO*PG8>`l@k{y+&mHL^Z*^T51=G<>z`8IQAf6v8_LumG!Of50T2P>-8wC@1Pf?!8t zi~P1~r&rhdqlc}H3U#;bQK1LddYTxIQa$G8?PW8lN1@=+_hU(@{K-|H@sE%KywfqOwSWm*b{EHRT zS#>_B8?1M}=C@}G5lR-Q@LNgziFl~!P|@L{5j!8FG_xZ15DCY<`QoKGT8MN-_hhXD zm0;|61yyvO8(!}gW=*_*p+MvSquu?Oxl=&Gka-!1gMg{Eei@+9XDJaruKt4_L^L}c znLF(bJea-8L2o}M-pR)B&r9zGF6&37_sPefN`>J=)`O+n`3#Gzj141V+xl&3hZM1+ zS!$m_mmyLwVAVs;)B&w%pV@_8#elO5nXpQJazKZ4>nxwvLY@>m07rd~IH=Su!$*fNVy%!!OPf1rI&c28g6}5t)qPIgu zhZXYeHuz0CyEnHgG?vA>L_Je^G-dz6TIow&t8f=*BxnC|mDt)<1RS4;O6KD#thB7X zWUcb+)vKn*UJF~RkPM3mGaeOVXZ_QUz>prJyg&0|Cz&Ak@T!++36TMjqI@mmn}5 z=pr5$^FQIFMR0sAZnk7c5kn4{zg43F&V~%Tq^7V^ie*#wO{^}1{V=&;f@lSzZA8J} zR+z2zg_281$V|yxD+FS6M47^^oF|qUX1io%bM5=Yg~icHsd_ZKBd@KR>D4v<*c()f z(~-w{j`~ucq6++$`XqdwqYoVU>C{sC;6QYyE9{PpNEQNP6Qj)1ll>b~0GY<~nH848 zoc^-=GmygdrE76Xk!~y%l3$n-Tk4yj1eNebU*~IhS%>f-Ahe>9&X-Dxo-2~fWpVA$ zd8w}r0;wa9D_nd0N#rtdY?g2Rxfo)P9?3DpocUwR%DO~L13QEhMfMllS2LvOg$jCC z2Y$!&M^63Yv-K}<>cE^UrNv^*}`Sm2W#4ihuGWE8i+W` zuxFIvUW{$^Q$&Sw>~n_nXNP50hqMKg=4WZsBoFyUR~i zj>3{0?+YEBWnJfst`M#4o)$ln^(yRp&;g>Lh$Mr0N%#)zn9f-Ce1;LNUCe~56ic9#R6+OAo*!QDUEzZwPTyG?X3)U5KcSG!QM=#L_+ek<3 zzoJFNs&ecp-Y5$RsUmijeYu@AjvNTO6T|-4Clzv$m#;bVNdNcPda6*LGBJ*4JurmR zcSH4K<>TPfT=?>%tLQh{M7$!p*s)+;9(Wv^gie4~SyZQd6x!*^_*2k606eY@=R%9T zR$4gymyA3Xyr&-+gU5{@;+4T-*w4E6`eSS6xFwX-j(|HLYn%|Iw?~+9m_k z`r2xXb>daM`BTRC!As(kL4WMKQta#vSdx_7I-SD>x4uZyEk7**~Lkw zwbptu#bJ7IW29R{+3LsTR`;Y%pum-9D&B}zvX{T?YHVnEl6 zCKo>K&dEHYht(!weoYmdq5$}=@YV4$Y-W8j{3QAbEOIKoTM`Rifq4_7+&m(h@1`## zM)PAgIwX?lt+acQsbbu;urudz{Hda01i}5nENIb;-*5i~pp`LT^E%5@WhAy_#!s%C zEQ%qCj!8~A_f3L2hoLG2?zb}4yRAuyqf^&6MUsWjznOA{>RALrhc##scqq%%Ks?L7&MG$VqGPw0KvgL1- zrNmZLTrtKku0AjisO5PQO<;#PW|4Sr`ufkI5vGC>T=O5OjV-~4xXNV!Ec-xb+n19x z#n#DWM{?U5R-Y(WM<=L}+>-Orme~^bU1#pQ%zT_ww{NI^qxWU|%I-h7Z7;#Z%l7BA zS?0hJcWbP}Q%S8=6VBD|x&H5Cak&L!4`vKPS>`+3+_R~ z=az! z&YkCs(6W(u+Sts3%AHXiGoF~Ij%OfrAgG}$>8k<~lB@;s1{OjcsT&nXqt9I9_Y!P5 zqp2=1r><1H<5{THurA^MWkL2Uzy3ZuLZv2Gu53w+oUgUY=?k-`^G!bzDyVJaDy2(h z)+>F99xov$&uHhX{^nO@gMy%y6NxJ7D;m_zg3{ExI0VTRXJ;{K-_F3PgUZ*B3$Rc> zh5suCP-ue(FqW44k}JxE6YSa2uSvflYr%BKm`T~AO1$G5aFkeh3ZhioJ}}XE;vAD_ zVp-4y_h2~s%gH|e+zYaPuvJtCU|n6p^uK~RhiO}fVoPL!mys}w9;pps97Cu$AF_x! zOD%y?hs-nUe9X$Ol{w#yL*h1)o&wYs>bg@c(ah7GkP;zmuTStj-it6xF(xEZd;X1cN5kn!X$*2E$F5gmr z!U+?X|1myY@f&t)?x6AiUYIZUxt0MGUF%QPh=5T#Ic2YY5o6bai`to#Gf(}`)j(mt zDvl2qP0bQ0+}fX|cIq>V&vWc+2)#@vi{o$4Q`4G?LM%%e&yLwQZhn7H*%_<}rtoy2#w9hcZOjg7dH z&JkZ-Q6~;S>waDPNe49OY)>uYt+~8_UCt1yOtgK@eybbr+fJ7rN%ugzK8Yzr(1(5J zZt@Jwx1}Q|0dup?QP$=?i{n^%@A()KiBv?3qquk7=s$_<9HqMKRQ`eIVB31xq;vNw zX@%I|vlvfT+TrZlcN|j*e_>og;Ii@y~u`S|ARGxkRV zm^Z>#sjd^(vGlRVL~#6Uc;tKRg=+vtSm zA`eixWs}Ls3zZgwX7<+--E2tHGlyX=4Xr^Ykxsvt!#o65n$o+Y+fc!1*LOdsLQO;Z+@^ z>@R&zxS9YXH+CRowbm$#A!5aXM$-Xq=DYWze6l2_e-}j(pL=3FabT1X@*US83v@;E zb(OnA=4*A8Z=^$6dp|J}(h_1dB}OouC5gTph3{@WvX-f`= zst;_g<6bBBFT1?8*5o_t%w6Ul-F#Bg*gD^um>8)e*!kkkzey3ep2XkS%{qz!EKwGfJfC$y#gn9prdPhAdm=SaA=@`{az4LwQ7kI>SxV zzZvoFA3Y@YEF1M&@@~qRN$MtHUTeyIbMrkz1yh0lGG2tLU(i~Y^Cf);{-ZqIUdK~s zinM^;A#XqM-0?MDs7nUPf07zM?#jetJKxlRA>U|vI_j&nF219n^YBFHe8eT3Dp}~> z6B0W?-IXCLA@_Np?~;X&9>nx^;|6jGqe|6mmw9#CCFtuoG}F=yLOBa`Gj*N2n>vjA zIyzSO2+3U@XihA-PL31 zf0%L-_02&Bb$q>l-@S9rlR?sRvh)x^+#tXPm2wow?Jv+h^v3lgRZWCf6u^2KO_>;qLb&VwFjGZ93PCDWQfrG@GcuH+kLRA!jkh|a^ zdC^zYc#@+WmMY)L+E>Xb<-GWRHD6|$TExn$IXb5NZ6fJ~nYyC>_=Qb$|I&->5x1O2 z|Cyn8^Tse`=w801N~9=R_>g>`tiZ_iS5~e-15|-HuIkziFLXp3)IxK>kmZmTFm8EtN2G zcE|#_wZ`l#Cw@ZyQ9vK6otH=6+tTqfn&(1nAnq>dIAZ4gyzVP*lL&14@v5CDw*J$K!A-uc8-A+8t=xvtP@^5?(y{ZFwKR!R% zQuVX(a!XYko1fJzP>emd?%$y9Ym1%;QdXslKsZ*BpF^6}yJBy1970|j)t%9Qg*lZ) zQ$>|y?KAPJp&E$Wx)8-_m`{hBo{bvDoLE3>`8!SI%%UJLxl-N#hpto?C-!N#i@p;b z*~8~hJw0$A&){F{#c* zA27BX?>{HfDv<;s7~Y~GQZ|K-@5>UOmN$1Es~$H0Ml4Kg znOh(tZM%#eu`a-(Tfymp(Zr!sIi#hv97idMvn_I4IWb1|v|iHxtb8Rgw0Ofld8Cxg z#K_!oZEGJU9Tf5SbK|py8d@=3Heh+_@h!ejYb{^pYuH)>iRo)WqSj)l-nqoXToEA7 zTA;P`C_62PJ62zYaInG^oPn;NZ7vqAGMoOf3CiR{3z6t7)Cv@K{1!oGHz`&50NuKV zRo<~iCPpgq?1!k$ihWWDAWU4B^M;1mD3?60Aj^(GN1cC1#H~EhsjRikVnz!58GLcU zFP3-K7NAYlS?kZXFWk+jX0P-sTF*JhNFsZkU$+*_luVncO!AJGK$|36SBQC@J|`=` zyib$a%%K>^S#!*2uU+?7#+&n{nOgEUvaoTaPQV`NOscpqPl6a|bk&B)M&{FE}ilA7Z2bmQeZ~{IcE!yhm zyVU|xO){q(d2YyJo<-k73+8j9^=W+L;5^gr_O2m?mG2kwS>rZ-30o*7+w)`mC+$y^ z!8mSzUtDWlL=X1`v{uQ>!W;K4msfJ;k8^mqHhNHQ zzI1dV=f~=BA>33Gds1Rp*S0=LYa-&QXE7X`3YsI%5%M53;2bHA77s0$M|@r)@B+M9 zU*W=huE6{lf1LQPLLb*Zzab9j!J_13Ek|Ml(go74zUFolBN`q{7wV9fXj zVD#2ksVCSfCAUaRIPsSfY<`M$Y;UmUr4D0FW#|_s<~;Rd-pbKPa_wNm{2DOVcvF8+ zRYE91h;CA9HvKr3GX9?^S$j4?3HVV>vULH)S$n+jWWj|Xpts%!paN+I5#OQPR2j9i z_~$l;$hh@L&X_FinJWZEdYSwc!IFBAwvO;hy((XoVkFPHu4JLr(ov%5pyLxR;`*P$ zTD_SGi%Fk;PTww@y=jg<%Fl|V_c$m_cj?LV3zcr1>w$mSB~7o(TUHQ5Ex1#E=j61|njBCmq;Rlj`G znc6Y-lITaQtLA%|m|DNRu_^A3alic~rYp+M-4e~$E8y5c!z@;JKueY_x@qB{93}wR zwwhr~Q$n4G;T%Gwr?ID;{=+fjIq?=wsIbd`F)mic251g3@U?(*F_k8K7xL(36+D~sXYH0_Qr zet>MIAkFRmeT<~i!{j2?y@bwXCzMm%N9j+tLj>txXJPI6j76q)uD>JkE;vN4lya_$ z4Qus%80ue@`gi_c>(9*(bKJeb+@=oy}z%qLtpgDIkC1-p_krpJ;X+O+- z-1^RZD%fu&?EIu5`(i!O^VF^vOteTvWr}{f8BzCnBF9*XU z=$7suw_on`A7H~t(scf-Pf$hN7`*h@_o^r7T8m!$2fvd$?YRaxIpfK{355>I=;9%Q zN#}{8rzo-|O7Fs-yT2s|FS+Ks>G!{d7_DC^108dbrLw@n>JoW@7LAsc7+S-iX(t9I zYb|o7Et32S*FfJGNc>7{d(@b0fYwzG1&4NGe0-M&rJaIWs~Eayr2tWro6h2Q9D?tw zs2R}{RMvn~2wBzQy$)%uFY+SsN&zk`1Od&yVU+JQIzPxuyy1;1fSBT&5?RVz%&M1eU?z|wl;*e!PVnDs zmYKP;Xt|Km#K5C+rOn56(YrwTx5sqs&=-!m7=S0!gUZ^?z$Z{+q_MjFj_YQBdjjF- z4`un7{3>PG&81cKJLcQk9iQbJ9}!4di3(_%CTGmq4*@?7DMBSm@^Or+trmMTTV07WHBMaaFd2W^DyN>3 zjP87e#>0(r2h?=)Tk7T$uGLf+vinYyA9?7*L#PnGv(}Z^@1D#L6V-CV!5wnRrIEO$ z(IcmNml7eU!N!&o;pC~QYa_V8Fg)BQqFU88yHx&_k@$;;mY!(2?D9&YL%-dngg`B< z!LEngE(Hp?(#Gr`{jSutKl__JR>+kHa{EPA+)UNAzxbt#pZp4)iBV1j&k1^iuTSk0 zJ&FK8at+$IeU;1vht7`L9w_73^KTh{*06Ak5ozT0KFQ%|$F`UjU9C3Ol8snX%YE85 z2xUcM;)72{FGu7a#$OTR=JP_ER{biqgK?BpJ5)&&f+RlUIu?X4S>F1XICktl%Q-UWJ* zuw3VI39HoLAEl9X35)HtFl*?t&$?UsH``?_t8UO}eoV%e%Piv3S#EybwPkX!ZD~cF z`52s*Twf^n7Raw8ls^?F#iR3sf>5%q5Iz(pv=0eZ#J$5m@>G9tTV>)cj@mp`{+8T$ z?0qJ4*Fh;V)Rb%J`XJQI^&Tw7~-m4t0Rsp388 z?|kmLD#s68NYOI6XeGurAz?G>zyeb4bIMszo{D);;SaNzFiwpd#^J(g8ur1Z6-ere z2l!H!5teR-Avj?l>E#$l)m(y=s~6O5LqY~uV3%g(k>bw^xiM|*rzb9wU!2%7PjW)} zv4?Ul0oyHcKkq(OutQ3<#eLO#ZYyp+s;YiUwh9L6+7UOYwJa2&s@Pd-d`Eum@YQOp zc)z`zLZs%)u+Rxl2x0fz9g;LJ7N|UQWAnDJaqmO2bO?f*SH-H_8=X!l0*1$)tlnd2 zHT!rSdtD(Pg!2;-$r?^uPTM>?Io*?-oaX?y`MBm00rC+A_7Z0fo4Hw4fDK%ldimF= zA1t3oWLI*B=oi66q?{kc7%r&pybV6>$KbGkBtA=~(fz6sR_Rzg*0nc9M5${#3_wbZ zoVsPYV`DlZPwX|dbFQ4QS`p9cIw!Gj$8Nu9e*FQ!h*-ap09`{Xzj3rzYuU#4_$`w> zT8pS!u`0191AivLZB+k@Pt{*qA)0^$V~~^IIfcT;8yRx0}CY6bAVudbd8*Q#*;k2E&7p zHKC%L=jr^Y6+FOuui@>Dcn1tq?oO}uuQ~08d2p)CXUx-tJcBHCy$^FfC!YxY%m5xI zWAlrGZpkr_T;=wzd_=5!*`?$wh|wJ6 z_9q>KAAF(UYXb$S$zmlozb=cCxGuoe<~i{#M}N5*Qk~ARPr}HO9i5>rEgC{pa#eKq zx=)d>rRF0Jgz^v;qJbB36BBneAGpbu1^^Id#R(Gm+a%#guF)5+K>D7(S6G1U)g$(W zWUYwt=*ZWsJgaafDI6eXaJXrZ%Rgi#rK%T$4D8A@(Qd z8|f)8aO;`&Xn*3pQSJ6WNmHUY))KCM8z_(<>9f4TD!ijlipo1V6`v$Fmt5#eA2+r> z)z99>9ZJS9`vo!=k$n*}Y~v#vgIwM;SlzSb@6?$c;%=l!6GwL0m zkl2O_fpRLE`%^OVb64zNPD~Xf5XY4@P)1|7{MHzT5fkOLlHP9{Tz4Tq)T4eD#EE1` z3QD$#k`)Bu4D%_Y{BcO4Snlv4DAEzK?i4GVD~R%9QLzL>u*Ob{Sb6-66Y~<6D$tM< z`wf{}6{C5{mRRCX$&ovH;yOa9OozHKV{{7h*Na2>wP))fMrG4i?^*Zl2u;$ri?yZh zSrqzcXc{n@o^a?d-1%~WAVwIaT(;ST&!7rKm}H)Ww1@XN^5QjQAuCl8DgP9Mr$R!z z=1%c{&bpCF9<_U>V{F4Jqvg*UP0ylfjr^4rtTSHG3#l2vjHkYakBt3juwSG-TuWf} ziTnJ~;}ZjKtuc8L*>5)As|aS}T+fEnMW{lPqjKI`h*MI970sruWHg7~xel$m*0&Ck zLgw!ix%Sf&(TUdajy;y#yvnsdtjhyN^?T8ovRf5B%{7vb*jLJ&oU+)?Y|SYyA>Uyy0iCZ-=Fe5dT!d$FEm;_3$QHL; z0KbO#&~#74oG!PIUF;zmeX$qg4jyH9;$YI9SNhb!h*t2IAMRM7CLTW0aZ9@M&u?9X2G+vzGM=tyLm zunzt9F0_r2Pc0Qxu=@$^W_yqQ)UalyWaB@x-UKTa5%ygcjq+^_h>I(DVNzEk zzb`P7?Ml6xD}B~?L{cRVdHLa#@Xik-k>MvLC3XRq+K+Kc z0;ZRnzUcqhMLj`a)wVs8q1t}?UlLwoO;ar*;aKFvk7W+MsijKT^xNxrR-tjh%9uWI z_MaWoKZIbHi^`r@b5XCyRNer`Sm^6&(c<_`(=)c`8nY4E(`BD>h;vqd5L4bkem4g& z$zzrXS-}4Ne@b9b9to2L(%Y21?aG(&x$qJAVHNLz1U-S&_JT((XXfw2m|6I zE~s#2O#f(~*`w@J!IAwl*i<3WWusILLUv8N(lEVUvQOuxPn5qcmtkfs2E}rz2^44i zDb?F02a!t;U~fB(Qm=qT1+P4NJt`LMJZFAAv_dpSO9v8Iro6Vn{A=ti>^5R?VfI6J zX)SLGR+2NOb&F+i#O@}a4p1{%ajsWu{hid{ZrGLtG^suOb-|T>4v7Mbli z#mYe2%n#s2s3k(N9KPftOZSKHc4@80f%o)ZIIvxnI7reI*h8D-uc&BSOt#+<`_U;( zy{Q(VcO;TuUjczVE+bI#5K&Lz;mq+)f_mS$7ufB&gmN;Nx$|vs#e*P3Yn{bd*0QQp z6`x||nO$x5$g3mvEz&116yD4lpCBV8hBN$jn_BlA{;2GDS1|&4R-n7Qsn@rvOjcAB|vrDLs9AjH2n_oDu9AXrlrGE{5 zAwQa5zel=uV)V@}w!CQN+e_mVIM>YYjhrR+oSchH&Ulz9MNH>jZx!cOG&CINE0fDhR8qMdqY)D&^*L&|@-l1MIsT2d?N-^&$@+hY zWVekZJ&-Rt-kw>-R*DJ&VAsU>y{%8(qN~GJbvWQ003KU7d{hae% zXwiN`*mGNbF}EBd4S-l{d7fw;RS8e^+xN@bH`SL=#A4<#p{Zd;{x_Y*n@wTA* z*d*E7r)T9?BzF44)vre{3!867@^7vJ3bB!ViPz@0b_CltmCn~%e(W?Xi1x03hz{~& zd$<^4X4sk_x56@1F!qSrRMNNmpay}HPVI-$zw{iF`&^q4&<^;WycO{8rDpQRQP~l_ zvUI+fSYlnJk5l>z8wum|gXm@?USr5wK9d~UDjzvjpm2WdAlIG>V!Rd9b1p z*m2K_T07b$j?)#D1tob>%^5xRb-)g|U>7T}H=mbW0LVu2?42@G zh|8fTz%;0cz+P)u)7j_m_1WSXF^AYoA#0KN23qUWq5_*X&s1HG*bfs4GWLc_B_++*-#6VSsOaJM#`C>eW}x_dTzS7%4f@Nccua_ zxR0sVZ$Bk38D%H7AXMM_3!#oNQrJc@@Sinqlnnez_02O+=KLdqR-jVSBYE|OMnF)iU2ho4X2RXREL zJVB~d!t?5W`+j=}Ge%u^=RUtG&r4y?cDZoE$dagRmj0SDMV>1rDY`SX*7xa-qHHz$ z8`(OOGj*P&PNsq~`MxY;bnzCcGd4fRK3$D&6}D0v(~@!&ba!J7v{#CsXvyR>$d7BR zl1<`9bc#}1cCn1jgJj}djP$7#byKlnh1~MvFZ>gY74E9U{dXL&XWbSo$!eB_lrpY7 z%)pAI8nL(G4P^Qx#ICi94VbB+W^)%cLe^!ammx@ENuV?ol&U#)_vx$A)sq+rcIebQRDx)`#EJqR0xFa)VG(Y=}78>J@|V}Iet6V@X4=uaR;-f zjqKS(cg7>B?IZ6Hr*(itCJrOpxc(qpcHQcD9G0y#bq6UiLT-O>_)At@_W6>HeW`mf zYBS?=&x9Q5-MK^A0^7?J6npK4PqTiaVy^KA+csdnt`$=sXSsw;tfYa~TSzwWrHD)} zd4ztPp$LrO@qTkyb+U-UAE>XH;@o_XNHR<&L7y@;Q#Z0mN@|SpgNlcTk~MkefgQlY z)gfHQ-qc|~cm`X1tK=X)mP}4O$BDNlXFnC7GHnzAHVw(%S~E?Oi#YROWPU&=d2z)s zokUL_8;={Cjb4-_(@q@rams}|ffd{?{#^g@kI=s-ek)F-L{p!RoseGb;Ft6|^;;#~ zUJ`pFranOOMhlepH(;dZ_=V9SF>17I3$(^;oR%$6>5|;-dum&ou8y^ z%*8lUCR@RMS}eO|=}@SB@%{M9@`LNE;PngW$<4SqoAYddvd>6v^t;~r5i#CdILV_F z;&Wp0O%RPLE|7Ud2ohqnr{PFt8rTyKVMrwl&wNPyJk?_Qx7l4qQYsf$egMP7f5Nq4 zEaKuMKoh@VDxPEPJ>=y@MKW0OoLO`hHvbwnpRO}^X7Lk7W`VrtwU(!4&8>BqP(#|* z-LkwJi?N3{mPD7yg~ZWF1Hz)Bq5SE();F})m#H9UivZ+pw)cr;H`*1VYY`1%hf6?f zt~cttPt#g2fmp)Qn5;TAf|s97sSPMDk!cWKai$?*QpxsX3%|A~V%C;OvgUp4IBX=~ zAJAHtz#oZSfy5zp)Qhy1ph!m{9IfSi5pR+ydC&YzG`CAuuaH2G;4TmhMH z>No)_(GyU*W<-AGs3}5&u}<9%qmp;?LN(gAMq-itnD@paj<)3@R~!fU&TjJJ!ZtxA>@-`;4domzao)+rAiS&MU4cD^ zcRZ}(9o1*rXE}AsoI0yjyAz#v*o4yV8r3e}c_%E$yGHfyEu2ROc@Eb(k}vfvuQT+* z?dhec=FOFG-!-8*LEok`bwc85f5zLHMj&_iL)Agw7C(iPZ!08UpWj!h?u@-kuD;@5 z1ZzhLo~iRs$#>TA=f9VAJRtXVBx~NEd;0o0WKELE8doSpBu+=z0d4L{M| zBBq7(hoB2af6cY}{0;^`$+nU{(m!N8sCMl0PTZV zm_+CPR6RX7?Rl(7T_8X?`3F1))v5Ne=MAY7$I3xaQQ{DHy$0Hr7p0n1L9YB3`i~^d z0TDTbV3OaGw9xeN+4-?^;cKYg7dy?Bnn$NMnC!xNBPKKVsuWM~A2C&V`X!Je1E>FUyuOPhPrm2@NDpifCj=p1VZ4RD zx}^S|9v?nU+xkNqv5C{Mq^Tg$;C)sf9f%~C`KeS>qz`qJ#LndeV&dS$_|Zb=@)JES zH;l*$MDA$T6G!smM<>*gwy#5O{IY9Nn6z%Optv1I_<_zgq<=p~-#iD?|2ghCkp9-V z=ZbXm*fVS6noTVXiG9Q{U|UZN=DVXg7rAHnm*FW^ce*(IA@ov>;jx!{wfSAvSaJG% zS+(jt(Q}9%=tYwMH!(hvA4??$C*Bv}dflSv3+@unN8ve@V+Rz$$oei_3v%=;WyJ)a z@?$q}TSusO=3Qf7`!ZkY>NN26KlUbzEaQIC;jfRa z&kzHdetVmR*yJB?2c2)89{nm`kG_UidYQZuDty;qmy-8bKKML~ndgy|A0UrIN>MCt*sQl>U2W zJgMtcJpLZS6z$R-fxn#r2871x|6yLp3A+J)j=CfJOHmOm)Dg+A^{2l*)zVA7SbpSoI!5(S$x-j{KsxUUO&n0ud|1I!pc#_yk2^3y_k33Fb1B~~%E zv7}B3#XVkUzN05zEzuM2D5|K(b=z`k`il0%!47JY{T?bh(zX!#I6)TgOg2EHbhEHX zZ+SdQr%3ms>WshyTdt;cs>S+^);FU0<`+HKkm3l8Wr=3jiyEX1LisOD) z3WZ2=;vQwi)NW9AU-_WntqovDolRft9`fT)p|;wqBIY9LTwWaG59Pq?P@iEAZ30zX2#4iV7+_VjD+0MwfPs2`5KVVo} z+XYm!CTN)7ldthF7xkuhX7!OIq%8DD{|gODkLllzm#3~jNqqP7_!|kujc|$la^82; zgS8w4^HjT*l$IiMU*h1SZ3g#wZ@enG?qLVyXA`XhG$R8hk0>I0Fp7FVokHs-~V z6d}j9wV&f=CfV9q)BNRmTd-pJX}?LqwvE?nEf>RL2#JHg7E|Yl-N}}ik+{L{Pt6(A z?*f|RpAV!w_)_8R-S3oLc4yZ+W#LDtCI7aFzXNkDy^feXi`U1M^m*ZT2ME>kOI-2} z|IYY+_}$?!7u-*5(C+VQAP*3yX}e%Vb|WKfuc3sv@ILj!EhH9?(Yxq-F$rQ56;G9Q z+h0INxZlCA_*X6sI=8NfE<<-b6TL-IDmr7yBy%v#fw2Pv{J0tW8j=+4=1wq*@h7LZ>?LjF119J&IxELE!;3**)q=U;(eP@vJ9||Lm!sJynnEyzBY7Bp4b19S`j^eCP zkH4Y%&FBRUI4Sv;`1PS0U$6mmz3LbO(HF9ah;@rd-iUZecVyw=_J)#%>VK`DpI)x! zBk_LFSDkGY{p#3|%k-XlA4AlG!2d(pxxhzNT#rAS1PBnl;id5%G}Xjc6YC?9M0bG&YahrIYaYxLt!S}^7*{en3uY|Ro?LWRaWS2 zH+BB|cJ!NNcICe?qB3L*qc-0R@Lw?BS>cxxt91JU;!YtH?)d}_hZVhxvG#=k99M)hVoOaU_g#FO#?T9=c3dPrZ8^uTXx zMR_ju#>T9DdZ3JFG{%KR%&qfKn>2`q8a^$ajp@Q+R$Ly4ga96o;(QXN*@VHCW#5GGiplGIy^ABztp(_sihM{L%HV7stTG#pPWjR}9 zXn#RPvIhmHA|%*;i>M)1%gw+pcR%aBHlc32lGF`x5R6kT#Cm|b)yf(u|1gdJZzRx= zWnb}Mp^I$JdY%;j^(NsJv`aCBvgm9?gpcN*9sd@4qlW{Ung@iMZ+TjdQQrreyR*B3oLsNTRTl~^y5hSujrM;E@MhnthFf}5>3(M1`0 ztEG)6P3IG%+GR-D7Q$1D`(0u`)w1K1&$JBsp059Gsehi-|5`@<#>zdQ{*ik67nYQN zsBIA8(TNK1N5t>{r}-U46H-{_ci;j!NS9@S3^ofy3SlAvWn76?=D*gfbxd4K#3@3< zWj!8XjYnB2T^3ybvk$e}0yK2I&z-xu-ygkrZ-rzEa)Yp;BDaVbMFi_b5X=9O+Vba%DQ}#wlRij_5TujS?D#}D1NGCTc20HN1$4cuUg&&NZDbc>QWxj=Z=0 zLqT$>HnB!DEAnd|`?SdnB9ycqc0+s|3O^q? z;$e(duolctW_#-VL=k6Y(D~L$pStVr&bE8B3?h|llxaPUrF6K?_HyzScOMx_t97JM zXy+hZy^Ef6s?Y#)UV1$VJ}o#xo@Zk+JDdo{G~Sl)nb}^?AwN1&jJBb7dUX5<+^YrK z9|Mttv1zs`6aE`6-$WMzVUq2fKC@lwFFd|}O2Hv)M}xCox)3@$H9iNGVm3K{ldLA3 zpb}!$lpj-f2- zBcnSQVTP$`!`W4sJb(Z1u=ptHH|mLl=pm>~LpccrsG!-XY;jmhJ1H{Loz()QEfI z77OEEDFSs$$J6$6%18F~4a*-)SuJAZgkQEJi?g!A8##2Dl~`|VZ}m=YvC4wZB_1AV z?=iN3GBNiwFT5M0;;YsQA$IVgyAhVh{0R7yVU~@2^{76R+Y^z6B!8@Ky(-M(laj4$H7JGYg-J?30 zFWkKN$Iv43gR$suX~;yffGDd=;Def@>mbLAlcK{ZeNqED2IBMb|PO43-4d^35f&j{c5_f zRN=Mggz9o;$VkY%$q`b{j~1mt0MqDSGtDLahjyz8@`*?+rhl-f0_bx}9;6R>r2Sj3 z2)>uGwXsXZAJHjD6IHRA9GqtAyNGsGSzBSna`+wt6t>Pzb)Khd+fjAPd_I(IZe#9@ zi+_!~zNp_r{XfpEU%gBHXPNqEcVA&YuHB-I{FJnR>Z5da>|wzob&WQ;J*ue{m^Ylz<3;0icJvMii<#We zj@0-GRZyDtn=JT^k?UQ55(Y{(U>Byw>Jo$}87p09X`h&fSxet&=?ML|zS2TtS(;v$ z{Rr&3-8ERRv2^q-Pc5w2O>^v?nnLLdzh&-kh5K6~zv`Dljia@5?AW8tybCZr+JY*% z7*jjU&nENJ%a3|ce-deyq?j@4iUB#YR}!3vGCv3SQ4>w=&@B-YM0xD4or!}@t!^A8 zU62!k6qoylbkCCfU$`wMtGkVzF4C75I(I$W&@_O2C88F2)=zupek6D%FRdL~Y+6@1WenwMrGJP^?6WY4HPy%2#2{{&&3tnaIU4e&-NZ(M@D$l(AIxGn$4lfB<vGG zmwIu2XSZw!`;CLOF*Hxw*G7l=qcEy{`u%}Mwf{Ghnp6z1PA5isqdU$Ur6CzaLVEv( z!=o77I#c+n?QI$Nh4Aj>9{7Rww(q6ir5O$G-VU?np!tPw4nVHah`=CzSDsvackvZ= zA>n1ZI8Q^@E%oU>VHuO}^0v*vW>JWx9x^t^-6$~gWbA`U{5V`Q)T@232EeC z`Bvz9gt|B+Laq+|EO+3;;D9f+!Gz-h9>bKi!5|453r`HP#$!;e2{q6JWDN!wdT1)o}bS2x6}R>!ma7^%*x)-DUqX|(3!GkYVf&AtxTa& zT0RgqH%s)BG@C2haJ7Auz4{%rW`VFeU@hJo`7ED+Otmrn`iG8Xy-yC*Zr3K ztk7wi-_i0WA;NB|L=1dUO9;`M8TU6sqinehobzM?UgZ&leLV7_-*1*CPzI9OcS*L^ z1$4GxTxND4Gdnpsrv13GS-J5bg9-mn@|P6(c@?LWpLRWvJpU(in1?)V=+6e|k4w&7 zewNY~yhbFthY@+z#Mo(G26NT0Th(Ji=F<{uecIguvPmRdpd8R4|{4^MoSyt0``k z)=xJAGvoPF2A%UZCBW}B8>eQzwJszuvBT7Kuc59JzF8M5ep+L3=f7~je6Qa6%_q(M z*Ui7oXihu+E=?Rn=dwFL%q8;!naaNvuA-Cvia5dcSugJ-SlDo z5l-MTII>A*{pNiZ{gM&Fg0Qq-F77=mo|I{bJ}Qe!aOKQkG$IR0M(6Ku17-#zjJn*B zg8`8PlnKUuDxawrrCd}qL9PDmQqM^Wqp<K@lebBmMQJ!+VLlI2VNegRmx zJn>~jmlL{m-u z}-M%tP-W4-?dW z-udP{i0q%^hbN_EXmnG3YXAAj5*y8VnhOPuqP2Kej~PL>l*9v1t)~bdCN`$WBMDFR zRvU*PuF?{RqG0R>PeWzryc&BAEz95a^_3m->+nsP5F_3vU=tHhWpPAAKGBEkogDcJ z$Cx~C+m!_xEl|d=59b`822kILW{WvW$-QUdHGTp-(MoWTgDrxfK<^!Zi&o1+x_<-k zvHWnPuiD88ot?NrW2%H$NMozc3zw~}cGuUSlhqsUR;Rp1m-y7wg%>_D3%B@^lwH|l zTYjuaDbF0O52-yn8205U`CQ9y);d_+*62{eUKd#FZbE^^qjRuXDzXWYc)_m5P2MqZDW>m^%1aGXMS9#eS`}sHS`Pi>f`_f#;bNJZdZNPEO*l%<_6i&$9Ga4uLVzUUMHH z)btH;=-e7PdMs3`7La)&5EAchAi?pik*F3TMo{A)AlzV%nr_;A*Ol+|`0Kj0ztoA< zVldCF7F}+Ty&FPsqsJL;H*G(hTTp<|DpW1raRw5^XWQscXJRM)$)i8gw5&fm(+n=9 zX<+j9s;{P1b7lnqBs(8i<^#Itu|>_K1E5^jyzULPwVI$muG7anYKw z%oRrgMyj2T~6Z^y2Ld6AGF}pa7q($-me{r_Ja~X#$z>E zUeot!M{ljr>g+D{Utfn4$PTx*Rn*74Jjpe>xWW;EWs?^}0!VdBqx$Xw%%B{>3132l zpb=WsjVvh4l89NmR7m8p$m{|>KA^G9Jvn4Y=RiCMrf2GhDgC7=`e1Hy8F4O5eML6I zj$g@EOTLW2fKe*oicE(y%KMqB$k9`*mRHYYT}2=e<&`gl&1r zYS~MgzPB^{x<0%i`G3I+Rrg0G2nUmQ1VwOR>>(%;e~D*U;$i3stg7`e*CP{SxW6@9 zSgHQ80v%w$5mMCooagYZDH3>PWV_ThY(njPWn5t=OGC%^F9gBM#DD^4ls-0^<|hK1 zK0i51-!R{36>C2kp4Oi*12j>K|f`I-xP&z{WuS!!R7yLajX^ zmS4=d=AhgLnp03;x=Cv35W40Vx^H4#sb9UqnDk`WvyStJVs%~h6Gmgb$JX;^`{9J{ zSk{Z5)DB;4Jltuiwc>lZ(h<99^bc&$A-P za$GkeeV6-)O4^do(Ly^q=Gxb@v$TWD;oR(OHp}GH(m^$v`Sm&Y{PcmXcG{DdgTcP_ zGHHqTH2pY9le>nTq{+)@GW@%flpc2+fM^R9zR0oZ0p~jIHzLyvz^U_x+1MAY~x4c>-q~ z^ELnTJm0b&*)Wg4eR5d(*pK)>)IX2reUa;~vs#}>HxSBfpoj(}Fub<2%WS+M>JRN( z4+W`PANta>^tQ~>df}4so2U!^c#`hkdLlUVQ&6Dw9O*!8s#n@L1chqtSaSxI`K>^o zK8`5+ohP7F{mJUPR@e6mD1#xhHwLB@x?B?u|@bxNLa$ZeU&{{%RvA}6tY?lfF#z+ zIBp_mpL1HIf1$NvQx1!B6M;fnx%opK z+nbV)PEu3l2{i$QnU5GOF*$yBqUA9kXD8?5e)3K_AG?l2eb715_hmgF)fx4Tq`uUA zoWXp^C9LtYq{NAHEdFVk1MMy+Jze*KbgBN?(X09;((L1ha|e6EQ%+LV*N>~pthV@5 z)S~sfG(Fc#AB14t>iUo`#L3A;i5l?fgzCfV)eW0iPLB47_ao1YGq3SurhdFK!)L*A zwk{RkFN90bYbIks7bq{g1^D;yenQ<=%W7KB90eEjXTjIC5DAH=in*A2d`l5s5BA(Sp33L&emFG!tusl5`rb$t3L{>c= z^MZui(Ac~K=rkU0;o$`6!_TX-9>{d3H1q~I%`azC(3>~&5*s}@*doRryTCeb#s z3S$^=r%L^%U3k_K#3(C1LDHbWS#h5{$?^KQoR?mEiacG$6B@#W62Z1<^Woe)JiXoo zKu!vmACBd&rkz-*rdRz*oGVXUHuq6EySO3ay8Z&eUy(VY1?ncT+&Ve>!kX48aDVo;hB;k> zq`sEznuK2`Vh!Q#7YXhQmylul29A#z@)8nw>uIK(GoW0JlxtUsml&3}C}7r$+9U6m zrr)>9`!5mJ-24~h{gm|kbMo#$?cMwz@y>FM|Jtli6_tFED&72r@_rFgsQW%!-Y+#I z#}8&qm-m;``4f1T`Xq>19o+d%u)Wtj@08SFY)-dxtTm?n(m`^W!>m_tX zd^BIz$L4t%AXFyp?-#-WQz;OC zMdxeQ{8OE2wS1R!-+GAx6aNjlG6Q*xEYS~H?!+eRsS!$&HUDC`VY~n^q{L~Umk@?r zI>0gSOV!;Q5XE_j|DMjl|2Y9lv3E2{th4zvDKu&dEk=EpYUbnOc@jiL={pWlwFm?a zGU9}Z$c$mg_XQ2vxZkV&EHCga9;fn1|6+45&Pl3n{@+&Z;&!mOC|EixPmN&3F~}>A z%FM_GN}8sYb_s!$OIAbKgk8FHgzfn&@!D?|+^uIVscV_NAb!p7$2<8@P3Y3oE7$z0 z>v@h}2^mpW@swy&mrKGO3>m9n?6*-vT>O`@JwxUyHs;WSCV4WG!fU7d>Nl^O+}Eqj z>kr-63wf0~UWCLAw0FAp83L6dyAzG0BIwk3jqx+++%Ti3rhwteaN5ukEr1Nf-S(2|?lz#X(zl?bwZlYBIQmo@-Ll^g`E10Zcku z)t7adB2&g#l+h?T{b;Rd17Obb{%Q%N$JsuqY4oEFp54A<`|TsW8wePTEwzOblqub` zfqj8a5}M_m>qXE7#WJZq?Du$J?w#Skw`>l7lc7n)Al3HcV`JXH3!myX z5Y>bTGJMLxfWg$tXR?{i;mDEVZf>p4BXOuCZVCUZ`RHXPB1WKmPxH}Bu;y{H-S+f0 zSCq}PKH3~A1lr;HR9I$nj30;kJdyqHG#{OYKO)Co#j_fN%{il+#a8zoM%kHG$+pDx z$@w^B-MYhCx6L2>h9~lV87sE-aBgXKcxYsE5y_GFZ=t!4L)OJzhpcJ6kwrOoBJ;@1}G7<%8xOU+vrOt zGcdvf+m;KEi05prp5=+o)siqkXS4~};kTbJqw=7*jQC5p5x^?Y z>FFY9QYdexb<3E~!CxxvQd5ZgsWC%43DP1LRoL0)R6}d>wftCo9#Ci6Z?G>x0*Kcx zAq^WPG8EyVXiLrg6BU9T8Y|N4D{?en+M7Qgi^(dsVx8mS7XZ|tSpbz>FHFOR4Tr4T zHn{#A%%41#;PF(lDSj%u*)y%n&#B{!Zt^-6>C^H4g;+a$=%wvJrF6WfR>Vry(q;(`GclHH zvAhPWmw?YxORzRmH=Q@*ogB~lWqs0wu3L^h&Yvo+Nh7*xn)Ow7EFF78EZ@?15xuXj zU{F1CrA9M2W;`>XIY^UfXc8}J`bmv>QZt+!ron;ZqiQqd?C5a9y_o2!?l;Mb<3A@q z1+VY<>j6`5{RLw;4&i>}Uf;I3UeJ55?|gr7Jpo00kxk8kwi=JWa?AX8Oq?#?k!`-x zj(}%#Llcn(w$!wZ_JDGXZ+Qn1otE{BkUDP!^s_Ub7wO0|3G)vz+Y6a(YxU@1o?g8& zG(Y9Wg;OBZ;%D16zd-}T9UeRO{=E6iIL6+#sw`B&I&aKtK8l_58_a2WheSxI0-3iT zs<$qdFx5Aj+;uc;a?g#IQj(mT$br%?cXOzsk4J_Bv)2Z>?TpMnNs1hMhF}JrZTfOi z)s73ZzEw^{#eY(Q;PDi0{Sl9ZVUR*>!};}DY20=szL_2UV{tRt@NL}<&WFma0n(D0 zPZv^}{5{14(oxA?jI>(64#Ht$FF3=TR(Mq;8$#1bjW7Vh-sFwa)w)$`{nS8Z4sW;I zt(Ph>fpkAMt5+84V`i60H!9+j$khNy8zX5t^gS7xR#|_1H0PaWwO*)^$Ato>Q1M0f z<%RI@f?h~DDFjz%0w@~(u!V?#`G69T2XSsjLelH#$Zm<6Y6aMFJ=Nys1R29+FUd4R z-r0@v;aMt)uB4$Y0hiNFvE4s*y&g06Jwmt0`)%@GEhKvWcnSVmhVEEj`Zp0Zf;i7T zP4=-d!)N9o-S)AoMjEV)@vb2jLYex3=;q8ozG(5DomT7rfa5HiZvrh2>vYej{qU^T zvq_0xr&m)mjj=& z#D4DlR(pfex11ycW_$<;m#xn%yXz-N zu<K)YcM9gl=C?Rx}LHR;T=BRZP(9u~^^_ez(hAQhiDGZJjsY6n>n(4>F zMW2~?YXw)JXAxodB2!+Qscetsyc& zMbNzkzYT8cir#XDHV{N_8Ov`G@1^{f@LSGrnf2#eDrY)3R^SdduG=>*tIc<9vQa{n zN2d(wt9Pb&`nW`hqXX7*ITwoad--MPUN&DSuF za_4AtJi)}dunpL|YXVu_HU2E8ri3#>HMP9d@UISn;hQlN^9{z5Y1nI+dki$i=I2uL zbEWwa6*4Q@nCDEch&Gnsn~kdkR-VaK9QcGfjGv>O;fdl>);&HyySr{A^IE`do`ufT zyz=ecbtAHzy8QBvwyEq!561D*7-Eb+75-%i?VtP=bjJ$;URYbhfwpP96h?0-N!AB; z`rGOXxPMO9HZjkcnjae4ykJxow`V1uDc_!WN{^qh-(b6Wcc)aSJ<3*ba9vh%J2o_} zGj(ZyTkXFi{t@GDEa1E_dPn@%*xuChkR9?2J<#wk^iJ@+;grk3tY;HBFrbjVbm=XW~VGa6yksjx7bf=$RM@ z)yCg|PZM$Qt()bme0gytN>fAAOa9V*Xt;9y4I$n4>$|3Gt|9Vtrsd^Su-q;@akqsJ za6#B)FJ>!U-qXvqgh7Y9B=U5W+cPCJO<`!1z||lw--pz9US^^Fd4d-?+Jm*Ga#3-R zyKHK`Go6O~P`*2i`o0u~eA)nFZmnK0j8EdPu>Ml~&((MFa~O2{eekL19E7+H+O=kz z)=6+4K?z6>Kts&VmqC#A!&wyd;;)(JCY0~dH`$O%rYQPYal4-T5~q8y`@uOt58xuZBM`BAY-wacs1~1e|cnxKPfIaOZbAE~%@~ zg5T090R8wkxshCE2aQ$38;ngzK?v;XEFjcAkvVJ!;|*yyZ9#Ca2h>g7-l2T6fS}J* znZ!^#da50p%KbCi!Vo*<8X8c8;Y2Jc*2e&OEb@WPISvTdt<$tKNL=51`6f5=sGr>| z>~(5IeQYB5-S2nqLJW!37J-{3`u=EKOCNV**pN}o$r6d21VbqBA$}CF`-d(|w7~o5 zy|>U8Z77V+$xmEpH+_M(t37qv;cZ_o^NOmgr+`6qJT9P_$sCp7tCJ-8P=LN zTN7m}HU;5CbX%Xq!DExwU)jBw06g^t5tUtffcqVWut0OY^cTZUekbdh$ghzuxN42! z8E#Q?fL|CzO&`}&hTjP|K0R9TK0$MdC+L-Uf*u{#v4K$yMhpB=JD=t&RLh>m^6)!dNYw#nOQ|n!W_y6UG$%mtsFy-rZIf$UnpzBv_drwk z3JQdFLWjKh!RQ5YoTNU-UpyYJ;_oP_vbO?2Q%%7vD5%XJ5Fm`rNnjT`#Xe>yKP0=Hv z%^F3k1U@FAPv8sBa>r$F`GnI0ece9GkP;Xq?bH0DSqegR?939j9UVU{69$g92>Lh9!SM- z>~Sm{mJ#x|AjuYtK%&Eoy_;!_*UD#2Q}f3)7Fw|duuFqwqi=HSp+;7AFYJgaZX;3^g=zu2Tw2rbS07>`%2>VuXRltt$>l3d{MyMVAaixQ z($bi|S=n6sRhzHMsV4Dfjiu@Nap^H426*he5t`($tj`VI3X)NmAN8|JRrVs`iB~MX z^6FAvT(RY#u9yf~ceAHiNOG+2*Qe)pp3kLB|IOiwwt#PkCjB;Y=t=X^hMLqfe zbFA@S{?ZK`!JBsx-nWxm_bego@l?MLX$Cd4=tcnHIEq|@Q^FzHEP6`T8X;U*CWH$3 zExSXlA-#3a($})?@SBnu=Orko`fg<)ern`>mp{UWkgw6VUtwN2`BS4q#={ScumWjK z;_Y#y_0sg3eyx1F#)@6)A^?@C(Dp%oZ@CY08l3Ho z@@?`fQr}M}pG4%LI>eoRzqKB(U#CtEP?-v@H3ir}k@)nWeBCV8%7offfit9lQ`g8) zL9PSTmb}sNd@1klz{L;s2)%|J3}c(l4MF3D?wSp{f%X+IE%@+jE%zW#Uif?TbY5~|QHXIE9RC_QOJLObbS1(5pL@){3VGfQxTfOpyRZpHZ7^ZcqTWUhQH>*X&uvr$cPl^z zDL~)n$sRZDfU&nT6LgaJYZLRJY34oxRSE5g4vDCt5gZ;@aQuPcC;ky+Epk1_kSFF@ zEnKiA#%@HzTk`bT*s(_L4WjPA6-*W=Xy@T(#4^-v*l zkn01Sgd@ejdWMLjmfsnl0+E2z*8n@ifuY(R4kC(1Yo?);56ig`DFmSue+oqC*ywEx z&`<^_F4t&b4@k-9+SyoLCQcsFC1oZeqt?SA#DpH{S5IMZiRM5Snc=CkbXLD9rdE(8 z`ht`m7p7_Zvm&%P@K!b;2mS1>?5vvT=qJ_3LB_yeT$AN=i^PJA>lNBbl8?$Dpp7M>Z?pA* zlvPC*VZPv+;$G=$$wnQa8)X=|aI`4I5?71r{d1Z$rvR-65zYEl4;D|Muit`=$nWVl z$f%9tB4A|W(gW346>0M(WW4-`tw(-NccLRY_gc&Li;g$6mkTQ#Pw3*cm}jLLXHjtx zU1HR=m-t&HL3Ry{^1Y~cUu^Yr7d z5OAtT=tsG7M6Oqx0jRjXl+|NYRP)c#)oSnTXYd+|^j(T6PaYN(&thBb6iX0hu-!DJ zcn)*z?e@3lcoHF@r`3W%R!b2rSdXj++DP9xt0h+&R_`mgMY&<#3ta= za;Lr{P|L*~Mm0hwPRiu3VF2p&RafYv*IlVJ`6Y4`nl{~X5&j6H+RXIcM;S-EtMb33;0!7t-xb~uJ&KCh} zpZXSUvme7Fa`E@-Bi_}H)J`fjanq%rCvN&_Z**#ccD@o z%DNwn=wsc{k$|I^!f3#BSkAx4?|@Ld8b`dH=b7!*!>G<`871pd&JJ0vONc8F25#4LuQL(4_7z$L+5q@L(;`&S!T(N)zVqsLJ zORd&C+MuIEo>w!en@6=yf*ncUyHt~4KQE3a=uSVWdiI3uj{z&TQZ$3Z7xzfZ745 z+3a*v6lA@(xCU7Ko)6>K(b<9Da_M-gkLZt`eCVNYT-dkt>0xbHoedTEe=&{p0KBV9 zzE*yiB^hh`99)d_pJT1C$kY2e1mDBiVMtbXQTS?E>N9AEC8?I4hOpAZz|V-Nks>WU z^aS!qb4|9Y%WJQjS3!k6X_oVVQ{kjA>p#yF) zY%LSYAbr#P(fTSjF?^Mq7YBpU#-fI%T@z69F&pGG#B#G6#uahnHq;xHKf4fd+F!aE zd+2a9`TTl>*DZ+=XoF%*367hgtdc2VK7qe2g?l~X|IWkHfj&jiGTH2M`L0`l3M15l%VATys8DHZ=R|vR}F$G&;dVH&k z!~z~}ku3yQeFb<55o>;Nf!V*&qNcC1chM(W*7VhReBHiLo2u3HFQa z8?v9YeJ>Jx{Ln*VCz$H40ZhOx5^&X&T6$suO)xsUOjAoh2?&XYPzI2;lM>Qb7p(kHeY6&8i?>xCJcVER1}?zv2>&0S@eb~B*MxqOZ*rvi-Jv?!LoXTWyM(y zm6sNWrh#Hvg{;leEvP;tz%bE)M)xL80m}`B%~Bk>t?VU64lC;r*MB7iga&a3m@JZJ zNlHLCq!_Wcj<6Q3)fg&M^YMQ%oo2`fphqTAL8!>Iw~8^#wYPGCPKv1bqM>5c;<&mA z`1o}s#*+$P4={}Yqq&qOGQy}l{P(~~7*+fxJ=1f!tx?nuLGr(V>@w_syamCOYi<3i z^Cdm-Ld<_aUsFXX^azF2Pty7Gj?4d+H9g`%7 z4?*l&p5u!}Gh|LO7s10*FoasGzwTE*#Qp&EvQq8^i!uw{O(8WuogX|dKOp(Hr1LM; zNUJr)u{@>%Uic-z76bG*uGm|9DMKkQUv?9FNR%~WwKULItOkCxcAusKY9U>6QKny= z%L^Zx*d#yUYZOnvzRo#01g-_(ssnVoib*l=@!#S?Vk$HYQ_OiYWJc8`tgHANV7tp} z6Z?`S<4ZrzL<)RDGf-sS7g`s6l|)q1e^N53B^o2~=vSpiS-k2V6hkczvSbH}r^s!A z%I*vlzp~eAeHc)imkj09;Q_04F)wI3)Fjh-UZt$o17at*iwrLC$l>lqY2*6>bu-$5 zCoVYy<(oG2W+^md6kU~jY1LFe_|YAo2UhsjXkkJNXlf;xiViztB%vLSc|v2>OJ01E z6w_Ba#J?)S7EDuT7~P^=qZVdPwB1>v?ZTMaquG(Y%@KBGvg0EpKQ`vOHyL(#7N0`L zWHnw*65G6L)!#IHf_~>z=)#gC5TD8`cJYk;CSxBrSIanC6P8xfLzhV5gC8N-WC*NW zUxJLeWB-vZkyJykV`IQd&R#EIR-Y;stfrI)mL92KNb~!l?tVi|29?m*w<0p-cD6Jk81S-Z2d7`NWm#D z>$-JC;Xg}C4Jl@=sf%bB%28LNEkFm?-Uy(9wg6Vn!Q6J~VQpz@nOu|V&)_Zg%^DFo zr(a7CO5L7aZa`2Mf|Sw8#6Rk%B{|kG2e)k?aT?1Euct z#eS#1*jA_fQa<7-ek~x1zf$yO@g4YVl8mQ#f|O|E8imUmVhu&oYmDNOAiQQ#aDkWbvH}8iFMKqV#t8`+HM{e@22IHy~~{e ziFcMNAXiE!_bUhBbL0c|PP8Qq6b@hl`Ql91aK-7JyAjH)m065(q;H8iSX3=uCF@8> z?G_hYCL_8qSJ_*HOrZCZpsCFdkIATrEP_VQq^ZFe9JNSHMGvA@FP#23Fc#zDRO<97f>d$rVV zwFIQJ1U5Kx~RI)P0|B=3ntrl6}5>Dz8?G_lSj*EMmiyzgp)nK*A#SPXWeLK$w@`!?)`vEOwCE3H z`5|m8BYeWN`$a`*1kThnt1b+Y!imu2cZnq`;8Bl&w^G0_N(J`Z9^)FXR)7wXV`IWk z#jm8g^26~zP>><;1EU;qv06$1$Xa=djG!^-h%v1C6v=CaRArqokmQgkODiibLBW#n zS4-dv?U-t{TrG8+%YW-cDX>~R%x3&oG#%-?BXnz|Z;=+1*GVN-i&&Ah&8b?Jz*^eO zS_gk5M=s!t&(oiMNgqRZk>mH0E*2|2Ed~e0+i$Jl?m8jXI_S0eMCyw-OD?r%twX@c zipvG6`oi&bf)?>Rc|D<@=%E^u1Zm>0(io$ydQc%#lCoZbfqr0l_}|ngU0o+%#Yal@ zq6MwP%90h&;dR|edFmIK!O*OALI~sUr(R`4G`@$|b+XMA-{v*OPpZ=5hj(v>7G{F1LuC?N3SI9)5J5UIJ1GH#_Wk`)(A(+S_mge5DzMLvj) z@!nvV=B(9P3TdW9Dt;J3!5iw!8)fsS!`1u&XZl=eqvlPqZ)TkL zXp?MY4cA){N?k8E&h@}qXZo>~Nj5I-UO-o^pmIc9gV>v)N-b8a18<1N7hpq*&8LG> zb_NIW#r9i-MxpYt%lHQUlp?_IxW%h+>%8PoW zT$&0MmkAf_55{H|agtvB{L9q5dc5C`-N#OEzg|#2cmDX*OrGDYJgV@4NmMsYsuOtK z-zoAdo{hHJLmO*3Wh6603G`pxBLrgoTjqXIA-`%RHk$C!MBmWYjE5@aXo# zF)efWp1*>c>9U^>rZA&wWsth5IN3!G3rumLYrT^W%xu^9TPb=yn z9sMMBkah zH;q^jC>4kX@<6sCb<+e9u_7nl6^)uLzw+kJ;wBn6$p*`r9)dnm}u8dsF=b9Yo~YuqX=H1XQ?FDu;BjpW1)5 zyDGG{_JUh9^ty8Z=*_Nsy8e{ETO)24g4U2M-N+k$XQS03n;}{z2+fQ1l@MAvHs(up zXvkmsy{r!SpZcsrgHlQuAVyzvV_<@y(7;| z(tIClEMdFy0ru3wlc9BTp;oAG1AsJ>s$kxFW_dnTXOiDninVwrLrs8FYSBxS-y$K7 zYk@vXeQ+7eWU#Om5e55u>a?`oQ>Ryt8E%YLV2y?H3qs=EVLm}-~pI=Vu5ja_r->2v&zNqdDu;0sAn@K?MlNk`b$7=np z0Q}rLqMF}TT;zL^%FT+nHZ;jv`4qWKp4GCM2P4O|tkY@Jt=6Y`a98JUR14QOa14s6 z7Z~=9e)W^;WOd=+2|XCQUa)kRdJr2>s+3>cspY>bWgL^UP@-DK(Nm$@+Ent)=XCRH zfY%Pf6doj~nzFxHi)7-#Lcpih&@cuJ{$^`$E_s~<1b zXUjM3YT>#}p7jcb3t*P56~Q!f*RwjZGr2krsY_%AUSI}{{FX8P)3iG(K_&i*njC99 z5;;29TJalD#g5I+BO(x9J!Nb{Fcp!$;Z}HfMTRX#??52b+{m~I7fmeG`kR0&;C%&j;@ApY7FF>!nCqPk zGV7`$qVkEAOME=(98R~&L7EDPk=O0m^9&{)Imuo1_!VX{TPQDI+&Il{5~Y3^_H(an z=xG)%HXnU01vP>rgK9*Q#~1S6tukPM(53cWD1D5M)Su(lYUk86HZPD15A}s&(JYRY zwFjg3l_8mNjjcbx;R6f4Q!z9&&W@gC<8S*lT-IWZpG1!4%>R&3PT_r$JuJKr50;VR z7~=4oJ+*nFIuU&UqgGq(NG^WHn(3{WJkm%kk-q%!@2&N<1#Cn7Vy54jP@s#-U7F2& zk|kFq$!Rdt?w!I8yiA^^G+K4Ryeun4%BrQM@M>vEY|iIj3?e?K$4>PAqsn)QjAIfD z)JQlQ6kuaDKw^Nwo}Jzulbg7H%H9O++nx)^@9B z4`_gkJYf+oibVdDh9xTqVlnGinsvD0_rFAa@hW$PFNvCFV+Qj^8rmsh;ZcsaSt5zd!_ zR^Ox=cke5@sQ4}|D$4#>8Jl0VcnTtP?NTitXgpK-826DXE|VQby~BHq=FNiL_9fG^ zIWFDT{LW>v_*Unt4xp2|`e5k)-%#$Lo#YN3=0kxkc?lDsYIuQu29r60)0qOcngg3k z?ttk72}vIr+-2~JE>Iu6;!^N&;nS1+6Zob9pO&2157t|$i%zYUUD_pAREr)X*2aVC zQF=?5r@exZyteSFbqs8Q9qQ_0a8b~_UGjGJ4jdQ^#*dPx$A@P)YTCXd=a-nZTiLF! zm1(>}rm+m$f=pvMXqVi_6yYAycBz;h#M9&I#iac^|t@DeaR z$xrcud?hnJi0dnqj_)qsrLq}wQh%Xvfx9uWlbANTKdv_iFNU6__=P#kOMGIzpuzD5 zXJe3qBeM}nK?=EIkxtr91&)JtYRv5hdL`(4j!F4CQyCyH74&7#oT)cow z89COb-2(BnrT>jd)9Xa0%Z@%)JV$<+*}oRZ`xIGYfjQ!h8I0aACp27N+Ge$iPeD(e zCPkv~G-@WkE80@LKxeR}Bl*HssO*o$eexUoWAT&vogiGl*{g{RY=U!8gh`$tz2qZWgeo%&Cr$EX99d#P3*cQW@1o|C#4t9eMSh7ov`k}NA z#LWJj_}`QGD&7pT+;^!%Kac_pQ~bYlW|Dt>M>nH(kgczxSu5=eNFx}7c6~liD@&+> z+tw1Qk$5fNmIAdbVwBcc+fLF__}R@`c72aOv19Gc)Wv8Fo8UF6X#5@7Uy)huWt>IWvS0k7ynS))ntgwhLcDKMJ(PDTP-D5Dmk4> zGD^vpg0{L;K;7z=`UyrnDSgja^XvN`+48vucim}>iR&hA>s>M`Xy$ZiVUVLoK~D!h z0Jhc8TVLACdD22+J{F|bqdw-9;s2kUkF{P6#8wq$tXOeH957-nF{VAQpFE^%)#lJfGK%sAqseG6E^*p8_GMgIARXp6ksrO{k z`jv*qjyzaK*SChQv|~PI+E=6=lkvx0f&qb~m(aC(uD10>ItCHZrCy|+2)bIlfd zy7b&VPa#vz_cuaGnOBaResaG%pBl{}q14;t#Sgc!jM{Qku2jU}->biy2wqO^lX;I0 z_N{5e+-3l`kL}e|FAcxzzdX(Nyp%G*XUstgZPPgRI<#5`B={4v-t8@*;yHQco4cFk z-~=ztn;7fvW|XL7h?d-3+`%<7Pm(5ow(~--{KTriFJSf5R_N58S~E3HEhir4Hj+L~ z(t9Eu^21+GWQ}>?jQ1!{PvkG|8zsH+*j+o4)snhUu_yA!WC7<76=p@{NlK5t@C8p$ zAbCyz4_A`n&q!2GTMEj?L7CfJ5nT1r0h(kRP&VA9g$EL z)Li4{O9W1-hN5lh$7$)uRq4kk(~pm*AAg&E+?0OoNIwdyxlJ^tA7`Z>XQUsS(~k?% zk8{$Gz3E4VexWh1Cvt#Cw~4HDffDzzJ5t6|PIp93UR}{0@$+*=cSKxDPw9?$&D-_- zupz}jNJXG?N1k1|lk>T;G5y#vHHGFr)tlrj7YBs3Ektj^Y~Uu!1CaCb!|F*iGzq(% zc^=TUzZilrhh_DuL{LY+n2MCLJv=hO3gluQXD)TVEi?kU@zoTCVEgZm!kLk8<$F}* z0ibH#6YkZ^nFA(9|CICJ)}NQH0%mNBf*vK;*~e|QEx6h-Bv{ef&mmK1{cTbB${S3{1HYkSvPSqW!hMK-FIMD6wvsvKAeTmv*?n^{~aAQTJJWgTT0~%L~YD6Nd3NgC_LFieAwN%A79X z5Za@afME?TtMjF?t$|!_B1=%Cf$$!7V{6s3V2_(U=YKB)Lmdloo0E~A-4To zV|W}tNUn$2Rqt{lN}uu8M+-SO+{eX+Rk_Cgq!&eWxm&H(vQ3Zr!&0>?MR1)76&&k5 zzT~qflyr`sSZdh`rD9_qJV||_>2@@;Rp-$0b%iS35?V(dbB*-MV1bN5JN*8D1~Jmj zIHfw&iVa`FqZo5)DjG(6`(%G5cL;PXPTil^liLZbLXbQXaqgqm?MueTYS^gQibmd< z7vM%Hf+N)Th!U=+pvdoRzo3z`ymEGV^tNDh7?(W;9SKO19dtUvd+Q?`@`9Cn!kbWg zumOF*-AkBmxe@0p#7-MmQxF<17?`3joVAX07OLroJrWJ}F@_Te(s_bJtl=s$S+Y0Y>eEHqJVG_*F$lug3png{# zVSU_R7+q4uflg;f<(o^Zf`kQ#PA%i~Uw!Ak94+$rzG((@O65BUwPns7Rg3Gf28$@$ zDLodoJ|1LciYbG*dzfH^yP9gYX4E9g%P04m%9sURRkDH&=HotRcj7r&X~*ps0q28- z(fKoglf5GDvi6sE%Wx2uqD!wV=Kx{D8I8SzJe%0I4vsnI*;L-G2lhRGbUp&yErc(t zJ2n>2!Cl*ay;q{d-;VT=PT!^M8CZ?~miw?Wn!*0`E z9CRESIZ)Wr8@eTOsIYDF(8MHx7pFvDW%rV4;z4+Z-cOd^)LHCMJm<)>J@&_G;G2($JTEI{NtaOGfMdME>RR zjXhl7)Ga+qwA98%UoEW`G5XTy#BNuoj;j~Hi`uson-rA{)_H$Q0lCc2#SIDfZkovI7mx@2RblT=?~8m%*1F+cZ4Zf&-2Su*l2}~8*HYfum zw3WB`>kOX8e>LHr0r3S>`%y*G(LcO8_9{d!XXlaq&d$=_ZOYT-?CgAh(CqD3p$kR{ zsEi#&l%?1&v-Oxp!;I_HLUj&pv&^mzM(;*~*@c5S{;_!`@yoUomPEDlfB?7~*#Rqn z8c7%M9l?@9qD+I(cG>RF79ZwmW#a{ zlSx^L_U2F@uaHCQwlk+-rZa6=Qmb5YM{EMpLt_~<+Ko5UIlx3bHRgg#va@bT-h7{K&su$|uAqGTj=k(| z5(pr&*>mKe!+F_U&dcU*NTG+J_i2PN^vzm5RDZu?FWBq5KyDW_+X$%--2(N|5xPtg z!@Zf6XI2u~D@P58dexgzg2qhy5^s}yi3#8Nkq57fTL+D$X9W4xzYk2p595^cOvwqrj$a;%Psp?5!0V7u+fk%Lo>Nc5k+ z9eaI@kql73K2bbigv@v(`CmD=>4Jb58Ce98&f&1-x;#S8s_DYQoDGSqP5I`d!^3A0 zN#^ub-q5I-wgeWgtSvBN0Ib2bn&WFtq*`^iLH#6$P4iJ1w&kU1#zggEA%C&Yuo!ndz~(BH8nrE<|ohm6b$V}o`Q5I z&sRg8Ucvr`JWBK<9p6fOfqC;*S==%J0j?tAYZ?SxgVeeM_L z&%Ok^YiC~$*XVY|rVRE}zA^85qP}o=ZX41?ZiA;23u~TbwLZccm*@dRJ0d;k>6GJ9 z#NO;I&;`N|0A*#j(-n@h8$I0V)hJPVV8{N-E@zvyQa-2NLJsaH@lQVJT=F1{!#xgj zxF*jEaoVK@qD-jCnxSn(Vc+_6{qE}{|#B*pHP-z2pWe^fZKHoVJ*h8Wn?!8-*V z1T@VAl6u>2k~8`v@x@~|Ag^zV*5}t(VkzBZEnm(@L@pkdwO|s%Yn$dZ`7ujff{1vr zx8`nwzc$(VxS>e+BOSF$$HRHw9@kh9eou%WO8%f9T&UMa#fJ7+{jt{0&@Vs(=zHi4 z0^Mu3C-oW(Q;g>&0AJZT*YOdZOTr)NxC}9WfiSEJBxOAi0BfzTAD&4MPXf`H42b+; z7b1Zs4`>+eW;601R0y6`;p?_68yjs5Ze> z0~GSCIK4sF=aLwgz5A2Ms~nVv^Z!*#W7`@WB$lnJmhwW|K#>B%R&Cb-U_%Mz6VP*iziAoF1n(pQFIgQ5`kj|Hd^9WVj3cZjziYmD;rKyPcG{c3q1)~cK{cUrH2c6l7g0ed1=Fcm+AvUEj z4;`cQi+azYdII?o?z5oK%x`x#LUNapcFh?O-;Vb8OpL&1~8T9Z= zHvR!6S)tRpOJ1L%iBF2<-lYlD>^qRhm=$e|OzBOj`4P2_aid?-PZ>p7a#)Ah2APeX z_j!Hi-x*G`r^-Y&75QT`q5J;giUbE%iATDTh*>WYJO0SL%O=M1FAg~W(7Y-1cfB2m z^cz=y23Il)N-OhOU^L_`%tx|dfTOdCV$)RARtpSz8;~sq@eW}YisEwI!KD{|f{HHq zi5RiY^${uLktbPhJ{@!pwSUJyeapnq>$UX#B}AapD=|*|JgD`1+<>rO?O{9!eYfOt zUu+VbLTIM!q6IewUT(N)-86UU;cn9D6;RquI;lc(_*J!#<3$$5U~pe948 zu)-4J87!gyHkgsrB3=K^wXC(+m^X0=lh<$JwZ$!^ne<9l{sPB0gq(lMM-^djQQcriC~G3Z%|b7g(vRH|gwo0lBKcS3xOQCHAE z9{5KP$5Vvh7TtD(^9hIb!3Q}HFT>ZJYgZl!qoM91^bym=(7eb#tGS=Bo;#0m8pL0@ecrx+CkJy?ZosoK z;Ne!Gx5K}YrK?*Y4mpFjV?Wk1(FXFpHYy!#8=Jr@Z1%F6D;5Vn>OqMe@l$VUW&>kI#Fe5~r?fajIw#d4cK9=U;7g9kuqMdJZ}0q1CXmNUAz za?8BIV$|a9kH|~8o{nJUKSD#D&TVmjW&Ze0P8%E=W7cLJsimMhDH+&sfb%X$uk?-|JF0FtP+C+>`n_#7}e5uyN=Qf zRs@D}+NMAz{YQuBdZh{vjmwFZcH8L{j$$~?=-y!E`{57j*CYj14$;}Aopxt`4tM-) za%@Bb(O&Bmzu;Nj0e+!Yb$z~QARm1gc|rwC_lj)G2OXZePjEEW5*LPO>^QH-FJi>u zo1#YJ2Bl}Zq+889Q*0?8461-9x|}5yGT>!DxFW~5Mtc%LHp?_cieD<( zM%2#5t0Ddgqvor8^MTU>&e8bq$;4h08}p5G#6PfHZ_>oKl3L%SfgLN@kQT~0F1|6# z1zrSmr|y$lC;OxzQK-jIb0BvF?4b+AqZ5l*;5x2+2tQ!&*8Pa8uBZu;pvr78=HFBKWc8x77}# zI>110NXrAFk3D!Tg+o^bjsLNR7FxnNd-+Hz`YtQ)(w}cHC6e#qTb*Rp-Gf$ycFwF z?bofFtzWHc-M1t_qTm9kKyX8Cb&eO1xIjQ9|MzF+oO90wy8Qlqy(+nLW_jkBXP;+g zvQXxh@m=fDknQqTYOhffQjR|vCq*~sDvYi}t3wB<64(RC@VLn|Hk17y|Y ztk%fpsAgaYuAVDy{5USZV`mn#IQyt;WYzS->>u<76Bnjr|0+lnYOQVgp)A|7+VmiK zF&K!+7Nj&c?5Znd?yO1NW4j|IgDE+V3S~*a;P5CvRevNlQ5D;U#A=Tlyl%{q>^*XqnZmrZBGjYzExI-X!39aSgo0t|fz3z^@nG0t zCdr6085K1x96LObIeKk^JlRBx{?!OQ&hHRT=a~M~&Mpe&r%#_Fbxx?IX=npXW?|&1 zsn^Yi$ZAVXtv(Y=x)D<`j;&6xr$NyE*QV% zE<3hdAnW2Vey2H;F;IKQG=4kqieK8nkR;p`RIsvtLF#yg%CzNKSkKiA6WpJvgSjwr z|6O(8dyh?B_skCujOvRBw3=(A-Wcl9xL;o5_sI#)vqzo28P3K;Ng{L2+C*b^uu^z; zp(|KAsVjmQc!l{BLIZ^_TGl1Gb6v5x{8ZazAU~wBC zolAB~f(7q1YgjIz-V^@@v#P3iR4jGzU*oCs^B21D`_lTesRA`XVOnQdrRUB}99hsu z$gtZZe{@~_MD@;{Ih9Y5P9GsG80>@w#N+()2$=*+$ax|kWi_&#SK;$40&8ax0jTEH zrdC46KWbG%oF4XJcv((^!J{@A2;)&AQ-T{ETg@ggCkn{v+lh6yDXtIovh#BCngyOH zsqm)G&~?<7zm;hFwx}^1(>_Rbx{B@nq~yhM1GrwK40AhC{z`2f+P|lWZLR57YbU3f z81aOXq=JPpsuC=$E}lN7CNmC?r+P5S0=T(98>CoRGWJRk?`b(2`k7E-_oU<~rxPm? z(Q9WVOvjkD1==!uFH!fdRS>xCiP#y%$ryIRE=AhB(n{4k4#OECNtdAEy4UW3)I zx&57%Qx615))`tFpLHt)-285Q_DB3naeTI{WZg1aM>GQqmpz{H_KMbQQRUp}*k?N_ zKql}AwbrJOTZ3m! zHN|YvR~3YtE!zER4EKCO`Glw}OVnkNorKzYaGK1FVU!K3l`w6*au63j%p-y3o2l9i zL@yOMbN3zc1GEWSIAM4K2fZ2+jm=M_FDdbM1rfYdbc#5F|GE4?q%!Y%rQ5`CnbOIn zD4Dw733>h9)gYgW0IfdC)lZ_B*{Q%9(`M9;yzkXgjiby<*6HJrx=<>!nQK~VmDt|P z#v&q`M-O+Qb4kWV0dQ{ceg}3z+vjZN;+Za$k!$nuD!;m%D~mz?C;b{fnvvLcVN>f> zAG<)mHEG$)RgeGN*%`*z#eP1l7`i1{Y-TVv(AD!UpkVPKxYgF@{GjTG^e{Y(Ea}AL zP|#{dzN7uQFXg?bJH>Y(A9k?!oPe(l=C|i-#I*n#oFU867n4PfEw_{W(&#o5rtEHi zH8TVQPkjF+XJ1yV%kQZAq%b4BaTPekdLs_lX^MbJHu4M^DiBC~9Bh5fR5O49g7Wm( z1I>4cNvdc=`}gioEKGBI@MnM1!k9rbU8oK_8IbJquFPoRc1C2 zC)0?)P?eSEL1S}sq~#*p^5Y`23KHSE)(gn=?4-%a`0U@oL^YXnKyVeT`VWG4C+x4_ zos-WU62Yy+d=_c9`k=e<%84>6R9EKgHR71D^+x;4qjtWcLdh<5HO=hy8Kt&X5%4dj ze|IH@`6&?L>>|25CfH}$`pub0EPxxCu3^i&qa^#7e)BC>zsi@7;cVvGfV{&4v+0z0 zF?(t@z3FS98n}Jzr0JK`WX|K^>k~LE#dkd>(#i0_9NO#GYK9fP3e~y!ah=+vL}Mo% zbSGlpo9#79x_g$q`R@~j7fdqWD%h@)ZG#d|VT#J{xUZzXEd+096g)0e(?We*Xs$a3 zKD@@~gqNA}1ea$|Q zTi*%A1>O}9X#kIl(~r<}#H$;TvGg4<t5{jlL8{N+swt6wrifM@o3e6yzi3-Z7f7bNz zA_3^3qSs;(BwcV8D_k7(p=F*%H^Fb4f1tHK%bZCuE1MoyRR3!5*&Iz_0H{A?k@<7d z4LQ^i-jw+42G(VB;*-DhpXqOkqd$TamIR+;ykKyb93kMQn0B%UgK|!(J8GO_9;CRo z@-reZ^-lz!&FvI6w@$C9$(+R%Yl=sU?yW!M$l40sZgVVSX>=y7q3V@S@5^?@%FrMu zN#oqGgEUUJd6|uN)MC`XbN2Bh%pKk%Ll(xHj)7uEajJc+{@snOr59;U-~n$^2g)*> zb=1}YyL^2gv;A^1N7>z((x>=oP6KqSFBxmp3GwDxmH@lV<@|1HjW_FdM4CS0H)U&6 zt@ia>O;jtME<4z7HCn%m+WZV( zg74P5`8D62{8E0xoeUhmaR!y@uM@ z(S$*%Z4|a7|1{k$5l=V2vs3BumDgO%My@{vb6y)fGzYb8P4I5>cpbfIGkc!)tO(=w zUCn(c>S2%o@ypu$vLco7QlS5H{#&T}OzBPj&!^naOZ}gJ(NDjF!~C+j1gnFx9sROL zD9ej}cT|J`6byhM3C%z;B9@4;VS2+4$-3> zM@bU$L|US}BN6LNlq1(I?w_C%uF9L>#o4{CJvgge6ThuBEPe(h3{Cr$=ER>mzkl|H zoV5{XF$E zjI1q-Exwe$j%$G<%qHZhqSQ{+&&vPOmLik(1`r^D88Mxd99;h~k&a}`cIF&tr_Jo8 zD#Tgx_uIGVPZuZ!{k_n=)&3;UQ4jaD2HCBFKDF_wk@0e@EcS_cU3;xESKf*@{gFR_}^f%K-^zd*bK`-#I(_7K zgdV?3u-gb?0CR^0Y!NVdXmlu)`A|gfS$aPxMj!fyZxYQP;nSkc<|3x}fC0P$+f7tn z|I77xg4zC8>od{Zafn@?wZU{JAqcfz`q!=3v94r`^}72%uGcO9|JUpOf4^QM?0UWT zV@F$mLWEs5MoaSt{sQ_}rX3nCjU{XQ=8vTp%6}BB%`L6oX`A8SU;TZ5K}TAjBR2aU zuFow2t-1BNr`0RjVtphphAV?;6b95=sVd9HscmT?%{+IG4R)COv563d9v7sS6aHJLQU(rhX^G_PT z?UNd-ML&(}6)r0mB2VEji^$y~OqzD@g8NK%?i}*(Eb|8ZD0V5KVfG0JFCE3~H6A`c z5@<*ki!M{86|MYRm_jv3oc6I3nwx!NYxYPLa4V9F)J-pPDBMrae~))hrL(6Fc0W9VB)J$7zM&QJ6FeS7L>vI7eA8D0Le}jIjpT}Km zHmQQD4-IYLn>=q{Zz?astMJh79o(4~Ed}{IC~e>$@4CL%wa4p+00F)FKs4_LTCgo{ z?CZi5HUHHS!BuU`Q9S!{ra2djgH)yH)uq(V6zsjEXQ z39DnlYP?DEFoM|yc0}OgE_7NKm?vl+HgnGl(KwM_FXX;xFS5g3x2L)tv?Q-Mqa;W= z@y_}T-P$l+`H)NLQuLs^Ki&TJc&Pro?OM4BRJ+{OI-#E0URB6vu_j|`^Zp>=DyC_d zXWWV)m7&8f4{!y`qa%SV&%h!rk7u$2@p(2M*$#Ry;Xu5|!KvHzU1O$hQrKkkY20}V z9i}&V@6$0Wd1IkE7N$&mXa86O7My2}`+$+SHI~Ah{vx)kJrOcJ{Y|OD)BtQ@Z)faP z&4<1EL@|%^RkPz$jYU!au`-5B5EBL79BXiIz5UStqWxOTdFQ`W$)qA02ksO6yHsiR zb1N3CYd`}X1L9$`#nm&ksg+GdZMXqyjo=cLZ za+}b)vkjFSn}Uc&4dmhDG5NhL&IeT!<1^!Iv8}fq909%QrbL0}O5;sj{nBf{ z*azGter)_4o9!+Ls)8K`iJcAq&~Ot<+=ha?50UtP zQ#%bU?|9GgS0Ha&1Z?WFgaEydazGOM27ulqzO#T@rfxnNKs}vx_1kMFuVn{w&H{?- zD6VLQG`6ue<+;`IVFflRy4_3I7OYo-XLXS7!@d8tCFPoha=a+fId?5J{N$m!f5J_LKQE~%=c-Kf;Rng|&r9O7-veJH!Wpaf8cks%d)_z{ z$M&ROaUQ|L4I6lqXFLy@P6XhTfQ_ZIbbbK0x|hnU~CCZ;os}6vV)xc_>nz zYI7G$Lg;vg97132y@RuvA!p*Y`B`sD4Rc5MG!UXSd-T+={+mG zU-8k}=h!D__4i)mr?X!Ybsr`b$fMjA z#i+i>!+4Wo9iw(morc?qA+*BXFl`?A(CHd09Uh^I;yJ|F*&1CgdEl*N{a81*@kfHY zmYV!0OfVUvt~eN9`R|a}=AT3Ok}YM4mWftEY=gimVegWKJnTJPZJ{2u_2GK-*F&PL z2%r#UBOyKW{0E@j=j-D^NcLrP)}P6vwIQ($=CtjBHpjm@@0rY0!L*B&?AD5oamL%fi+8CEJpmTKC5ISCzSg(Uv_C)zfbZ(jkSR%&$asAoO_6|DJ z0nq+O^Bzo+!=UlRB9-U*+fL_+_zjzgM zylvX<{P&G%MOfM$q3w;W;MUt3?1uk#U-3a!=Wm%!`jsmOF%~KHdA`- zcQKw~Ec#Wg)TB0~35#Q`_n_ZnJS-0%6VOREdT5Z=+H|rc{`ApDuAAS7$Ed#P)3`Qv zZu;mWIUpP7P!vgzt@RhqWuzW<<4P#)M^E;+0f$BsnMr6RWo=Aan@sNr+4WSjC5s8W zdDT~VnrAK5VZ=#`VChNhZBBxCd3Ug!J>HE)-l1q+YhN{v8C(=O@1G~%XRhjj(v*LC zpZ(X}pHmYrcS)iAansO6eBqBFJ;6)uEsh1h?OW`Pu*YNhZ zmPZsUsZGByRJwR#`m_eIJ>H2D9vN?5w-;R4IKM=1;$Fkb zmyGzwrdR=IU%z4{+Oqw45}Q{A*@M)Ma<(>8R~6VR>9uV}cvt*ueMdoqRKPF$Vohu} zq@bW}4TCTMTWfr-#`}cjGKa?EP22Dl-|CuTEa%<>Wy_w->8KeC6#OnG}>}5ioMJ>bs1B}S$+M!i#Yf7-#NUN!70|h9gmJ)hNqz6iJ za$YC(XecjoLvJ)C{n4K3`8q|LnNpH%(RwCZCRjidnd67Its#~dKw4d5CJ||2s+po2 zS_n^HS%j@1RPoqJPz{13FZ|;~dMqn2mX3JDn?JCKc#@nyh#2xv2!DVG=FA^u`lF1C zgQjvP0rh4KHvhAGh=|k=0IqFKKW6uVT^!)_7pUcM0`&J7MU%bRv&?rIVr%DFeRB!Nx(qK zSgRv|c+&(rW9_lEs?*)t&QHeL>{>VLi(V(6q!4w2{d1F*8l$hoI?{*rRQaTM(>?-5 z305|PLJ3$(iLZ(`>Glz#q4F}~OI8LJXHYMlI0rWM4fUT2jK zM_lpd9Z9=QWTqFj9Og{~sOt3Bz*OC~s>Cis5eVc&4KH&9xOccFn><^lZ0VIOzzSkoqao?s~RA=j>Wt0>9oU$D>o$h$a`U7b5E;;$)cImCOL zTGl_b-*@EFdkVZA=-Y1*Z~CiBzT-dXFWWvZ@oD*~CHU%lSaSAhxt2?aM>=Yb@j>v{ zeTjwG%P~pLgvA>RK@4h*DOs_H&E99Nh&$d~CL-k~H|uHnlA|`$WPf)w4ZQ=g&8a{q z3G-y%egPhPQ7h1oh(~1OQ;c^UeBtaDeN3;_cgY&n(HkE-RC5iI|E3D(X=Z*ZL7nQ4 z>@fY>Ru)%tg%^=0sS8najpfaZcFEhgIi|!c>vUnQL{0i5uuEcQVa>K}oF;-#!-mo* zO1PQ6dr*{2Uokl7A_YtO(mUSt1!xZz5!c`RrEgTwdW-lR*c)3;HV`tm|4@Uo7I`F^ zY$+NlAI}}EMD}q2(`TaQftF$iaJK{E#>9y%z?biXfpmAccG?!aX{*Z{T~@0B#IC(rpIyr|mgUz#ztm=w#_5y`N(4Ms zmq2s~hWjA8hHC~5j5pr|2GG@GiEixJG;|i!({N20<&XO1gKfEpjh2)zaODJmsSW|C zu_He0xz4M%;eCqzWn&EMj9>pTl9A6h*?JV+Wcw#v-Nc^i>#ieLMsH&oiJsJD$Jjwi zvpc4wW30`$D}-KDFm`yy*pUN?hDyLTotH-dE6n|cc?6KK1fcG)K|p@a#2~QImogtBFLLdM@I^+h!t>g8!36Ita@;QnL2WTL0;DUP z8_x&JW0n3}c&Y<#x+Ev2P*H5{Zc1+^T%hT@-Tl#CcRSq3ZjPXW>^=~uy!Iz42iHr4 z>z%x{RQKaAo6jdu#!3ERIf1$T^O&?EadbvA17U`}g6RddE=wZwA;+ zoTCAD{f`6e5)814xL|I8!pH#s9z&M}73BQuT#F&>4{|#2 z@g>vRJnVz`Q{+X0du*sgj8o=^Lf6{si8qx&Mqs@882U+MZnZ*(h@Z(4XI8lH5S1g; z#s1zVXu!#r|GNLjX`=qK+Q*Od-{SZGmfhd?`=9my-v1H1x&B)=>;C}1|I4HO??~kU zeuo;17>kquA&(=J1n^I6hrA6YY`jm@OW4?#!e?iI3h}%!JSF`~`Pz8;4jQG~J2?_Vh_-4|jl&@}n z|6acO<6SezAMbsfu)Oga^hM*%Jd))b7^%b68s7fQ1m`5<1^b9>CqmuBK%h_OlP>AzVoMBC$b1bVvL z^<+g`PDTAmpEarJ?z2---pv$mF3z?>5A?q;)FZ-Z9=+B6chzV0zh_?mMIuIj{r??) zo(`z(Ly|?Jvg}1Z2tS>a2W!8b_aLP?{P_Hn6J_#re0dhI?p{A|tUH5#Bx_{Kh@18z zRxa*6k8(waHP&|2RZxB(XYq)+pfVzPg)1Z?~iw{g07MI9p#UALu9;<{qYXja=f$t+wrRW@ebZ>yf=xg z3ZJ`1#yh}{w~H=kv7*r*7U_h%ntdT87zFTgp*@f-oO^E)6_#Ne*i0=GdK96M>w*s@ zyR9Fdl%l&B6Y-~(I92v>5ywwT3y7cAW@`Hum>&=U2wj7H*5HI)!GC;vfiC6GSTt)W zMI5-Ej$|gr@$F;m@>X^OIoNDwFeFDd0#{$bJmN<&0}N%(MOM5a9PP=~jJ7SKh4kuA zLB5_H%?m?38iC#;UE4X*xtpWw-&|pyf{4}rXvLsh-Pq`(e8u?eVe}J>YU>_zls}i9`Eu@@pnu!B@y1X{2+LXR zg&EG(()spYZ+;VH3*KP}*+rDX1oMEZZjRIy2XeuSMp1 zt2#COCy@7A-zv2x>Nb(1Tz&^t@u!yRoTGVfVWf3C4TBnVZ}t+jA$~S?Hax`Y;dH|b zZRQW_HYwWZWoVRx_rH=ax0N_~b`)e4l`kK@=lFep$^&`UKwX{N5wjD!2O3G+_^jVT za$5%R15W=ZNVN0<@{#nUr7O%b&@I51oUd{Y)H$Dnu4nmA$WP+kZ@6lNU_^D>>XK$f z?pG6!D4cL$ZQV*6zgYjC^$D-*FTk9~o2(-pUM>eQ^=dy9BcvxVq6iQY+M+Z9n z&Gv0lgoOzfZ>VmZA2S1cH-YO3W!x22!sf|dGv3iwY~p}3hr1{RKK=&t!6!C1%7nbS+S9ergm zDJUSRtb3n7h(p`tTxc{|zC77xip+$eP$6ua62JLEcQ90q`FFJc?+J`%Nu+C{suS{f=)V{z<&ddp= z!Tm81QDdur{v;TcizuAMK?cwiP62`8c=I2KnN4h&QR2Oi#}U%&mhcz{&&gcTC}cPS z)UMZr!MwG$oyJ>@q`Rafdq4g<5g26AEpZz?IYx1jLgf{_!XL*Iresa` z{j1SJEVGt)$v9MZ^Ppn!Qxp*Kc&196f zLIAYjShseQC)>ci`TQ}(&4|gEeDPMIbl9I%XdC=0V9wJ7nd6Wl zepHPqO(LT@QQI=K-3galZ3tB4KaOr7z7#dmW7v^wudy-1VI_Vbv^ISfI;2g<#Hk@` zXgqgL#%J%q&OvSFtifF1!ucPNT1%1;kKXG=u1Vy)aY;rShQjGWCP%?KvWKuk!=7AA z5hs9)hT5#H_{5f?q5d-)vx~rNq}qM8d>z?3K@)6r zN!+jr+H=5;5&d@(b-n-NKRJCCG~K-2&cJQez}{*zO>OLYivy6UQZxn9^qJjnu+{d( zBJrL@RjNZQM9qL@4q%a)$PkU6#$+5uO_?KBH~=fxrY?eCwNcTTZ zU?cMimG7+Ag+qC}wf1#>`+8`7^Y-LZKk%R0bMK#Z?_QyOI64BDRAgXy1-a|#e1oO0 zt&P9G-+tJ?XwMw>4ccqrlq4KWZR7F~AUCg{{p#a~#RzxEZPY@qKD3B6^oz_C$a{26$ zUe|}=`X!E-TorE`$xMSjsyio?!E6PvkJ(|BwMqRQaPNKE1=Wa;2ZG6*d@w2j>C}8S zyJrcW#Y)t)Any`f6E``cg53U4eD+UpL&IX_I?1xrR@3AXQ+tTx9j}y1%wM*8WE>g#r`Jw8}^4iG!}1a#I|ed$Mw?G~W5OgTqoEz|dtZL5pC ztB9vDhj2UJUx!`-H0Xf3FTH&~0dzmKQJY?@djk{Z%r|8!@R#hmvp_>Yh@?>6-lc54 zCAFB`&lsqcwhC2qW0onSnOcmGwjil3)Au~vXoI=lZ=og9iLiBVxE2RQTV(oYWM}g4Q~cP?6uk z-4+PnUPdZ8mvE~!~#U)Bp~J_+v7rHokTBDtoS=oMXr zmOf@UEP{z&m}oOc<+ui$8a2x4P4Q0M>uOAqIAILRz%o)bCkN5V*3u0*25zICYqn~k zUsJbpQp#)5Y^EZo+)j;rk@ooY%UCUeG^>z~6VLRz|B*}V7{psa8)ttdTLugz`!*tx zZsFueoa>MPPpMas2(8(J=!rQjr82j2125BD97U!5Fy};ppU}7(E3DMm!@~y zQL!3>tT694SV{>tZVCw*1X1QgycJNjTor34eI5ejV)pUo=lKjkxGuh7PE&LJQmZuV zHnVr9t5M($k<4t_G2EBZmXwY);SBw~HEY-p7r)-Iwt*?8dL@^PPN-#~0&#DnjZtp7b z)~Cw#1SoFaa6rvvxx;l@gyomNPJiOFU8hFr)?gp`?JeqE8`L`u6(82SeT#acF5BPS zXn(OS>Kz!=bEgcB$JqEzk?p46uhm4y;;DU3Mii`A$R2_&$>JZL?7^&!%Mr&3_omsZ z_8h>KiS>(6_;$PD@?{WM9Y-e%-MLb)L{icf*n^8!JZ^;r{syUv1?Gsp@>$@ZR~-w~ z>k~5E?%p;~e=FUrBW|H?l3RB3DY-4;q+!u+UZp-L3%8w`Jr-0r3`jzjj5T81d(2y~ z?mIr*%a?BfcfCL0i~1C>?q9bK_nv+?H*eX^Fu$8=wi|!?-M}4ELXwoOe9AhSx|R~w zTXu>iw#{RU18i5Z#E`twQcC@$a)&ohK)?6}?x5{h3dp0rL(6+>ySI^_IE0B}ohc-c z4j47=hy;65+Mf!J85NrHxoCl+{Q7d$5WC0GB>dDMTKqEM<#2qzgxm2;pqCYdi) zhA)Xom9f+yTw;lnTynO<-g-OI<>Eehc*FSI$#t0%2;E#W5|Y5zNUS_!IUVc@%@R3y zQQOa565}?5N73i;=Ktfryu@IG!0N$!iq@e4Va-VbO4giwFWI)L$h#hg+h$=&KpXQ1 z=`4)#P?P-0PA1SK+c6P~doR5y_)AX%ST#WG*}8RwI=NG`$U6>1`XB6y6LZVWYER$- z_;$H~+Qwi)E{Tc4n}#+ZXXT*&zzM@G`QpQO@^J7*{6?K2jyK=OfAi;0MSGa97EON2 z?=?OC^48gD1vU!#MHr=C6O4b8mEL*Z_V$`D@1yS29+mB6qx%PtgC$8d(Ayty%ouhWyi?KiW~Ba_1OkJa@khd{ z-+^m;AtKuYg^rparBAn7y2zGpKG<`S(n!aDq*#YFU?<0M9|OK$;5crv=3@^L zH$!$?J44`|jTEWn>NLnv>9)SMHP_+^xM>%9!x?fbRUU{`*`6xg_H`%-+DgrXBQ-z4 zpWI57(<4<{Ri*qDj_$-#eJhqE*MF@vL^9%7gm1kS}e4 z>})W-ghsqkL~x`#C%xt=xsT7RvC z_GQ-n`as#H)^30@8v+x|E2KCWoRc91Tuq^22d&r?Udv(6le9xKlRTKm2nP6gXuE8&eZwbww67_#d}+@CM!2^Q;d$1#wV;->T~QHGc=8BN9t|t z^5R5!ccPqQwEa^*<{)4_G5#(y0w~CDP7TL1;?PYDP0wZl=Wn=<4S2H;1^an;uN<7w zo{2lJV&mSyWgy&Da*_}-QtwQy8#_79QBR32ZU*J-V*UPh>&QI2pIGJ?`u~#A#OYZ4 zLSCaz7V%1*9fLm^0W_^5o4@qQH#n4Y9~U>eTjO%^2|FGSBJlF-DTBCJP#yOUOq5?v z@I+uaD0B65_mQ}lW_iZ!ZBWRoXT=2Ddv{^o>ig{_t*?o&#;B2FjDh>h^B4_&uiQ>QAiO zkyy}_ah2^n!9C+N-iR`L#g;GTHo*4l4jRkt!p{(ZDro$+C|3N z-)?wEt2e!;KmF>;71zEm^iBnfx4$0aZsRU<^0G}qJTl=9K85#txbZoSrepbM)ui7T zm*$DQN8aH7g%j&uC0D?dtN%m%{sVf{-l(3eS(-XXE0L@tvtcnjtu0@>B9-6O8Y?Py66B$Tp=U9@?u0ImgHAL1y2E zO)~Ii-woP=ax--Z%*EPQet7Mbx&^0v1<%5;gfp&~Xs?Ii8o77OY1gVHFQis}IDNNs z=OBD673(~41L{>LGx=2i88C+*&f`OxGh9AjfcX=31rMP3MP4Zp4$H3VZgpMw zxwAIeyR;An^`{U=_>W?s z&x%q*8F?>8R)yL`}@_;;6fGHQ6t|! zB@M{YcdpQvmzmOg7P!$W!qGmv!H)LCix51ZO6q$j@u%cprgWNLKNi-1+SPBY=HjuM z^gq|XnVoSpqBB0*QxmJ%0Bc5V`in&Qhl%nY zo^y=<`a!-;to-M-LuxBOxo-Ey?_;S8k>1s56H@+FtS9txVj^?x5wT?Xdr_9#{`yV5 z6Ej7-YeBlrQ7p4#z;EljT+sjk%%f7>1`#<=NRH7F;Ycq-=JYnoHSMQ37x=g`FOUub`0>nWre<$FP1wh9HBr(pl9lu0vt#_A-J7q_4$nl`GflMf zFke|yRw+l%0Y6tiLj)pVwl_^Fln^kfF?;YuL;+XW6o~m|1Vvow#ZsnbaIqxJYvMP^ z`OP6~gINi|Br4yJ&(;TDUnMe^$AXR#1m*?mMBA)_@Dr8XVDeS{+mZyYAvhxf!@S1# zJp8f`8!X)hbFoHDugT62?HTZ}p)(>yu4Nz4I~fFVqCR`ix#9i5dGkkpe;3Zb+bGc; z9CD>;%naaeg2tXcsgoLe3hOWPvC?h+Ob(^43bd$Le)&}hv+2VnpE#cxAW~|@yw(_< zZ@2jlR!UT|*Z%Fb%r~1uMYs8gPZ9Ox^K+)On)wKvK$a)ja){1%Tg}%kSGT!|kKV=1 zH`p&d!LInsRg<77Cp`S+c+(8VEeY1fe8r5&_Z#ZCn56)dt2;DuhuNQ;PM6p4f2xU~pI|H85yH%Qnu~8EsrJgqedzxE-slF0k%f9^>9;H*(sc@w))t$S zD9D^FrC^THpC|G1Lzk)IPrG^GY9*Av2POMcH#5br^Z;^u&D%)i=OpPVVT8WZkmPGw zB70f5|I7`C6G>U?@=LuPuf~|jJ73J*-_mj7ipOCc2u{6-;xU4PO&h>Sz*;K56loMN zlYak#--Pou^Z_r}!E|jz(_J(p(j`dL%RT&T?5&Gme_O6Ry`f?WkMOqK<>1d0ZCky5 zZERuet$@7MeS3tn_4{#tyUm1}^hOm_`*7A-IGx#}@>i|Ma5jW~Kg;jN-aZ^=*{tGO z^cc*#`UUAWa}nzT@S0PAB2HK$)G))+9k>d(@sOak*P_6lxY?NF_)R}rf%Lbu7@Xb9 z(R1asLx;wjxK)?sva|U1awy^2p@X69!NHI(YAI?mQ(|1ugmyAdW1&P>XacZ8pL=j$ zF8cSXxpMnh54I!xPVcAF%;k<*`NS$BFwIoDL6OzSIqf!mTqiqJemL#b>@$J?RJs0& zDm!Yo1;$)!;U(R zq+YHD{MGBZ50&mi{8wrSdnNL6>LA1Ws9Wg9U!3)(ftly{zvYJ0E@Q${cRp&5;dhh^ zcEcKouFZo8N!I2%r~}{~5)bFPkZ7Cz`R)A=efm>Oe-%tp&nXC)%*lOBOtWzMJo;F)( zCVUeL_Ac|oU684bJ;VVzrq?D$!(=mNO!TE=m${ZDH&;66mVAXbRIB>fA0i1ly!f2G6I!Rk?VH03 z)9=)8ZvhXkJhuX)2E-p|ra}rB_VphMt`|Ui?@JQSW+ZyS-WoSHGiq>lJ7@9_oC=>? zbio@lnmSgu$^qJm!O>kC zS`jW6$DUjM>{|81YJSUubLLCbl{2{=)Q?;7oI2JmlLgakN}&=nqu-+bvktnBukKGB zgHWO)SEWUGvUvFD;+!c*w8chroF`+k-_B=*=7I2Y@U;eF$xI?T*JV=NY0bY*Vc02~ zA;L*8`6gOtZwT!>%@@s&kJwv!+yC|{Xel;7nHJNb73Vr(n2Z5->-cjf_(CL;qEMOA z0h;=68O)93*7RvQ4@df;rXgkt0o~erDE@m^Scrt=Ob4c&O?{R%wQaoy=CB~f7WPtY z3s2g$l=LoztodBOdxDJJT><4cFNrobn5)oX`gaJvh|a7N3rSx!g)tg9CNW^QFgRxBFEE$xU(%UFAbJWBZn(F`}luyz1 z5q@~+h~qS=oomTX#;EqDa(P=YU$a}-(n1!*8@Rzm!zu*2411gZx%=EnB#n{X)60GO zV-s})8;;a_#_9gEY6g}l9(zo&`5nCz>a2e!-LGPajeaSJ_8L`L~gen$)^sirFVq8!BY?0teFNK3g`P{tJS=^b?mr&=c`{TcDi~pv5sWjX@?R1317$t(JNpMpP&r&|qblZGEGOGbHZA|iyMQ&?T70QxkJo=IB}g3k z9il9~yr*9J9aoh}TCvRL2?<9E`SZTZxjGsaA|t3&V!D1&D0ju#LR|e2kE;ICG{I@K)IL^Q%b2wd~ZV0T_X2#c0Oob&>Fbnd(TCU8|lP?co&a+s0#utlD zv6Lh}rBKYO;E;=%JApW!K11^$iS;8eX5x2uEv-4{uwrO=JU!2A< z!+;Y+Xr#a-oZGlMR=+^N^XgaW=cIT|MD_x1AL?cS!gFo;_%I(0>4sZ$VLj9?^C+8l zyfD>FHR46Q{s$!9G`b1ImAcJdBgS}gS~kcH+7$g5%rU7itY-9MchSR>YKG$+XN zaKv~DL&AzDGyAZ5JvuYAq^3;ahpZc)ktXY-WO!~bBBWPquUa--WSf|5#0-PF2cPD3 zVI73}F(NP0%Xp#I9KiZJO`bZ@y8oseP!S*#oZaPmy^UV0A$)7OIaT-L{{bqA52&cQ zRlF=Mx7PgMIhv_r3r_q5FW9(W4u6(!JIr3c)JXMv=(WB5SiobR4upqmZST`@e}?)e zQ(8$a#`LRXvFs!AIqW~Z)J!^0_HnP(A6k4-5{NIRy|bz;h~BoiCn0|P_AQt4{IMR* z7e0xG@h}b8w<4Ur>%gLKWX3Svr zG+{3J1_VLDqV_YpoDVmL)MZlF>>vbf}IRH}=*a0cs_nk473;)x$%x zQn{H@^EJEn$l9s$F;Fiu(rf6oIz2CYJWW$PSYI==3I}1<2}P5T`c+YC*a%nT_jiZi z-{44pU!NZC&=@`pL>sN45J{oRTG6ZP-f&3RAt*igXqSY&enjJr9 zN>@GZ8%kZ~NDGzM&wjP^2YmB#-}iLiKV#UyekO*hB%7&nQifCeUGh<7x$quMjpETY zoHh6@+y`AJxX;tdqlA0s3n#$rJVo@13y6l)OEy8w*R|$F6}nR=BhAnJPNem6__h6w z^yTkSxQ1ZJ+Sk5(^mWveQbE4RFjHWR=2kjAFg4t*JLtDx&BU`J^&UMl>JcNpY6!~s z8p7PN`A6rk`SL(12HLMgJRkKK%Y7mkZR{yZ9q$C(_0R*B%yd3!b7NOmI+ao&L1^61 zUo0KyJfAAgOExo^gRQ7NAzxzb0fR-EVKst~hS~XnKNZMZ*%wwnE>s4Tb>&iAHzj}5 zDKdXOQIbwn9RkE#`R>mT55dmQya2)~ds1bN&e!m3a}YnzwVV=wHd=X1W$45Aq^ww=MF1umF#_E)-}RpP~Z z{M>tc{W)UAeu;>xAhlz4gx><)A6 zc4f@P>`HI}^P5r4@&GaIHRyi96OZq2nHy&s~$*_SN7 zksU+yGJ8Fio#QWY4_6w40@ELf27iuX%>9B07MWA>DyksiV71Ix2%9(|<)A0wR6O+V z_=^V0Y_c6LIAfD4@*A9Tj|CJ<3%rhOqby4vadAH#zc_jsE`WFeLI5qWRNe_pdIf@% zp&shnZ5!D$wm-8AkOA<%;!9cF<~9ZmLA?V5C(;ZayZB+>1Tq7Z*_G=ucpuUyhfXtq zj(~em0Jqh<+Mf*MnQd#y&hop%C(W%)GZRm95qKmHe2SjB)`h2sIk>0)TuS{_-zn${ zrJ38jGcPN2CBxT8aj;&JU*=1czeQ7MH{S={q6;0S>f?~c!)(Mevgsg^Efid%My*HE{)Ir zU#7#}Y-)eRTfF&i{Bl(;r%Lwk+E1zQf!-0Ivwv`3OD&>W?GM?0-)NB~Z>hhJt1lj( zjV)!LUJdP8*L;)1 z<>EBUSEocY%Srt3Zl%&3a{&Y?BslrU1_A#isZ%Q6C97x`g_1XDtN4~v5&R$2e_<A?0U|0lxPV5YBOo$82V zSx2b;C6z4z$5N^CUG)2N7J(*hu1WCzU}se^-wVtOoPB_4c2W(HX^kv*2@Mb3j13xd!yN#_N0H7_&n_A--T3(VPt*{m<*MDJ=edzZ#{cwxc-~V z6DtqV1cV6}W5t;hxP$|hT_0pON>3@$9ypHL+T7A#8tw#)yiyc{6bmiPaQKU0Dzid| zG9^f_jCpZs>=g&uc`cFxJz66o6U%%A^T^}a5>s@NVsO49v6B?7o4=veTt7@x?=fxW zPnjZIEVdl@FX3e^xLy#(w@PI08rn+1xb$n;ZMaBbTnt`Y&Yqr*X@k&pslD8k2 zuEBC*qOOO;5v~buV&J&A9OPSV-#U7t)*VHCwu557{)^b4%j~blID@F^>uL}w*5qJ4 zfO{Na(U>IkEuhEVEbyEieRXVHng?u4-=_;Jgt|?|{VqOw7We+xC$LvilPa&;Q8t)1 zWWGk*pfz#y{{ih@1tYxP`{)g-gG#NPo<07Q`r>5nXHfFw^18Q9P*WCBAolZmU z?e}sjb;o9Q-p|ddCUbVo?EgnCV7%$qnz|LDo^rlquQ14m_X~>~%tw%~)h(2L^e0y~ zT4iV1x=!CX0pX;16I> zXAyIrn+m<#a4mzJ=>s`R0KLh22lh^;wATN0Gm6gpJD>dF9Y1uL|Jj%$E$?Bl6Xj>J zev>nMM4X%$PiCqIC(}u8{)iM?pm*f z&pA{Po9pU$-Du;$eoHU2CRm4gk5#hf6&uB=F}?`sGJSuBNQF1M%!9{S@ol4&#Eu?9 znmb{`w+}&|E*V;@ic3)7(G4L`T2oNq`%ALO5Cn1{GJP-n1CdR|FKcinw0}YmVV}nc zAT5{rJQB$+6DqQRDqasvA*iSFqq(9&|HUdtlMFWq5 znJJUS>+`blqcex@Ly$*Aze)kdq#^9c=XsQT_y4qKYZVpdJnr!f(VC^s7vyXU6-J*@1IY(%t+7 zs&4baiOhWFJoV?t16+JLvwezlGZjnB`=?ORdBa2Cl1(40U7I=Vr$Q2PT=AM$5mM&w zDDlAC+%o=P-{+JjPA|lQ>o&X4J~4CCEFi6)$l0lmt9Pw04Ic|}&Q+72p$GfT!SDP-=55ZV zj>}>Zt*IB>qt|Wt#*w92gt4b@qET}^WAVcoM$XAwL=RZ_DKXepbI6 zYndX<#lWb-Huv$cCrt%)ZobZ5@g1^-W_w0$>>=`&+Ry3DQ4S1k&)kd&5qM`Vybk7I z|8lW^APgUZ7_#$n{fnN4hut4*Ea+~AA0Z4J2ZpWJOth0Whq-^nvjMMWCT((40|PV! z148^B2q~*F+#H9W<{iPeNIb&Qb2$DR9QqRd2heW9xhj91Gen9(}g+uV9(g4bbrv}QT!-qUN+Ys(ckvQBbtv<+q|qNQQvKJ_ng z**|?I>Xw)nPw;V?eZ$EYU3S*|?u-|}>&p8=ymJ2p{FuF;`{eWACrnsE;)O4BJm8^& zXCKAfY@r^Ux(D*Awr683&=g@in#o+}C%bZFP-vDSIi1MJOxQ#+*52_Oj;9}Oa1MsU zuK^N?i}NVN;n;@j9gnPT=sn0i6~BG>)D0=$5nku5bW-K2a%uP zA;r4SvSrormpU8G@XF5Vhb7~Wyr=X8dOIv}2NlPq*E4-veR6eX{G!I5ZKu7d4oX*C z-KU_@9A4QuV?o7|igmR%kFD@pf~b|7xOSZ(HQV9nhhQ~{&?s`GSH-K(%#R=a?Or#V zJAbBsx!u!75O~50ITx>aEBpHpExk|xzPo!ZGaqAr5??T{J>U$DeXArI2KmU~A?2pa z;I*))6A?h(WJ*}q-55Vyp=&v&ZarP!-T)cXW5OnLLdO-_fQ4ZsC+r zA8oDw9`2+H`#;U^QnJZRk9HZ0$P?Ek@+6;MrynChT|_8l+Rk8Z={$(I+{ZEu?86GW>czE82!-^z|peD;k1pskfM zAMOq0%vz|bUxJK!&C*q(+|f7P1;@0Em`6hbEZGC0FDlx^G4ljfDC}Jt5$PNbC2eiE zIcgI3ta86865AiZQ@O$*8f!fki9b8jG~>L ztF;3|MxT?vz8}C{7C*96egT@zLr$N$<^6)ia@+MIpr2gsLPTC{jlKt=#Vi9u8s@?A z5iU7a*0e|g$x!U)In7nA+gK<&W>6UHKRElq?~Nd82V}ng|1wwIOW`96|IAX=s})Op zatM^7y1r2s3X3E{H4~}G*r$dCyTfr5jC3M^D$bO?O(eh%ySiqaGkM*4r@&@vhxhP5 zGNsS{PI9u#T>2g==xN(TKOd(RE6}M_Lm5OYM6iDqoY8p0>|vSI9#uiqnm5qv*p2?Y zy3Mh&^;>y;ja$!jbx@<*JdfyaXP@{q<(Q28VR{X4qs`F$0VVitj%I2^;>yw0^;|t8 zP!E4MSKn;Zg1Mf$%t4ASS~C%g#yb;VWixpZ#^<;1!>ZUpSfPL1DU>WUJ77Qf)nP93i;9F(6?K|ZDXPZj z#V6qsPenrpYfg1?Z#aCz5RE+q@SyO%RT?ioZjt#ExfRGjyLAxCYbrWXzYp#GqcJ)l zvNJ!q_J9}szG$TmSMqY@=qcO34%_i}2e%-+V%9+JA|&{?yu` zyu*CY-5|m&M;V0gYJ8jR=+pay)t=}hM?mYGWiDX)Zw|~>?dgD0U#yXhD})5}39gr; zXb4U9ZQRti{wSzv@oyt)?HF7vf?*Q`rFt<(kBJcX_YpND8+;DUo7FrZ=(}j1eK2P) zKq52wPH&F6ya$(WVISx0^M_$^db~Ue&~wO8B*@_cNQ%llJJx#B!GuB!z1j*;vr#6k z#jkaQ0N|O8ffP0efxV!inMz0WYOX{^MOr{rIry#oRr=S$`WkXX>*E0#S#!M#-O+B! z=*O23l4t7j90iJ~zBm+Dh1E8GZu&FWW|gnxQ{dLwZX@IS@s5NA8p~5=vpD}xND=L1 z`Me9##@{Y9cbea^0s&)b8{#?E50{EWkq#QPecDG`stIi(1#32i7E+FMtPl;`6gK1A zw0;ObwA;OdE+c!6CA4;JL_aa`qT3?tzdL>TXC<^)bSIe3Wexq~ti%_O657d4Or|%8 zJu;3qrsXR<){-mi|;a&E1shw?oT(^CGCVfkI8p z$AU>fWtl^#QNrBP@=YP-Gk@-tFvWcRz2}OE;GPMKLx_7Z<$G_9aLf-tQ-mt$*6fE4 zh!jQU&pVWG$$CnGeS_;GoI{{0tfHHxY>pSNR8I^4E70hzCM=cxOUXf)xq-0NOmV zc=_^G@f(+-_+!8ds+EE3CZ<%7^WWJ@ck=UhjIkw0obZ8UFgZtoG)j#y4XhAmHWbZ% z9*$j?xd%|VAlnU-?pnXZ)Ply$cv&je$ev@qb&axUPQ^#v{2ci&G-oVR6fInXGePHO z_JtkZQ+zJk-bsdqmYyK_A1IhB2k49_t3L)C(Y2f9&`}Y=x-1{{(Rj3lk44w^ROMT# z0g|7yeqtGG8#^gZFq9TCObGPVOQQa#v&ZPbM)TB%!w&hJ*mp0{lY*zBRo8*cs#ysA zV10s5rVxO$Z~6Co_yRdV+j$>q$sz~9t!?ys<{Qe)qNR}^`xEq$7WXBd(fnbnVn8sv zp&UXou}sM-<@8H#|6i9U;L5YVwQwB!*L4txmfoJEkrbV81_OYuOE>EG6TDX>IpQMK zPl144(g(XF1JQ-eS+Oh4qRodZ?2b{FnY|e8ymX88up-5>q?^G)bB+*5uY;jSjrLCj zMKBAwpJBXGA4j5LA|JQVpH+SkQJw1uwN+q+jG!N*0u|=sL^i)XMC2JP5{R4_)G-rn zfu+SFF}tt+%#&O?ga#W`Z4YJy?R6+^q@<)70k+_)#(7nz={Vjizlq52t7x0O+qc*9 z<-?wDN9Df^dw*5ONo8APi4i0C*s8y<>-)KGtiJ%gH1_mMkrCcyHdf`jl1XKO_m%E+ zhD^8Hfs|+YelRh!uc*p$sqeA`^sUn@iV1IVp6K$2(Z*;3s}*wVmlw~QxoRYHO0?{8 z_BqOL9P#wcoOI0`^n(3o^fUZpcOjGM*SvRFOG(CO9eu8;P4{yC#pa2iZ>w8)0KT;- z^Q+!Xn>MXqGGx)d4g4>~!}2T!E$MapOXyAn{}C7eKhV%XcQtHKP3n{GtmUt&0utCh zPc|+uOqLsS7;+5M@8|R;5{-V!)K9KM(qreq=g{mSG8CXLVo@KNyR4Avu*YR~U__*Y zJ$>@FM1(sob7_^%6{+%>hg162#PnKip6Dda!?{|=P^MpfK5QN`t<4hgE}^+ zd@XYq)9`g?+U9JL>uPR*OMpzW;V4+(Kvk&x7GkR=PZ$)N>#=Ku32X1k>1akt1@!Fa zdO3Zj<}_5VmyQ&Ar#~Di8b%Q}D@E)ZU!KiewMd8pN~O7(bzq3=L);#b&6pac9%o=} z&^fY=J}K#AE}}$ffZDK97SF_Hjj6AxdjF*3jXtYes0z)4BLaKcPbK{w#$=!|t^M@C zp_muE=HYS!-o4)9CN0{zxewXj`|B0iuVa_YU$oEI+_UozSF9Auv-3PKKKm3_-YsdxI^pChR86P{K#K4$ z?CnUo{&sc2j(G&%3OatS&JyW-ZUi^ij?!WEkDj|sMY=QQwhl9eGVa7pe^i^<_gb`E z;{tqw*7&$DpSzG+CTTx6F5rC~6{goVzEMol)Uj)QgH702kpUY8*!pngXf*a){ANaG zHI&Ea4t|E3c3X6ZKg}S%TH-G9RoLVm6rDS(Ko3g$n-pV?Wi`k>XRr zGp2_&NzI+~&sB7YOnVNAe1l*4!XC5N1>tk?Vz-_==wmKBL{{hOign&ldI{Fs`Wx;n zR=8Xvzw?*N2I{pW9iZMWpa|D4#t9Ch#-45bl6cc&Lgrnn3FQyB1l@zRb2ZQ*r7j5O zXKD1G>(t>!tJ}NH(~Xu!@topK%HIpBf(p(FR9K`5nKNW}SmF&)dLXlMiXnH*Ypy_I zRd%|`E|O@c`Blb?)Cl7t<}|nkFq~g8On`_%+s#163iw3%E$(tUaK%Veq zLBejzB=Z8&8mM;#>JY~xQT~a{i8@4RzH{F<-5B{kNZ;#DAPt5Jr@0EBMJudF<*ULJ zRrrakuq0YxsVXpjLKW_G722W|o}_~1R)K%VNDF>yi26KBO+G~~1NAM@CcjdXdVj{} z_)Y5KPn|IILN$59HMua__@X`rGFII({Ap-_IJX&FU4SKOZ z=-R^_oMYTZ6-{1peQqCZvO?hj=3b>f?{*cs(-HJOtEIE@jJe+x^cjDeOPr6&{UNI7fY^d^i(+pF=dYOVs2kw*)sto5=rruGVi->NnYmkuRi{ z)2!xLo9*2F+__Zav^w%&)nN_?s`%4f#eJ!WK8|ptuRna&oi$5+!VZ1Rg85YK$giBU z=a9vdscV&cms!J9t;t4PI7^+0qplbp$PFLP>klI4xBxLHes+M?9fs+-8ANz45&MI4 zKnu<8R$^nTwV9dMF__(|K2Lv%U(uo(`=Fdz*(HWv#u9ZN@{X|-NN{4 z+`hZ?^oIbtxR^Ao9z9XB)G9N-WGY!8+XN^ql5DYyj`ju{PVoEnF&SA zj8QHFAoKHf+T?10kALa^{2m(o5B`tZW=4;sW1DP>2edzb*QD7(do!7S$Lw{0M9VLS zmX$B(B8ok-=bk%<6mz%#_!g#N^XmFb*i>v>6H85SrK6z(cJS+$yPq%d6ExW$*Z}95 zpTmh{h_Y+0^unXS0Bmz_E1e9PPdD|?a`NT$0F9wJ<8lg|e-BTWaY)&Xo8u0F zk}4^#ir+flX6Kvx57sc;Qsj}h?_0#=5jEZ{hf;sg563>Q40+lIUyU! z4zO$Cbx<0O7m2tf4c5$)Q{T5AjRQxwexlUHDmmdTK*Vsn5Pynh9jA|sR6n(x6$9pW3)N8Kk{E& zMJvIlh`&!-ZQm1e-7PfdsXJK!lZq35BDuZe!D}I49pNKT_#WsuZC8X<2icK388f zjx0zOtNXp_(S|(o;uAspUq;(sq4pWQSY)S=o>Xjp31L!nDb>_jDJu}f8@Aft&e8t% zSAQ+V=HLo>*0hJHZ*7-+5O%y`79y~>-FouL&*y)`{E%o8BhPKp$oN%rgC#HxZGz7~ z5*NjP{G~oDK})i@vMp5-f9Yqjiq^`-sUV(MH+I12P6%}Rz>3z6u_clMme?97D?lBl zj~_6rwve;e{ml-;@r=e6M`%*=YUZk{BIz*_9&OZ-T-O!m2}vHeJGY!D5ek>>41Y!1 z%=ZUdyd`2avQ#@vDYcn?^E&pL(emm~63w2soEgohBqMW$Ihoo%eLkoBNv*);b`Z{i zs~{ZbHjQHk8Lje^w4?becv8M`m{z&fcw+^7KKsT_rau&`W4 zmZ?PjtgUAT$VxP20#5kb3GJjEh~Pvn;y7Q=88=N0B^bL6P3O-H&IASP9i5b z4qa70DRD$<=*Y|2jUfuJd^Tmi(%0~gqT$<_gXq{&52w&Ae2JNS7gSy)+~u#EXZ}o3 z+g{(1eVR_px|f4y4V&X9nm^;m=hE+x=`#+h`t?u%X9}I>p9e9`-dO#lz+|x*zxPUb zXU>RmE?lF`p%T+n`8{!~fB z#vjF-b^wBgjlYOD?ZhAP*%wok7H}evpKtm zO}1tM8GjP!CY#AW;xi}aPnUe7bU268&nhsB_jhF2B=?#lC>Q}+q@8HXO$8RAYglk4 z1U4%v&lMff0fam}6>8y3@N73byBmEOC8|S5MWft`?fb7!I5RpCzAA zr+R$(CE_8?s`N*eQqA@N2(YJ6?5yThL-e8Bd|i&gJXP(UniLa7G_T=@gR1e1n0cGg zWLi<7`Cj|QGp*ne>^4=};hIDpfrHQe4FY5+U-CFY>j%*O5yKiu_0sm~jxZi0aHu%2 zXO=OTC(GyDIgS|2d`j%PLj+ziUjq3?`&q8NJEmkVpcfGnaOp~>aTPuQ-kYeCvU$cw_n~Gng=6MCm8<0-WBWBeVs<1 zZ!WeQ}B31#ix74c@ z@9!XWE>W&`p&8KGkf|Ef%wZYQclT^}nQ<}xTRKqylT@>8;S;t_ZPd|Jhi z+-2YKO*27GZ>IE71?`&GCDQaisW`RAbMS|TFuT$<20>`V^~3yD!Uc4r@8R6P7^b*B zRG7n0s14a?mQ@7vTRKUBf&4W*2m~oa{(lxXtYN2lL;yM5fGwob88)mqzBvBUu1!l) zN0G<44Xw<1`?35e`C&|A!!_7ws~cv-4v@Qm_B~i?&3w17C@&NU23O1l2B&kT%4r5MOUHM=PB_+fM08Yn3wiocYA7Yx^w0x zZM00+o^xD7erM6pWnTZg#h^utd2}yiM;#VZm-%L2A_<74F4MnE0;S8v6fLM`(4HK8 z%S^yVl;3!?x(cXo4`gm0|5*E2-F3+dqd<7vG^BGcUcGwK@X+!1$Q{11Arx zm@Z!-nfe&hB7JvtvgJfPa}*Z~)@>GwHc9+#PtRSy!c1gX<579|>XoXo(7b04q_mm$hHlk$c-?{(;g@i|KCy9|HuJ}=H@wxK zj~{=~P><2VYCf03z?S5P7R*lb^oy3td|TTFr|Z_5<6d?Y!R6lE{L%L}RIX_$der=X zyq$Y|l*RS%v$;Sb;1iS}sA$m8CW=VBv_yhtVIxm)QM{v~Vo^)gT7(VY4T8G?)@2cu z)@!{~f4^F5t8Fa`D&_(S7mER^5>akeopnJ0m8-b#_sl%cvzv&0Kkq*;{nYF;mosP1 zoH=vm%$YOgCm#+~!UYx6rn}$zW?F_g>-r|}^?t25$df`mL7;oj{WKV-1FK2q6esh+ zlRIP@en`(Bi5?;z;4d-)dfnWw_f8Bsj1kYfR40$fSCc_kOYj_SMLR>DJ|9UH%|`cP z?{?}*B3||}rl61y3eszL+I;qkyFPjJ^bR>jMhrx4L=aw4frs>^`&l)z$9k?p$L!sxl$hU4e_ zmt!-mcUgV4Xi`JK--wp)Q7h6NWYgpv{OP7t9SaI=1WX%p2y-MWNT(JqpasD|))w_t zADVLs{w%j(zL>+9%7)&hpH!Gg!Nie%p=y6n`+Z%JBUFGRxhIJA>V4M!)k>=mks+fDS%)-9(*Ii@i{-0`v#LLx5P(y^O zIV-~8yHCCShM?3UkKDEB_zmm&=6KPt9VY0ri0>xSZWyR*c4!`NI_r7Lm4%N_IsLmx4aZiDU82GU5YGsgbc#cNtEGfRRr7cbgN4^{S3 zL9b}z6pe&B8p;@HMB?wjS#3oM>9(C1(6cf@d?O{^rUVpvnb@WPHZ1T2doJ%|es4Zv zXFoiGaGbJc|3sq?9Bwn^LXb=Gu~#cgykhn+*V%3>+ec@xNUaT_yUDYS|VV!5t$ z+QiFc${us?@?}jv5}M@A<`*fA)AAE%vOqBMM1vT(HgP^59p%>>avPcVPnV?lt1q_x zi%ZC(;kW1-d5^igEm*$EZrs|lro7oM-(@$hIJ%~McR1;*-4u)-8;o9p4Pz90PlVp^ zK3sKHpP))OV$hXZZ6tM^-?yVL%GiCT1wUTrSFKR@Q_Ag~UhX^SXC3Pga_avzqyDJ> zhx%W#xT}hOdib!+u24NXRyO;Ew14zc%FE!&V($bA8fs7ol$6rx+vf}M+BK*qWU$*0 zeIc}tO4>g#4exe=CteBatPCXPQd5o+Q9qGFU9Z_9;T$(;R5WQ|)5nG-7S)Y>f4`!5q z>%W)3UCKXpd>VfIzn4E(%C9^&UH;m`l~2?6NXm1gVI6azZMSvtifI^A+Er;rB}RsU zB+|9oHe1HDke|s9)B0scdMribR*Qd|rs&5a#pNmgr7*4%(GT6%y!Wd|krM2qtG`C8toZoets*V=t4n&NL7Uy3W1sGeY;XpDzpDHb zQQ!q;a;5px)N%inmhbPXEj@n}pAGmmE;b9-ewBY%e438_5qzSPGw^xoh5wF^(6dVO zuT_RuhR4J=va!fu;(aPibEq)CaTC>WN(g&1mS?vhefiybS9a)JwKB#A&MMuf+5JsE zc#l>-@Z@Y)m8|(0EN_*HD~NVOH1bak?W;kpNUk+IL7=K>2I7k3TaN#jhvsN!|6{cM z7B!4-vp!X{5PDkm$Y3p5f;-KR8EB-cf@|-tkoY|cLkIrp$Zr=)`2>vVmjCtP%6F0n zeW?T^7yXGl!4D%BCS*W82B;ZCf<7J3SG?{oWzV1rpqPu#$H)9v{O3#g zt$otv|I)d9=6>7Tzs=FhCMybYZmRN$3m!c4BG#5B`^+l|h?84a)7a4Ug81+r#zAMKq1ZZf~Q?Z#Bki&>YJ>{E@o~`w7F(&vZkOphzYOpi|m?$_2yTu_bcD~ zRp|XHmS6oh+7%l(vY2vhFm{X`yC^>xyNGxpBm21eBug>xI6^|Zd{wY~eT`nh*9p~X z%DHLMX2*L)?eccJv8gLR*9H53h@-&dmBHvSLcw4ZXwi{ zON56S^f&Zl{ga&r-L~p;P+`CiN~*O*)Lu$$yFVy5C0}&p*<`HFE*N#&^O*iVppav~ zboMt6YphncAr+m%+H5w?(mRCENH|ibs~0^*&KZhUbqr|fU60tA*hi@@d(ohrMO7bd zd-`|z%2d&1=VUV|fH%b()CYSy7x(cNaGHx9cH&TfHSUkn|8})7Lz-vm`|4CW z04vV?wVotc_TqVW%WnzQb|MsW*-fiLRh{lff0@x9JED_RKeS^t_ujO?*pAgL0|fUK zx^;jl_>W1;?A>c-fe1n$=Zc2pE}w>Rt*b*QrJK zIBg^!?_{s*NdHYe()8E#=mEZJYz93(`lb^-l1Kc29%faNt6peet5pT}k~k$8Sii2e zzdN`pB4UERzR4Qz(ifq*K%^AOpLR@6&zFw)4gz0f(K$xBH~hOw7(%$OiI4}7X;&xzMI&V7`(-_rF{#vfovTzi@hZIm{!4d_Ku>p_P*3WFS`$}7 zdb0W@4OiE2RNmJ~GMbVa7EZI;TGR`d6yngLM~a?#uy?{u+Adnf{h58fX#+6c){2R7 z!7b^PO5O5|yE}gdfXTmrgaFa<<*yn-yZxBG_j@%_z@yW}?alJh<)u6}Taf^k^?-p_DSSoidwX4a#hw3}~7S z;Nz5$@ngHJ|IJhzOsUs0ONHE0GKl=EQ-7zF3j~-8DXlJ~lyttB66C2F7y%`Y5tjIu z&+a6o&e&}R>JY7}BF;!WLj~fc`Zk~y(nEKEv~t3Z(YyaLn2jF(K!guuRnWNCOlVhe z=AZsRfa?iCxByiO*E$n|SBw5rdw2Z~d&0-oTy`2RsdlM{yJ8U*L!|qVkG ztowbe6Y8HwY7UTjdJFS6lgb_Sako9g(9+nkG-f!_nJ6u)D@mf40PRKqYrbWJ;JR=y zc8LJg5y(G;w@R-#Tp?{yr|ZgC^xz#`A9_mVuDWs^y*2BJJHG^-n5nMrPWR|hW}Vw3 zEqOBRzovapp8ShZXKIJ+G{|=KCKNSz-Yq+a0Dtt|n9S6p-5I3%jZik) z8`XWY(a{;6qC-S;c59VbpCS>%`vjd znalRq^%yUcG?q~nV(a#~xiGHeBt1ciyU6*+}WdLVx4PDj3F#mi`iiZajb{t#{;^ zSuc=? zq0O~`P`Ts6_0NLzgc&2AS?>Nt^zmi7Zi`f?eHWzLZq`#rZFxkqcZc$w?$gc9Wy=-M z>n_-fbe9{CW4J%3zWIZ*RpazjtlPbE%((5Olo9xHAC1k3xYLZSyElvQck(tE5wXTt zkwuhB6@7!Z3UA~}iDElU*R!n1Msj!!=p#^c-l*Cn_wueLc~tzKv)TV`#cL)wAV}QM z@pzu9q54IL1=LA;wL*+T;*6Q}s~#t}1-xI<17-<9#! zq6>Ji_u$;_*^}4ebEQrNo)JB>mPzkB`V=iNRG{2*qAv>M!_wd)@C~K74kH4Q}G*B@RZV zDvy@L?`~4!-`~rTj6?%5cL+Bkq=YcV0`ExNGc++0Us6og%T&>zfs9)GT1d#SB?6=i zkBui)2OtpmLlCX>C+ItG}w&wP|J5w z5ubnq((328>_Z&&Qy)j14VK;{d;tQeDQqa*B?Gzd`fCfk^nRpNR*uBeGIA387fE$b zm-NJu{YMA>$Di8&Rz9Thhxg~>%75 zjvf7^!`~zPVWZHN*g{ihUFc30F#l3HYPj?EK>Wf~&4iDu%M#VmcNa*r6IyMny4hM3 z$jkC`uq3ggZ-PYnPA>7L(H;Hajuo;|rZ$GoeAA-;4)*`0LroyWkPN5H^%5JQbSg{3xWA z@&zka&9!~C+nXU(;*(r?lrPKEpNmINK#;6Qf7Z9)hz$CfI`2jCNnb2@`LXh+tm?J? z@OQpyet%E&zq!X3YG$ADleB(-=_$Wv!YZboRn6;vO?2@KU&Z@#kI$d^AI5wVkqOQY z#LwRgRhP)fT2D7dR)$XU^x66yH4|2(>niHDA-vp^0!AK-TI2UXH;22GOW>H&HG!{B^whr{ctErZOT;^KChr zmIk38S%Y~sp;}bC=un2ATP5-W&k~-NI3+u)d?WO=7Ii-%^`1DG>JlRQDn6W+kjecl z!oihvX_rw}j?D86S)Yw#YbG-9s^_0{`-326@58wj)}Ce+^pF9Q{^cucqAP6Qhc$cK zhFgnzWzX&9MBV0sj9sy68Y8|-_5C3p>GNFAzRBA}nG`9On2jfbgtOQ%DO=T7LLru! zQhkPsCmc)HW51Bau0@TYtaf%y6KbwEK9UY815FB81-;p-D?4pxc=2 z%6Qlu`Dq;*x1*IIzxoI^(FOGQPzqFJ5I)nc~@xxpMcUs;mocl89h&|U_# zZDD#qJK#!J9=gGFr8_Ej=4z8{}C#H&@~+BU;Xb7`R$I<|2w~xXQ20A z`HdHaHnZ{q@@U4Fpk zDBn%cTWHrr8Nd^@Jyo=j3e@`_{zRLT|B|GrNqWd6WzaXAWW^kI7wO@H1=jDRjpP$j z<9btLys0tQUC+@#W5X^WG4(;TIdMBQmcqkL;WKq%9mg_VK253J zW%9orAo%L1r90-oNd~RHXYvoQq;|~z$mIXiFxxRk3eoKGkxFWcm@Sa`F`eKm4sPqlVmvYVwuuNdDHPSBLd^ zmcDs_c6HA6zN|My-Uda3mKcp^?Kvbjh7e4_2|IN}gm2X=Y#^%Z_h46_S(F@Z+Vjs1 z9naz<2Z#5ZE_U|)>Q9fs-^0u~e#_KNqRJ>q?-^&|Oc%cwb&+YeUtRTJdfJ&Q3+5<9 zd!ak@L?gmFv^vJqQO&vWSvsu5+Wsp{ScwFOFY*L$W5Nw2fq&Ko1d_mr-L3;kthK`% zbCi9E2~hU6k>v5<3Fy}NyOssx*aK0u9wfUZ_k^sfxwnz!s1e91xIWDfs6&U3uQB0P zfdBA*nX`Y^FY656dkEu1PH3oDvP#Xy<)Z_KK*NdobF64otBAiZR|87z_!V$R6CoRj zPH6CHUSMi1Vsu<(O%}j3er7*7n2U90`II<0Y8q1lUMliTAUEp-t9~g%?j_#v%^~;n zs9i;9L81-wDAP`_#OX*NghSBXh^gx+B^$CPwUA%#&P$e(fR}TZ{smhF8d~$3D;PxQp9wCJjtIf`CTOc?Zf6@FZmKeFY)wY^Q$GFr7bHl^RW4+O1^k4C9Xegeox7l zqlmOc*YBFx5e(?fpx|FDE<7Tv60LK+^D}uvZX(=S;FS&pQDM=hsZgofY)0NNv&mY0VL>(>*&A+g9uO`9hr6B|VvR@+Bp? zN&3TMU%a{qC{+$trquHEe9^HHldpRGV(LS_XC3)woa9imwn5)$4mopLb7Zto;_sqk ztvNFE8ll83-Zz=o@^SM5CtDBS1v*6hgHLh{e= zu6v+EEj#oOTuiMbC0cVdeBItg`95Fj8|oxhk>J+=1R&=NbV&Cv@e02+JZq`n2SW%y z1X|_r6j>w`eCaAE_|jW`x|1x;A><@YQcm*1u4uiEuy-UStbF6FlU{`i<14F;Sv^!&1_+UM*x~1B0ArgWIN>|8ERs=CA@JG>aR~5 z^bTQ{8mT*w6Y7p8%A;sjeaO%S;SspnnVjkjh$H!lE6Mcu3F=Yme6Lh@rxYkB#!#@s zdK}(9)-@xGdR~XxERQ%>$^LZ+7ocnzR{jR%ue`((SvNcECDaZZ zi?g`Gd<1PLi`@0U#)nS>(4>!7$&I$+(NRnt%B=P3*Nlu4@cJf-orRK;gVBTb2jgW~ z))cHbFy&3u0WFV2wXdv)L%7Q{I0JC)AI)83bv>fr70Y8g8iHJWAkHPQmIrTp1lxOk z`MTuW4*z$&|Mn>Tbx9;@%C!&3js-i6vtY1|!cL{mC)rt5gk8}-YS0E)R41tz+6Je8 zTsB+F%(3HRQ})ntC6jb~@4aKe#HpZu0VYDKiSiCm9Z*^$C3E?s#~@NN&-tl`;YAIK z=fDXpBN#w@U6CQPX=pTy`oZ6{5-CLjIYs+gOqWJhDGE#C=kc}@q* zrjG1E>LlB@iW4i*3+6lnD$sALfbgx>6!0_r2-N*5cMb#jeg3p_xtBU2P@fg_DUqOA z?5$3e$zS;mohR16^qqZ?6~?_Ji^-?Aq5IWxmSJt+0O4$ZbUUu)4DxuXAtaGb*H_CV zh+nFktED_z-mGOZKpjR}-UAw66YoW<`(fQ;@^PCtq*QBmNDp<;k`@6xNxD|nhC8n{ zCl7>V>no>6`-8p%w(n~-h-UmAO8O)%D0n&W)sY?ZZ7>WV5MVxFzR^(O62;pL1cW5F{gK)YK-XooLv6soCx=Rma_ui!L=vZ^L$iM}RPs0_m z!`!dOM|Ho?8PkLaL7l&n{7m*8t3ID4J4ejDiCZWHhEMc#|2Q*>d8SsNDW4)MB{#1R@N%SMBgol$1Pk-7*^a|yRhZq*V= zX37D~D{gl36+dr@g%1*9TS}vvJQ?(DNo`5%6ME*#Yt6eqZNrG2?QpPuD@^Mv z8SP(8`wk_WMOux8*w%f|3;P)dum;xCWS<}72#1V9V~Na!R2nO&hW{vZ>_nx>OFZ`2 z$ezf}PCmE5km7$CdY2SGMuiR5Y~B!%<-AzP<4{BMz)!!Ri>K@PY2aPa8D6na7187@ zuTa&v>r|}2XDUy@+HOboHWrf`=&8mst08K2nuDyY?qpV7{P2!VM6CdjfT~T>WEzjRg_gz;LugbRL zr=0<|7#KDr^C4#4&})foQgK2kz>%39AK|P0g5@*Y9@>T1FE@~jZATxQiARb)pK#(= z*hBkpz4q3Z62UtgAwc+ry}ZY<%qkSc6mnp}Z=bN2hzB7aFm6BZJ- z`}drRr`VM>WxH6;Yr^}!4-s$cf<;)#9M>=1Og9lnySsni<&+GA}IxV<{b+~Q4qY;2W*5yOqV93l27s&Np2uH`KB~{ zYEPYxElOQL^!SIvy}=3Ti9Fq-*XkYAU0WO-cUp5K&;O<PmNvqIu494(mhzqB1sp6yekIpU21oaanjBVEGb68_whbK2XU#o%V^%CkSb6x+$0lH`9& z#z;#$zh?8q6|iT=^Q2Dtn(W@KvR6x<;jdP%g7N1|>VP8q$4OhT;i6|%X^`yJSPe?l zDQKX2i_MWwi;is~i@|@8Dwv|J@KaQ_sWe`6Fs-KDAiqkJ=jrv<<%{roYVEK>P)Q9ZkrkJDK41~E&>0|9&n91=5sK|F#35A8C159?PZ=dXShs2 z!YM?Xj6KDg^C?Y{a=tH}l%y3&%C3oDkzF~+iioO2`I_kam|&)fZUv}D)Yo!AS z*~rzLg?dFLkFPJ$eA9=Dp4R`Y}ZHt)y|Du61S; zmCtAf+J~X;z#yw$tQ%PPGkYNyfJMQeSrNIGu-s&Ss0z?W`Dta<-+}P;jU<-8o?-tGs~Dz zPZpRiql~JMP*Z>|QVdg1WR}!J3t~w2$!W#G&p`kr=$i!I8Tc{Vo(b3% z3@sA7$kL5k^#|ccgUQb~Ys0@48N_&4^L>C6m8(9*4?$Qf&^h>BY1ygHTs zNdGw1!2gAvn=tq2ioI6kXaEQUSDs`oY^)rI`)B@It9~A=Q+b3HiSXB2v@g0pRW$Di zdC04F;SX_snnH)uw*h-3b2<62)|V4%_9ygQZfrpC1kGxK3$^2)W%60#VaC{yG%(Yk zJ(yD1meq|N3*MsDxqv$Wy(ulOQ^MMRtPu&`Dm#{^Kh-SijBNjoAbzU($(;}njgVUV z)ooNU7@RRfD)U+`Y$0LwHmJgV9gzJD)mZP3q}2$m$|QT~4w!i4K{1H>tT_@NlOR{+ z4U)A>kYo_<)P^TB!WdSc28?)jjhSH0o&ru%9G66ah*$A_lcR- zn@mSx9(Ri*MrP*h!eYYS7yYTR^4G^4yb5tuR_)*QUJNrsTN)*@6*N4wC`gdYQjW^|5F!c9Ls z)uhHWsnOWsTyvB;*IX;-n&A&~>{AgTyKn6!-VXJmWJ2A@)*4NckC&Z_LuXb$HK$454 zdt@a);l~H^gTw8j^K>cvKRCxvT*B-S*`gPRGwFL`6aUHZk73>eT3-Cb$!sBV)dW4) z+{G%3us!ik2Rm7G|Na3b#IVaEC5MWC-^gi)vzb=4I}XW7Wq&(hH90FA-t2^zc9i!2sewLwocJ4G>H z#K-0Mj5SY>r)>qX0dJnxYR4}dmO>4nrq%?y|Dg8UFgg~&-+%;#fdaMmw%ZkWX^lRh zeJKtTzrT~y-x~D|YOP~YclwPRfI3&Ytou*Dh1sHb2Z_I#*WKf#L`CyG-DMih)$xC$ z&E=w+PpgqFZ<(@(A!}yJiC^9<7^e(>)?Mga(F3ZAS%R|PpQl@WpL+YV%le5~OSDau zf=Mbi&rIsbgYG>KNLe9F3MtRKO?2*#xre7^;#ZJI^hK>iTaijWYvQ-{fm$2vy&IT1 zW=O1{N)(gl?O%VuAH~8SEmLK}!z>VIodx1>szMf!A*xM(6Zitz-&Jz%|Ujck|ekv`})2|MW+5IA7xX!lGSnApceLb9V+ z0H$NA7$dv(mLoAdRSj7ojkQ)4@+V6jh2-eScR~-W8c8HUO7Wpn z;QXBkK}w=GP1F_Yz|aUSeYrTB6CoKifo8~jsukHGh|d2r1uJ=N+JdMMzkV;Nn&dL@ zcSPD4$QArZ;1vx@=%1B0TlGXWO4Z$)jT`$-*&(Q@xv86A_|z}OjI~93u+answqqhK$V$gMTuWj=8hG3%jI#`x5$3jwNW{WIwu z&dy>StldaE14t10;Pht7Fwr*;u+K%-5)d*J=@-`Z^f>G%?93U&C>gFL|<;Z|E!H@9&JJ@Y6iqL<>x^&v3Dl_b@b7w*W+VbMi_k zx}FdGcUnhko=+^(4RFJchlrSpTV*PC<5i{a2!MGj!;%;wV-ww(ctU^}p4u_g9fj(f z+);Q6?49h1=fZNQl9PB%I=3<*fiyLjrpKp)e4e#^x)#J3g1EEdcOEfP3SB~0w;@{i zvmcQFck4;aSlvh!`m?$r57EEO>h=HwD65+sI}f(zT+b&Cg?0>|1*^W1S;oWeL0__} zE_EF{Z*qeO*c_S>w|p2HcuXF zhj;kwvSjEYVOiXv&NW6i44tgx!T3~>F|uX-=HFV*>X#LawJ8|gYnL^LdUJ&(pdB{V z2FlPe=wI6)s65KTY*zMJ%Va=17|B9J_;eTe60^Wx4R^TmK;s3^OAOv232_f8-=AxTഝVR~(wME+LkBttrYts6(H_jc_Cb zL6o(N_z^7oO2#05rB4-ux19TlX#%B26-qzAKPz#j)QIj)u3LcPTMb<7%|ETDCq!J4 zg#7Kq&kB2+r^-*aqw`7zbNfy8{x$G$6p#;Hv; zm4TdY-dlA5M4REralC)#QztMfq^NBz>PnpAkENmDLAIc;homzQ2glDtHv-FdsFvFtOquL zA|=o0E-fq(FAZ^Pd4a#y{96Sk9TsCAb8IYsa01v-&e4Q60h>c;`52#gggkkSM~5`l zSPO^!nCt(NYX4`f?zA4o__LoH&_3<51a`FnZdI@iGoxGCf8@RWI=iU`gVA zqMX1Yw@h(%Zv}Dy8D*#Qy+exz0YCIMs2dmk&lq^3yIUe9ukxu6++m$vYA#oDbeNJ> zj>4Gm!x%EgXSpvJdtQ@3?6vhj5uOqQVEH;Cxg#soGyie+ZPLXlFkfQ0l&N@C({E}( z#-UAyh-3N{{I$Y=l`k>B=oz!n- zyQXZXI#MJjG&yi9tHRP9X0&kRcyDO#UL$hoN#g^R7;_e~rW;K2%;g*oy3@khQ2S{DmA!S+hsU7{rT?9wPVK4A8CWau7@2=lJ(^9Fupc z?i!>R7HUm2<=>vorN|=zn0N){LCZ%E)X|xs zhIsN5*4HyrO<6%_qFb55f8rMd(8Lym1Z27TU>t~D3rO)lHe5r2;F(4nnW$bPUlStH z3SdM_THkQFTH~QMk*~Y&iMTK#j_IUWfQ-;fbP+wtyWe5Xzl)tPwQJNnNDsXcjt#cs z)BA{hW$2{hX@lMMOw`1d5tT?5h`wL_dKb)uNa$h@9Z^`@OV8ViDaBJZ=(}ab%qz5p z22r^k=ttn8mP3(^TSL)KxfVY4MmsMGmG5Ie@C zcJ*?bEDfP?+AX(T)iKs`S3s5-;CRc735Kk8K^Aj8;I-v~aF@_8%;#LF!p~Xpq^-S*FP41|gHEuDnjTwt=E%F8p-mA+v3Rhjcur_pf3Cut+n%Jp&@Q9QE@N zBIVQlHTOXP=3u>AJ$|D%p|K*gs1KX;n2mr*ya3{}5#mA!770!5seEfx1p8kxA2l>c zQ%FQDZEV26Sx4b?FoiH+H6QaH6$9d^>M}F}<6ClFI;ycSg-4qU`1mguGDIKtFu91b zAaOw^s@>r0%! zZwyh5>J|Awxo0L_4#&cp(q&gyfJ4xa?TWp2^y^x7nNOBfp$E!A$i!C`ytrm~T#aennA4gFI4|I0FR(>o2RsADLk?vl}QQOM$247@;~R!MQAkTsA=NXihI+rllr|x1s)RUq zkPVwli6gI>pw0JbFhADQIiw1xdrYOlT zHJ<(mUp@oB>h{q9#d#R&w2|*pM?DI&dNL)(q;`b>WP9^1$h60*AFF3m(I2QMBUHWI z0g34>sJ3hpahwQHFRJKR3Z6cB$nEZ`q%ol{sVkYd~o8 zWnV!AaSJu`mYoh$mnc$~XRF(!A{06c$Py1FPwGT>r;090m{=4tKZi-G6%qDXq94$p z!gI?s##ZD-QiWd}Mu0Y|6=7H-Ey`rtJXpHer0zpua0%gmT}rF-AcmWy{_v(=l&EBG z(mjNL^fK$o6b_aVas>cS8=AHzWYULljs@^|IKlC@tpicvzjBRGyV`mQ zI(OXTo^>+8McyS=0K?U{oC7_$i1<4!4?4CYwhTF>;C!-dpE`x@f12u3n0j)2?b2EZ zow!5;)a`rCX?Y^eCD&x0UpfAedbQ?#!S{^>o66#$licS&TGa%SY;h?!gr;A2o-XsM zK^4$1W}LoW=*H9KUBJ9K zrGj4QpCw7mDqvtvG4bEzy;!VLrjtm6p6+TWZ>t!2O_3F_N2lfU>^B(f%;i&G7Y<~( z{;aGAP~$yWxACsL+<>lR}i`044JSP z<~)gV0(23BU@T4NI4RFdir}6oC8d)-*Wsm>RacvlmeBx~Q0_I7Tp`q;eg!ts77Yw) z4wLrwH2h#h1)uQu%mj7}dB@8hAYN1`f2`^@HJhPvUpm!t-`fv4=aHBUdu(TKuE$*a z1;_JN@~tX46IpVaMy*j$8`l7qvKF+4a696!7L{xjn?Z>t|D=H+JR8)rdd9Fm=&$G4 z$w@3*UK}(p`8fJSd>Za@MU$!KtWmdQR&xs{Zlt=bm+twg;ZMEAkOw4w3;W1)f!2CJ z{)wd1Xtsn-gC5_V>VO9uj{K#wqzEOC$BBbR%)z<7|53C{7sfc%t_EF*$a*_dUi@&q zJ_21Herq=XMe-ur-Nvi56wwnk`!aV>Q5gEDuix&xSl2;ieIJ&HcQ>(V)$=!VwU+)n z``5H+SgoS82Lji62#EZUFhdRjD#h1d24SCm55f!?IP_A#$-qJOs3PB;-Rgf17<#+y z>uk?|yg0M{0@~MpFs#+WA{CtZ(XbMn)u1(~B{vC4q?bEoLB5YiFO-k{wY zx?J;vCf$_`KGN+N9Sd0t5_uCzP)bySr%6%I0^L=A9CWL=b?cOL~ru#2_FnEv5hUz1F>55M0+w~SMS@^ zF2t~A2WcxpS@kY$0cAN*Jh{>hQRRIsMZStd(jq{*!I|3CSXMyu#)AR;XeQup6jkpV zjoev!+SOD_nCFgOTI5O&X9sWrI6!HWyGG=s*dlqs?TDJ_cQvtlzRgN52gij!)njBN z<1dk+%%MKgd#!WK({w0$uavfr)l}#a_A;AH=3A0^NKC7Q&@!3oOBetM8Y+$ag=BSv zBcMfalXfc&tCkyNl)ZA&oZ7~emo%r`-BRwUj^%EZa!V*@cKB{-^@zwS2b_AG6JIk- zB!S!gQt7uyU~d$r|HEMkSV=EWI*PW5c;(DT>YV}iXw4a<&&QmD6Y%w#vDYMfq~*UI zJK7CgFFq-S>KafsN^?(|5p&aM=BC{3&^+wLUtIrp;T{Pa37Sl3$xqzgUmnTGv==z~ z4Et0RcpjspZ~$>|`QP+z;SM}I^$u^BY9>Wu_YBlYdTA^^Q3u1Eb~hJ>C9Wn)Y@i}>!Ve&3iZ`g*rj|dwkD?~O%IN%0NcKIojQsR zQaax$y&mnslzuc*{yOybY)j)<_GH|UJAb2PgP}Nhe$5ocW6E*~>DV_!e_g@8BD~KR zZJpNNTQHVEe2HIiq5yMbOBU%u#s_(2_5umFYT}lW{#g(UpjcQtyWixhRJgDF44&O>*z!+YBMD3K>ROX z2DH|g%ns0fKalpHLt?pQh0Xr~ebj|QWT(7F#o3QKEv!+8UIS~r^$Q&#u1y9k9DWb8 z*(tS4eK1Dwb8}fG(&MZ7*D1JKT_5nVFr@k@(@Ww##7^ zY+u0%{d&$Bh!D{QxyXW0#3-7}HXNR@Takj-U}*`I6dTESZVI{Aq%|MzIDITMj6ot6 zC;fJ7W-W(54E;r2-z`g#a29i@MPcnu;cyf_;G4#Jx=X#vP=LWDrkxhJ&?PQ*k6r4f zzoy4nD_pL=3gv$V={TlGaK>_*_#PsnRTDO`wAxkX!SoJ*x`QJyv_9a;gG)X1)CT!U z&HZz%0wCTC*qWKc;Y~`tj`G1p*OuvL08Wpc`t)@O zx!dicOxLfe!_l+D5uQE8&|?wBO<_-nHg}?kLvPO>J_RbcOWbNzd!1+P#srn6{>hy2 z*3OAPK|+6xI+f8sFa97N9(}7L97V1OMBA%SL=r365-_u-GGvUM9 z_v7842p`PzTTeIn!=KFbum7aFVD)snnj0S@csKD?x)T_B{9}`UX8Se}tFN}VTuI9b zM32QA$|tkljE6rS%VLKxYk!KPc z&>#cTP#5}AO02(k6TYS;C4TXb>wbdzneZmltK4?t1yq9{ptWoB&K8C<_Mzdl=a> z>EBej|B(f3WAUE?bQ;ShK?i#DR*e@yFWsJ|-z@WDHx{)Xe|DCh&;@e(QH-p3L(5;R z!VjL$$faiV6$`9>1Q&xaW5xL&tj@!YpUdy>kPwFL-S@iLE$dE+*g2OP$t{`drFUPl zLr~6l)n!2>joEVQ!7E`r@fa#8b>_Ip{N>i%J&clLu)yDpGG;NDejzdSW?ZR*>)}9C zIx~o)O6_4kokUTcSq=%%xYHTZxkkZ`mb<#5SOn|y&T7v+7+wsJ;jqpVh7Kx7Wz_(T9GS+ zwiT=I*{togqO<5>FdB{6Do3-5_;lQyQSosn69_>n(7(duzcS&~o}}%Z+r@EIJL??M zVs*c|)P zfkXJvea>K-fwAU_{jes(hg?n=WL#B~Td2jFdxaJZ6H|`&5%zv00lgzyzBRvH=!$;F z&0bd-23_yqNR`LR;?{w== z6mq+SgK^zoq=nn@`d{Yp1HAPhc3RE)((+488Pi$%_kg%+Q)0f95bl!__fsO#m5g$v zs=oN~dWM#z1(3$p$P+l(Ze334R87S)>%J!#-kSKpvl0*S!|21~PVgnJ;fs>6dc~Hh zn_ZNhifYA6h-Ddz1QV{4exI=)&EO9SZF3)f2^stWU*iZY$3>;871q)G!HmXv2uq7{ zCzp;Kr~cP1&}_}c5@U!cGh-O2z}9~8(-#*}y8?LDybokpE0Wf{x8w(!AI@&tE#G<| zMhZb=ju$DmA6ovJM-$7wsELmom=Yg{C#yh(Ac(WWLf-g|B_-8>=Oy9t9k9`kWG_d` zGiB5mVM?xo5^B1g+r8qw+HarJljhp@k-pcRXr|KaN;Xl;Sv;I&#$lg;Fkey9DRjf( z3YG8ZR11_}qJm2w6{A+7ka0+lZ$w zJ=Wu7(e7hC!v|8)wonCdise=?w$tO+5@(BJlePOqzNC&uYUgt^yY^eO&>4al*kDn- zRErY!QOWFR8*099RJgJc0oPQ!Gx@i4d$GL7AD4ruP#3e`Xw93=w+wdzKdV&wys*lS z^*dd#e*KI;Y(DL=Q~6cFW3P-7WwUtgVJ{Yle*L6Gcv+P>LlXNUCWqow(Vc6gH=L%V ziq7RHvMyCLj33eEsiK=Ck79%6*_gTVaVD3H16&}`yY&>2LhbT>(XZ_CR!%j`_u2gk z0JR~+-6&olTE_rBx`YzZqG#n#M%(5*WXU_%7O%Vc-_YgtGabZ}tJD3z9D+wbWz;5N zKY@ZzKIY??_b9<4)^84^;1q8I6I^(qC`XdTHLcN#JV&o_KHtItsU5p~gS>jfVG}7V zgu;aJyo&)P1R5d)(!81~TKO*loHq!-QseBuP^L96=P6%SL2gzRMF;ln6m-|Z5(=&rV2Sav}4ump!41m zT;kQRUfw1AOiY)tm4Cvo;R89LW7w@6?N0MjF`gRzSl!*t4b7vu+~v=_DJa6>A=#lW ztyv<-!UwWLN3&ZFRe3-p?kMBm@6HX<_Suu=PsOItW;>Q6^CFvh5pITcH`VUQtfLBO zF}c{;FP1mBGs_4Z_IW@}kDq*-9P?j|-w^zi#TS$#Eu!>@5e!hF_sJ_+LK` zq?ZV(gK>L2wEW~)DE6}#gkqv%1t=tzGcvK3s<-|ZA=*Ihf=>I3jP;oY*Tj0Ho}`;m zd2E~5^Yg!zp^xQ#Nh0pD;RfbESCrnL+$)G2sOJAc0#jAIZ{7EIp&`M;!wr2ZR@Y*M zz<$=9y5^ZW{l2t4VI#Bu8oTk6oSNvz!JN|`1!_&Sy3pTRO>Gc7u?Ib4LGA8;UHlbn zUmMvpnRmM&rDbF+*e5?#bk}OW8G{gNUm!lFA-wJF&_Idnx8-1{#Lnq=F6I5NR|yQj z>3~|rIIyIODyT@k(r{jHiip#>Ip<^EjWu)Le{EtS#9|#A0*?{Ld6hKXz}np<7J&Z@_Oj*koS ztLs0VJ01vP%YxX`0g%1>Sd5O>Pfhw#15`NgR z3h^5)YpwC^;|4%5TK$TN?5Rd^c;PfT`F|J0@-7rvv=JQV{~dxumTOn)_Fn>(QT$9E zEe8bQ<~~EuKAECcJSM}0)XT{`hVNJ*lH+4LX6HLh7d?Pm2+U*)dB!gykmYfEitI;II%-X0lW}1rzrRgq(h# zJ}O=8v8XqHVj~#F5&5w(`RaU%kenh}ox|_6L=*WO^9^I8+%RpzusDCF+&v7X2=exz zPu84`G%*ZJ>#WFk@-t8J>Lu=H`MSlhf0p{wAB4x`eNmiYWZGMie17Q6lyTT8_3Tr_ z7HZh9Q*qjeVGrq#=LPP={H&b`Os$b!?!aUX4u!CEY5Io#sWVd;vqT92W>wZO?)f!@ zZW@N=t)aGx>Y60IJ(T7GTO`q)Iz!M&;K!2o*BaJO>bDN#-fKhX1aF-%CJ=+m%4g~1 zzYDa@SekWl{2S@gZ*asP2%t;owbA<2Z^@%6AE_pQuT(Vo8o^qVzm-ulshX14n^cM1 z=&q06?!8z&i$xD3N-VGV5fPHLJRi6tRLrnGS|oC&3#o#DdW<2GF86YKBL_dmU8)M^ z=(nGOi$#930cAJyJ6I*U`D^>noH%~T++AEu7Ap)XHD%lRksQ?0l}mhjl6DV7f0-K_ z(>;3WH#sPqoc@Gcxtpn?Bfg7X%E$M`A&k7cR4uy#aHw65!-f^%1_jn$qc0(UX&=F! zD#~4srf{VYPMS>qN5)g{H_%IC*(|To##vj*E}pf2#E6l_R%A29G^<*3BJxw%)e6H= zSq{st;ODgS9Ck%jH0)Y?p3AD=KMh)%Rq=1sL@IIE^mW=1)s`KIW(n?LN_EW^wX7)5 z{}wVq)Q?cqbTM1PnO0>v{49m1jn(`dI^Xc}Dwj{9UZWc*B|u&_eB74Ns5{u<3~v3LTeG@+;(*7H znoZYu8N#X3-wba3o0ln}9y7o_N``t44$w<Gihk21qN?A2gbnLA zhR)>ilagjh8dguvf>^iIdl*agqYy`m_fG+G4aVCgyPj(bsZ#q9u$D`8=9qB0of>dI zRn7j|Z7-I0KSj#-a1McU;0LwcQ1zYi%}e?)E{pZt86YGw=TpO?DA_%3)6-)*I!#NX zV3jvi65>5B)9L+A(|<-3rizZC$qblFp>QhsxH+$j<@KN(O|P!CwCvIcD0Q?dpbE%K z75&nH|H$F9V`GIC>ub9uFNpQ5_%Kw48p;w#cURVaNtO30l`49kDrs;sBY+pXm@Va) zLiP3UHSm{o$-5|-{I?w&LmoaAMN>&le&o$JQ57P{;;hMBamfxHhr(WaTr6)arQp_{ zN}{nOkI?g1t%fnuf~7f@cZM!y)!#>b_Mv<+!5EZSk3xQfF)CE-szUV(K}cR7Y_I)_ zE*veHt$Avp(4Bi&0EGVDV@6-j_e^S9YxEn?9l?-8}0Yu<2$D0{XmOPA7-Z8z_*lCk&@A zZPsgJz-3$^xKwo^{S>`z&#mK9MgqUIMo>;SZ!$ygVJ7BigKnpqow;we2jiOJF5nsm!haTye|MYCF zDmD0MdIYT30)<`N0&*Up`}054|Iz*^Sj6kk4|`JL(KFCgx@Y)Li$evtP=wx+P>X^( zv9D`&*hhnUCw|fG=M8Y%A7aMSEsDP1s3z+6d0ni{>%QkdXYkMaw6{!c#)JBHbQR+v zgMocW>X&->S8;Z{HdxkJ<7@CUAYJ%RRWCHfcp$4!s9dhC$9p}*h_scu>x=NJ9*-u* zgB84D?c~A?Ue(RSs!H~@HL4SxHUn9i^FjBoUp4q~jtGK7M_=t1h6y5}48ydmIq0;8 zz!y{oeeJPX3*bo-Hd?P;4C`Lj)15}EylHgVDo~`QKx`EFt$^*oZxr~gz&5J+WnJwj zwCFbD_9y7uP2ytbZXtRK&!NZnPU!F?&zKl{9W4qxojrx}Aelc~;!@R}rmK>V>Y5$?0LrBYv|8b9e(uwH|Yeu&M#idK@3!hie@bV{COJ1}?7 zOicC!uOaUOFqqLGi%#SIvry(xVP_>$7)QhV&$J$3v6a=}&)O$ItqAwzMQbU%CT$O+#L+Wv zkuCk)Msm^0EFLFpT(4y3pIhG+dRP4SK4Xth?9O%IvVw48Yo{z`YSgd0v%4P?UZGU* zuiqc=t=ij$EmK$lCd;QH|LxRXeC`hRgKscwsrAV~{c0=1tv~G}cweVBtnkI;V2qe6 zd#b(bYv4V9{|`qO5Ic5UN9aN#)6N1KEgE7V@y_qqHo?}KEyrKHHG33c(i*kS>CIo^ zzSPV>!73}V4KeSI&(60qy5KnY*MSD_7w+p7>bqDN*RNI3GP=I4+PBII<42nc@WPdT z=fYt}Ssx}!6x=a-%;cmFll1_ZOVYBH)s1-5(X|DQq4U|jOg7xhq{8#-P9gm7+1b*C z#=EjZy)}d(A7g-RKk)Imk|E?1*H8kr5?8kr@vv-Sv-H2m7^$S^fr0dqQ|7CcUOBJ- z!vvMkwHd|loMReNKDN4mqdl#w5{P#rpJyNF%I+_#KeS*s_c0#;ZKDboP}GH7m*;a$ zN2G9FIyR$$=}!xiZ%{LF9wc*Gdzfuir`EhyrcqWGE0(sDV)Jc^y6wP}4go~Q<@3x= z-svEnDqbLpY+2jnGnj?bkERO^yKU=~jQPrDI=qEIyk9lgYrn7ynk2hPPdQmD%T@@_ zIF*IM+S?t#^KJJnM{AG2*pp$g1i7@DGGFHuTNytMhH0F4 z0(Gr9iVPs-)z>pb2PW4ePBuZ zckPY*|F)7`#x1|SapP$RS=8E_HmuyVt$o>sPd2UGMA`?WZFpx>WBbl_wW*O#9s0PP z|Cuo%z6eOS>Zv1PoyP-dx@RvF>kAQv@fvGu)=9b}J>h8} zzAqzyNH~@(RgHN~uixqos!6VXomr}8dsQjCBgY##pr|9Mmbea~;K?U-w_9I!V$y^+ z>VMO9OTX8ID^}~^5q+UW*}BSCDcz*OGQs(=-l0L)!_SYE?fahCyydC8_Gv;LW~LYx zqQ0$1e=sxUEXQCpvm2*kX^055wa%10k{Z;bmG-;()*fS^t6>i~ zqO#b~fw8vTsjoZq|2Y3I$?Q~miLvG~<@CHgoKTo}v79rB9Ox(-pe0g4JvLvn8`1zh zO(w?KR<_Gsu*d~bWK3>k%GeBko!$)?dF!c`{BVI9>OmI<66o?is>ei6 zc7z^xpeLVk^w&(d{}8wmW+{1}Cr{`@siw7-vSaxCk!kBp*|8XxMG}XO02n7l)ICbq z@53k`A2kCjR!CF>b_CH?1w4q@#Ouv)SD>INyxV6+U|~a~DRdM%+L7>+C_l1W3rGjY z8Kuklmi5SZAWPVEdPn&5H%p%3uUL8wPP3lD8r z^|PTRRp*t|0f?#*{s?JjV=w7kU5Z3o3f?DJA-_;G>qU?Dzb=rZ&Lz}=Yu`u3nThrb zWGm7_34T|}@B2v+4K`0c2)ia3@>|SxT1=sFio#^gZG(U4K%+ip2R$aG3Zeze(llLN zf_g`LB+8g-m06U_qzxo+9qE|UF=ras8$3I@XvD)sC5(zuWJFs=1nIcuvV%xV;3CN3j2Z67hj3mww|pE;UvgWjKdeI{w5 zpGr;r{6gv8#1=l-uQ($SI=cgaJu(!XD8Ejeh{>nr?KXLlp;429 zrRghH-~-O+J(aF$4Qkai7-HSO4?0=(yA8F7Mn!tR%f1&325p$kd>E8>AzxN_DBZ$v zRWcs%Y-cYUa30A(zEM;cv}RdrQNeV>RhY1JqeD<;#;Bsv?ob%h7J_TM!AUA@TB!>a z26Z9y9?A9yqPt2MG^ROAMIi^!R`d^;P&8XgPt|K&uB4OdZMH(7+bVBc)KNm7N4WJ6 zV>U&9nE70IcCjRJyIJV(wIU2Z2|l8JkBcZEFrH=@bcan(=yFkzEZte%OX;q3K+u;X z=|QFkFyepdW*KUxbTHkY#CpaB7H0-ab3Yiq4-Ki*W>`Q|1IBfSPw>22*d=2UMjtnw zFS?y0zune#yAzXX;QtPP=HcLHhfc_VJ^ckJzFP=;6tEM=YQ#H=h_Zb$8Xu~14;q_V zP1sfqNL~O~VLj1cRX5fkX%OId=0^}?p8|^;O6EG$-gS*%oG87t9&hwp)hl@<{wjiN z+(7rwo_mm%O~6HAKjyKFf!@Ro)(kQ@aUYzZ`wNJo>68cSCfRU|PO|XJ_Igor+v_<<_RzdIp?5gE~fjC5D25w>mD6^N3~t-}e_IL5ra~-2Ng6 zre=Tf2h?0CB>Rir9V!SF26{b03LnD~h-EcVYX12C;(K>Hab(A>#Nl=ugeA+| zZ7f(|{E>naYOq`S@pM|vqei3R-RCBIJM%5vklR`%C*wFDHBK##72;7BO2~bWLSt)} zt2SwQ9~e5K`@P5c<))CtOB~T_cxxbr@tx}E3>4*bD{lK#?j)JZ#2b!t>O%;N2_t$1 zXUVeq&ED=3{gQ5g~oUiQA zjnOtNuAGq0(=h;f*#{St$p27b&xk(RwbkIMHry?GhQwQIqN=n?s{idm>C2N7Nujw! zF;$SKM4R8AM@-~cFxJOCfT`OC& zp5t2$Vf>8xFFkJ^(Tl{aQ{oh2;Hf>59GoJKE*9tU$Sz~p@ zzwW))ZN~vbTdi4R01<7AR%7ab%ZRtTRc*U7jgooZCs3e!@YwCxC$+)Dc3rIW)8`I8 zCb*-svglgHV3Q5W7^ z*pT2>Bnwab-VfF6J2XdW#=MWp>G|snJ49AnZ*o}oCJjC!;DssuZ;$7PYC`ZjQ!6n= zV3_rDf}DULN#flXB#)BwDlI=~8Q1j2sfF6z=?hM7qJ>(&HM@v(``Z2#zxqVvRO7+C zp;zV4{6%yvQzpFa#Lx($O7sag`l5C(W>5`Ms-|Mal%g7%@FSz74H71Q#r~|@;$mL`! z#%2qDd1@?DveW#SS0XwvDGN&q&5tKbWag9de92(*S~gU2YQL~V z7$-;|z6SkwUdamns#!C{+pbu6{Ih#y1im03>GQNF|D306qyn#mvAKGML_11sQ~q)e zv+rv@DZheG`DZOUB9>LR|1?BEz?y?UE-8(?vsuRh1Mh4ua6osVs0+mwVMsxcFmOU= zy1($wMWdsrBMNLHqDoOa+7;dkawL`?o~Ln@TiV3BXZE*|50?d0`BbS$Cjp?oxrJ2I zE!hca`m!_HLLRS*x%4e{>09d3x74L?sY~Bdm%e}z`m&*tQR^`E)BNW3AbuC?$eWsM z)}m3q2vUA0TwPthv2H&>GT(4}2`Fpn%2-t)!z5E8?r-BIDQnJhq#VH!2(Pk-GbitS zCEjaMJalne4Qz{hYLI}q4OAi~T0#8cHi@@6M~q#(csk>~@Ihu7SH*KP2#_a7qJtj3py!%|0h%ej0r70C78J-VxUEKF3;-zw{(zItLo3o7cy`QiCSKv`ZY;f=M@t9>-*!}1R^0*O zz<+*(?)Z2CD6#W_2BEeVLZ}0Sd2|Gk+dvkt4M0(YJQO8(cY}`tp=2OF>RqhXybQ`i zvcGqLq-))U-z_Kem+?TfJK{?(@llC}_7_Y$b98)EwtF5nk^yl?ZFKZq8OQVE0Z_7* zUd-vsB~&?3Ftt5+Dm$E-a4O$=;L4++!mNEKUdRAFFp7@=e*5FAIkMW@UR|H6My*eF zhnPm5f+qLEta^FwDNxW(fGGJ9Bj_&oe60FyARj((VW^Kae~te6D_$jpOyL8!TlLFH zk+^aV`ft$@$RJrBqLXELFh*cKF@fkzYX*!VcvH(2mmWA#BMioiTF|xboKhyrC9g45SCzd-vEQ)53xD=OsO6_d<2rHaNUxU$im2BAeBo*A7`F z?`6cKM6x7&pa#}aTp$Q| zf(DHB!dO9wTpFwwBB&Xe;EYZX1r;p{AtaC*5+pN#79ltj;dC5`t!?f5`dWK=``T*X z*0&b%g60B*ix)0lK)fK{<}h4>T5bmN|E+!YnG950`~N>*zwiG%U!I)V=j{7ld+oK? zUVH7e+lm!u;Z(7$IG=1?sGrXSq~>Kt!w-A2!#EzluJImOSFFoVJa~`8ra0v%A8b@m z0bnHs30(_5=#Ys6f+grC-q;JY@ke%LnwxzEn^7iWrmYU0>E*5vl#0{`8}hb=d}3BN z>@YuD1>Oofk4(+Sb&egmUSr%IweJ%-)&mybrr8Szqfs>OAOz(mE_2+!ULvf6clgY> zHES~)L?W-SV`6REcZsol8Wkfq!&p8^y&XebwaEbKmR*@6A;x-+4qqbet~MTR;&QvsuBIZ* zU<#Z0b_e?7HY+FlL>xCkq@1c^P}6w@SZe1$HF3okqB%j&K(f#u{Ut?XCpb7j(c2mj z+k1Hc{lMSS%j$er5_=RaZtvy6KA7cSjo6FWz4D^#!->*(R-c9)>BHZMzRa{G^g>Z2 zAsSF8W659fjo;vA;mRO18s=z(Yocs(b3j=`v*`(AdOT#nrZ;n6wqB6z%mfsyV7K{( zdrbroFB=F>nTg0w1J%@6-~af6#yy+@nRFJrc}_=t+-@`sq0*^vfBc~VPg1Uc@NwM4 zyTWZeq~0^1R6#0p(aYMA@HjPWUI2C^*39%X~qFF{wyq<_4d6 z%m+o2`0=a@hcLyP&1Clg-wq+LM>|}ZyQtY%?(dl%l2s)AJJX0Spp6A~bJtLPXOia3 zbt$U>_miqPr=n1FAp}%tX2V{pBBdMAK4igsE=B9Io)*A8pnPPq04rSP+dh*8u9xjf zx$fjh;@oP1_-?uHwD+7nluUit%xlU$^tSl*t<-+=W3&y%Y48E@9(W7h@-pmZ{c=tP zFGA6x1L`S9XzX4i0MQO`ZW2tLO1|T*5}A<91BhbgR?6F2avcww(wnMq*5ZTd;)!m} z!G)}>kNA*ZjZ^XP+Pm;nZ0}Rm#3;P5wQV2cNZbp2BJ=Z@+ggof^}x;-yOZ@TN;yZ{ z#t0q2UXLuOpc=A{+?SRP z@yKJPI%<60y=U9Tzq5>0!Nwdf3)hEoA_w}*+I7Rdg9LwyZ1gZ15@Z*#(abFu;`&qb zkytoiHcY19`B0}&G)%K93r|@fR!@qNn^fUu)$Zb z)7?_E-MGHR8=su*^19dhiauv?Y>aCK>zJ6n&I#88RM*f$EHh&1C|C z&p-qUPW+e(02djdlMoJUP@_PuSOc`Vk-U282b3CBR$T3H( zcX&py+FFgFE0OP*yPJ<@MUEQAvi-dTT-h;ZEORsJKM+yBr;)ZiONmI`_ zX}`+boXBIaOOSW*PSHq8(cH)pQoq&SMS*nXY(5&^E$p<*7G~OW ztMwVeQE4wQnChn1FvOe)(0u)iH)U8^00d^%Xt)MA2xmJ&>9eIzc<`bQ%bQ@@dW{(` z$GHD7;seo=5g$2Z)|$bri}E}baPOu`_IzV3`-!6d_5#e@jaXp+P4v)EqQO1vX67>< zaLLX=tnUgMd1zY-2r}c{A+~sIT0}V>ngBY$2!J!y4rh=8Gy#LHPmv2Z$~HnMM`noO zoH&6j3dcHV24l}>4BuqjCs(!#*=x8NgdVXM2FJZf)2xQGSQap12Op51Bb2JBpLdnO zPt!mlvDTNg!08WPY*M~DZdRu}U62HhmCzduYkFQNq%bG4&=xGsNZiTfD&xEySFTt2 zo_sTkW+!e?A=?uH;iV;uqWL*Sdt#$~&(;@+nb!gjtzrz|PIME;beE=0+-J#i#YVW9t1M=?md_A!? zmdyp2kU6#{jOg3+7;O-$Bs0A*9rl-PGYW9{t+B*si1_U3XuJ^SsosM3tXqW7Q|Qm^ za^Mbu_yOp?fRriJCY5|YAm542x7lQWJ#GqwqbI6)&`%5wLt8geke(qG)Fv{iJfzYa zQEX;(3h&(CVfR5*-@;KPvGMtEfoIDXtC`Tb))KY#6_qSyy`6>5Zig&i8fqkmgvX1q zg~;Lli$3Haz`}D&%l9H7zCIp&Jl`AZ14F%achNep*~eIZ714o=)E>2FBGDpgnv`U= zXecF4*-Jm^wpzE7ggN{$1JEs3yLX(~6xo>z10)_^<40?VxRfl!_JfR={l@*@0)AX9 z-^QYWTjwuq2jG)r5K=j&I<63T6qFNeY&c3x##VR;<7fEcxdXWSVUk;&bd|K=_@p6r z?qqX77X)d3l5v}&Z%mPEjr-qa3kzzH{pv0$d-7V@(=qOE!+L3Vxj1Vd>&6@p=36CR-!3!BD_h!P741>Y9^PT!)J6;6VtN7a z0#6yCf|5gUBZt+e5#6arxUp=4FhCB)Diw-F8B#pSr5hxf({@6{%laLuxkmDHCjWQ4 z7E7}~C2{&)w+;Mnj2DLOe+_>afYumDT#c= z8OpZA#k|b&$T;s3Ma_Lz&}4h!IDDaqqR!eU!F{LM-hN1W6ZLT?-c#XWPG8zmJhwA0 zG?9RE$;>B1qz*FaT`5vE>j`Ql^ib2rK7dki-tq{D?dz7F>x;+Y^`b0pys*AtgLMvL zNr5@Ubft4Yk2u0$7|TCl{6hay3y|m=c-SkYh7G*Z7DWgWrH~B8v^2BAOI&rUYTmLx z(|Vn!%sNi@(cRo(WslVYdt`Kx{nI~{Z0(Vx=m&5O%3;zMM9j5aDl{1n4$40v9_#Rs z`bQ^#17cj1)d_%=tL`a5hOt9O=|A>3*nN@-Jf7Fc&5m_Ly^;C(7*;AiJ9M^%X^S-Z z9?U{yzFc~^0&n3L_QEfOPJ2$+hpoGM%66~O@Na}kYuIMV<5ey8dC!-38ZO1_SLulJ zc;ddCPFkm^!7pfl*Zl^nLZRV4_Xo*S;uo)QAM_Tz9P01oa!hZ0;s99PoP`OqC7xB! zQ6FSvewHZ7cO=NoXO6LK5Cw2>e^Ga$cri#I&26F}E(I`Ow=My}8~_OTr8ZP}rc{_$ zr^jW|accD$6i{`l^^~BN;6Zd(S5#?)uV^|Pg0p0P1)}DZ;f)pNKTd)_kcmR8Yba6j z2$^Y!z?APbi}S4|#7gtF=JD+y25k5IYeC-sCU+tFvmlzl+!=Mo1E(fc;uMVS9?-zI z7Sizatg)P=D%nK!x z6H9E7tIxDu>uEv+7{Z1!I-IE}vEVudVL=0DEb9UMKbGCVAJR~vB~3&TnY-!(U%U#T z{9#K~p9#F;eKnM&(J@W~>Tx0mzGTPS*NCFxp|{x+jI{SJY6+bpn}YTzNNa+XE2Pw> z4b;+QRWnS}?2QbLU7c6Nu}AYY6bKe{6ZVrO)(sh$lXkruR5@e7wzjuxRqyycMyrc~}K|I2b>BNw8>p-6)&?DCx?d!*5Nj zb4k0?OIA9k5^^Hf<0W=hfufD`HX^tPojz(puh6;dkf4Ln=KHZ4swssHIWF(L-Ovv?69IE%%q!Lq|N`yFk zkye4|W2#E5H?{H&>b9JD5y9OXgh0V;u05ucKKRiRQAJ=!i|OnVGd8^ zP6NC7g!|s%C|%<3C}xW84<#dzzYse*eSd`!xy0?$ReRSeZF*H3Hd%JMMNB7*Cc>IN zCzQBYwP622s|dOv_)3N!$V&bWjua05LK$S^-;6WrLg(>Dfl1pXD6kSBQ5|l-d^)&t>Urnpx{P_~P?@dB~N@$~e zHOZG48J z!uK-2_y8+g5zmat8L=ffh&V?(LF#(PQ)1B7s3uEd7?Isq7Q0*nT|@{XY`_7&8U8pE zmDp7(l;e>;pYU;|oN^2f@NhQr&owgeM3w1x3@5sJlG2ypRM{hveTL*A6?m@oC4l{F zXZ@q=X)EvdXbBk-Ig7~H#%)S2Zj?{qs&wV88Gs}G{I=*{c>JpM2iF#du^ls@Xw2Y+ zr`3Pr!7xwx7?ujBbEzZVFl8p65iu#zwZBGiVkQgulOs6wOS{h~vZpwRTI^7h19r%t ztL>XXUoJJ<#f)SoD~(u9Zqd6&yp8(8`Ye43H#W8x((wmSiEy=z?M3A~k;UHN7X@TJ#AmE$&hxXe~$_9NQzy9NWuixQBdXbw9S3=u_$RV(Kt0efSe) zQOwlS))@moXzNf~GM z#~1Wt{~o&~#*5=f`nGNVspxn(nS{=1d1S%igJY=Xm|me0l{UOiYC0xmUx{>fdo!iJ zV|05b*zHxv=*_p3wKGjG?bKcMPo?M907=hLwFFdy62c$Zm2D8C9m)Tq0hG|0d|XkX zlzxP^hZfI1f2-j8oBr|}#R`O+XES`;;4DD+vl3$`iL{%8Pr_1>wlPbyPLYlM zK;%ef_|riA#!P>~hQu@q0KBV5wn`q}L#@K%a=pBh*$~Mr`IOkV4xO3uWp@-`G42qh z^JtPp511R0OKtqZT&;AuGMQHxxjXL|eQqD!;_P@nc9cR>WYZvEeuNm0%}=at1zKRW z;w2qe)@m#1uruZ44WLN8cZ)*vYk@iFkm(kEE3w~e<9DGf$^^fIMROsh0?a8%`y2L~WOC5i_^r&}buCyw~7DZ`C;yt8qa#Y!P=BA^S z=FR!$*gRzu#dzlC{3Hf(id|jyeORw3(T1LxPqN!I=AiH6S5SJpq3E=#$Npffq>%0e7cZ<;_4Nvh7zyWuP1T?W(Z~_UDj*(Lr5@VQD@=;<~OYuq7 zUqQ);UV@)5J~rbyfi%`Gya4RbZeKwQi$)`Q2B9z$%Pzt-*A;AH>Pd3$Lg=~h7&0)a z3f4#tP#w!sGE`?jW=xM9qhX(9z}i|kPdnG@i$GMhRg@c|c zB&Eiu5+W$*!9BP-7Z~uOfz^vw>s2Ryu5}?;0me{Emt_#9&|pzFP$nnzOF@JsB0)Gu znq}*I&%|m;?v4URBs0Bz-d&y0V8uM%&yu@Pos1K%1XIFrq#_<dIF;OQwRBM1QbUk{$Y1S2C5VYd*S37I^lgig}|a2h^*~_(5p${ajz{ zwj7r0%rhmZ0(c`$X9!)-mHq9?$UPoVe4mV+@;iD3%A0(g#x+Vgk=t~xqQlKEP(`&yVTqX7b(RgzZ)p(2n~^K zh6^eq3!xAF*|iBGj{7;><&_w}VdseRyV zAPSlz&4p~hc$Z?!W^?$zXx2h5jXNyb&;>hD5#M5NT6lq)7$$ZneKTR;7p-xxE$%Et zDir=4xgXO;cT{8chWb&naG$l^3x%q6!4tFf-?OV&(26qC9laI?0hy44P&dq-<00aY zrGFJL14=l}@Q^gNXW_XihUixaJ<>uJed3GVkt4ti-RX}{aj)$@EKu}L-Ec9YZoG)P z!Dortq`)t{CoS2+Z>2K4;p7alWcj$7tTR?NI`jA-yUZcU zA3|*u)q&iYO^Qx?-J7h#j3YIFMLsO{n_n8yDGV~1T-SE@XxqT`3aiX*X){N|VG5Dj z`h{y8$INw>B#Tcp=mEgb_&)*u@dbukv$56zRX_is?G{%~a25EcRslX;^QZR(fR2m_ za`D=;Gb$@t^)r0m2$Z)UvV>eRp^Bz-2GeNhT<=PWA?v4g%|~o0+o44B(Tsu(;7T$! zVW^zqsDqGui(Y5EdPQj$Mcd#mk+@|DjV;>z998c$#7kW9Vqv9 zY|K#TmbotSfv30NLu7r4IVLCZJOU;{8KypWc`wGcFp*3n)->orCH68mlo6K;ZO)UG zpi=TOCk$1_k`wzUU{h9BCy%?sA14#6g7TQ`5}Vk~VF@2P!+6mZNfgRn)=xzx&CFg% zUdd-z*s%1s`7OmyoCC^P%trEi}hJwKVgjYy+!!XFAT`hLGIVub#* zL%+IHLegwZqh4hq^&>%1S^}FM&p(kK4+fe>lrzaLYCTnm?^L16#m_pmo-7o&SOR9s zn_sB1qR*bhXkBZfI8>SCJ9R-h;Mjt@m>RrAO^d2zZZH})<1ZaW9-|XKM7$&I<)BOx z$U7#N1FnSAfJus?f0mH@+R`ZcW{Ub7iar^^yHND`P;|j}DJaYop$i%46k^YMX_=o6 z(hGFXIw1oW@B|Lb2l8v}0p^iAbKS|cPJgH1t>z06JWzd^Pkn;C%%`gQYWDa5%uGI_ zI6l<)2zGA*qb(VsUG(lGd%3AOQ8wG=ZcUqCAooVY0@@KQuk7V7N8#q3fKj?SHNTXt5W9ym!$gkOLYD`4 zjGwfWHmr~5y&=e?s8A^=P-4E}jeLw~NcbO&hCq~j=dJU_XBS4AF7x$Bl$eLS^~a06 z#@M#NR*MXlj_C})#X`V{o+`VQ@Y7f{%fg|Vy=PkX}1v?q*h>iot@^TlKW zSyc2RpHQE*namfhY4e4M7z+-2xyxbsQ&d`-q6wT4{gV7+V}*8z9RpX6aBp5FRq6Px zp*-Kx)BN$)l(eSs)F-wCM-?%~EHdS=o#8t<-=DA6%Vd zL|3TR?2+mj%bwvw;As9gFD6nRSv)L0YOfw-)JT$s2h~ef_|6i98+t$4i>$$^abh{b zdvCnNjrFciX_XBN<33sGE3^cEr7}i?Fo%d6sK#R6G|;Uu@6=-cbs~vSWrQ9h8{GQ%!{>4c*n6hC$*=ch*aLl9u)C*v6?-d z>c8DJUPL{2?bX6hYz}{3TKTkTpxvYJ8|% z{E3M6`hvPXh-c;EeVtbc;Vp!oo@y-ln5bgvr?(Ry*6i*tpFI`G+80l3W;eFE`GXAq z)?H%SaH-P&aW_3{5W@*ckaG?fWQeh}{Uv5A8wGTWj1O;QmnZq%*mb!qbGxb4R<6G} zk%a|3^GIlBAbz)-(+3OZL|Pd4aDp(TP4@lVh+%dauz0KeMf>N8z3p&6U#vWrWHM&@ zbiqFuzaEwbuIsRSZl5@Eq|FKQRdMkMe>S2CI6WLFvgVyaC4%N_7%}^IRcuRPa=GGX z1Qt?k!-LQk0g(dC{j!KkHA^i>jAgrtff%NEIF|I?nEvkNhqGPlD)hcU0OM(kvE1Ut zd$6hAX!tP@_a17JwVlyWO$3gW6dMf>@CE|fYqsmz8nc;^$OoRt`bmi^#bZn_L=qP; z@nnQNYMN0i)IxGjoNI^cWiCQ45jzeS7!_JxN~&x`r0S_`XsTpN93DEYYpHG}t|l$? zuxLLgS%~V7?1xV^B1eWB(E#6(BSS-or`}FHx0`8ZTM8c}euo0agkD_@U3Lb8D#r0w z76Cgw8VJnqmrTx#fS($&)-YdGAb!oPUm?k~wQi5nymopjNOnZ20N z)Y&;d?>P>)5nblXH)Ru%L3%KSt4r=BFU!x5fJCjy1uAOQzc)e91HzgJipyDewUX}| zKPmXNO3Z1*M2KLWLqq{ML)ecRWmHz>mYbi(lg<;$2B)Y~3zNg{qgU=AT`QzuHtf82 z@h7gSz$kHzFsrbFBerlQ3MRc~3wLmbGVX=F_gqn;pO0XF0XWqO;ud0AlO5Bg z?PMQ_^%@Xhqk)NKcfkB2VS|fC)WGKF;8zP128c>!MU(9vL91I-1?#0$b&#(b#Sa=5ZStG%d({%kyDCcp8y-~y z@=NO`HDt*~;(PV&O7GjTth-d84PTYtOe6YmwiHQjbdgKP3lI2L>1>dhqF9O)FPJahV3RcxWN-yHsuTFoQC5g>o zIQ?lu{9>TR8ydM6(oQdR53r!hk?xTN)Knw7Ui_u+%-4OBuP-K`S4ZJ@|lT^LTV2} zuWbBN_@a;rY=}@;mUXoqn1(Nx8qsC+!RYTA(Q_zQ;SG$CgEz9V;J`oC-p8|y2JR8= z^uBu{;t1LtLU~P@U_n0M;G4vM>ucg)2A#L7bz*#(B)%&DPlc;HO`0L+n+1FfsLrqH zFPZwMCMJGJlJ}9Do=~BiLCvo!;8I9{^%SuiMH&{p4B()kmT4@>0W6AF;S>G-qkzh2 z_@yArDgl`FQ+Y*|&A2~6niGI;Z9v*7`Gv0&e^N@{i-gVk6Cs-3NcfQSa5A(SPYmy= z66TP9e^mroS+i7>VxLTnIIW5=ROzelbLFD)r|a%2o6~ zM9WiZ9&z@DxR7Jb<-!aSv7sr59^1%;y=8ay4ri)ic9Pjgmuu}r_O9ClZazCuGOqgo zklsOEO6AC@*h=*tT$61?$Iw7Y$3bVoJB6y&FUX*NQO3nKUi#y=WITH*nMedp`?lIK z5g(1_D6uyEK?ltsJL$6qb;Tfeuc*@mB&66-UvDgvd4K7dL_+N>&cST2$we|(yRBiW z%EgS^i=l(rF_wqyGBd|;m#pDyXL72i0+RS!B*to{$Tk*TK`b*N+n5IupY24tX?9rm zkW&}D)4LLku+|`N!!N`89Ag)zvn@k6`N>p(AW~X@Y%EAF-c_U*5NMR=16G6U2(7z_ zk)nYJDrnNuezaaTD0{cpznsBPoNwJtG}T2P z;qQ!rLz4$Au&!ei`^&p#Qa@Aa5Mmq8fS#%)xYk8f8_tvZ$_0<8S4_1INlSz+NVVi+ zeA4FRrMcEFmc>Gq?KqN=MNj#S7sP6y zM`fu~j6ceJ(B_3WDD$Jg6A?xz>)mbJ_njEv6f5!Mz}51WzYdEY&Dv7T5 zEe&Mx6%y+uZccOfS#dX`-9$hS#8(ZSp-jKY{=a>d0Tw0xZ0E1I3YrlMbZp&a`m_9T ze}_3f$9kG_WIc4k`T|{u5lyibO!>2HYAqc?V42RDGD z{CIVT-}vJO?xUcF=K4UvI;0hz(t>rRCg<+1?g%tn9?>cbBGp5PdNy~N?g(EZEK7&K zXieRq5@rle+H48+NuH(W17C~{yY*o<61ZJ$ACkv4$o|Z59!l7vCA5lF8se8)FCgZ+ z=Mh_^!bl{uY_&<&cpbq4Hp?KtEhtQiED1e8kt)9JFK3}Ov4Lp{nO;+Ps6S5asYQQI z`>?#}mFy?*6nix%l)g_&hbf>G`-*Po;xyaL=?U;(yhoNMjv>sFSOLoUV2$}kWan$8 zN(I7evU=h)dH2ZSEMrL(vTuY^>z=|X&$^xMQBORWFhC06mg0a{yTD|CC zqG^CkCv9Hf*I63BCSRQd!`ydiJn#M>XFB`)SZ81mU2wN0D^S#8UfWT(FIa#k8-@!u zp-KxE+}M%K<%(ynVQXST$%x8%_1Gys?zIO9pOlt^1=r*?@5~;*@=;FKpMF0H%? zT~P;1)aCa0k~7i5n&|co6>i6qxBxU0cE%%`i_K!7uT>g66dl6stI5a@VwadZY&)uQ z?4bRsG4atbd>i7|bo!fjc>+DQs*y|5nvDfV8=5#Z)%)-tMTzuQ<#IZG@$O3iS_2jz zCN-nqhzjY2az>-KrJ_w*w=fT15{kX>TeEj)Uygnq5p77+PY;N-Z)1sEzGSw@$zPAK zX#&#bU3qSfdTf`f_E7Y`)O7druc@hG42;x zsLx#Q@OvVIBwy?b;8RS_B6EEWQ5Z#!G?t!A(Q->EI%-S5=#H)hzJyFCGMYjxs$Ni% z4mhww%rp8c+gSF6d_AecvPN`je^$!x^N6=&Lgx=CrK;n28g`gvWpZJ#9f=z?d!4)neqtsS~y6 z{mLIC3e87DxEE$ehJt~UU-4rk1H5Bnt8i%!9$1Rx$ovu!V326oR1#6(8-$>zJ$!?) z{9AbSK0&;>(B*+M^}1e~aun@V=4i zHBl|pfeM#ijh3-puencBhqbB^@meMY)LAg~n*-wbbEyvlTl&|NxX&X^wW)(#t;MsHRFoW#3w+N;CEgOZp z{faR9;uAS+U(lpjXh1IPu>55o2Y;?O#7HSNe%fYbvVNtWQzgg}D#&`+#^;76wE=2e zuhH9fVh<=}KMAiHpbL10T4#}+AcBKKo1XXwI7H-^1uKB+xDlzK zpXLK>%+wjnNAoUI#?L5#F77)}iiBn+smU8gq#kecFb6as5=|NA8r92w&aoD{&%!dg6qPgg>@r#z|W+QqJ zEz?BIZ7i#nHZK@3QQGY_JD@LYJQlUx+sAAfGI78}S_qC#bYrnYLuTn;=ma@8U959= zLw{|P5JF)18kyKcn*`k;V9G?vK*m4Hu;#jjXL*ZW5#tU8B!Xbu^~1meXd>3$C$g1$ zGwvinAAJZ)Dw{+jffZYRia;jb=r3SJd;w?ImiQS3>2c&wTz-xXNxu|5AAST7PUHxp z(Xfh~x~iBM;Z%=h7*VHF*s@muGu?%5R~;od@FBt?L$$=qHk1)vO{b1H=p|8}>`y<8 z?U3qde&nzHhg4I4G$SH=vmzh&ckedsv*Km~rOwl2ApQsVAXTzkOw^HXG@};w6y3|k zDom;}RZwn`^teXD|6+XV%48VJe$JPos>+ann=cM(k%L;2Cw_mn1_liB>cr6V6ap{X zL6zw(kHXN%nhfh^&@47K3$j*}Fyg~my@0mG?ib0;#oI`?fMgXw5d4pxPq@`nBH37$ z0d7ws%wFU^u?Z3fimKRsH#>Tde^#-a2fdc zPnU!RNcRrC4+k*Oa?85BzkRG4M}j&5K9jHbI(T_-MxPmBy~YXBM)&OWdnQ@&`c_yp(zwhgzRg!_?1Sag*>A4RU2Bt0z6)ixbyM66#yb z;Wg`B0uzs)so+eR$sfvq4WHr+8iKUkSF+4zA`w zwm}tT&?cZL3<9%;II8c{fia50WJKH4s$sSeQqU6>iqo92BB+Y#J;6ndxF`3!pwH0c zx!@z^Ij~FYWY%_|MiII;zehMN-2}v>r+lMq8nb8(CS%EeybRZ7XooWRk>JG}S;X1N zWwME2thkxkhb^7t8IF7tvs+OMR|56=IRX@`xZ&))(^As#uz|cEiwZKrjm9G&Hrk zi2Dnd8c>f7%=dT>Un?VSKJ|vNka02!F2a`G2^Y8c*Y@->H2JNx`O=Q*lV*KN5^`&0 zfhsJTramwgFp+mq#)$=AE9Q8*N?s9uTACyY&k~$>MI*nAR;@$)^ zQpFXBoDVBYqEP-rXinrfQT8t(8X%MNjQLKF6?%>5!v@q2cA^HJxQ?fg@ zQjubB!hlMr7!GX@0?R`9ZtEf7M?J^;uF|JgT#5^t`>hznLh@%|Vt<_8CRd&1ZDfL33}g9sh!Z}gxc+0# zketuEYJqgD0Ao=P>nBtneTud93sHw+Scg8hE+(47x)DvfGl}Z*4T1VtqXeg~&q*9b zvyg$w!6I!5{hBV5VRu}B9+A{ZHVig1>Y=gLWrBYWYmplL1DNCK#wNQPFCr0do1}Y; zAHK^MyJ`G#W>#4Vmq}WPcwv&8U7JD|D*>d5MW?Ij#-b1>hU{(xwH(w%&s-9yF=?$p zZNrP|P7rwzhhQj0rAWREs8u&ReQrcYQY>)Sz;<$z-W9-~1~-%9t)k-;qk?E(IIU&> zrka#{PR=h}u9t*05v0xJ1Ehb42P7RV*A@0Y^r#v!3pdE(7@6bWERL#^RF~-yodgZ ztQm0vdKk+^i3G2F*GfSb!>1(!R!IC~*W?Qe`U*WO03w3R`U?STyA^dI>2pPRtJ%a_ zOGH&|v)S@HltfbMaVd?kW`caF0V`QDPAw6OIT5YHd{A?9n}Ul7K^(w@8mP}Q2o8*o zG)m1&G|E~-5(Xa(h&?u+>?6o2)A3hK!EHhV9Nw`Ed{+Q48hVlm`7i$!RZq3|PCOM_ zW5!Ar?M{Hu(QD{FhTg$73_Vc`B20nV`WYVz(8lr$K#+6*w}8Tat1r2VC13!LJ*wuN zS<3fT|2P4Mc30)SFxwl@-Z5jVD z5&b260|iiG_}tX`T(otK6+NZkFN_tNNfqo1bGd^QiqLTHQ)}|uUGqEoG1QX?QZ*=| zB*+$nno-VZz85-O2q=E1UaeX|1;TrMBjE+2Ld)sn6{i}}NmQhuy?Li>=MAktG9I&l zG^hT^Z0g7hV?AJAYBY!fp5JUAvEE;>jb%yXaQB5h8#f<@1o4Wvdoy6FU*rajIW0Ut zA1NEqRh3S&rwpfBzW^0CC>i>T{-V!~$V<`*?zh;WMnMVOOcPIX035T-$j8~m z%Vuc5hgsLHJH0(xNLP1;v0^OeF7N2PynC2qx#;Mi) zkpKrTO3ZP2-sTUoxwpVubii1CHz0V4GF-+3=km%x80sD68-R&uQG}G$Y!(ybP`OUw z&xD(;1zU^WGnTu6mY0BDN?)a&$}3fmH`NQ|`$=Ae32XrziC4~ZxtH_SMC&3*D!Ma7g0QIYAt?Y}Ltz5vt%>*oS*d2R6@;*jY%)^@xW7I&k2W;JZID8~AprpCn^?qdC; znmdoiNpl;X1RP@7`!R}CYihY|9>vO=S%)(0wDlitdq&mHsaUBQc0E6hWM=ooNTe9r znp-XD1zGz7j)VvQ=YLHc9e5*w+^uc>0N4DSXug7tD9~C6j|-4t%OIyvtJUtzC<$d9 z+-coUd?75dzvZ;t`VEYPKRzjA#1`ubCUu{=kqiqyCqFrF!$v?`=!*Dtn2@5aE``G8 zI`7tIKaCK!Ce3S408!6cQH6-%xX;bt75$;Ef+6suC?po>rWWiQg&5{2P8m zQjX^J5^nZD;x3q7vkmnTOA$S4&JOSq|HF~>8QzAxCFa{k35)y#1@9%~zBe0pcoFH% zQ0r!J0_#FsVH^|$hS=tQnb)Lr>L!lh$_+#$mq7W#yu|xIgFpB(L+;F=BaCPr^%*rk zZ^4<7Px^;05evR6I~3v;w2KwrmV3`rHIvHzhNvudhPuU#GFuL#u*=878+<|nwLO#U z>#VmSCzl7i0ZhrPYrbrU#xUEgW!_Tslqu_0V1xFsn$NnW{KI>#VglIsgj8CaC{-bk z6{4G767X6dl5z3DwhT@Q;OFgm6?gA&=^*b`7C+GRh9JpUCNBEEw{5I4pyh4rPMuxW zDjKK9n=Fh55HGodb=qSDvj4itIt?G8JI{X`A7;g(wH&>sK(k$e+B^UnjM%~9f``F@ zdM58k>v>AiFn+Tdc%x6ZyH%7^6iQfk)4>V_tb^1}?oq;ILB1$6a0!&TiTvdFy^OX% z{OlisUc34S;zRGVbLshhHbY}Gd>GLqLj z^m44)UTGW4n#|q>ZuJ)>QL3K%u^Ru9CN^HFANY$}z2=P_^Y-`;z2T4h&VlpV$>lHX zev=^tU~FD!rA&*hK^Cqt2S3WO07d(Q@vQrTMJ;uMV}X25J;?q)BAa5i!qGnH4Yfvc z>j@NXnzyeNgL3Y5tt0G9($QUh2F~qSVZ4t`Xf3cO~HC7b6nHPWv*c#qT zl6xfyVmjB#GJd`~&W))!UgG?n{7$%E^(6ikA@3zsKY<$5FDwMzA5bL7o)BUgW(!v0 zn9kT9ghq^ZC=}h^=#Njx@*BRbeo;7w8TZHU>1eyUgNnG4(oan5#-JgUdMIFX6 z5xWOqy(Y3j-F?xxRftrfW2bvpd3X`o^!FOZ^yq4O1Bz2xx6i zDf&OekjnT}Wv)aO@Oeq3>cT`N)%Y-@;9avts*qlT?L9V8x)*hmR1Myd!td`%;VhVK zm6-_&HPM7I$9L52N#oDc?ptfCYgY@nSaAnZoztx{seNRBR;;_wOtAzQC^{V8>$UYJ zRzk=KUhO}D_w#~zUJi!PiX4p9a;DwfX561mQ@w3tREOHUS0IvvnazxRV6aH)!8Xq} z(dZNTZXjOQ;lwR5Kc#yE=I3-Y>J8iKk9y`YI&6I4WA5kmVzc_UB$F7CUGLW)on$nK zX{Y-6ndh;QoZ&adv{(&zz~hbnkgdLnHhq9_AvPjDYTVhWky+#x4?@a1! z+&`8N$4VsfrSIwz;dZSRqFZjELCQAKix_7O8wLRSvv z6s@AwOR|4V7MH9wq^IzIVGQ#_)1@HrSL;bvRx`s z2Akv&F#=`NW9miL)ao;x~nVW%H|PCn1PUt=`ifO}m;XF8(wGw^*K;CQjFv3T**WH=%w;68ib9T_?LVKn zE|o9p6!G4FVJt&7f$mQv{{Yg*g7pZVGFbt0w@~wuOuu`xfAJ9yV-zK0!SHzACcoLY zUxJ z_z38&PfinYpYX%f?QY^KZ@RhcQt9S2clB+U0LR>H1ix@2Gjb?j48%9n2En=Hw;$<~;ICP%J3R6q}-F zv1@YBC`<@v1jsR(7}}-;oyv;hEnOEKgp?eK&H4Qai@3>O>G@f~$K0qS`-#^CEOgu{ z=A?YR7O}z51i81leC1OLA z6FjJ2jnDX*yR|}Ccy)iA^!3={qdncL+Zu(zdEChUKIps&hi(%GVcImKR*yz zoj)!<^oi@^XFnY*O4dzw_zPGejyfu>#;Zt=Es&G+C%L>121YXT#qR0|7=PRnbbl6f zV}*A!^O&eiLbT_xFvLo38(Mtt&U=CzNiOikum}~uiHspd!m9|!CY9QqrFfQXv?Z~J zGn@M)91{%Q+r4rqpI%Ob*F=pG9uxvmP27qB66iHKHGc_#|0`PXH~M2&{3jcuYBTCp zLW1#uSDePT`PZbe`ODBn{@7)xy@ya3ICQYp9onPLIamofW~BOjZKxXEOrj@*3UB^0 z%ppKQ6>S-!)$ZIaaJo9D(#RFR?gPSy|9}*~v+pC{aG7OM3RII!6+6y4kJ{x6_5*_O}VRT*-&*(kH6YuUG|JF3Pw4BUhX! zH82_sHFkRk`ReR@8a1(w(JnjDRkG$3tZoaP%c58eN;zn0X3vqVbRkdq+p_QATmG=| zJefQ-ex#j$9t`}w<{Q``4dldX5G5VU79}+I-uUhAqSl4>dRM7`7PT%qlxUF}H#DJi zU__UaudLEglV5l)C(4<#ef3#FX@w2P8k(Buk@Sc6+gFl|Dho(9Us{&=8 z1QYhczc<0ZH)pZFgn!@cHU04KH*_F%*}A(F1EX)F(13WL#LP8H-sRjqyAxg%5%Lnl z0Z5PyA58|7kx^Sp1}2j!iM{YU^O8v}v#1wn1k10^i+S@f+7P~Y9MoF;7z>Q0tR9>r zKP5kz9V}|DLj!v^RZ1{MPAe(O4JEPtyw2TP*?2NOJM_&j`Q9ot4r4zBtwzJ?KtE_- zOsNV9#Iqo;K?>*rT#Sf>`y&?2Z6YhyhaxQFKJ@EcvPE+Xizh&BL|>BHmEXdO;re`Y zQZ6WnfRMGrc#Oo3eh?BPHe*>W{Wy7w(>@dFQ-G?Ansl97r+5LSzu+_Ua1qFxZ_1h_ zz@)_aW)!2}o%c$x=(V|5u$EFIwF`Q!tc{!$6Vg(=27g=d8aqfVsCIRf#wVUeX}hx; z4u<b(9#ZkhZq>r?Zj3o?>P!@;-}P z2*KdG5nDJ6zcqnKv;8W6^G;S;JLce&y4xG~_w{c5z}sVUu*W*3otAEIQX)?F zMiM=}vD*%ZCd(aI+@sWt z*JGpqIcXJ*?)0Cx$=_p}E{foY)ia!r1l(Mgs$Y8eI@aGiQz6M`REEhv+4M@E7f|#< zcBf#!-@<}OB1`sq1*ITnP*OB=QH?j(W5wQUYY}tNA_A(#>%@dPW)I+9xH)hgof@lvSBx=_{~`Nnfa%RU?lj^y~vHiUlaJp7^En~xT3kORQWngEaDpym1;@ZF}LEzr;V*qi49 zzAje6Shh&|B7R2?U%cYrxOnvir?ACg3xr-AYwty~0|K%YCjP=^RACgGy|rln!XcmyC*Q}JvIsMx1+StPF8Eh)^H&eErzpel{76njk&cz zR+CP`bvj{Fa!nV15sbN$XL>Q*jGAGns`g&SFmB9kJQ07Zy$_LelROgJ|2R@xpC~=k4EDj#9@~3!%?X-4?Ye( z%uAL+I^$P=sw6`3pc^)7$2Z>G66ubv(^&TBt;KC#J}mxB-B`H^gDeeclHc(3|W zZXUkX8}mZgZji-!pJa*ZpA`RfDZ-*T2U<(Z*I&;?#S34P3ye3EFuO7=Hf>EHsc2CR z^%iXj{ZALaY&TvDmsmrp_0c5kyorF0IUzcnd5XJqji_w#8!Jxp#@t?;AB~C>r`U)p z$@2^90Ka?gqIy*D*sb{uuoROjBM~mj@>#8vJNp)0D67iQt!kwlL{n*GK$d~`A0UdT z7I?hM=oID^5VXrl4cBub1kssv_U)t6vnhqA61;*ZlWb#tQM{C7S&C!j3jN8fG`pz= z>R9xwm?f?>`#8xOmrfkuwaau04901d9Juo&Bb?vW)zCr<%=t)tR8>=zT2P=Gb(ShB z`5X8|yHe8MB9`q;HYP;Nwas-QX_dgi`hg6v6`9_pZWY72Qw_Lcm*yuXNiBT?#<5Zh zDm(Ek3rE~1)ME00aRSScexn2{_^#OC#I0m4@Ej>;K~c7g_r4z1k2nmk?LnjDRW7s} zoH$>&k^<0K>-E4rKQ8yOE#17cl$HMg65+YJyD|6bJ(a+k7ZP zdJ-|%WOQaW$`)u(x~+SljrItL#U`lG0A)MgSxIm*?PK=W&Al9eqls5S_6A&;JR`OK zqlr$r6spTBg+CXIU{anasZ-ut$w$@r(eU=$@V*jr$O`e_qMgn4x}AwbOf@V;v5kl+ zCE)vMwhdo^yHSb|!vt3*nuR0^gq1){Pa=RPz&vwqeUU4ilRTpl7n{Da#5LE+NihcP zo>oV=%lH#};4HCxCjHN4QsL7pvGwhmcjMANDMW4+x*L=`*9Y5JDFUdGg0?d`Vkd~J zPRj%p6v5v@dFkwX&fbcMzSi0bCQ}}5HQ;P zX8nRJ3}~JuH8`Pg+1aGJoaeHtqxqrI-sDZ*`V`AN9F z^#pFCv9WXD&5%8D$7=Ud?OveWe%kG(-GlV2#Q#LQZ)$g~c2{Wkx7z)wcJJ5jTgduHAFB zd#ZMi546kOr``9o`>J+d)b4+4_fhRG)9ym;)@b(*?T*v#mD(My-E*~js&)?!(Cyal zo7!Eg-4)vXt#*H^-TSpWSG$$ky;-|swR@>{FVJp3?RL}d!Si+b+I>^IYqh&VyT8@$ zPqll$cIRrhQoA>6_fqX%pxu7Tb>@p$_EA*PyZ8rl-IDBt_-Ot~%g+!C-gmc99nZIs zO2qrd_<)z~?3-_YuVIw32tk%%_*(pC_AK_`8M0bno5p`Z4{PFWNRQH*rK}%Irp{ z6FYo!bN%t_3U$>6o<@O*?Glc~(E=(C7kfCXs_BW}|Ni%=Qzn)XwD`Jwm&_c}n%2ft z{=b$~Du3BH_?rB`P5z6^^E(%z<*sa9!SFVJnN_tz6>wdy%G7FS3UeCJIk1v6if(9k ztadNe?giTIr`_Yoh$a3$?Y^hoSGD`1cK=(uk81aR?atM1rFL)D?pW;(*Y2s>J>FlJ zr`=bz`=WAXzJvV#(Qo#yA}>ogY~jBYKRaKRFRWoW2jMdcIDZo<>SioyCJM*4<9$cZr*P-|vp;@L z&OE+jcjd?x&t4BTx;vkryK)onGsEL4m#GAl6+fzinJNU#Q+b1XC9_r%C+3nhCyFU_ zM3XUC6+4(>sqlOe@yE|uO^oEf!;kX5UEVQnS5G4OE+PkO0qCgK3-NQ@@?G8ay`K15 zq*pBQuafW};er$3gNFM;1z&qV2obR0YU~CNvH+%E|BwuhTOb9>N_@#46g$MPvrhnX zBa#73s2C+pi632ij2tz8G?F_?eC8To%*%ogbBV9{W3w^NoqfSYqDs;fGHCf=-bHVC z`>JD%56mvWIxs~G*A(wNfWX8{5)$hPhnC2DOmc5bjSqjU2hu4bt{6$gpQ?yru5LT^ zOmv5NmB{?jO!X(QlE0I`ZQuX<`>%q)(hHuiNH6Q;{y;>1!rvu)y^0iQBji<Tk(EJ*1eA)|Wavn+X5P{LXygg!)bWwq4&`?Rw6) zznk>;MZbD~*w2S_mGH{#o-LKHpIByB=>L6l0rx%oIyyUePTk+pc{a~bp3yu{^1Q-R z-QLl;54VG70r3Xc)Ou>lW>pk?1Mm$9hRbfQm{V6(TXVUGch4omFCKo8$1|pO_T6); zX3PwE29GK5j3_F)c-TdS7ZpmB;k9#S4E2n^Y0R)2t7<(1BnNLeG_!V2-Q}JehkF7Q zbHa6H)m5|V%4gm^)Ds-;@elV*ukehmnh~z3no}{ocBto;;hr%S)fIJ~0VhfuD1Z19?ii+uVs_0o|HFtaFRLrQV3jt2oHqL(U<>{3LN#rt!z1ti%=3;;^RiAqg#7Zm9qSy%Gm`f_o@;r2&(niv6_3REdryM!r`Y1# z#8budIZq!V;Tg=?(?5{z-*M%U_m#Nv^dK)k&wrkMtn-0? z@~I}iJU8n{$zIZs{A7B5m8-h8ysX+atAY_V-BnptQ9a!io;|m$I$V)Le^*sasA5LN z98Xov^osee2hKg#d7*yxoCE&wINy#ty1(uBB=Pp}Ea3YQ?dQaC+|h*3sIHwxHKvq| zyV>RP3?5kL88onNP=Rak7*D8T*6i9jWpnQ4EB$?L8J;r=TvuG>npIX+o+Rk)rUNWTvH>gTGz z6ZUm!zRv&a%6VCr9XzQHz4Z6{7aZ&S51uA=s?(oge5XIF2ixJ|{;z%}QnvHl#``oL zDf9cdb9nC2-{SWm??2a%Q?HZ19sfs(FHd@UCrsQwk6y7Q68$8KGZX#Hbejum>oi# znPqhz<~0ZN=Try{uBZu>2~~Ebmt0;uYj&AT2=i1eR0U3`niGLs=T(JfQc%}`te7)L zu(QTvzsxMFna&h6BR&7>TI#OaRz5RabEjwUEaoOpl`}%7Rn;&yT%n5jAs3TX`JJBH zxfOFNt83@E7}(XisPeL!8rn!3!m}!9Sk17yaNX<*#`APfdF`CqaEOVI1nIG+S5RUpc3CmWSf%s%S*n$=SQkl2Lr- zk9(i;EjhZb{|3)1c3xpm)E|Sts%%?!87q2#Q7%qJVee98m=nHttkW}@cb?gI@P51P%KN|Kxsf`| zuK*5}$2uS8nJ+HiD|xo@9F}MLvCfL`!x!;{c%nRK@I0hldH+{DQocJgGb=0mWX{Q^ zJJ4sFNf|V-e2{C<=*a`8Ux^E^;)1-_RFusT@?KprzY1#TVa+j%IiqW>U{i3JTV(m5 zczAooF|%TR+4PF?s#&C(UNxgCl$xAe<7c}{DqQm?xrPma(WzzP#Mo)W`#MB_ZgnDkVi`M44hs7O9zFl(y{DZST(VX5>6>S z8(qSzDpnxmG3W<_TsKV|J8Yyq)7njg536vcV%$3@m3u{9dD-j=PhG`b;fk8_3YXJF z?XT8QDsfK^apf=i$Im+pp*i;+>lD{{|0e0aNgT;f;>{%gljDDtpNd1hAHIjR7HKWq zZ}J>oVCVlVX&>kPO#OXwng1)l@A=@crid2*d?*Uh5*5HJ-g#t*NSZ!OgOWbHSHal!b7R$<(+) zGb{L+!_qZe6C%>QnN`&luJ3%uHGDY#!(CTg;kxn)*XS!;3;7-Gy7Efb<(IogSJYNs z=^AYlR9-a8FJ3Goacl~BrJ4XdrM7%==8d*nqusl-J6F5+YIm`AmuNSp-G{XMbL~E& z-Ct<;KehWi?fyZ#f70$V+I>;Gt=e6u-B-2yx^~~v?%UdZPrDyz_ap6os@>h%-K*Vp z?S8J^BihZFV8fx8c2C!?JzM_m@ASp+96TYOdY;$y(}?>R&u@7C#Pc*y6VE1|sXX(e z$2#xjS;`aV`8S>?cvkYP`-*2KegmtKWeglX;^G;uVTB_u`tHS-Tzc8aBG>TwlWNAf zSOZp97q|w_=3aIFoawN3wvxIH8aR;oUDlO@s>5YdW{2x$R@!JV2>ECn`Ps7nbDo|5 zWPSelPsVrJ`T*&s=(MB1VICcvkayCr;@#!W$jIa`OaEn`^w;el{B{5T_?Pp)`Oh8^ zg~hg;`mImB*)PQ>eWyNiPC2DV57h>j{EGkIX;FQBy&cz;m7Ds@pE=Xxa`oupcJ=V| z@DI*Ry*gN<4CDfv^tH&w6jgIxx$Dl?ouQd zFrXe}9&pN|B{yEKOiStcet6dGIwBAPMfcQ|*TO^AR>IK=Q>~b7d;U z>3?IS&(dXRwp^}u+V_zZ3hIg!t&B)&*_D#sA+mOF-W<(iG8lJ%@-T2*LX z6*$E}M@+Bwy=qc{5TMXZWGhwW;4D(Qnu>W&CP?z9Oc8k~PBpyWlqnT=@l!UPRr-{= z@HBoILGsJy#*`_u5sFTkGQDapzg4y~O|9ssOoNx4GG*F)<60)*Y9 z2=ya{+2Un+5gDtN9MagSlWl(JSK9r9cK=7a&uaH2?XK4D2JLRr?wi_uN4vYUo6zol z?H<%_hjue>x6ALLT|>KlwR^U9&(rP&+Wn4pFV(KlO|=Iky!N!(+Ukn38Z8`mWDW{# zQltakTgXdl?`ZZM;oAS^FXs-setoptSG!HxJx99(v^zw*Bed(pyIg;JwHwebglW$7 zvQXLI6s$eiJMhR}fGsum2=YPM*>5V4b%WY6K;|uSc9{z6s_v=qAlD#H`Am33dlxJ< zmdI1`(wPXsSConkV~0Sv3irdu?uDo2naz7KkG%gg&yRGPM$$=oU3X1`Y=Wfy?3ybf zI-3A$%Q#hiUCq=tlKna1*=&F4_G+nnfGuy?;e7*ktJV8BwQrIF#K}3b=>RZK z_U-BlA$F#dW_f*WOO%HKM z-k_R+0}EMAxLj{!9`8(lHt~J(BjG3e{jd1mOgYZu)Je*bcNgYp(#wqF=RA(T^X<6y zo*Oy}vt_nXuuxneM5JOih*TA-f~C`&HNc|%o@dp5UFHvLw|;@`p0B?ftL*O^@3dWy zc8j%J_F=Ag+p5#go@Iw;R@?6EnYPuei^UbM(sA}@FwlHYqw9t zPWQTYr)u}xQQLolb{n<3NxS14?C@OeHfr}z+HH5fHC_}hwBv8m?$ky0_hZ`Ksr$KJ zyc05rSKH;zUgfI1>7iK31^m10l z+U=M6(;j`*-$~16^(hguv;Kbg$;JJj_H8rG9+)cDdWsQLJT{Xy9(Sr~Vp`P<*Vis9 z|51BV31ug|nk5O#i*U_sSbi6~`dxA$MUfO=z@p4!v&#dg^Ww3&Wtr>;*-YRd*guCK zb>=F(gdI(lrZ<9AakKs(_TB|PuBzPoUr8oy(hD6*fdU1lw1pJfGA*SPC@{IT38cx8 zq-~JOM@6lQ7!i0Y2O%P25s-*QtD;6MS`^y< z_gQ+=FNN zld~T~qU`(X?2t=deLJ1~0&CjjZjQ4?Del}fG1pXNfD22eJDT*P;@4twPWH98 z>bpRys;#X`E)ZIo1j(Hvne;FcOAJaEgei|Uu3t#0{v%;^(f+yKnZI?%oc?my=&^?! z{ix9g?ss%ggJVBeezw z{pma3Q>}G%is=)F8zbA2d~2%OTDW<3nNO89;i|WOZ=i<&{zjMzZ6k=Vdv~laGUNbP5LfnXjx84#re!#WQm}ot*Rzm4ZBQ@$z&}gGpl`7V0) zlM3gy?3heB22I9cvU^DsyL{%XXi#{1X-hV6+ood-DN6D^clTeG^d(@%l(+nQrHm;#N)<(kd9Z1Pn377e*B9S>}pXrP` z_S4dQb>wsanbOjc=wY&jE^*OoJ0Ey%lhfa$+nxSxrF)M)SE;)`>nY#kt*+)i_5?Cs%>NEQ8^g~AS* z!prJPV>?&QGQJTmqs6KJ@doEKJNn5+Kf|EU;7o(_4NkT828mMtoQ&E6udiyWuA(i} zw${lFK0Sg{GaZStYIKT9N=cgtQL-S$DNf2TqStLAG?We{qqAzOTEiv<>J%cUZh!~{ z$f+!gNh!A2NE- z=GMQZFrNuD~#?n zdXCW}KXk%>KdBu(o1`aIk5Pkmo8d9u!}@U2$=9$FP@(6Up!aFH}%ca?LgQ*Qr$u!g}?yuBoN0 zt)o+2NnAD=&Iwnww|2I%^QN5#COSgxZ8e;;S^anJ0dRYuy~C}qX<#l`)7jR>U?=V@ z`9+vBvA?ydnV8$1HGZY16Y@vf*KsCnY3EF-TH=hm5e!JWlU1>nL>7pxU7zt3y)fL? z!ezhSovm%m{d;{#XS9i*su#6oChgbt%oOiIEvj0Z}7%erbC7|*2;L0h#y7A4?6J<8!g1tY^#q?93*^=ZbZ(S z2wB}{(%=2M(|(^b`rt1d{S~A4|JKoeW&PiD^xqpjcEHgOS^J+mx@tH5)5JI4=-sb4 z{*#S9^qQkjxBf<-WA#@Z|I4lY8;+i9^ub>`dXdp%CjRS;UTO4NqX&$Ai_xPty~C^D z(LP}QPs6U_y3WUBO2&OzbaL*QDc5d*P+>`k8APPBLoF^W%0J(^3&-q0PA2+~>oyb> z%=y^dc+;NtTrCd|FxjA|R<)VHk)R6aCuSXXZqIas&3k^o?BcWC(^)1}l9t96NlIJX zOLytEHC3uwr$#biZ{FF=(3K>6S&lx_Xfq-+?My=E=ssl}vbv#m*yW#d_2ac?sAtp+=i!VjBy4x8~i5sXf;<9YhF6(;x8!~`7YZhXca4*8|KFVWPBwbvu%n-0^w{4WeX7xe=J@9s z-K)N>*Fk3)ec+Fd{XC;bkCR@fDd`I|)50TK>1dAVwyUQo@!TCt?D2-o@of6M=6L<~ z_(ziRWAvwuzwceg|38dgY5ZTXx+x!9UdrhEjep-!C;Xt%2mavbwmf4-KQ;%re8BjS zbbWE$Wfl4&Cn>C@^T{(NYqbjxLD2%Z%c1mAcBD}?bjsG(sxEs$+RU1^xi+n;<}r|( zR{pSamRkVaL8}RKy(vz$;d;)4a?xq6iB>kf$&E?|Fg9P*OSJ5&WjUa=wvL%7=Npvv zk1->cxO7!Ytn(yB+GJx(gLpKsh_pu4wMq0%%s}|tT0hrM1TD3#%?4<|wIh{X(ORed zM#nqtFKG123`bvPbidI*V)TL0j(^DL{bL=y*618pBW26htmvv(d)-YRnXzu&w3=yM(`u&`Oe>sr-59j(wwy$vR*Hitl;{2*z4x4zR zCpqo?h|vd6arCsC&HTXlk2CtfM8|)U(St@m&FEgE`>g$mj{SK?j~acBwKuxWXYgdl zexdR2Gsn02ABKU?2edxId2~kY?CQ0226NW()wZ%CD3i%L9$CTBSq(2VXFyp&k_~Ju ziJBYea@)aKOI51;p(96YV@HnGgSQzj`h6}gy2ztRZcn@P1BQEy?Q}5Da{QcgK3!FA zoH+Aa_YHY26{?&OK|h`8a4xK!oj+!qkh59H**_$wXuGe-*<&R8jhvq@mo}yidh1A* zY;-gS)|YtgzU$}Cno-L$%I0wzZ6djqo9q_5yOcGlk7S$!d&fUx|7FbliG#J#~BXQnyM6fgrh zY}_w3?$*v7=gY>=?N(vj4ia8!)>eirA=*lkYO{AWDx^~QnaZkaU&RF|{@nVgZ_&?y zNhhKGFznF^=W(aS*;YH-+}dyg+VEz?P4pJKQzH5Ppjmm8El=()rdN9s*P%{yvgqx4 zKFst;Nu^2kc{k}?^z|M&D$q3F&dNBziCNNmTEqdvsH{m9Wl zq|b;PEAil5M6T_*5tPN#Cgy=`&O^LvK>tRtvt9MPk78uo0gtumw}6;C!!u?uYfn6S zXM7VCU>w@b;|Thpgf4&N0>6sPxIo4Z#uR-{bXtwAj~O$n&byG*WuK?V*v4jd0!w3J z?rxr6m>a&7pPFqj%YUJNrt){O*wN*zc*HmI&Az}l%^y#{IYO56{EXZgX{xH{c9qSa z^ zgV>6_9QT)qJ^u0H5ceh|@ThZ)$G+yE+g^BL4q3}ac+&~&60+?v?M;m}u9wi*RxOny z_xyMQHC~q1&$dcw=pWF|D zaMhfw>u1b1X}r*;`NH^Zu6UYcRgG~_o}!gFXogMPP1OUX)6;ctOpLp31E=+5$tw|N zaw2w?*=1IuxP8{9q|lhA%d=#9cc-d%wr|$Uxms@R@M;7u;~huhWs)LOQ)YAccScwQ zbGAXqUI;lo)3Noo2%VyDtvB<^Y-c|8xY46i9sLJJ_XZq&pV2Fe9Q{W|4_xBtFB*Ly z$I)Lgdc@cd8olo{$Nzw}Kg-d7XY`=4AF|=kbo>t)eQ1iKzi0G5lRkBalb=fCKg#Ic zvmN`fM)%Hi^hrkdpYQ0Vn&L437AL;o49993=NNrxx??}Z=mSPS#pr!T zpJ4QEqh}gDV)U<={KXTLM4b2E{OM+c`rM95bhw#$K4*maZ#$Q6H4XB{CFggZG!1S= z&WU@N?FL+(Rhdi1F_|*Fcd}fohcZ1|md!qwF%sInDp#@m%TVns*sAl!WG0l$)p0!KfJ3IP?9`S|d%VevATB+u! zP;OCSPMOlHk1~5vA=yGKY6y#XxygnVy+1=2nCbfrDGjPd|0QGYqcK**k44{S8-nwj zx{}COUNwimmm9uPj%pmN$5n2}IdfLFGhl>uP;QvVgCFaW2sX!9fpJG&l)>!s0;Q(n zUE9<|8i<6w7_PV-XDLE1&$W@n<+xsT@qFvzFdb#Cj&tRh%g0uV0 zfv%xpCce)(;RkMT!e<%1Z>6K}wDFrzpSjB%&*--&>Auel_kScQ{IG*tWrTifz1SXM zdI*JSUwh-6o*G+KD-S?9n@2c1G^oywx`nLNa+lTySA!-$%G{s&mC>WsPX2yx^ub0) zf5+(FtDDW5mxSL%e%F#4cb?;LM*f4k#9(dd1@cjAvH^f*N3 z2l}-W!_M+@eLz2tdz$M7nE+TX=jo1Nx4xWr62(iAJu~#|Dif3e>p}En({0(dcX0%*63d2Ut#n>y%YYUMpt1+ud(5c zf2+|W4UWG%EyCp;NK6dOtCAR^d0r&`uv)S)x=o+=ZYLin8$8>f4)Ou*)6T!0S8T{z z2+d@9*z(1UWyjknddzhqD}vm;dW)ItNo!>x)@N68XVlKIZ|VA=a6H`SEQh)5ZMx<} zWa+|cyk+^&k)!t_{m6?*6bT^WE>;^m)%ryTF8PEFk*w>xW7ac1uRCn!c|$ilUiLmh z+%RgH+?z}M4&N-d&l0~x_-p;Mf9fs#bpM|&{ri!eIkyL_joh|N{D#bN58LD3EGvkK zpG{ZX|0?4bbufNB6Hh#(@wfWW_Z&7z_)A&CzBA7~db72ZJBW#&J#NCKMP9Sf z$lXqEFwP%$^yncz2LD0wBL2zpLH7R{KZ*BWwV!Tmp9B-R8JVTwEcX;fBhr4yAtxYw zLQE#X7Ue7#E~3SWlU)}hqltEzHaYqoy+ zg{!`{uktflcRqd9Kh{1JJSp!dS9$*aZx5U@``$(KKC)z)=uQ~hF75r)jDX`l#o#1^ z$=Acuew|--PHtZQVpUaL!%i#ZdaH`@mwt5`@^3~T8Isd+T|nE}`%deB;OI9V))>8Q zjvmhjCe~=Y?Pp)o82-_Z1bv}cY7TgCb-^Wqo9;cm84SF+(Z31oKI_THUIOQz`_8Iy zxhgt%$=V-e=c?HLPpz20I#=y0u5bSSj$C!c-IWI_ewiD%>+4--?m0Ox_@_5sI&k_m zdH4Ku>8R(2qVenO$L`qwrLVrQanVIvdw)9ZuZ5RhvGq{i`x73S_rRC#dOqXZ*U!jp z-C3J8YM(tVlANTUIsU0F>0kf)-Y+bE=iRG*n^v-L>AW-j?-s1N^&^YE*M9SBB`0?# zg^H`qiq%t9!)ieX*VsIO!9S%!%$3ZJwvbu$3UR|S$w?x~HqQ-l<4!+k;5_`2l~=Mf zyRdj>!khC%eJ3~SI2HJ^$_kg@r++1!`{muD&V4kqAdywVPV>b@B`apCpuB31sUv&i zxI13QdB47eg>9}ISgp(o7L}Bh>6pq}xk-H-tMa%N_Xv6ahg?yxrH`4cT%hnUHm@1w z$|ULjch<7vJevwThjBKBOTt5PL5G?(Q=S5nCC3@B6QJ|D~A9jq!|fkMr# zTYvQEk3N0$s2^N}6d@~-CdB7==9f-sC>2S1Uw;3V;r_{b<(G&12fr{}4@?{WetW(z zHtZsOQvPuNl}Y-fr1WJa#pg@Xy-E7yB)#(f;py`wrC$Yyrzayx_a^DSBt5obc>I+& zB>@m}~fcMZd-hLSV!}9yE{e0MeJ{&(j96vuifBerof7*G#hxlJC0bcC2 z6j6g|DhPgNB1^OIZt#aJj!c0MfM-uqY8KoF-iOSG$H1#tJXr`2fQOMhxH?6tTaX~U z8{CMj6nk*;sY+GCeP9>T1doDWKq6uTZbLfYec+2o7km)>8?p{Ai#4A{qVR6;%Upoo z1&@J8kbA|3#hjVQMz|jgBR%j4cq_6K-VJ^Y>4W!!`;ooyL2%R=tgOMk;9O)MJOI`p z1Mmp=b7a5RgBP%HbO7!Le}lXQ9|9+xsnmOLAGiwf{F1QXw~=x1e(()s5_|}}^DN37 z-VMHmWQn^Vi&B2LA6$bhgh#=RNB|xK$N2bw8{7*{MS@}v<|F0sAXtl3z$4&u$V&JC zIBTj>4Pp;gA{}tS9%LQ754`alrEY^qz4}f149o`Rqmj%Fp zaB%qrN)^E?!QUa}@F8$I3xt($KllpL1Rn%_b7;eG!5K(YY`~ki{4}_km>>a}0PT_(vq;SELQRd;!OU2f!W3BzPbA$R%`9;o$g7 zDO0{3}ukSC_LF7HNPFf}^jXeZsw9 zD{>n=0)8IphIfNkUPXDr17I`KCpO?M$fIHdUK5}^;X$w$>4*1$-$eGo`@x?h&%y`6 z)NINS?gej0UWIpq_ag`4F|aU)_6ZMy>ySh6DEK0B7(NK5=Q1W7ARcfUk^%RFS0m%# zL2y0dg?EE5Ad}&P;Icg07Q7Pt7BUOo4|?+%E8v1-m(YfVgRuh2A1?T2A>#vl2>iy? zj0^C7@Pum^hs7RTg{*@|z=w)R2RsITYboU^_FzphWhge_vq(RD0Q_nR?M-aJmyrYT zLGT^qAY26*myoyMLGU5uFgyl6el6wjCSk#xQrZuG04j%$nmyt(!6np?#1doBE zmyt)f7kqd*{S@8@-m`+XBsO5~4YVJ4P!L%MkAQzgZiBOPMSTvr3*HU3RWhc*qhL-g z=MH!fd;xh9J_x=UCcp3@aAO_q0UiUtj~svxfO+-g9UcTLkhkEKU_CMfkAQ2CL+~j0 zP2@1VAAGTaF%3QlUe`$5_%&g{-N+<(AIRI))f9LH{4+8it|EN92nmQg_tQy8sOdFw~+|EA3UjzaSZMQZ$fT^N5Ri_(AUHsd=BY>4}c%#0v-WBjywvFf}cV5!n?ut+sUuEgWbrp;tr1fH0=cL1s_26!(-s%>uDSCe()9K z5PT3^bO-eg4}gy&o`cwcHJ_uO!z19e-HfsDO7IzEK70TiMDpN*<#&^Ju>n7ZRKTO) zBgjg4A2{Y7$^q^L??EE)82ARV4n72a_6wZ9;N9RZ^vH5Pao+%11aj=gagRxE~z7k+On& z!BZcgzrcOqYl!E!*n=}3WXy#7!CR2&@NV#&hZsY|9c+wI*6;`zMT+2pZz4hX5IFu} z@&@;U-$qu#`@z#UbFPQ`z%9sa@ILUhM;J5VgJ9Dx#$R{@Jc{(e)o$8zA7iGtgA>0( z+kpGPJHJNW;oacfkJ1<5F>uw_X%Au#zKLY~jf_JP3XU z$%1!-Pwu6izz4unzC}9`d+-lP5qt=o`EBwD_k($lGbX@;V8Qno2ZV#0koE9BaB4qo zKa{(fCs=IAbs!w@Y7Gw9^l>J-;jN9^#jINWB}d=W-$XXR`@#BGh)e9j?;wwg zJNOo|7d`~;c#SfF_kkz>f--^ozyPuzF8J~wWda`r-$jPt>X)2Pkwfqx_{Og&ledTy zoOpn?0r!FD{hGD`_k)-JhB*T~05%~D;Sq2>5`cGu-$4rC{ovULDId5G-1=L}1l|W; z@;j~<-~sRz4Ylg@#JP1C6JSpzrb#GJe@JjH^zc7Bm zec;k}$cos51IS^q2iN?Ky8Z)o2iA;EQ9gJC>_-;D1=o*BQF-ug@V66E)JpgeILDi! zI^ceA{D~S5yHirsK5+*JPD@d*!UZRso}z}} zUhoR!J$L|IG9^W2{E_s7Pa>1y1A=FgX1EXZ^R@5kaKY!10);X&{PWI$}dA!I*X@Wd?21nvXp&rDH+@Bp|Qc?;eL{vA04SLb7c zyeBr`y@>h~X$BWvkfJ>B0N6JxMNNVWzKzU+tJ%~q;)i#GPa%uo1K@-?lp)*;u11RB zQE>8wDXIeQ1AmP)z=yyy{j>*h2k)Otxxr&#^E{3LkAS<74e&lNI6p;ggbRKd*(vtm zsEa8#xEFjJ>4*1&nG0w$;tt-8JS+C#rb|-Pi|{^h+(ODqY(UTDl$CJsa>O%4Ie_O~ zMO%XV!A@j4JPJOMO*y~^1al|{u>r$K5j+CEfKe6fXBe!ARELT%vzkHdf=_Vgs%% zOHt+UDEJyu3m*h8D5u`xey|-`CpO?dBp)ET@VyzYyX3A_^Aigd&K zz-N&SVh=WbiM+!j;CGRo@P6>e$X@s$n6iPohI_%AkZ0jhun&0^E_nWZ)HU1>Rw9Ss zfi!hvg%5x$znsE(02}aW#4qmP?>CZ9_z-yagS2ya41Dk*;uQ|w5u@F} zyTLUNQ>O4Jc=jgRG~5R^Y+($9N5C;#$qU>IRv=HpE5T19&%(RGJ9-)SgoB^hMxTX8 z!9&OpTy0NLZHPLAJGcSy!eijr9n>A%3(iBP!vo;G$Skn|pG6kJ2f)wnq)y@8;FMkD z5$*$LAQf;w_$aax-VaXSO+AS__%N~#-Up7_L;r<)!7+UtPdK;<=@lDr+E<7R?gzht z^uznX-yqMzhrq~J8L!}iPay~31K{Ydk$1QktViAx8*nv}@ebtxZbZhxV_@y0lnFcn z{t)rO2f)T}QV!w{K8WOrJ@_V41Rnys_L6sa6wLS*alyUdbw~tW3HBmUcpvyKau-~E zJB7y_s1LCL-$#1I{W~e@YGf}w2z~~665b6?c#OJ+d%rcm&*y zbin(-=aDY>0C?Hc)HOT+UiQDJYvJHGkqz*EaLPXVEZhf%k-hK;_;;iqu71e58yOHA zumRaGHel*Av~#!@tV7-sckoT*FnkC+^FPS@-v|pnj%2|5!9V-yuctA@KD9>K#4^p8I3!8twCkau9p4^r!fV4VeBCV-ws9ZblvxdvMy%Xaiyo&e%`;7Y@ddSK)&1AaB9d%Zvla zA$SygA5n*K|2gL!Bm*7=zlwO_{osCNGJFvH$SWzz2d@NgL1w|b!TXW<@ECXqSqN9J z(teNtJOF+kDG+-wuaF3Q2)yqXlodP%zJx^KgW!tSDMxrEcq?)* zyc_)EApI6T1YY_}#s_!+d=cq~4}!magYpp@aKi!G2|NbA{wC=b8}K{7<-GPTc?VN} zPkVrS!L5i7-Ul8=X2I24)FUzRN2T6aLN^0r!GmK)T>D@Cjr+d;mP*JdVUt$A3gY1J3fGdvBcHxy^J8}RX1;2nCgvY>LN69a|4@^_3>JZ!uK82`%5D)lb zN~-e02f;C^scH(`3q}ziT<|N1AKnlC8CeKdX{l-s5`g=`wMc>3gZ)TQ?7@kiR8;}@ zfyGEIJP3XQX%c&IH_`#`1Al?6gAan|rl+dg;C}E1dOFWLxOgmO z^&aU5A48_V`^9Zss+tZL{5~=ZJ^=d0Q$BFP)kpz63ciN~;p&7`^<$(0J_wHUQa*4m zIQqm?6%h`$AyIe~d=|M2J^*G;OjR4i2Fyiz;6dRRl<*L)lsJ_vSArLN&o z(0>m3f(w2Hc@^FdzJa_29|D(8qfX(KVEuI3{}GM_-hzyScY_ZjUU(n)ePj}R0Q@B~ z89oH2%pkvTFE|zP!Tn%1G94ZSZ$Pr(mEZ%&EO-ojAMwN0xzt@2{Rr*_y)#o)o^bH| z^HWth+z)@l};Qz`Y{qOpE}5yI*+*Kw5DO7aG;1ot3W@IG+WM^kx5#G@kM(>GAgVh=745k~C6ah2o+?gei{ zBJgf-TovgTckr{wUE&U|t0ph-D0qJ@d4b136{h^*g6AMl!u_D9j`D|l!S5ma;r-wn z$N~5ecx65H0uO-A$YFQ{{2HQCum|5m#=%uXs%k_g!6V?k-#V-4(b;k1HXmrhxdc;AcJt#naWE_Q`JFu1pGX5 z2;MF3t0^CG2Uo2jKdEr=A!IT<24;3qCU7r!5;6BMaeup!X)) z30!dWzcD@w2YWw3IfxCo@n+gQJO;MilB(9jqu}q5Zuk(`dMo7skAUMo$(RfGg15?5awp>+JP02C3}p*fcTpan zrEQ5l_$OpOdMBoG9z4uVg zVh{f83#rU6$S?TudkF)Ng74gq9bA1mRs9Cp3HNVg+(7n%HMGkD4O$qPIH&VPcu2nWA}6v1QQmLHH8cptdw$yC)K9Q+&7 z0as5kRv=Na2k&~CHUsYl&w7UT1owe|Ms|w*f6zaEMEc=^ho7Sji2L)5Z^*0gK5)U0 zNi#eEZh4VDBJSWrFH@)K91HyAugDwR|0ZROEQAaG90|Y&!S|3NxcW6^`y0jtcodxb zK6!x$K>w&TbsJo8=IAuFQ8?Iy^ui zVt-PaYM+#*^59YM&XdzrIlLR}J%u!hJ@`|k0X_)6aw_=|4&FXFjd>;Z;7^claR;+c zOH=p4gWwO54e$Z5=nTq4+`+v_FT5Z8&J=Xv;OS?ksb}Fnun>7w?7`=eLHGbT=`6}Y zY`~Mwrc8x{xk$!n`q$Jn^*3Z3T%D7qG7&G_3zi|1;FaJ5$P{=C+=@(x_krI;vf%yT z+0!UDxDV_?=EDQi)6^}qtNMc?ZEO&P`Jb|46xmHzGx{ zj{4#6UoL@^^i-8TFU6lSZnPSgli?p-IVzGK^z^3=aQYuJN{yK}%Aa28iKGQn`%^qz zyWZ-&2Rwzk7+sp?PpwRe9G8;{?KjWZiJmO<=}|eU{*+4mQn+sRgEmcayd0M)vpWyg&*s6MCU+V}ir<#K9M^5@<`Tag(;crnjl%CS z>9xm@cr(&f#;qQIT4ib^C5Ud*XVdGB&&{pByUc{UZERfjcs7h2GsksIn?Gr9!lmuE zx!S%rQ+aP4=O0@+CXyM<=pQxU8B86*&YkC5N2o)4{E_;Xgq8Yt*O5Ez66T4dKAD_O z+h?r1?F-56L;O>jBh%A#{@lkDKPxu979~B&`ErLHH%g6baMkMx8R~@6G5*ZTjL7KV zsD95t>fmwnH!L3VTAh?vDf8+P%3RX4$&|I!<0e-F{kZGxc-$RFa=ylT(#NKpuRLif!DG&8 z?)W5a?lJVlOm$+)DQbAFQN}h8?y%CHN?qfH+s~ag$=|%;F{XTQy*lSccO143laDFx zww#mwGCe9Y45z~{&Y@2Y$n_Q;HYm+0zZ;vDCmUB_^IZciyeNOz7^Xg86 zn%^X$lk+7u6Ef6!6RpVh^t z&BT4Yu|)FY?l;L{Mw(|O=g%F+T@Mm>wX09M?S#*BjbZLI951c9d`>*JeB8&>Hm4nH zbG$g+VRapyIJ}M|+!M()LVdc^FXeHM>wF_^?N)b9V=r!-lHBCjw#~S?#P@=ZqhnpX-J<74p z7^lu?7{^#}#8lc#? zE`M9*7TtNWZW5-{RcE5R``AcxQ;FvulTI7YznT_FdydJQ;MjCEwjnjqhLUxOU)pxE zyOfWJq@~TIMf(0o*AK^Q*U90ft-LY97?fO3$$7|RU64G)+oZ&M?shs-I&^!vWB4^}@>powr5#7y_25pY^|x)totESEIcs}7Opc>^ zgnpYG_IT-Yr`27iZg+`G&IdM5DZ^V`^O)o|WbNF?v~J00%h8=DYa{h%xkY!n+&sB` zS$jd5Us~NAMvg7xjcz~Z9ov4~w(hdgW9jL`$5LrW$=6_VU7m89vL%(0th?isW7&Re z)9Nl0n^tKv|7v~Pz9R9t>(9o2Jap&V+Kfq8V^Y>D=d4eex%)_R+3WSJJC9w@I$k;3 zV^~aXPww=%d9vL|eL3029%Ce1>u1qj{?;uyKDUh>Z>;SHrPb{x;gaLEag2nvE(OPU z)EMSL|8o7>u=5C;e9UBh!YDPNE6s_&4SRQ*lhgG<=@_Z3-Q|`X*U1^`*4-R^GNk&Z9~Tk$+v)y#XQIFAxz1;X&vix$=dPFJ_Mr1|$FcbkJNG%&j#ch7O1$=bZTraw zp*=6S%RISGrMxzcaPHITJn7hUYWIIX);&4>?zASCyQKN76VzGj)79iIk23SjqL0eC*^rCi@?+jp)3U9D6)l#@he2WBuLrZQJAV#&36h>h?4~ z#l7m2qVqO6(dLrt&b>~dkFoyPbA-F>lk+Zmjy;=F5AOJr{gT7FZQXfx^M7@ma;MQ9 z-j<*HSRdrZ9-<8H`{=z0|7sY!w&b=?&a16+ciQya92Dx+EQPoVLVOCv7C1^}jSdGP>`aNkL4$=->q$(+cVjF&Z;39QMSz?#f>#>Uyl zjzuH&KX-XZx;K#*DTjHIXYO&fC5^rAx^bsn!d4S@{OI)YrI~4_MfU4`Ajxe+!aZTG zuP0@EYEoK!g{wbpfVTBP_7e6D6V_>)Ipz$NXq%GHC)^_^>C)r#$;Xz7&8s_KQr^cq z59o7GCgnRqT*nK;o=Q8a>NaQNAE~b0WopwVZnlqFx8(k1-AB3ynp~bEwF600a-4Q7 zOfH*|!t4DbX^C}9iO-6pf0L;*ImRYi8rY1~#>D1UmyN_@+oq(&_ScctaqTe$ZJF5q zX#K3aMH{#HpOUFgSwD(>-?E15iMKVp{*V6+z4m@c5;=K+N0Os_nXD$m2pGr;n^tUPZk&(q5DwepOxJc}#O=E`%X@*J`} zXDrVh%X7T)+^Rg6D$mf$^S<)@uRQB3&mYS(s`6}u7e9G!SDwq2XI|y`VtKAuo*|ZJ zUFA7jc}7{DO_t|x<#}0oE?Ax`mS>>l9g^~XMtR4iysuB*K`HO6koPpoJN4w9k^M7p zn~jZM>G$%|iX?a(dyqBvI8N^NAvnB5vl=pB2z$os1+~xh0@;*Izr=+|~ zQr;mc?~asrMn-QZ3~|bP0p?n!wMp}ezF-cKp-v6OdB%DX4!U6k@}O?juL zyfaYV6)5lTl=lt#KzZMlJOeAwfyncn^4y<1Zzj)j$ulhSOpZKfFVAS4L)*I$xe6&p zRw6edUC5ot24oAe7kLJG1$hg34;g!jN1cJpK`uoKk?W9E$U5Y9ybue4RQyv0ojUt19=J=Kwd!(B5xx{kc>+`>QrPZG8JAgM1fx9@&rl3i%7-x!j{BA=8n|kgJj9NEqorK8f6oY(n-R-$tHBUO-+$4kCvT zwTM06$Qj7F$b4i8QjSz3Eyx<=4&;l-R^(CSd&qOhE6A^rcahOoP+rJ1WF8VgijkE_ zBhrQ3f_x6yi0nl6B2OaEAul7pMh+vRuJouAk+YDQ$fZaDvK(QAruC&Mk4jgg)M!3G z#@tGcKrvqO;*-fsVmf#>M9jb*(yin zsyvmi7ON$yKozR1)itU}Emg&;gx4Wnt4dWF&xS2i%hd{1p{`ROQP=Zj&5x=Zcp+h> zs#4XeM%AjYs#Eor=Sb$prS z6Y6FaRkx^H)hE?$>Qm}=^=Y+U-J$MOpW)fE&#KR<&#P{Ax4K7tLEWqVU42n~Nj}|a zK1>NK3RTtCI-k=uN|X6)qV-Ht^%r|34nnr^HSo69Hb<-Otkd7S8LsKC?rQ5AzV*=@ z?x=G7`S2QFHdpgeUnfF|!ue>s%Yly+)=+kmJ{_WCZ6xec zJM}lp65i(fXZnMtMIl#q!d+p$f+$}raYDMk|g=&rW91Tt;-1{4idFXbr*O`c#Ryz6)7(A9Y&KU`35WB@N9P?lfvMO;?zo9 zA6A6;y9^fY9 z-_bfOWgTr*HR0-b%}J%X>Y}qHe$X|1`9kx!Bps_KYTwKUdf zE=^9i1*a**n?}g_R;8HB2e$cctYhBVQd6a?8|R2_njz;)nDO!t=`T`Czet^8eJZu#hZpG^-ZnSd~MPgHng@n>B2{N4c`!U4s0BntJ>oRjW$$cOI>T6^VRLP zcwdww$hZCC2N+iP;+x1jN+LU=v_@>CosmS0jqNq|o8Q`-55^~&R;aN((U4FQO%_E` zW_uZG;tQdYdD9bgN6_)Q+j9t|!}&U?*rM6EI#(OxE~l6xiK>-0THV^(6y`H`5=!(M zsbIdu7-P^b}|YKJ;??{KG@;I<5*@?lNMv_s?8I2-FQ)Rh?N+M%9ni`w=*0b2+lxWQ;uGWkeZ_RKg2||W9P+SrX6lHh=#UarqMKNs>>TZ*$YpUA# zsGUxe^EFzBw(*Utj#gKlbhNtL1mC(G)|k<0AWk`S$Tvx4%+y10XxPAn=EQ0J^ao6x z;T1nu!FIF`Yd!X?6B5!QAKsR9n!GgX6Nc4paok@t_?E#V2FDdR{yu{X3>Fz& zX|T=U?FKg({Hnnx4F1C4+XlxJI`L02IL}~_!D@r64R#ysG5CbRXAJ(r;9CYWu6E)- z-Qf8KZ5afNzT9A~!8U`p8vLTc9)r6Le%s*F246Dx3xmHic*x)pgBjO2>6&P8iotUY z&M~;a;FSgo43-;QX|UE{lfl&nKVfja!FvqeZ*Y^rodzE@_?W>b4L)b^C4;{(_-lhh z1`iudEpqZV&fqBqeFo1rIN#uv1`7<98oa?^lfe#ypD?)I;5`O68r)*A&)~NWK51~k z;Li;nF!+|izZz6aoqUWjc#6Su49+sRz+k}O)dtHA-e9o7V2i=k2BQY=GbaC9;f~zSwzhCf2cM0v!Hm~29^+P6 ztA65gE!Wp#II0s zor?>>M2ioQ#-}AysD`)tX}XOHxf1EF$X1V2kl6y zx2=oLE^m=fw?$Ci8ZQ@CHbbFMQw?h&wc!YjDkPUstZ0TpwXAcrguA#nEJJH-t#-Uv z(F}!}t3x%NZG5Ab`lvydOE|8p%{O}~c@?T^Zm$pN;OZGA3n?vp(d6=!P)LsPc*~h#U3Ijy+Ofn%WC?br?I`Ni7QxDp;@G%N?ix5p+PpO>tXe6RgnhuQ5YYBrp1L| z5ozP964uTYVOUkcIwKK$TVY+bjrCN#YWL`B3D>mBkyfMC&-JUdU`xG}^-Ui6xU-aB z)YBYpu4$n3ZqW=+b*pBqv8gY5$gnp25=>v}stquG(X1ZyP<76M)I-8ovtX%W9{CnE zYbNv`DvOfc<>6vgA7i&>a*REiwNgf36J~1TQQ>vc4ETzKa9K^^ok*~j8iIXCSfsjb zm3mB=j`X|2+9TLKu2~0pdB)SOhcI5NbM)uCm6JipZoEC?!*gLOZt&tJUBZ_LN0o|v0-P9vmQQXsnqn3Rp+=2P1^ z;#^AVQev9TP6odgNuRqsW?>a%he9qkr8)(3h;I=)DN?9U=NM8Wa%5WuqB*7Ilk_w2GZ`!_G#9N1j2Ik>rEbLHlS z&5_Mro1>f8Z|>f_VRLMA@8-VEdpGxQ-nV&R^Zw0)n-6Xt+I)Dk>dENw_Dt^a^fP11Ywxc9UHf(o?ApI;aM!_I zL%R;|QoAd6H|&n=?%EyQy?%H1?hU(RyL)%{?cTeam$fQy3Ioxm^_#jkZP*ms)Vrx~ z)80+}oAzxQ*tCDs;HHC{hBh7Eq&8=4_HLfM`TxB<6!ZjpDtan=8hRoxJrBeNV~O(n&)4d)B@)z+PhWtX7qY{C-;6(X?6E*=#BOE_Nwg} z+r8T-Z})A_+V0=JaC=~T!S>+xitUx#8@5NbcWsYuU%$P3`-bhY?Y-Okw(s5EzkT2K zf$jUZ4{krWeQ5jP?P^EH4)2c1JA6B`cKCNJ+!5GOup_vmVn^kUh8>X|T|1&X*6--v zv0+DSNAHfl9ea25@7T9vV8`H&p&e?ccc*Wst+oE$`*siP-oJZr_rcwPJq3G$dn)$) zE49Xpgw>%DZENYw{0Dl4}br~5|~9erb_%W^k3h4=lYHD>%Y{rgE@09bpLN}tGTegre^knIrHcEFJx08 z%7v`%)iu^%*m%*x1sB$=t!L4CPMONRY$c1)^=lT)_pj_)xFED({%q*%NNamz*X;Ii z!|b+>(CUk4w}n^xS5|j6Hr1|-$7}5gJ6p$b(Y%$;q12M%Wxnh(-@G~VE>S7bEHdj8GBhKnL+gZoCiv>Pn$v6?rn^W`OR2(0d+DjQ#BljiJ(y74I zjM1{T>eGKZTVw?^TP=6n9w(W@4N@}Vy3SFmS1%O% zYFj(yMoMai$1;znjy(X*A0c#wlC>4Or=?m&iD*`9xQVggtxheSdf*w z2~Cadlu6GycsXt@vUH#Nh@?CoD|?q~RO+J=pWAJA!M}xIde+>M?jS zroFc4zjwO~SaWY|Yi&`f0}_P|wkGcUvIMlwk@{w8NTt3Z2eOW9tn2S3cI#T#%pDk& z`i>-aj477Z_DEAzhuo&3kEhJz@;p7`fnok^CsV1J+RFx_`)b#T$*#>+H`4A?&p5`t zu|?dcYxhyyxT$VhMNO-;i{)6PuRYAop2m)~+$3sU)fiTubytW78;vO|l|D_EpWc>S ztJ3G{8bFi!NMB$z98~&cqNQhy=WiO{+avDUs({DrnLk)Qiu;#xw=wg{n9oM(0%%`QiGgR71SSRAav|Y{5?7vsy=xkZl(z>Q4q<6Y!J|PjPiH`EW zdNAMtAUT#$-NMpTnuI%v^M>3`C)?Z-#1qRM%$?NJBIZukPkBLG92^Epy=4#bxEhx=2Xhx6Ayc1Zfam9&1pUZ-{@B=qA7c zap;=Z+}veC{8~cXrX8Fhzxe?c>$QUk^ILK0p4i?VHer4*VK!(7C(K(C>qhNh!u(NO zdLe-YQmCQ6Ut#%e>TTHpH3 zJS5KjG!3bus+KT2e|4`Cn{d0zd`JABoXEzD76M75P?>)dw|x`qbelLV%Ci&e2(L2V z6=h&z9hIpv|1QdlnDInM=08N)Ke4W+0p&eWUY%Ii{xR-VXTC4m;3T=P%01Lfm9mfg z9u%Ehq*foA)K0(WwBO#*++os@k|H4wTOTPu<+RHbI>nXK2ri6dNJ_65R33)SlylpnLp~~_@$FWKsZdDd6DlC_n%2Zmy$A(>8l)XeBrmQ@>s7QGd7E4Q(SEzKCp1-uD z^t#Z}!m_2=<+%kRC%~wL!Qy44DR*fwlwF(`$|=n*#^|DXoKfP59?62OA{ifSQC1>} zEK`|DX4yr{T!&I)65gfx*=1!VxuL@1@=$q+loNT&FH&QVHCY@SW-%^dk)4~nZ0WM1 z?DG6j$+Ge#C56RHLfNIHV|>CUSXxq4vSe9)Xjz$fQ)Kz-goJl~u<#h~2?=j0ucG{H z!gv#o!DVFyp{2U4)rkpbiNA;{$5%~EIOmlt%dyTUC7i8wNl;BnxLRXePfoZ7v-3h_ z1tq2Bp~cIJbIS`$iq$E@eM|Gpmz5TmsZ$fqi%XW3hOS;lWfv7LEi6})6JAtT`HJjP z(ps9oIG>gxC6kk1oWHm*mo}|VOPChr22C?MJ>f{@8V)>za_K?x}_eNLS>R0I!lq7ov;wcWyQ8sib`k}RA=E*+R&WiS(R}-`l~U! zaJX48JNFvO*VIB`aanQP(m&j?{JLO%QGQt&)j)h@YVL4vNsB*}b6qG{QBtbrB|N1$ zmXc@&T&cw}8Cxg>bbTb8Md6Bf>>KCHPhlsU#!!h(dQ zv=pZzLT-yoMzn}WurT4RyF{?8VAlj64 zJSeQ7G%t7iSIoP3!SSkj(FYg~ujwm>SEG9D&dt~v?BYu zve05qTAbptF`!YaK*C^IF++WkbjzarVwIinC@UyjTs~LjBwRj{FP*lcoU?YwIaYC= z%1!vwrSPZQwEiC6b^@OJ$GM3H_EicRt(IS^5 ze2PoTDcxXpVQDre3RRHsu`>XfV>svXLYJpWprX4G!r#KTwt>=`j z$PSu$qIS?}PIuD$zD+Z4*wXV~ov=ijJ*kqYFyVxWs9i zY68;`$zh&A>V#Z@v)3U=>>-}=Bv3yB{@0yvnaPcnnz$?OoNj8@9vAo_#}i#eN^`MGz3 zobwhJmeO#<^F${r75e8M8a)y9u9DCM;cQ5_+ml zOFYr?(Ck`>itPbF_m$Kn5xn$sbGDHb}dz^`$15pWy8nRR(FM{c~*$SvPV6lGHWu#NIVwI zPx7jDyxLc{%?}+LFsyy`VfOw%?Y#k*Tt$^Od?yniNPq?c1`XI?z$jtpOeP^<7ADCA z5;K8;Odw#SyQjNnrb$nC)7_Iy_*k9Qs8OR1ippwsx6!O-ccYGLbn^@9s96^^u7jeB z`yq}iu88Q4ijr?d{oYfjPTi`mp2^rd`~T0g&wn4t-23*adaF*II(4dU-Ft=Rw4nJQ z;}rTCcP7T-LYUrFCFN?%PX*2CM7^m?3+i4&kGlk)FL{YWH!tAzex{c}>Uvta070X)DRl=Vr z(CWmrDMYScPmcdA3)!?1Go?@fqY}oT;2aCuvqWItD<(Rj)a?}TEyFA)glKK_0&5ruPCftBpOcXq zCmWplA`4N@wM1sB?u*5H@GMT6o*_E*qEpoxvr3oErwVif&l9>>3C$yC`VCmG^uEvI z`~|Rk<#M^_U2-Z3;2xL0m8EVB&E0y_M%4vQ8zisZe4IogC*iI~zJkyU=cmZIxqF4% zN6jcr5pPk6^39_;5d+A5rKmyFy)uC#bK`N9Mr9stj^nuyUI&ij!??I5ejol8vK4okmCmuko6*I5_RJYj6ooUm9#Y2g}oVC*{1WPkZHQ zbaB#|_?b>lZZqh$=dZ?l)lNA#cp3i8Yiinf4n92x+d%3oPJquP4c`3-qtx zq+idqE>Y+6ET)r)>4h&uW>krVZNc@#f6^Qn>;JrC7pgQz?Zs_s`t2E~(>Hk0qXPWj zG?N?LSVNUt|9Fy1?)_5^!Krg(FG2I(rlyN8?z=5ExT&u%_9vYeUpxmJikm(Ju{i;G z3y*r;)mO;ofh%~JoO1Jk2=d=6;Bs< z&{V0C&h(aB`bp8{N*ZbToF9kcb3NSi1)c)JEl`}pNzhG6M(OS%{0qZ$>0?DZ~Kp zG7N&>{{gO`iF{Kohb>Faah<+u3Zg-?wh>1F7dPSm$=4uCz~AO3+8-DS!fmNTJ^gUU z^fGUOWEcJ?E>8DuPOSV9<&I}M@&EVQ!WX?r35+^A0WsvDQfy^4n zw4XrcR>;IoATt1&5hzBr1ETHIvMwI3zWK(mr422m{4BFh(RwEGvEyYlVa%gU%Ctl`z za*v^26s(=9WL!HWVX=AgX~yPzFyYUKRxfprf(Y5wuUA+cWvA2CJ%T*A?l`GAq&y!0 z&gID^i`lR&BK~hE2$v^Xw>%{GbI4RxnIWSR9tYA6m4?P$oyIa37ozlA$SK%pg?eyCG9_kT(Mvp2p@W6>X~}*+K3F(uKwAP=>bkjXB6ifwW`YGL-ookWmL22SRV(4rQJI zl5mip0wEX4GE`=43CczdLS;S{Rr+1$sw&SmxJ3X2L<<4g@cWwq!m4B;p_kfy5l-Fpzc!`7)5IgM1rE z*Gui3pQL;i+sH40j5^31`0sWU3Ecz#`y<+Nls=8$!sCw_&Cw|X5Y!!sWHhHS9L0jn zqjT`bIb<}C0fnhShWar=Q0r*ncf=}0)uL$<=l+O)1`0A}y!y3P4ty#j?0?`=kFfN@ z8gGa_ANeFMVXf91%_+A_AyYJ5Zge4)Tnm{=$ww98Mj-UP0J0ydNf6u)guW!e2=@H# zAawL)f?)q@>n0%dM*lEXA5wKWND0WegWLlo_6j@IJAot|x)1p z3_|ak!P;y&a=9(@ZO9ZIsC; zmbnZFeKR3Mt^zXVARB;8ILK{4BFpSlDIg;Tfp6Iq$4h&wE>gk^saow+14uQj5pKaD z;&No(L|n`5RPP27bC3ss(3cj%T6_Y?h(T0&pt1>rkX?KcG6{62Fx5AKj5){;fJ9c< zGS3onB_quN-VD@=77VJZ1fiVgz=S8SvXRq)j9qQ#b0Lt(D{Z6&NKqrKja5LZtL=O? zP(FWRBiks|H8#=d;!R$gNy?iUT4cZ4y5WJKLHY1FEW|lmK9gVTGece%EIOOHDpGw z6B%Ns2}<>P8<`KJ>joP+A4t(bE(1afwxNDj0U0rf_<8nYke|m3B$P&*o2K^j3z3iZ z^Xdi1wT`|68XN9n9oF;neM7y9yQ^Ev1lWnM-mE@h5E8f%`AlxL+adv^dW)nYnF5e; zjj(e57m%)7?No0Dl5mjs0Vz7jK_J5p@+lx=4ssO8#5OzUM}Q=5vymr&j5^5EK#I_6 zXi?7r2@*DP66QKZ2f>H2f=LIt7)X24PPLp;ImkL7BMx#CkTC~I0*Ut6`P>C$*dSuz z+=67`r!Rm;QRMKLH4E1f$RunQ|AC9NrcF2rt8MaK?p+20kHUBq2V(1KH8_hv) z9N$5}i~2?kLix}Z_C%kZik>u>bdY}q63yB&KLIl4ApZ%Zc&9CM@@cSD2ca4E#132L zLLlu}zX+}Oav)<4ay5`hfn}P5LG?fqxC}zIxDGNC8ewhR3S_b-Pa3K4u$on#Jdj}rc^!}u2YD;W?6gz8pJW{5uYp8%*)pR*ss@pM!R=1A+Jb(8 z0L40HtzYQ%I(pKHZN3W@=^kS9of!Gp=E-Vra)iY57tt@cf4#uVhZ0b~7@k4DxE-mb zUtq%M^^AXn#>R$p3)W$xS>_fzYB3=)sTP(DHHs@-#HG{So4%>pZ zIe2Yx92!duPu&(6bJhD8tLHYW-b7`>k{_mjP#(v;V>l|aK zDYq%r>sm-~%i5yffXtXF4=(B)-v?a5n8Ef;GJZZPVodNa(?yK2i<@fJ_*K@+shBQ4?>KRCp&r2C-)< z_BKIiCSK^dGl=XHgp_kR@`=9PMy{ls9b_$#*gI^QP9P%&xqe;uX38t}PFv;{$P`Be z!AfB)G^h#a(ea5HC+uP;UJrUOlqV#zytq?pwQos*g0>9O!cUp&z(SG|7ar>Aj1X`TjjQZ)|y^* zUVu06*~e_#>R#B`Ng!x$8nJKHi>SnJX3w)o1OhT4Ph$>KmZd_Ag6Yl=U0MM;{gh@Bdc$Oc;a; z`3|Hienn&m`5=(US8e1IKoSn}Ss-H$@)gSGYj&#dk&H&zqJB#Gply}Lh${FEkV!{o zZUol*bT49LW|Ewc(p@Q96E2c*kEwgH*^mMxP75`9z}HiS?3Yvh_ZR->-6H=t4#K3CU+_ z%RJMvOl)^fXUd^zihg3>A&rrKC};PWHT~#Sn(7Os%v)R{eWjwoq{vKNugiOgOWoCxEp7t1a_Wl5vpdfCS&OW#*iPc;ovvat4sD2^(2VG7fSjki@^) zG95soKeUmXD4!n*f>#pt6w^ zJ|~FE7AeOJg3qnu#z!d_`A?C7^DEH%i0G~~dz;;Og943LlYbByVt*aDV$gf2`4J$I zCP9#`dX3ZwOq*qBnv0jHlqU^B`RqrkB3_Rb=5r9pfUZ!9TpGzYR0c`LSyacv&QO; z%S8PIWJa|NceiJN1TPXjlgw`@9|t+*9N3S8oCBo&#hQx}sTtDfB0C@2Q59TZBUb|% zagZBN~VJimd==*vWgkU21p$PyblgAi=@4!z$?fV3M# z<%~tL*cG70}rDGOfcYM#gJN^B34QwgpJ6)z=g5sjjf|`4Y)E$ajFmR@yQ@ z0+Mi$-vEhTY0Jzz7xv>IF9woWWy@R!Wb8uGhS(VE5NOkWtU@4!vW{7^F&&{Lq@Olr zp^zGT*3sZ<_&T=3Lsm)nRP696V~26!-b1F`KUN*2KKWEMbOdv&RQRp!&B`gd<<@U z?bP-|1$zK08(wF(>E8jFaF8zoiFMdAk5DQH`2mo~2C)Rnc@oHogUrGDS!AP~&x?RW zHGOpbIe+A(%!GzSnq0YOp~564^asBl%t$gh19YYns=+n^r5>>e3bqpNiFOGggD$N$Bvmn)SDQZC4U)rKup{6Eg_QQ&0FZG9`6S7F#FjYe+M#r&`$MhAd$bZk(17cWje?L zAfpa)DUjfA?Nlo%m4jSQGJj{wYy;BeAbmha9Ar0;$RRt`TPT%5l*aJ5;HW`JWACF> zAGK3`n2?X#$YCI38fgyDFK9zq(7sf>7UlC5$V`lKDs*5RKLFDH2|J%(07*DVfID4P z2RQ>s^zZFd7XT@K%0{jLGU^~}fQ&oH%|O~eZKp~Q@((sL0A$QTb^(bVv1Q%}q-YQs zyV?Jce}4Z(xK9ng86LCdpY_;%%=lh<0^2>r_in(9iz7yQKIJoxkc{19GRicKA|H}= z4;57+JRp+kb|n510($P*lf+$NXYfYa`=PPm|6v_AJDLgkK16yJ83liw^z0xH0*QP^ z=c8o)kz^cX9LS`DJOL#7pq=VzAY%@4oKk((mYIvS!>-TSNCZgLK^6fCK5xssf>Jq1 z8<0r{xfaO8n4M}9kk}V&WGj#n2T21-e9@N40~vLY-IULlY?-$MX>YN6<@QcW zsnX#AAc=ppQ+*mpdaJ^Pg&7lZfb z9=9Dbx0A-c|yp$qD9?K9|c7b3Opap{_1S>^d+Xlz*PaH<}6Ib_B} zMx|N}WWqtNC(VD&Zm+FCMjRvsr0O7LAi*&^)oX#YJIFgJ9|!p>Ad#=zss5H^9ONH> zOghMyfmHEo#n9@$17yM=5|1UmAu{QnxO&oV#2{4pf2W+sMTU?`AnlLX$nPj;2RRih z`jg+ZWflObK58RTAQRuVk;{OLeOHivd}^u}e3Abx2(hoEd|Jik30V(hRLjf>RI>ve zUM@1yFSy;QUkssNJW5H;G28tj0-NW4v3SaUu@7TB_lu>FslP@PU8ZZ%v=@%ay{kxt zr+zU!gMJYY`$e|kjYD`319TWXuJ!Dd7;`wi+b|(AL0^0qHsEA}5rbq43{}4;?FB@= z<8#~~l>)!4s{IL(p<28K>>~!j+iY?LyzqJ4Ajn5SlLqOHs|iQY{(U>2KIAj(ASEE< z4ss8W$b_V-WXcs76ZS_5LgjfgWJV2A%HZQUn3zO1y z`=KDH1w%g;q*r7N0vWt@G?Pylgv#>~k=uadS$$Xwl z__QFs@mw~Kw}e*>LNZ^0%(y|2Pj7Cp+#h)+Or?OJ%OI5M+ekI+Am1lsrSw6P`8kk; zgB%Ak>7;7LorZA4Y9Y7k2GLZlk~4@uRF zt=AbWi$o>`A(@LI)9#dT8IUCo@=74%I@O#&btX!>N@_ZgsU-1#|CmWdS0vJYrO1d! z;vNUI=}|%$i5iYZvsM0TtUc&4Vw1Td@vVyJ&+)Vy^{5sySpdr`>(X0A)tQ z1^=r?iapX8?4G%%UU7XlGSMUTK(f;B`K0Th=ZRnF*6~V%S8ir1Xu@<5`K7cVWrGi$ zrH5T4o#`7h2o?7xRT0hluSeiR=AMEz5Ui#nx#9Q14_dzBlMNxkxqMi_s)N~wO zvBQy}3pIz$oVcHUD(t6)a!oJADxX?Ojs*BNX%K~eJQ@YO99oSuX&HsA1TuDw=oxFq zEh|>Ut{OH733Whb*hzH*kZ}juMmghUcpT*x@Lkv;d^oYoAe2uJrE-t~ATd0WVNxNw zNbMLl2&LK$nNbJX109$GAF=i0+i9UUn_MN z8)F>;ZMq+~v4$wAIcCkqWF1>9yeS6TfH7Hzj0Hu&Y=?)eU-?w*klxe9c6bm!(EWb* zP_wof*cjda)MEi|$h$@cAg!XG|M z5MuuoWMXFvBG34Y8KhFcQ$CQJG>CfS2T0-^Nk#em4*3)p*vKhS_-6+>8_1}GECMp_ zAeRD}bdVU3$hmgTR|APTNC%KE2e|=A!a=qJsX9mjNN}E=^KKxKg*LK>kn?TiULdg- z2|`+>mL9o45ab!`hfMWi8#x4|>!mjG5Rk}X8~G}wa*!v1OghMKfwaF&QV~0CQHsQD z1WzyEomVzOpX9AB6QmDAQ3?^_c&i{9z@tcl1UOE<2<5rTM$Q4!wOSBj@5NIP2?yx| zebFG4&jplft)wDk36OD(z(=Vr7+}wd_$ZRO5;E;uu>FWRna(KPvjAo!)55w6sndInzWf|HEb!X7i% zYf+{fp|R>Zt!GCwA!RpYg7tP=#DNT7XCrq4No=r@3Xlnd^rb4~5`x4=kx|cgW708h z5c>U%luxI~5b|CiqYiQaNO7|*GYVw<20>`elb(=C+$ad;^B`m*T{bcfWW+(fM>03r zGEb3=gFFXh_-2;jCO+k2#0i_l>WVw!L|$&cL6BUIk-#8gnXGxT%>Rt2N2I8r`j|D# z6pO;*4bOKD;tw|IT8E6Mhv1Q6FeuObChKQ)D%QIY_2=>Q7;r3L)f{A*V>7VK*3dGu z`Kpj7AZm~G8Z zNV}_@%($&?9isUoQKNb5S3VWZKWQ{SA3soAx`&FEu^7p%e69N2Yn?uc~3ixJv z?H!*^=rGu-dqJo^C%;=E6B8NrOA<)r7Q64{fs8rGp8}b5khcM8zg1EpR!ThSnt z_4_Cv2l+6NV4I{OnNc7`gU};Td8}}>-=wucBEf@@iQHzV`U;RP2l*Eu6B?Nlq_DC? zeVN_>rnGA8hxG%rX$iE6VZ}OT&3^Qlax^NovBzTJQ?ZRnV;lSNgS!T6W?Rxzn)aeq zT|1=QR3tnD`*|R=pTSD+D$jm?0v*O~*Lt49etr#^;?1H}VsCmWTIbc$I#{(W#c|(i z*dQb`A2MCrMTU?U14%f@%YYOOf&!{fIgJ^FQZ0u}`)ed0$WZVeOx_^~$y@`ONYX|& z0Ewj-nG-0(fZUj=h4>b(JK4rvm?dqX%*-*{wy^>2j^+T@Uc(_LVjKIQYxdY=KS*F5 zVjIKKQ<`vdJUGpg;8PWEOlKSV9sZ+AKh&;XO#^v9KZd#hg9OQB>DBg&uZ*6dUd$dM zD;`3D$$J}_w&!u>AS=HJ?P^4`A8Cl4Hc2@-ueir!f5>BhfyX}95c^Z7*Rpqd?E5_S zH=)38-5+j z|9?=gwl>t6>qY;%ht%R(X=U~0nz}Mqt`?8f1S6?AB#QfIQ+K?X3NM?l4WYMUwBk0! zj_5jVLD}{|X7n}O0`h~LS}r5@-zddu`v=nRCee^!P>62cypKbRo9UiTcmQb=^LwS0PL-R9~i&g@@SBD@=P;a%Hra z-Bu)hC$v%QV;$DBAClP(nW~mymc2lt)Aq_C?4aOyo$R_fh{u84jTr~77@?g13i(9( zr3J;Fxja<&4?)IN;~&!&RcZHnzE1nYg%?X(Ox;h@rY%t1+J3t3=I+JYIy+YBUur!+ z;Gl(N*2Sii(QVo#mZ|zFOwl?xpN`v&4(s)0#nrK9gTrNIg3}h50wGcSj|EZac?tfV zi~qwzm-+T~K&ri_B(U((}68sWwyAon=(LrkB}xd-R0)Y+MtWPDblz zqtTE07;C&glPhNMkggq5L|}H zvp}kNffU^Z4&U6x?49nd_ zu!!k8)v~$Cjg%=T>0fU*nuiOo^q10>)=BQPOM_teF4o3W{p&KwRN+QL9bQGL9OQZ+ zlR6)+cpAuFM`jn0Vo5Yl?C%2-DGMUKfXhSmLiem)-PK=*j3PDh)GM<@dZvU;8zAW( z(iS78Eokg_4{3|1Ok0o)-Hiwj(erywmZ%$g9)OiwdX{=ks+vwwX0Ki_!Xd6ba#6Rq2(B=lX# zL@Ht}gggTz<{-ZXGVCCcmm$@lo$7KRRR>uQWWqtV0ZHt%Q}t6S2YDlq_FW?5YxPBF zLW(NFLp;e$v^qImE9=;J%vf(73)iFYLbzqJCG4@szcuqF?VrDNvRE$yqIhp{Su8YP zT;h`)nM!0x*R+n+X_3Iln3#kSVrPFG>LQHvW=k9M&`;rh!Bwa4Xl;xbZTNQ;#a8L- zYdnMY*BGh%A3?j*LF+)xO`pZTKHn#`%$Z8YkH-K9U(hlUB}4mmUDb+3MV01>OD!c* zQgh6fMNx)d*X5yAIQ|N--=81XGG{3nvN~5rboe8qL)u_Xkgfjr*PT{ePvlz;&YCQ&VXE9R$}0HZA);hjgQ&w51=m`G7z@{_nw$Ow zdAMFnTCfYhkPn_N5}rv5E;23X@*r!o<~cT9>*!gD(NDd(r;a!N#I#^MUBSl*u0K6j?F-0rWW;9@U+G@8;zCk zawZqh6d}|~!97#kyjT>=kba8L53S{e$E;bDwuCWhohBG}%^_n7-C<$B>dvc3TztY~ z)9Fy_XPeOvuc~2hRk>){)lw9p_@`6rCs)Y#-8oPUc8ZMgqMV8}MzMiA6wjK*bS?T# zEz;TEUV4~eWt$xrtU@h@rG#!#lzyd^S%%6?{>c@bwKD5DNSkTFsr|;2)3uW5%OM;*}5#iR~kh*V0-P>O~wS|jquncis4zB-R+vqTLp9e1Yklsz}uqUefyG-4&fhup$C4xX9 z$htU#y0Z+`{pD_9a5dZgqH_jypLfZL>;6GgcRUs-jjc-$M4eSh59CexuvyXX<{jUU9jAo#yS~;nyn~ z{;1DpGz-w}9x?3>rPmxX;wMbI*YWc@YKqRF-H#a^_TtT^t`Z3$`jJ1ILAxs%($Dgm z5{Ev4T4Xk2?4qf=+y97I{Nd6W)cs#g-E;WDifcg%L4M;is5{G0-H#$g{jp%S>OM4s zy04l+-JcHYUa0*710J^~RpRTSCvJC^p}Jp*io1J&rC#qAnyXhBERX+P4=@-LB zKU4d|jv46Zw?;pBJl^dVigqWu=ENIVKlSE}NTuY4_h=oONSITR#?A#t)UJdXJ^#xZ z)1$*aqYambNTsKA8Ew?LzpOQ%Fq$8xq|PBx zk;d)evu#lmrd|=JPIMs_b;#&&YK!VOI-EN4JlEDwoe_2qENXHF`e7N;&ozFT_5H8^ zvi;~0HWrpyt*O;|iu+LJ*uJUTw6ya^JVAz@qGg&@i4P#vFdlW}d&dMF1~TR#Ujou~ zuPyU+AdzXF(xQBR44H{rpc|Ys*Dru1UZWAUG7$P_8jI*S17U1*nw%vD;F^-=C!;}- zRcYm)S+h*tbB`FS%L#?~D?PVjtd90Q)4V!7WVJ95ZKyZow}b*Xlb(CcJkxUrYhI3uK00+Pi+)&!^s^pHrrAh%%(i|OFP0UL1tEz(6a9=D{Y>2}Z!mR-$IiNLD~?wd z?LspbKX&4}vkcY!pI}z?ruJG7%|Jf|qaR)?DB|;Y zc#7L7Y9(x={S&O8dgCCiw6Bbr?~;TYyv@qz}mGCvBO#2{~*d zZvhf~%0@m!`8deufQ)?FmiZQt=x1!?hm_BQHgX)u_-Ae8j90)CAQsl*QXs+SZJ8^9 zj5^2$AnjkUW$qxEqc(CEkm^?$3HQ>lR>`6sxY+w%iRb6x=4y@^%{a8RbQo)SwkDS_ zPI%Z@%NS&0j+}U#31gX6cz{>CGI~teZ7d2gcWXGqBw>Z%@8q|%HbQ4@@)B>uGNhk= z^P8NAx2Xb9?Nu)yy&HVmRefFNH<~ib#5j&1nahsnC=_ktQ7SXtGv_BbuLJD8RMy9+*Sh0@!~lP9G}f1{6q3fjBLVibD=ZlP zm#H--y^>|9JfAo>2=JJzb7IN>qXSWcQx71*%WJVn+3u3K!zQp z8%W}Loew1ndV!2;1n(j!W$@zmawY1dDnq6USv59MqTo+~L{a9D>n)Ve@1&oS%zH@Y z_crn&Ama}5Q6Rw|Y?&iKV(_=T4p3adcVTF=XUO$WkZBjoB&y&MAfwm^8Ll2YLC72% zc?w9-Y$Lw{GK`n!hpB!CWaK0pIb|vGIav^~F>VWL_w}dY3uUSd@{ez0Fl=zxhs5Eg zUQOeup?gUGq8OWP?0_qPE3q-!%1ylwdo%DaT&8tM9Bwq$w81S5WMxFV#MoiEt|6WS zZA^$Zyj+;{0+KmJG)CCVfsD+vkrgE4ARR!uP8Au^GjmbBegfkoErf^Yc@)rzqNWM4 z=Q?E;J?GF*SzdZ<%Q_X_-6z_h z65N4Q)oJ1clA-s?KP5FKAb1~;F(;n~fIMkrz@^?;GB)4lIszG^F*ELPnW-&a1;6S( zPu^Ht%vWW!|B<%XZ`z{W{#l(%TRdsnV(JKXqiGB5|I4GQR*0q%d~06Uy`F98F_&ei z?l1Stqvt6zvBOExd_685eLZgKKJ|D?`{%g7lMd@gr?;89@4`b9g#r6pT2zUL(aK%A z?)B_YrwYbJKdR>6Mylu;VlC27IUmx`mryhE)Zy{R*v3rsL*J$1ehMYj9AX=^=a2QX z7!pu~b%?L~3_LgAo7j~q*Vu+eg6Nquuni?cb$_YF!l$!c_j#~+uKU!sK|6J(v<=zI zCuKHwmyGqAx9ga;FmKm6faT?!z!)854`+kp|zE14awC!<^{VzTC{)X7^_t@|A*dO)S@AKG;4YA+nvG4QPk9q8G z_1K3RVjuC?X}^}M`J*2Du*W{!5c@ul{T`3~A&-5h$G*2A_Pri^#bZD0vFAPZk%rj! zcn@A24|dh8E3#NO|* zw|VRpkNskgeY_#|w8y^GW6yc)=XvapHN>9q*cW^3X^;H{9{WT??Atx|MIQThkDaDL zZfx^ZL+sl;_5~h$x5xe)1phAkWJB!T9{YTcz0+fV#$!L;5PO%$KG$RK@YtX9*nMA;|osmrw({_67&tv-HMi;9+_J8%*=QYH>!DD~cV{h}=$36B)L+l+M`!gQ{t$WJ;2Jn9{Yxd*q3ZIDU1-c8`6($3EXz`(qya0*}4h zV}HnFf2tw&M?LoW9($+9e%NE5Y>0i_W1s7>cX;fFJoe)au|MpwKZ_e=j^}S{^Vr|- zu?J^0vU+gTV}HhDU+S^H%VVF{5c`Bh++$zlvA@=1U)T`) z5s&>*k9~p1zSComHpG6|V?XAx&-d7K9{ZAp*hf8fdLyfA^K(7+w8tK6i2abq{*cH1 zJQm`d@~qtMv9D@~{h-Hw*ki}%g=^TmJ@)p7*bjK@hdlOYJoXJ9`-X4`+Xk!K98N=0pQyFr5<~K zL+m3S`yP+|sK>s@V=p$uzRzR7$76rUV?WztA8Lqwug6~T*bjT`^E~$9hS>Lb>^YD9 zkjMV}7Z^*>J6`rS#6Ik?r#J4YBuo z>}?)<#bf`N$3E5&d)i}P>apiM_5&XK!ws<~Jod#Nd)i}vug5;#5c@WdeSyc`?Xj0; ziM`unpYO4EdhBVB{jrAX-sQ2+_1HT+_FFvmiH6uaJ@)6Zfzyd4+E#n)?IF9~c|ML& zk9V{d1}nwEO1$Q|?C&qdQ?#mtQS4UrHmfS&HzXswloir+IsTl@`(SZf4#z?u5eK;f zNWwv`15(uprXo0Q12Sq5**VPRq206p1Q{s8I{e+U5&X)ziI_Y|VaiMnJ`Zxx z#+YbhiniDVndmus2eR3R$02q<9$)*%`j14eq{w}jQ#Sa6PhrltbbgCeh91j+$;UMOE36;v0iP@%Dwe^??mOguQ)6_aHf{I z@}S(`X+q=H9A3Tr)-ii>Ok0?J=Bqq^_)KUC`iquJYc;>}6`?rWU*keEHvCs9`zv{7%HpITjV?XS%(|b6yp1BvSJm9e(X^4HH$9~9Tf5u}U z@z}>2Vqf5~AMn_p^w@WL><>4@9`V@sd+d*U>^&a)cth;-J@)%N_D4PTPLKVuhS=wM z?E5_SV;*~}$3D>z`&^HGkH>!0V_)R4Kh+TX^QYJJrWF;B{jkUWOLRcj*G)FWe%xcv zdF+Qg_9r~{;|;Mt>#?Uj_5&U}z3;-Udw@-faEbIpnBH@1qOWPj_eqa^yT`uYW2d)~ zxa{*9Vt>YC@Ala5^VskA*dq9I!}Vt>?Q zU+l4`J@%-_zN8`cagTkG$G+WTpYO598e%`{vCs9`J3RJBB@Q={)vAWr$2|7uWfmY& zxBs2=l@EpNdM-nIzj-b*kSW=p8AQqH*mHRhw5t+E>%Py5WZn;%Fy<%ZAdnGB72u=R z_^f29R5ggqCOBuB{n1Jg1`q4-{13UMnnTtK412xVjb1BQZDziUYjO$S1~9rr)(S`l z!;*E#TET=_D`-cFpit|OdX1cWVlNtz*}AmXmB+l%^LcO0YBeUpFyQcs-IX82WKAP_ zmUMqZ?Atx|MIQTh)YxTzeK=3i7WH*wQFur_lXu=WE&8cS3G20K?YL7iS*`WV$SZ)*L061i1!VXD zjC2E$D20&>5Q*>^xd({MTo}0@$Uf}><^+RzeD?ziN~mXiq=X-a%;TmORI*P3shCuR zd=AJVy(5ToJ_bZay*UBi!+_piDDhKclJgHCBk#rHRL6mg7(G+Y^REbk1BM-M-%ITP z%6EJupYtIzUwc~4rv(TdFh#AyKLIkK->tzi+m+0b%pd+BGW|eCjSRl%or@=fU|BI3L0vUHK;X^{xIOM4fyh(XT*7YwX>-{B9f<5ZWtm@4 zKE`Ke%jIG+l?jRt*GbFaMV)*u29nV4*TOr&dUJy`rp+_0gFJ)FAv5CCYc&x0YCN-V z01xUAJ z;XeZMkU_HLp>(z{TS++D_$6e*GRNcij$%Ac*;#&UPMZQOWW-E{`r_5;}@|Gu-c^!}-ut%bI0Es$v|4SgU_BAIcW%9`Z^pu58PZ_0rB6cml z3}gX7;!&e;Db-&JMoq5-96SLT*~N|6MtY^w7EePa3fa*2J_|&4Yj7>*u7E{3WquJ5 zS)*o|xLeAb4GOF_T9up~@`%YY;MF zKh>YQ3OEjrD!w3iH)QT}`o%|q$TR74f-SjBrU<@KXEc8ZGWR&C9sx25xv&;b0HJ$# zjP#Z>8H!sTcG~MX$gDQCz|X%P} zfJ&DTC&62QY-;Mb!6Fswr=0D>N03l7M~90c6Fo;{@N4i&AasOf-V8+MAe{57 zfux<3jy6>_2#2XfVdX*H6XGNgvM5h*aUcfRIlES!CF=<-Ss~*dOe5TJR`j9y0Z!RNnyds40({U%=P3;}`CyWC{7f zfuL&giPPjl#{iTA>+t8u=dj5K^LGroK``X>!N}DZ9h~v~WkB|uGE;7CK(?C_Qhz7w z<+q4PZ(0kPF=IcJ>LwuJtRH@!z+l8-k3;5&@sP@^W5!B;HS|bD$g~-D%43*vHmL}? z56DBNri4_1$UPS|UZiL)Oh=gWry#S)$@v%%;twtSM?f|>sh$Hu{U($->6I869ZQG+ znSczn1L8yd8KEvm=5oj!bIN=zkceZM8-d75GPmigf$(aWXg&wzd8Zb41ChI3oa!Aw z7C2m0AdfqJ@S{MUbdb-GjFajcKoS><-{{5uElhxdNmFKJtANOFhI;-!Qjr4=qpxRx z3_Dgg7u%H54*S_a7CNKVg+Ly2+I=OEC!G>*1hU%E#%(|j072%uUlf5Hb7cM$$b1KR z2axIurKU(4d;rLPr-X-qbeL4=7wVg>0_oaf=lpL#MvS*1ng0W1 z)L2XJu2Qy=K~FLH#QEc}K~c#_y_)|7b$5F1SwQFrZT00Iay5|oPO9sH++*rR>9+!j zIPKLBBxhtWUMPS3m?X7 zIpmc25g^Yymhe*`at8|R_$JipNs>Z(5m3*aPISQ};ZO&^D6#W2@U7_7TWD>!jKbfIn%zZ{1^q%$r$ArUv17ya5axD_LYDs5$ z2K(a4QWDGeWx34edkb=&?CB|GcFI*Qix1c6M7pLBO}P7Ppi#y~RqY zC9b=DPo@uF@|)sEgigE&xABIJ-I!VyO9ccAl}a+zpGo_P^2theXC_{*BukaSBCqIs zIoAk57!KT%O7G6Am0_BM#%blECkYex#ME539-6EpOZS6(S&Dw*@s3<>b26L9g5wr0 zy)UT?jwPi5NX9V_C>Qd<2@~=L;$*QHuk0?Orl457ZR46XK{k(1l(z0n<_0rCa&U-N zqA}&g89b0aiYC`j^3ShQCxnf~acdu0c%A=^}|?y?ss5Lt%Yjb=zVG9eHCSs}$S{zzN$0s}w5zYSPEDMa99DBc!wmXzg&wVCGKyqRj+Kjx z-fSk9Rw{vZaBZDtt#dY`6m(L_IOzv)=>SpJ!R``sz;DSdbsgbDbDEbyYCK)$x5ofl7XpY z`UDrJjtX?e3q^e78N&tNb5X?QAfL&k<6MYxawonLlFz7dBwOCHPIo8Ol`ecBishWL}@tB{$!~yvol#L2j;4k+LlTNV}#0CmQ;yIUA4Q>Z8E=` zWT{kzQW|cy7b7Md1KM|pjLx}6BPz-8S4wAmE$f$a9Mno7Qp&~<-n@y=%asBmkECvx zawey`0@|UF?X8qBq^T5i)tk&=K}cO@%bVa6^*N1Ah$7Svh*Mxn@`E`o7Ru%vCn~;C z@=&Yt9p&uZI%^Ms=7W#H%9 z^MPd9`7Ko#l9Y!=u3O=AbIFO-P6?eW4QjfZV zN~xF~af~bYB({o6$m1{wCyfFlP=vRraPxLhtVDN&jgmUzeMo9umirMFgwah0Le`WH zQaa+&5PU|yT=SQ9Q*(B2=1Ulh`i&cZT_^|RswA^H2M90RvwUH9I0z~gQ09nY6l{ho zb)~GK1Cw`CB2#TRJUD_NjV`v%&%ebUFZq&?s#Ka73VM#E#uu&bA zogyd9W2{Sb17+PDIMV*K7RqZ!CbwIEdfU2IAEFDdm4)mcNS1cMykI#2o%Udi>g&zl z9h5UrC`B8Eo(x^53h2413f7Ewt4?3ug@ULv^k;;?bQ!3Bu~|W%wPJV)(un0Us=eTN z)!KDh4?=;u?Ag5|v%4(kdhG1WD2=dSYfvd+07dg=iv2V=A=-uxib)zDsn9f-;rG7q z_c9j++3?ubhS7MalI+X0tXQ_9r3d*Jn!PIUPCrIMEFWFQpuBolp#e0&a zQZ`dkVYj)WiIGNN3%YZzk|_dLTVdNHq}|Bup`6c-ZsS8IB!MJQYU1|##1!kh+8?g zVs@nbw2n%+v<&luXC*n+jJHS$TK9k{mMbY8!P79ZoCZTp{0wnW@Y^wvju_ z=CMpmwIt~weU`%zknE?MJUNnYC0`y?3Y-*sb9!}v5>RLjlrNVD@Zcy=D@o8fB~|Ch z{>J!VrFRwTz{OudlsCpxnJjeQ*BaoTcx%hbH=ZXNHm-!>J62 z0+gIcGw_8Jm#?Ux_T1t|HQBQ!7br{rV15T;P+F>tlT!_pCMkSlH?Fj=lmu2bF4fJG z={pAzCeom$LKtXWg<@?8i)xM!hU6posU07D{q*_O7I*tCEP zw79xhiJP(1iBWbKSW3-DbjHi6SEJ^G7?nyv8Eu?J>;mo3I9cIh1r(HHP=p4#0%)Y$ z;(1bw%-U~5V5$ZNg>YBtjYCc?5##TAGE1!bC|jg+9y9qfz3O+^DOP3HZKS4MuX-@f z$q_RNs|4rVeYqFt3uLZ_SxKxE91hCsY0zC~%1TMYnIlt$*DBJjSl4EdSRc2hLwv!# zQUz?KOn(yFkx=+8@yuPhd>@p`S0PSk)LqR_&@sh%DzM3;4(12tvs1Jh#d)YL3o37I zQz{ef6SttuvS_k}YDd#?PKS#@wG)|Eb`VEcbDZTAMZyDTN?2k^_LN~97$PXy1{K!e z4i<-?8`y_n5Fgl`M>wQ>@&?sHERB40@Vh}pjQ!XerAxp7mu3TdCS?Ic5$4b57z2`; zAH_ChLGEgl=r#_xJD%Bz(o#2VS-vtBqf&Geb49rfecH2ItEW4T6SMB4WNDz}=&D<- z!F6v&5mY>a8HD_Re(qjzNb3HK9#YVO8rF|gimXTwi;6=%z=&}7r;c3WiIpn6T&t!t zC0EAA-8L5VDjO#i%E*x`B-2~-&7*ORfi#w_sT00ZtjqE%DwQmgdOJ_39Z3 z=*xVW^Ib*F=-@Q~UrHpQ%m^8)St0xh|4L<{qJ>vx>FpMDORU0aqqxOd54nX5M6a3M z63?cyYI)<9IN}L4_qzq|1#>?YoD1WPY8r_=F{K!UOTrAd2tUxW^2)2!V0Sx8#^LGxn^FBjPgc!?Hmjbb^t4%ZJd6?gs+`0IBA#Jrgk~4;`SQE+dOI)#%4PvZsL2Wk zC(|UGDOmve2ZCIdhXbOrn$^BecS@_vR$F%J-ke@(*IG&K#DW+0kXsB&0-#=y);V%V z<2>-AeW?;y-A-lcT$7KYQgd==C0S{oETXLDZdCWs-(Z`R(l7!-T}1625cQGfL)={3 zHNnp&-h-ZKa<1-!sI<}|VH;6YBmA5nlwBU|VVhQoO6c8L({{+e55H?!a|Q!55ro#B zzOfL;42l|C88kZ;GyPo~AF|G+!PbWJ;vBGM3-Nrxoa)VMuzz$VvU8qj(h@DLr=z>jmUBLh zt7Sx_*PAyu;w)E`GgJ&DP!1qLDV20DRYg7EL6dM@&=pt%pv#sOD^{!udc#gajj!}f zdGI@-=uDnAbFzp@7HwCVT4xJtjtX<79zoA#!aLK>O+HoA7j?>a!_}$Ubga%WU+QN` z^-vsRcfl;x8SeAs^%(^98Ephmzv6~F*UB*{DVhK?ra}mX^LvhD)`44?0_Dn!kL66I89@rnzjxR z%=-Z`e^j@8)JUbgi0X}~Bi1*u=t8BVAaECMRbon(g+kE#&=+WfFP&k1oYsldIX_r~ zqNn;{dFbF&u~dH#T>1|5WrfgSkjYb3)$|{O?0`N~!4`uQcJWyei?4hZCK(vaX^fmX z|B7tM^vP?f`!KEg%9@a?NsjL65KXJ~QY>4c5>!()!l=5bieuhQ9tux;s=GjK%F1Xv zK`L3qtq$xuK>UCSgStR2YJE#pmlLeya$HF*N{cwSJ4`D>L;Nci#6ZjMKI2nFU~zKN zgsW_VK31gm+^SjdBZV0JhlXnWkJVES;K=#XB3KS>qQ*ZoZ4&!oa9^ljm6oF~2C=|~ z-mg5j>V0&^CB|TmHK8yiEft}lWQX(%3FfjrI%LdcTGW~WEe^IU*M!Ksb1)fRtg}u_ zg+l1_N$oW9NRi4dt5z+Cdw|L)I-wQG9@r$+i#V}4MBPa@+dG`G0~c%KysizEQDg@@ zS0FBTVdE`dU{!T6FE=u23}lxkKZb(gS~${Eq`-Gnkwk8zQflW1+=UQU)m-3$N2C2J zv~U$qBch-?2-yL+SNQJO@_^=I$c64?s#8UUNqEIv=8{9I@sI;~=%8nJ1&^ZPfeh-G z>5RHjPlZ>6L=*5{487m2Mq}}KcAzM`Vz475U5InEA5UZ6P<9ZSNq8!c$Jce=cyqjS zOxQs}^6eg$720n@!ouZZ*k4tqMD`v{MIObg}j0^f;0Y6mz^h6^9`gY3|Zj z?!~PQ>U7s{iLb*n1M5RKLz$&C0qciS85IZDfx6Vpk}IY++fYTRp4Q%*TDy`*^5WQ? zgLs2eaO>e&9k@hvGwiq~{fF05Sng%`-+vlU=@Q zC9g%}CKUBXcy<(SSF)^B*mBkKHq0QsCcJs;4c!|zua9?aS--X8#`w+aJ8xXaEwcWG z4z$W2k)V!nb^u)xtMG`#;?NUY)0XucIC6|*LJbE_8suNd1mallVw8$blRp%FdFzTG zGcXue_dwJczqTOWmmkEKq}!}EKLkpokvNuo*d#@DnvZ_RBt(8je1hBGacFyG8yAQK zFc_E&q@bLT=ijyRykzDjiFbEyp*fM7jTFn7!F1tr)k>EWE8dhCzXi4r`_616ovnq+ zOliGeg7|fC@m=e6``28q-F*Y?WD})cpQ&iM>k6ec=`{Qb^-b1z_~SZtbJopxJ>A!g zL#AuJDOC{PSiYfvxKb<3rOo<+gk`rFkfosa zUJ3-t`e&|%w@n4k12=}0OfNPJmU}3s_PQn(evFv+BZdxmdz}f&iT2?V6*+QF=$+xf55@+Y+11iUL?#?3x+^nEy<8!S!Sgh%B2f~v?~#N{gxhjSMx##XefXpOZ|XUy64!-GOv zo@O5Uw5L^aPMe(3hf*S1HYo0fV=Bm{;9!`~X+lEXN&kc;KJ-VC>w!<%m5Qf|erHp$ zPkf*!jyMHD_K@WXdemAF)#9p^!9Y)0WrCng`wS!!fn~8PgR*_yx*XNsg;&EADCZ)* zvO=9BDo^gn&_qnnYbD;NJ!g8Gnvzid#C{vHimPbCSCdc~g`Zy?$vUjcZq%}Cptq&# z3q7pB6fxvO%^1IFYPS{3Ae(Wu9#_(BP$XK`5Fw1CQ^z6{TJSDxiB8oL4+b$aH!8ne8cfeC;a?4xe*p)(K5acM1$mS?n04szOqNiD4)#%LhRu|DU7q9~>xs}X1xY;ad~vRPiJ>#$hZ}s^ZdQ^&_5~7X{;Z5J+F>#W_u*o^cH=MGL8i z5Lp8CR8Md475oV;6zQL6JuIxmbiG@A0(KlBnlDzYNS?Yh>_$hK-D+d`Mw%fi2h7jJ zu=5F}y71HmoC3tUb7jB?ps~538_os}ppwx)OELKw)OZ|k&8kt|W(-PM>iOM;TeIoR zI+`<2>!*0m1ozO13G1NQt){|Nnpj&MR7?vSXNr#XvGJ&9kGPVkK-h&CqVd&IE%yZV z?1@s#6a}!NNNqXL1noN19nd^-A*MuT%>IFC;Rtv7Myq<7?(U|<-PzO*o>I)j0%Wdf ze0;bmn{$-Ml=+GtddH(#VKGs4?v>d*dLSc78$}i*ubkSzJG)CQCF-JH_+k7_At7tW{^LZyC7jOBge; z1+Gq1fGSbfw|P)7UfV{OvFA-NqtLKxG8`!HBv((-JXMAU0PIgO?YR@rqQKo_X+}K- zCAOQ=yi+TQA~K2z!nUm$qE)2Aq;+jrnG2#6+;BFfQT-ZEoZ&Av8H9xi`#bv-e%MXd zh+kyI7tn-u$)U65U*w5QzM8`6A~~fCmm{2|Ku(H5A&joW+S`k5;e&3PTfbM3uk@Qs zYQhNGeZ;@3akDE^QWM~ODx>_BT8^Ry6p!CsFvEaOhjyi#s+@_upae!_O}}+<)SQsLZj-vFU+fMpYiE5u8Y!57k3Lg zPlc_qjS}kDJ2HvHu%1!c(*kr*H|E!AjhCF-Uii&P^#@N5W2le2lOZQOumewy;3}2^ z!ypvHhaK!;eBm{8UM8m~BU}zAE!fc!N}vXEtc%xnZCJWjK1wd{kuv?m36n_TI>ag3 zqwDU%%cJH(&&No%CL}>uaUPt6kJ2wb#7Hk~>kCm%@{GFNn}BBzrtj-Dfiz{JRznaG z;HE5YlkHNMn4OUKk=Y4}=^NnM7{a*&N>&@guzGQeO9Et?MJEwq4rH+O==@sW$4RY; z%nCKal5#1~nbZrC;6K$Qg@x^iNZkd)EyG;0kLN;E6Y7I1s8Y4pr3xvnwL>Mq?-`?! zqaTlxIVx_9#mL=8_RZq3U?I y)_lE%C_TT%Z6hm+rBGF^qL^q;f-|!ML^a_z(Q!HZu%|%NsMj;7=-l?6!T$vzrqZ+k literal 0 HcmV?d00001 diff --git a/lua54/lua54.exe b/lua54/lua54.exe new file mode 100644 index 0000000000000000000000000000000000000000..46f296c602934cad85c5457038f830a22746f0b5 GIT binary patch literal 122006 zcmd?Sdwf*Y)%blTBp4v^1Whzmk+BUmQJ{%eqM$Qmf@gFBQBYA)z(6Dl1d|y+ix8X{ z;W!Sat!-^#20 zftSykGN+;0y`ZV_wx*f$+;ubO&uxf*Z%zWSJSnNT-xO)Xxe`5Y;8zZ)*0GT3O&H5kWUex0zPTXB%WpCEvY&3Ab-1E zDskbDvQ)n`&8?E{x)N1DGC`jdyWD=-*NM7a+Wu*TPk~>Ko%R3Z+g;$&O246C_h*{6 zSP1=p_!}LZy*Nl-`8|9IiqdysN}oTsRy$+#teL@?q|uC~wepc}-^-`ZpGT{08{Om- z%p>DBd?bwU>GS8&W_U&~u*<1F(pPODAA#HF&!d%lMlZI@l?W}`_(-{*@aglX%H7)B zEJ?pPnXfH9!q8*F%r*89mU;PXhILPUYrHoEGXDUv!zWzlKr@002w^X1Vz zZKH#8RlBO35J$@WB~wI2=r43>ZmQ!+@fIFyxA5@P@#V_fzITD0Rx3e^1Mft_K7Y`8 z{P!vzIYYj;IPfkwzMN;gD)hg6b?U=?npQtrA8reW5&pQ;h~=s`Iwp;gNvy&lq|F8w=Zg1OpCS!uU{iwvQEDXh+XX|{;YRS6X-=_ zeokO!5j!&-%Xx@ctN$QNs|RK4nGn^ZhORXm=aV?g=~kPgIie)R%f zh!_1@1^K@rpl3OY)nm?`5}3XcH|3+0_r-Fp9JbRX@tN@_BR1|~Ravj5H_;~{`fMYXJ)&HS#fCjXt%`j{Y(Z`yHsY`E zLA0Sp7_RbWeuapPTmy>|sE*s)hBX7T9=J;vP z7drtS2fua5S2E4DdN94H*Q3mO?hUQALf-zzOb9kU=1%PCP0MI)rJ zu~@PP6E$;k`9@A6YXKH3I`}W1wuOdRH`7`yC&{C+d>!o9Zn|aq&E9F=n>&$*IkfV; zj5?d=(S0y#4LsAV(NayxN%Sy@pz?siC_hAC?Pr3E?&}&TLZCr@rzkV!r zlw4YOpG1xI$5N?Q8AzoJB+q~AyV=bOlqH z%CA%T`&Ht-nS7dY!;i!aiYZ+HPKLSJe8Xq1w@$}+VWb)6PT+q+hst`)4c5LBk)G8; zL-Qb6)-QP`e?AFHEgR;rZzBTM(@m5!4|q+>JZf~&FA5+*J0kZ5ZMHU(OtBi10K9}0i3e%O(T}RUXc{Fi=4R{rQa;+|H`{{Wv70i? zHwC+IQ%O-o!AR>@lyqt(eXy(Vv;vf3;GeWs`Y6>-2VS@*JGd@WWRSFA?le2B!x&lM zwh;g%(4;kwJi=r&J>JvO@W|W3V(;qn>8^qlh#Tc7#3!k1U4N&YDM<9q?3Bz)R3;)( zZg>7ho_qC|GdGlq8s27@ub6M>ksUT=AC}tLf^2-$_lcv!qbe&l>_ml)VJ3`tZMkJF zu*J_Px4v?s(@NZH#iXJyLWH+TuGl78eV)VZ1J*fo-f!;k#tQu5w(R)5sjt8O`tY{1 zwv1@uFKC$^?RyLU*wO;0d>#IHZI;;{Af~nZ!#f7}%Xa8dnUABX;Em+`*IPeD z*fRLv&Vy1W{&s45F>HH-R>Q6^K{4thhiBHYH|~%lwm( zKOT{n&DNJp7D)Q<7D}ZK>*Fs|sq_G``FD^4z5KKFg$K@|2?J)AVQ#ky8ile)Y+__a zA}Cq+(L78z4RlaKW38n$8Y2Q^4n1@An&!*H`T2RCBpMb0} zcW0V@K$?IA9zVj|M{f!3rvfM3GeD25fMbp*IOAMVZzXlpys>WLK}Z?yN$JrXM9%J~ zk7ShjTW@~z2t6x`EIvFhGk%^YY5=AjJK}$sp`iX$kzJWey|;L$clMLWomNg?6n5nH z($p?zeIQ9`{kocmifwj|kbVCVMfT|DLMNUJ02XZ%n`0_f&}@$}#MTx%xwlJC;zfTZ zO$sLgX3r2AS}rEZR=n`!8z+N5P(nD|ZB zlS(BzsHMu+h`qDZ+QQo3h)pRmjPTLyg+q;au;9xz13@=nrat>a@PagA4lQ<4JVT2o zr7kd*FmWdu0PM(jra+7WHVbzi6*2|?27*mtyVf7<+D`w4zSj2zI`*)F`XLXA9|M&U z^s~fD3iQ?I{$n;bY6_W@|OHP!==|sD*;pTxb1=M8n)*#5}p^V?DBg1$xVvQF`=wBG^YSN^Sj_ zS`6Ro^~flCq_4tq)>@7>V9fKl^yZeMbAvNmjxN-r4@t)2EUdHp1()#4+2JmiQP;%+ zVuNV=93Th(eo!ySKLbJcu3kwgq53I8zt}*9fTXpI(e%hNBFTjbYXLBw z?1^#TI9gLy*5y3=p{ml{B=mu$PG+?>v=+Ms$FAe2eZe{4Q*(-Crb-1Fv9Dz zNC^cLRjj8Ktwp0-w+bv}P^HJIHT?^vkRp!&N{YOL%&SGTG6*v|(3ZGjh>4sOBuvH` z$VLPKh22;duyaqz4aFeF>2%1RBCM_U2>MI90}ouzxUGbJ`h&7sfY&VbKL&tcsc%)dMGlEpky*`tj?N_tUlRvOx5^GN%xzJM zJWU2YRqa?UzyswQ;osUFYxb-8>I`Y)K}tFL37)%cs!^?CU%WPD-6=(WK@o5GgFHQI zfMBX=C0bWgGbM?5_=kek-78m zn+7Dd22dqF!Bx5bp7#&GZXWEN?cRjP8SVaOVfbpHl+Tvz_YW%3i$-laX6X@)E+pQP ze$=rr9d`P5$Pl{#qKwOR=If$QmypH+3QmmpakwA_xy>S5Sw4Pu@C(1|= zrS~*Sv{&iKfk6iVmzK`GGNRt0S(=1>Oxa)6#%)-1q@ z40TcyZtWs5@eAViJb9KNvI#_(`bW}TyD?dEzsd~itcp`3{*GDJ+L{{is1yFs3Ga2n zCMUer39oU&@lJT26MowXPjbR6Cp`G74S$alZgav8C;Yn;KI()&bi#X`u*nH;b;4_$ zaJ&KGo$y{KY;wX|o$wkbyxr;FIZk+* z6aHPbn;s9cp1^qAjP11bd3g3n4m=C@pi593!G&uut2ApxoybN0Q&QmjQXrQ9Bym;7 zx?K`%|KLt>T9it1sr3(4Cm?RaPU|{VDIj?{w+obN5)^C_W=cY#6gPL+)(VS+3sBEU zYpQ_inRL-NtnaNWM92`VhuZXK+73&l(oRQSB%kWPXKHi&g2Pgn133>#LcBIt*)e8O zR34cN-?QuXu8)*E1qk`y2@8aw;iD;YORy5##BFwjEte5LW2;ftf$g}pM?5{LlZ|jP z*Qop~^aWzSR1gE3n{G)=CJMe4ZOJTii!bhRT^SzRj-}ZV+MoPeZ+rNg|ER3Gfwupm zD`z0jj6*GPClFeV)QFvmp-%rQ+R{C>6C7`C_+t+`XPNYB^?8rJGwNsO607XYEOUaV z8i%*zgqbHL?8O^v1lDgx92TXDe*d!cmbooebURTDzf@5J5p#X2=m(NVv2t@ms_031 zIDbh=xzF5fC_O8?dPYT$`KeLSg`TSDG0xgzRPG3l5DVfki?}k}5h| zlEoL(u19_g35|I5Zd?XN?Ajd_IL2g|6tA|Fd>Zkb_QxoJ?ioW#@I&5HMf+YN8q4_= zfThNT!-#a{G(V|ngR-@@uAF(qggNx8i6}*f%#o++{X`4wXiuu>F9If>KmBe&ww;Gm z(Gb4Z3J^8R{*ouFc!ofzE(X{r+5xv*q8*@>SS-AkKauC~(E-6zv1U(ByOT=A*thK$ zYgb;cn~717mfxJJ7N*cKS;2u_YO5-IG%I*=_^2!B$pE?O7RCQzYhIVOFZ{a%m2U^% zF=7KCVlazw6YfN`-wy5TTgPgk#pD{tzBpbU!B^J!IZpx%AS0P%A|;@r;mLF>VKdAr zC8Y=Tp;JoA`JPrXhHuu>sV($%-Bf!N^zz>@^Vj?df{dNc9Lac7{adV;1xq}7*^-i4 zpI(NWJX|?YU(rr0F$QSSNXTx~W#4PKwxo6&@nJoSd5+zgd(fvBc->%hS216A7FelU zG>t@>FZY`VkQ7i^f|6p2CKeQgi~eyJoSHzvR=t!)E86r@)odxvg79LfNE830iR3Rt zKgEY_l=y=VeRwVdLSl(h2Oh~kKz?Fw@ADdYp4R*PJt-e>1Yp;D{l~jucEh#4^y%(b zK+y*a-&ZnKv^um_{^J?qNuFbC=0SAEyZUsfrOZeigENrTlck@R-7J&}qQ2jk7~U~u zt?aknnnvy%(prz(&+F&gF<*)0fA0yY_(s57kK2FM7qo&noJnHEF19_;zXe9+>rI6L z)|1tI0f|HcjUIijwvm6$=#Sriw1xs6YpUn8&_&ZCiNH zi)`yy+2P$^;&B=juC&dqy@ZU7j~dBqKAUGF=ksdonGzS+}V}$Te3t;SrhT!84nxrY!9FH zXOd)uckj0zgHCME)nboz<=jnS`9q&hBEcwIC(`3a6U`wffSNC3hV40F=}-6`vUIdB zc6xPmmmVEY3%=Oo>}px!UZ@_f%4JV5^c~bjURpo-VgqFJGFnK1*j3rGWjIKplXFo< zdh`UWZz&$Ad`}Ob2fYHZQHk%1Z6A8d|DvEJM5JPDfd0&6q$Lq>S>dDXIB9x>;~biP zUk`=CN8Ng)k;m{+j~=~-h!Lv{ut+ZJ^1FHh=HY-@{ghYyS?s^a63T9}=?|^3oTcd7 zie2Ei>hBO7vYgeW+J6#MP9&N+y{ogq zRb&el9INV~;{#mu!Fm9DnL16o65}CE_-JnEHvO5Q^w4i^^Do~d&7;W6u)Z= zG_bo4Dw;Kk$_Xi=Kij7Y3H8x$ssnG>PnS9`NtfgKUHEE^uIi^q*Q_66$j2NoHG__R zBLwM$wUdI0`vt;)VMTwCF6Mt2B#2eP#;V*{ZLT$eA|%T`rF98$+qOy1e=*m%A8=S< zQQf#WLG$;=!AHoOPoMM^yJ%t@n6~JVV-m%%MBG?>;f-5^_-mGTDvgC@Z@EGkBAm%3 zqB2ukoK5$%ypDTF!CpVu(i8c^vp%1Dk=OKERrr4rh6lsjbe?; z{4Vq5?_rMU?}|grzo%2pFyGuzNCk!&|L7L|?Z_AF= z=9!Z}AAqsXLO689cBt~a8G^hh&gFb~@HDEgNcQS7Or z?hYp6>xFRAWb%E%Pwl_3E{J6@y_)UI-zD3<{76;R6>dH94#kwH>MJ4=6>-(WNR;M? zY&X$GmpkJ6FG&}14PBlV)t_7oT1r&qsErV*#1T{FdtjDv-_)r@LXZ4Rsw=SwF{AwZ zQM>$qSTw0(Mrya<>&UYdUNlw7b8vz!%bU`2a#p6%9h8zr|7y$emOhQ9`F&`7HfsA+ zksCg;*b$7Bkbk0tT#t&!D=p~jr`R0|UI|=1B8wtN(EC(RBl56QSdT8ETpwv&MusY% zIGPd2Y;d*eGZ`YO(%(MP>d$1Z%hXbXx10>?w|Ldm6y2+9Bj;gjlE!|!$XDvl44zZu|GBO<+!cxj9-?_F}V??O2^VVcRqm4yzaGWVf zJ|ck4#n?2DGF>7FcSu4!U)DdY_ik1}RY9ugJ+k}2d;toll0UcizhgP$C`Z$qL%LET z@HAt;=2w!JDyp)<=LtYWiFiV*N>F&nRC$v6LRarolUT`uZ z1jDOY($^MPyRK1q{s*~D6-}UI^4~_RmOM7>ivB=q@;{mJCZ>YguWc%qb?CtgrKr zken)-ER}Y(TH<(31R=gHcSfC;^JMK6ZObd=L|JH1{qnntIYXvg8)E4%ujIJfAeLEn zd&yUXDt+vKyT1Ocw-B?CtILH+|N4ZV1Ta5UU&QjY5!@Lz&b9|% zlELD92MZ|DQQAvCUu-yjgO*-Pk+`|Ffd)fK0ic%a{jmeScpibKmXm6k_D;pl*5$p)9xksLqBVdYTu8SKAq7^FX=zWy{e^t z8~mWYVQzvSPXDETX$F7KSUF`-*6w$;d11&vzC49kigBM-9IOcMMMevM2uHS*rso%t zRmG1E_`wWbxwWaFkF2U@VxE%2TiUJRPMeJS23$L1%Uc=YB<#|ky&BP-GBiDmHf4sESH6JPIqzYe zK1r;b1IlIgreWgpTcgcDZ-$v<7?Ygkx`7gErYM&IZljU;w*5O5nq1Z#P8gqy3NXkE-NnHFHW2}Y}Vz%qZ!=a?bj|liI zfguR2?R=5R;kRjiIC#=-Dz$7-M^Kl-*%Yq8YELe2mB#_;x)s|Jw^6RIezfgB)%!ym zJR#O*C6!gBOHXR~(9OcPuBya{oxCzHi)4<5nmj54sgHcdPPX=6#Y}Wufi!;oPRn@Y z0L(~pd%)Zf;NWL@#WuhB5{DV&5nhTaoO_=s@SE!a1{&OgH{2xjmmdI z$?!f`a!!2 zbp3zyuOIqQdCbkX5|>cAmtM|%&>2OtEW!I81T~(^H z@#|Aj_SXMO2Gh+cu2xwW=r2s9>YKKIrSj`gGI>#EJeKtwsshcbykNDe8|oHJt1|uz z>e>ArAox}}{he(0S4~sq8_HYj*h_ZpUi>rlJ5h?%SDOgYZL|=2-$phnMse~nIzS7l z9Sm3m{&Z{L6>5`^F{rG6*?;H+8v5KiA5JIV z%k-D=J0`l(+vO4Frh>le*wkF*mfe8QXH+15waXiR&y6A*uRoJLIK2BE-=G~;E3*gt z!|g7A_{f0JNB;OV1B|jw)~z%Ec{w^RFuaF5Wpx)U$N)Q6fF=LtJ=Ez5ek+seR;gk< zMc|QeNx9ZGC0E+=#qo1txpMiJhwm!<3ZCl`R?ix`xfnl1aJo@9q0n!>&#a&;ZD~Q7Ym(0Jt@?Fj}pyWFMH1lJG*E@{^$VljD zfLKGxMqbnDHh!7h&u~*DH?%q}TA#xKBVKdVyBcMUm0J*q*BrBcRi)Z5(4W2bXuWw| zu{ohotvU2(uPaWn;Phy{>u2TwW%_$#epay?0_J9Y)c{E9t?bqx6rY)|tlew+^KhGx z5@BUjBNXq{K!_1oID9e z^~jARlO4UAkaf$Ubu8}60QNX;v^xo7lbN5Eo=nyVsW49V67et{9h}XHF zzwX*=ueIr;KXZawyGPoBC&>P~{zA75z4h=Y{0UZk3snWb;Oq6_@4L0&tLFa0`^^3A zJ;Td7;@Q_!ZU{Y(A6~8E0_MBcKI$d^6h5Nq4{ifRJ^nRP7d_;R_BA5I`l?I$R(+0~ zNG!1tta{~(@mo%URRttSCw@Nh{)lw6ik)&Ocy{6q0X@8hV(i0zBmOydZCl$-E?6de zjxzeiZ2@uq*d`F7zW}*I>juVPN%U{TAvomDsDL(nH2qYFFy=nWNr_Q{`%L`cu{KB)?zh6LQ9~6}&{K6W`@Q#zTf!KN5KN2t3ip1qt!GqR*6&g7JyuhzPm~ z53MWEBc~E^h?Pe)@k+XULE;>w0*L`jEuoXA#9-M#>3Z;gL=aXmmJ`FITlW6h999Jd zyUG}m9FNskijQs=n?7>TvR1Yc%GIG3u}T7Bf|N@J+btL)%4U(6+uDYM!AAx~Hh7o! zXf3GQ$!sJTY$RZXSXro-WPYZQFhip#BccPx2 z*&vw;I;`PHsZ@KmPc>DmcRspSHf-!k75BlDB`pdVUs$%c7x-c~;=EWw6B!mt|;kU>EUN`G%q;KVw zjCgO$d&_+0Eu0`k9K9s?*S!3a{a0+&)dm*L$^K3iCpa?I8PE*z!t#}{+3rQ3h_UCIn#qvMB$s||ZN++L1n*1M9m z;?~e(bZhCw0P~r9fmhbS9K2=ech$r&Ec3?vvL$b}a!ILUCDz}6_GwRxzHcoI+ zH+Tw|g@$f8p3RHYyUlZx_oHo;)Cq8QKn*A0^62>775l&p~v~^Pw(YU2|J~eZ4HY z&DZ>O`wu5XT{5AXTicIhF^vY#^sbf?@Z0QeKQi1KUgz>2w%U(om2E;+lCjAnIrkB4 zMv;3f|IK){Ptf`Fz#cEdKTC9M<<_;Jp3*0F$jSM=0s^DX8Yyb-Dn#zhSk*}Mmf0PC z*PSo&5Z+jAR^?j%`b@?sE3X4SS6yE3G?KU#eNfMS)RkBg9KZ>l#ey_n9mh2%k5q?2 ztVv$>Y-M%wxGVHQ(h@h{G*oZP<%~&6pQ^v0g{|^%PnP}@IT>T-d_93N^(+gU18I9p z*^~YB+v2&5bJubDjUmd+{=e$CgPi<2UcXiJ(QjfOiZVKHVn%1S@Gt+5W) z6iI7kJtDT0H{A2pk~^6iyp?Us8tiFtE9t^0V2ri)juRb8%vaWi@KzP*E4PqO2c{~G ze$XmM+8k{ieborEU0aao zgpAN$eRte`bY>3u*GaU$S{&6ZR@vAu2p?(mnw#|MPW{L2qRC{p1M6y|-`pB*mpx&x z?%(0+>@&Zh?)B&r`VpwB&ok;ec^lEM*R0FTF9$wRCkL5f!bcm?%N)f0aYs#bW4!P+ z5hkUDY7pFN^EFPky6yQ=JX@VE-&x(h_=56q+r_>?R<(K9+wyg#SD(=3-<|-#%Boc8 z4Hg1=v zeCvksu5$gscIkBGJI&|!nG$aPl){zoG=1!436!Gf(xUWyz`S8yMt1niR4CbJDv;Mi zWIC|dW7yteVjkXW?ohhkwLLKpp@EoozSkzd&#ceWBVzx9@S%0NdgK|^n~w!Oebuu( z2p!G85Ha!c$l_u7QG4|uqeiMkf1o1Sp*!TQR%yZ~1lb2|9UsGkaNHZOcHtfSh+f&W z(C?L%z7k91R|u#_#W=*>02<4QIYdu{h=axAtHcwb+6evzF#5f+fvE_Tc*icHo8pmA z$gA2=Kk-9So%zwb{Jj*GnHVpLJz_gf$UUw<(MoE0!yj{ye-EqKlR^K@-tmHO(6wQ_ z*b`erpVid8FZf;=pYk17Fh9-Y#3?ifhwbjES*^EzpYt_y0 z)-V4gb-5_sa`fqm`U*J*&(yRyKV2Tv?5;1@oCxRFX7goiwV03F-_0_%?~$#8m2XQ0 zSKAt$%yf0m`0bb8odnOHO!Bm(BE(T$6Lp_=U|mm^>vJOQ#k5Q_f* z-6AAXgt<=^QJHS31&O|DFC}opct&#)(8bZ3JOj&igI4Z+jts`rHvPc_5$~b47CrhS zdf`3XCTlxAI+p_8@NSPDeSjDOXs_8OCDX=8_+58+<5cT1NvO)h^Y>jQo~)o-O*3kR zS`K9EOgrCM=HlcMx8rD`QF+2kOZBa|RNeK_wtA+-(ZQ2?m+A>?Jay5Bm7nU7Qz&YL zKgCYf!$(Hz5kJr2BO`;jr{1*I?q!o2}vUAakLwN_|bwn=D-l45Tg*j^T~K31{x2U@y?Gy_1p-)t`2Q>=)Hp2_qly_Dac zV5Bsuk?nA!ofJ5o@gaO@+lh)bwbpORHS`yGE%Qw=43}S(E1k3gye7aTBP&4LwTgdg zr}g^B0RI8NTOChG;v>f%r(O%_CPrgTD*1~SEn;I6MpbOIQn08$uotr0QmMs-Yh46N z|C+DMt_*|pP)1f)+yktH!IzLkt;vNdYSq8bBI^M$O+>_}NI_Z2_l}ocE zNDQN(5S$^*We|EqR^~|$%(>r|15dfPI-hts!C(*5a9!C7M~Yeu*FNKEpW-$@D^!siGyg!y# z;%B3QiDj?f{M<5;#0-20lqjWtY58U~8ye!fln&{Ud6-@PSfO2+6Ekv-^b}TD6m}l1YexBI|5@NG)k!uzt7J`9o zA7HPRTeqDj(!e`a?bd%ptgcrGwn(e`Ggr9e{TUY>i?kW$J6^Sf@;)!KjUN4#8jxQm zu2n7)3zJvLEymF`+n;;}|!VkiDcwG4f~slGU1zq$S``>5aWe^+1Jv)^3*YnCLofsy8? zP4Nq$7BSvpI){FG!QIb-Dp%Me3#b{qYAxx)JM-Uqrk;9Er>Qd|77-$aimsQk_Sd0P(g+(5MqJ(mYc z7PknClq8L@v8G?M;J?341l^bJS3vqTAk!~S8O!^Wa*_qnYVWyRdOpY6EUH%C&$H!E zdYkk&7V?ug(t8SL8w#WvlSWiMpeZN{{}J2+Q+Au*6R! zDlggI=ci6T;wLuY?G$*St0bM8!h7+sC7vS3(KnKRZr^+WCR+RF4^b6!fj?X|0jR`0 zRm6kUwJ(%(>I=I)iOJ;j&To>xS=BE*U7oZ1ozGN4Y zU1sK(=pi;P_cJ-QfPhp!jmq-MHolF^M^ehPWE=B(@NPmh<$8 zZQt0eWg54B{0`YIs`vqGDyCNAedzCCilp%d`2LocvKWes6L(QevFLr$(_`T9w2~!> ztC$y!y55@@yiz4~Tk@`4upr>HtRy!(9U$?tOw{w=)!z zdNW?LGkW>ZM`0yalbJcGVn@_@jf!32Bd0GNs!lq)+VIE3Yfr7*wB&Ov>A-0$28;3H zWGh!}XZpNBxaV|v;mmwvX`QOZ**9e$+trOz*_B=P8TIq1ABazInZ-W+nG(coxDOh= zkeKU(Dz*v2c+|3a(JS1Z%^Kxcpp4BP)JKQuJtn@8@E-BMHNR=pu~3*2Ma|-qY731t z;$ROJ!(&$-!#vMRo+9*=*EjmL{)x2Cq(&?Dy;A8HYwVLy->^Q>%l@g>2i37MZh=5* z%-6!ZU#(GF_+At1-wN+Ofh(Q#6%Dw;ajGnyK#+bv%@;eXVxu>F%)L}!?S>Bs7>m`) zb;}M}Pn|02u*(bmQZ?}_iraf~jE8xHv?gA7U%=e2o_B8c!|O&wBjMPd8yoGShKDJzP- zW?T5+aBmrhMUVKPwcFYZ&R%n`c)s1Zy`70E`Z@EgH+Ds#Ec;}AL3vBV{cE7v-N>Nr zkTw|a;;m<=qNSP@lz1z+zXnT#x9M!Eebf4+7l_i`y+AHjKx8Y`nhcQS+ScBp%-^)0 zX>UMp(NZb;pgq2f_(N)Hb^13_W^1=~3SuVHvAf>Tm$ujY^`)KCVR?Ho=b#U`2zIr( zOHTXi+HwvU?7zG!J{E&zQ~ZjQ(Z0v+AGBSKT&m9BTy`|tc2J*||2Q=d>NluR_itIg z_d2DxT9pYEH~aJ*ba7Li(Y4ee?l2t*5V$ z!}(^1kCl*H%nTuE`<_DACLjCCpkfT#>J1+`Mco4zC|Te)H!os?I=|`)gsE;T9i0Cu z-OAny!1ell;>+`y8`JiR_|SmsGuuh=09ky7uj0y~=`s4sGih4h66TE6%FkJ)xkx2v z7KNus;qY^6rH%!07`^R0<>;$Em!~IHUUo&wNoS?^_8_#{svcQR*fq0(G*x_4E6c#U*DSwW*!PwgmejB3Ju9hi>jcR|p*}hr?u#AfxKgiX6?NY`Q;e46gGa=Xv zA%R2#e<~?VMQNV_MB)|Ff>|XC(DWyX7;PRer&30*(^tq{dG&hfI^iG^Hgz4JoOP0o z@O8*no9HpKZTVhJ^9-`W3SEKh+zb%p5N_i$;C}4dC<*NInv? zO;~O9!3iw#@1Ysxl02y2lD8GvLIS6~Y;ZLMa>vF@`N~`pIzY5rUm*){Dvc^osF%J* z&K7bu=_~$77Z~W$TM7(2?1lJkE??yihyF`0NXTV5*wY}|r(P*;U8i2Z@_Kv~#Swc> zIefZJeCEVAgI*hGah#=m<#?{Eu<=^>Bsg%33_OfCGAVEz7MEBhf<8b_>>&YTgF8jl!D}9&dFcS*T~Pf!;BP<{*$i60M=jNIYY7%q6Jyc z+43B1QyX~tBuBjMAohR~_T%K50lI|G$i(S@6G2GJP>bjPMusT-l1K$qotA&2lvaG6 z!YHiQug|HqQ`+?vJ)kXG@IA1_^Ca1hzhAu2D`k5})Bra(T{@vqfABG$c}HM3a)LM% zb~v44<3YSy2Hl6)Ts@>eIF`6f89$`~SX}hl<*K3iNica$56g`0m+`zVRFEOWi> zJrBcb{NNSBVmD23FwAE0gF|VTW1+%IHiXp*#o>NXvLro`6e?xzA7=bdr3;aVXeoAG zcD4Dg&)izg$-21LMtMI#D1xx7fNN?FTZ+yJVDTEUIb$exx z{_Bp&Ry}ezJ#&E6nhz52xUWmH08 z+auFkwjMDVLxm}9MP5fZ71Bo@LX*lS#~4GV_&oxCv9+GC#cL2ky|Dlcf{~9#;1c!mcHs|JSnZJ-fMR8#OZh-s3oQK zbH=(N1_tsvwSO-i+Xc#S%dI4oJF61cA!e}&*{HQjOOKCc<%QUKTjMNP>}Du2LT!e?aSOmvbOeX%E1ri?gI zs;ZZV-%8v|z6^ZQXGz5}yio?bjoxTR37=1j#m21}9_;+#A@D!g@&hpNhr$+L-T#9KQkK!(#C)6-C*z0T)n<6T& zRI++fB96pbmq-OL#$;XFlW+DzUr12!}`J&>24P;n^n9b57%4vFk@8Kj(4 zNu-bzSzn2?(GM+gIcsxJq#0`mBE=vu=Tk=UzArJ(r7;=Nwzi5c5Jif3as=R{^jHy5 z)gQc4WYH1a37cR(Xk&sFi7M1Co(xu1x$?K^V`0==;$*;jfvZL<0J-1fx^%ULi-;AX#~uwP<>` zm@0|WWK>Cx@-ix&nh;%weZHm=n+GdwN1vWR|NOt+tlVYhIha;oJ>d2oLKT8iDBcnJD|XRX`=6|)tq(nO-E{~(&$ z9wmEaGE8gCFYu)y3f5Qb z#xPLPXEKByL`qN$e+qw9sYjj?R;|wrGSAAOKYcCT91CtkHct0A2PpE*AMq zn--bTU&0&@8{2F)o<~QBZ4vh9_rE3jCMs_+JdUWoN+^@GDDmezS=sw3YkO)e8?22Nhk?6~CRtQ7^ zEg@HVZWvhEE2L}D3yeT!lf(uJDzwe^gpt-HQqvii(uu?zgfBB-1tjA%A#SmB!9))a zU~cYEbP*>=nlQmA_^f)AN_MBzyhNuG@9uDsr?vIbW*S5cWu^Qa_)!8q)$v3x_z2V)S2vm0J_dU*Yjk&$m8`I1q;w1Ou zhTXo~-0BX>oNF1zSIMj7WVtG?tViDUS}vv9?3^qILqytZxG0bJMZR#tM7 zV?T6FQ0|7V3B@#^PHl7=l}QfCEc`&t2T~_itK9M2@8>nIIkxCy& z1_Kb!*o4C1YB~17JMvqSv9x_sCl>`=U5pC4H&EHpTpIHibBay;;g~QUT*G+d^TnSW zZgX>Kz@kq&_2)Lax;veV-t6}gV>zGSE~6vMs5=mx%Zzl6n1yF@PsVfiIBUYx_E^rV zs*o%A2_=KoIMhoy^}L$lq^-#L2YI;VC*c}iO_Zt?RF(Th^SAJtXttK|TW5c|F2CI|ERajLJNncao+_KUOU(sib`3k44wj#UXuLms~ zi_1UMK%Kf*j9B(2PD%yu=KiYixWAH^6)>|mg|3x_8)vEW|4dS<=wfM+fmNCuYaaNQ z&};j9@v4;EN=m2L`vFQC20x=3(C#HurhoKXZS|orQS3{dGI&MIeq$ zZ?ygW>~)#!S#=n$9bB5?HF*;AwLR&sd}Y}lb>GKV!JCE?)}n`G zcka$(e*Lf80`QRWse5lhE{8*h!Nmz0$ zO!0g}aIXB6iTQ;kZy|2|GQ>Wyh)Vi}P@B2iFg+YdHQ{lcvk~~vpRrm~MN=0uW*$xN zM-?48hY?|kRct&QE4s7K|dTYgTfA9#h%5lc>^+vphP3BfmwE+Lnee8_Hn zifVn_TQ{kG28V~oc?APKs;jY^KbNOe(JPBEM82Y5cBRWP))dID;F(>XVtD~LEtY1^ z4+WId$ltgbXDQF}$9>Jyfa%CbpY!Ua(BEsmX1)>g=f>t2dfUIskxduZzWB|q%Fd-H zD$c3x&dSbZhpi65ST1i}XWxnzh&K*JEL*PX;cf6h zd}49Lj@ z^d=5GTujwwfnNO9Uv^ozCny-sZ;^$ZZ=g~O`@YbT21C_5X9EYu>_}xprPHdla zjmdp#yt-(YQT8gz6oYBcv6}dllW1*kcJxqaD4JiQ-q=YniDLu@&8eaeGAbHP@Gn79 zH%}2MH+$6F$uMRtB?geh?}2T!ZeZ@D_rdj!Y$NGXDFdkA{4$vfnvky>ir2U)!_Iry z?5^enfj@rZv1X++yZv47psj&H-D)?6-|oolPe{RO-mo>q8@sV5IL%~Nx0~Hq-t^wS+spMV zTNOwwOMm>z9&Ak;_~zytLF#jJD;bpD{+@5pW!xka^G!rzx_-i6Nolid{g? z;MX_5yJFY&-I<)^vkGAHDO+DT=VO$g6&eRS9hb1=lS&KwzCPdJ*m9Bf1N^RHn0WhL@MhI8?*AiFV|`XBvgU3EucaC%w#gHBVj3d2r2Fw#_`nQObSj>&2RN z`0k5e?FOo<50&EdQ+E$`Px`Tyi371#-0*OSntt<%b>+E}{juLO<33Dm0MtmRWhAKWQc zcJKzZm<-5%kzdT>`wmb~a{rD( z!{&#)Un2M@R5M7q?M|Jpno9moT`?y}$^0O9fu)kImYh%N(wJGDivzzeQ`9p9yenO* zMB)y4ro5wHrwW$7l8L;a9#F@=Fq4;$pyBwml8NH_7{iZN?BquISgG|cpoN}e=Z;LczX>mq&IND6Q%gAWVP|Ip9u^fzJI_ehKu#$+kW$u zg}A@=3nskN#0CPtfaYT}#BIsX8sanC%$4=cLtxtp{w^HCLSnho!qfYnfISm&GVKG_ z*XBMBz(Iz~xcI*0vgD~5dzdb!+oe&by&CKld81X@lTB*cdn5TPrT-mz8#cW!<2oND z_;1k9q3M2FvQ>v^&b$4*3J84U%#`Kl$Q^ zqu3jJjg>(STbK501tE>?rC84IzAYp^WXsj%5IY#EiN23CO$4s1)9>2Q^8J(+ti^$v zRa5!-!i)81Pw|=wudrmNwE^LVssHwOZ2%KYr2X_pa7o|(l#vg=8PbyP^qJqfs)anv z%RB%2E$P8SUNH7>7R(D-Y~w_roU%?{>o5o$y8{ywV9Tal&()@H8hJ z?1V?>*>Ltc;hRpl$qCmw;S)~yb0@st3Ga5o+nw-6C%n=LFLA zal&m**x`gvIN{Ho@Lnfua>5&(@Jc7VM1_cod4r~*3?hne3?pMj&O%92bdTlSE%6Nh z%kjgatCr3FZw9v(Sk8XLiq}*Dhhk9}o z;$MG?ra5GGg@&i$;WBurjg7${cMM|hr49Nk4J_HWK7irDJf?%tXuM+9%$3efw8iTyR%Y!)RA9&jI+G{4&)J{o<=V`&lM)$m#^Y3yu&2A3O z4K}-jjqU{?6x8f*8>_mRbLWDj`)rkXw!3shb6GFcOSN&$!C4LSwXw4s>o3#B1+Mi@ z_fO>7@l0&`h0`z4&K}WxwtN2U*|X?d(`})7v*!l~YoYl~v+Ejfo8NHv>{;%{1;K{K z`EDtg9xtKhnYYcpL`ZRyo!fk~`=;i)riKMU_f3FlzWL_C-i0$8=FYrz?raBuz@L4| zV7GhJY&&@MlC-zb zX|H)s<0ALM*-g#T--h}1jZO1rO5deSwNs|yw)u^~ojreHLsR2?As6D%)F2&gmU7oS zwlJ{8!>;bJI}> zbu;Hnjk>1UGZnx1zC^*8J-;z@+Z^}2+4CBk?$YLlX7YG&K^-nD8&2EN>p}CMbc$d3$vi}P5)ZZkSRX-P|HO-uV z8%$AywRF;T)z?f2OuD{ItDV^tY=G5s8=K)ly}KFiHPkoIox0iX(%QhJ$&(eNDUHGZ zW2q^bQkphp&TMB$3K3OO@Scub z%>moeGBt?k`H1Gy5h#ffx0SiwrDz6J%s3txa1Y#Voiz)Ln%yHr+=T1yhIx$KQO#&GB9*}%xp$A4<-YZ<;A{phf|7w^Q(fz=SKHJW6wT<=T+|SpBiTEF~n^=@ql!pFO3gJPz98n4j{X3Y%t zk-^gWp}BJrgKPW~r%ae$J+*ey#OjGtru+O?_@_+PCeECw`RCVbrP_Qgq|N1DgyG45 zrc%Fn+WtJ?#K$_%FZIcrNZMTIIY9V5Cw}Z7z4g;@{@;c#J1`z`!nuTF`Q$mz|M`1w z8_)HLAK%W$4&Hs!ANX5u-M;C4<7YT9-=^#|;sL@uJ}*6$NF2YFf|bGpicw9$T^P z@_%&Ch?U;I;qou<`gQHezU`N1AHC<7r;K`N?0Gj{ajiVtb@n@9iN}7Xsy#D@aSNVSUTFt;$_qX_f ztNxxdcl`A4+a4Kv{$r27_MJoi%5jfxCkuDxkQRpXPhN(XkQhh@52 zZ|Jmt>inFipI-f=311w!JTYL>n40s3l^-z{-FxHM7n_&8HK{1nTcLk!p4O}_)atY* zEvSvwW~ni#Y15l$2d6iNf(t^y>Gm9~O~38Gk_6I>R!={G3yym(IL?08p6p-#_~!+` zJuYAC|6FjW|8wba&z|F+=N$LE^0;Tu>;2(9{9ga(*5jUAj(c8t+_UHL{`Hw*wZ4A^ z$2}Jx_gsG5v*$Q^mLG@jmh<|zS9?zX=iScypT?o!xcaq@U69W2Ij;QY$35>q?)i$U zs!QCZi^p6v{h|v-Eof|RSj^NmXH?Vdh24QgwHzS+G(0LW*B9Odk9zXv7`)PWeMAR#D6maINt^TcHj^;@HvC{0>a<( zDIxw8;VB%g8A;qtcsZXj#65&p@EJ?ImT)p35Ahj<7nQP&C1nUNW#XJ7al%GEvxqMs z+{tGy@!f>)@mWlKKVii=&_v1;2KYpX*Ak9Bm-FY6PFTg~HMvLY1*bCK!LnBfixNNQVLX`OlBs@(8 zuWw=O6@;hYf2^9ZHxQnHzh)ci{mUpj{Ey;UiSRi5?KRL3!gcW1Tmk(M4*YBJR1+M2 z7oJ*#``|x@XFI|Z@UN~#I^n?oDxP+Pr{FKU66Ha-6#hXxdk{VX{~0_%!h!F<3VK60 z4*#mF@wQ%s$KZbn&s~HA|LN_}6@}qnat&x8Tm`@8TE>ndTnB#(MzY5#4F3T<#}PgP zpPK2@gb)9`dgujVFZ@qqWSB;H0{%VCs4K#U-`5IVAx!?^Hpu=Jz~NtaJ@O%32mhHH zpdW;%;Ma6QCj^JTsRwn4Fn!nRb{r=K5gvm-f#)uS$-mTq4G<3ec05NA_QC&JAKD1Q zQ}FNEhq54i1pa&WGj@W)@V}1dIl_Ve=qTzN;c@sc;&}<-8Tj8jfO@Ae{O&t2lR&r+ zzUNK`MW8(JYw(mJTnFEO7h~lJ$Kk*HE|d%58Tc>18}&hhKOfbc%2({3Z9J?kEiZHaw3b9Eblf zo~IBVhrjLt@IlxMKZxf9;lK|Zh8`#k{}1t;LU{5$NXPRE!c*}79nTEH)A0Wr&#MT} zz<&+TX@uE>Xm@ztK-dTWAw0SN06oM12%chu$KihhPbtC^@Jrr{_JVLJ{2@H&Azb%9 z#_}IR*}ept|AU73by1_x%*sk7k;Z%*q>XK)8~%6d=iJ`$aAy2Ev_of zUy{$3xSE;DVWtz!grr&OVWl4{u3FMp6!-WFC-bLrr`W*2BudViL5qR}O35P!n8 z>xJaX&tdru?kZOw!lXC#6y>v`M1ECXUv50d=bm&G<>eK9D?f)*vzIF~Khgh~q_f(? zRv*r1rK5Rl$xtrKe=!Rm%HJ>H#RaVRWA+*Xo${jl%5zG;EK4_8FqJo*Gvo4^gCHB^|GRft#GYknO5hpU)3f^r?c7U+G4hL$djE`Zm!iXh=yH1N~Z)<_M>34)ivpV zONRb4bwg!U+T*sOCR9hTjxXHsN84Y#8~+=jCDUz5X|w?7{h*!`N~f%*h_$&1}vMa z`=x;Tq#u<2UDAJ_^p8saap_M=e@gn((tlO@g{KAGmD1lJ{aWcaNxw(>LFuQYe^~mD zN`FH7PfP!K>CZ?%_ch5+`WvKQE&V3x_eeh|{gm_%N&l$yk4gV2>7S7PN$D$Ho>KAG z1*Ba1mrK7v`n#n+B>lUj|96vJe@^-@O8-sim%btBd!=6^eO2BL84pT7A^n5Wf4}q}mHy+>e@gn#O8-UazastD zrJwt2!D*TF%cZ|w`jyhJm42P{ua|z0^!G@AK>AVXk4k?``VUI~gVKLQ`X7`2!xE(ve@g zK?l*pAsoCclkuTYU@uN{1AD=KxeX@IRt=J>Ou%sp7E4gPT*M=xDDAm(x{;Va*grBb z5K4^FR+kAIz%dmdjGZrF(a?T6I}Wi6L_8J`MRD+(#C1lVCl!mx^L*sFOu`cWK&W4c z$f^lu1aM3kVl^Thiw68D;!!K&{OD60m||~-!v+JCeys?HQsP*dwW&DHa4G#J9*>ib zNF8jqh>s+P`eU(3$dBT(n@w1NgunqFW4D;F0aN%}O&A@D4~9r_drVk_t{;pfLkWHe z_6`ef=I=9MCWsAiSi-*_TkK)byG_Nh3s3Q=h~tD53WmF#$B{S?4n=|td-tR}#AW%D zDn1m8QGxlfFr`aGWqdy+MUBB4~M5hutv{f%_X^Uv2;OaC*+! z!v6AdyHXL9pk*Y=kCc1(m4bYNHu~eZ1OZH_cr?Y96099IF%nnEP(E%Au$7b|52Aac z`@>Oa@Z-7Np;T{_Uy=v~n@0m7ehUTVM}4u0-0nyy6lY(|?Fl7@!%>)~swVaixjm62 zl(UO2>9B9*&Ps&M}<2YtF2A~wy0cbE1iA6)B;S{?OvB6M^Zad(*MUr(PPDh})Pr|w>9P1a^ z*sX{~0`racq<492sILUzW_GH41Zua7>Tkw z5gJYo8e9za5HkWI^iRw{9~_NgCyN=>vm*EgLUh%JvFlx&J(NDoIuK6!Q#h`D(q-Tr ze=iHT4Oy=09AhNPVY#{f{#YW#%5$m9i=mIaI9G7GJa=Fqf_@Ks!np%bLXy?y4)D86 z1MDhfrz9oUaXi;yoDSx8MRnM;=lBDkSMu{(0<3I_2xn2UQT zfjBH`JeO{I_;E-0iCpw_a*X*Lh57h$oI=sSaGagY-KRugKg``9Nb+<>>%#w`4|>~e z)s?U)h`16m{;r=Jn!7rh+qZ6!BCra`@IUz(PVNgNQc7AVEsN`0UY7-mJ?&Bfue&6B zy1LI)_x9}RS&6;{{NJO2(t0jypY-4OmpUG=V*>uaryy@hI==>TSPqaXu-&%|{%QE7 z6nC*Pcs7^M_fj#8jky6G!+*+m6n-xJ3HW*NPr%QIKMfzA&AD)1gmnOI$U2;W56uD} zodte)7WmOw;K#DSNesLdcFZ9TN=b6^A0apru%C^1-7NSgka+S!7dv*bm1%4T;-pmR zDs~}G%9UX*?@z7F?6-%Y|xsE2KYp%PUpfo(=@cj zb3ylE$iMbX^PVX__;^ttcb;j!dGKAZSzGU>V^dpr&$4CJhw}5I2Xk^BxjQfK$uYP4 zEA-rxm-jh5NAE2t7=ECngtr@=f)1g!uR|9Ofi9@uXqYR}>SGe;7~iZ0U2s8yg!}Aa8IsoCk}F%E$8aKYEAD^{a)* zoDF9Td8uCRD=)_lk?amK)MZauJoLULOK!Q_<@!-JnHB|eyI(m}T--gIL}Z(2d*{rS zZil11#dzLjhpF&ipbZ=>DEPplWSrl&-URIj(&Ct0vE)AF$wixfg!)=XLY4^dZp+rs zaM|IWEs4@`6CWMZKG{E8X1d@!*!*nLEr0>a*qI_CA08 zYjP-g7U92n$m7AyN8O&8WXBN{||FEJW{l~Sn>HC*1&04NQMMddH zYHHFSzU;Cz`XP?<2OBn||ERK3;Je-5zkkh|6`AsoeM8o_XTo9gTWDUD38y|%>F7V|TKcBkkrt}a0{om7%Y}>|V8?UcV|KdkKO26^i zYw6cteKq~^bI+we69}Z={PnNXKl#u96!N3K59a58I8z?znC6MDOnA09jZOCc{XHvI zaJioQi@-d_jJ^D<>a8K{pm=euP0G%wG@DUQFC9AM!1;6SwEjla+|`OC_T~NpH+VJ zr!Ocul~3aUs{%eyz~;uWzzqADwY1l4}WOJ?R?P=KaeRO`q4WecQM-SW$^c8rqlf0PJ_Z{Oa2dC zdMWQWzxV93>3?|N`_kX|=tp_q^X+5DvdWKk_;{v#kiV1GRn!))y6UQ>nd!9dSd9GC z$A9*=+Z=LGKl!(#qv?mvJI{oF@NI8PKmFi?>5~%^Y3h&vCLT{?-e9KNd1HM0n@suU zi!WAwJKwVwcDbI+lz02~?W<^=jCIO-*iUwRDW%6H${bc4j8*%hkpURXEG6!kQ zqICgm31_EHN9&?Y-uI)Q{_a=4lKzCxm$jZV@w3ILFa62`52XL=YhUAi=ks6q0-rlj zpPCJyU}IRHI;G?LVDD7>6!2GPrqfz`HhPPN)A&n$Og2Y)p>Zr5ZO9VHlyAN`-zUiE zKeN$ZD2((zo9wDQ|MpjZl~sP&?IoG=+UGlDKbdn+nm^D!!9wMm507kSA@1nr&CW99 znVKD_5SY^ai0kWP5HCU8(LdimtpRVUotu!+QTN;(4Z5_80{_Hjs2O~ren*MISg z^nvFw`^>JG^Ss?pW+2HfRXm2T-TxSBK{+;^BOh&ytCm_A4&ZTwN1x!qo@g|&T$OgU*x zMj0NV_MDkUWuUz!$n_i9Qr2?8X6$;ucgd2QwRVSn7McqkUcS5}Q#N~?_MYwW%rNaO zf%f;a%CVR+8*SBY?_Bmdbaao~{ndxF^p%-%(mFpArsA}>MEf(e$FbOQQ2$PSq`jXZ zo6+>1MJM+Z6wvjlIUQ`M7q)cUTu8J(Lwg*wPq5%}U`{~eYPPY7?0q40s1D|mOQq3x zp~Ca|v`;{5ZPF>N`DopQ`gxLUmgc}TkIXg~v(u%vx)AxXzJ0}>Z@w_<;h;KGpAYvp zNvD2hLGtJ1{NFR-BlnC#XOd?zxYS3Y&dfQ)Y-K^)`_skHr#u(~8>n5V_0MAIP(Ija z0cgeWm}5WL+iY?ZoVWX>OS8@)7AgzMa2fh1yeWd;dANKbXFG~E?ROyWPMY)L$wByH zl}0}G@6o%%>#4z$&~Vee!!2Y*Ao&&JOVhupL;b`gAjBS`Ht*>TQRSj?qhROvL{&Ch)V zv-mF%Cc#gy6aJWs`)^{en&5B1j>wAWzrJaW8vezRKtDajL4~nf%jZQm#o$UM~N%D95i*t_unvG6dM310RG&$wzBE zjnfp;3-@0@h{tU{$HV)iX|&Tc+Fu&`UuoDy z8f`BPJ4qi=uC|E zI_W%*_HF6Bj?U<44vjX-&oAg4kIwpNe~3(q&d2CXiOy~49E$eKaQ>X8b2K`iqO%TK z&(YkQ&Jk$8mCm#142SknX)Qr#BebVY^K{yKr9E#t^PxFA?S;_ZEA1WAzCG<9)7~-d z!_xYM_GIa7g4Vor&PC^WwDzJs2s-LX0Bs0Yy>^Mmbmy$8k>HGimB?~#Jg?A1=7Cjf zY9&FL(c!(#iOgd~6#`|Yr4Gr=cR?0(fG)V6augK1742f4bvAU5=Nz6PFQaZ|p-w#4 zE6Uu@=YO-wZpycTqX-81+}miTQJ}8%QXao}N5d-5wT+&lD=9-Ur@V|;)Oj9H)u?dS zZiqc_@ZgR6T0A%0ec!z!J@+3#nF+tZvi@r5ed`0^-Q>1(J<#E~CUt+CClS4PRr_7{ zt^uztJOf0@=QX<)psf$=;zD?Cy%YY<2iiOb{0Q7UM$mPHkFgDn1A&!f1Qz}%Vpjuy z;GR7#mv?h~eiH2lE-!5*k#OSI0DsM{$o~7@#qm8?5d-DC+3^W_!GORQB^KhO%d_M6 z-GNGc$Ga)f!;kih3zl+uxDD&_)Zd#74c|N@p-!Thj4RCMN6R?vFK~41Xx4(1xuIlw#@M_i+4IPYXZ0LAMW94 z9?!XizqB;X`-*i=L=kIy_XFhOm-Fa4!dFg!oSA9_`U4N##^GBD7q(MO=DG45CoaVT zoI3q!Lad0;$#yXRfs;bobKP#@zZ2kfgoQa#x`3O1%_=)85AQee`#qjjR}emT1(_V;e{c;1c=2ya-ko;P92DaWAeg&}6jMbt0irCYO^u;EIC zm)@Wi8WjcEFOoFQX_km-(Z{8)%q+yJwa`qxN#=Zoj}cV3E$eK>C|bv>X@e=ExUWK( zWfC9WRTr#XvdXjO+;!Z<0I-aOH%f_FoM&ZmhAcgBtCNY;8D)1{OnG>xVj(Rp)<)Vg zvR@_8s!J?f9$K28E-;6kbUG)0#R}5gW}$;s7iOSSv;d=10U8GJDlV2IaP#@+EGfDm zle(Fkda*nkA2UG(_`2z>bt)}#CV%(qR4*18b$HC*6A|DybnmUKTypR7<>%8rGS+iN zzahDKv){dLUE#sP!dtM;{R`~rymYsHpC4x!V>vnB9m~!A1aNj??THop*>SX=gtheT zSZ}`!p7ZWcavYpXK+fC#0#*H+NP+eF#W)jqcENlz<#W4FU_I|$j11V%uSJ=ExM*KIzw$8A zyeB{Z-E-;S5bhkj3ujY=OM5=#o_XjYzHdT%C-i&-`#0pC7#!q#ITWV-j?eAgo2ES> z;{EZqHhxE&!oT|EFVpW|w~osO9sCUYL)mx9AQ;J~^sV?)SrttAod3{}38y&mr#&Z< zh4!}ym-en+``OP-Jc5CTc4tfJ1UC0A#baLnD8Is0{3%X#ru6^i`|i{DgU2(9pWUZ( zMVx<_=evvLPj@?MFYp6iZ~8Cy?9uq6E&S@?bI;ZKYv}nJRUYye%b)D+SXUR<%_CP_ zk^cD)eqicJ(MSDM*~?C0^s!wkeLntwcjukyPxkbfbZMWD_6RB8IQC6n{`R-|-W%=9 zP5AvLo}!O4PxB79lH;J;y-TIf$KQ^(m@xQj=gapN6}8%F&82@kUS^nVp7vQEC@s~> zt#Hw|Y**!0J|AnKKZQek$&aBOLsk=}(z5x~PQNiep8oZ(FwKJ_|JgtNQ~q0lO#E!; z$Jp0)qirnuzR!1_c!KlI{PS zLxjNI$NlkKgWS9QY{CbBG4rPsl9m zTyfaxdl$-E;nD9!p1R|X^gm+^qItpla4th>IQuz7yr|#DdC*R@?LOFnb{8*G9*mWv zsEaog{WJD)M+P+b@AGl?R0rGoZ#xaNx6gcN)vDr5zV`S%g@som-?#1g77|9Df4;}# z*+#TzUW0OEolio~AGPz#7UsW!EG*oO{%LY9ePHhNE1cnd;ULb*A)|RljdG68CNt%k zYfh9Y58nhwdqrl`Z(5(`>XC3dbF& zrRdvw&>w^kEnQk`&&T_%$?I?+S%RbTob+j&_T%WBt^?dXDW2=#)8+4V^F4$|&ouH8 z4SGQH-ZvTcLdVv*(yzTqd!p*Omv~n&#=Lmy@VJ;fPnRA*c9*^@1?g7qUm z8%JA7W8Z_%uV7bc^ev9_adj?EXX4cVQlCs`;BE*V4L*=47oYLGv}5!_l0H=2J9}rujV0qiGzaF_z}@bgs?UI2i9} zeMM_LHSa@3rQdqq(!p=Z2CNfB^cmF`zRht;NBxL$`Ky-YxZ#SHuG*$xmVedqB1#l4 zXPJ3&Au-nStSa5@&RJ(=Md(~xW`R_wPA~}XoQ&LrQfTeuS7^Bjp~#eSse5(rAY9G) zgM*!eweI@<{;T)&yVcJB2ABKd!Jh8!?%Kf{t{gOWR`g%Dy+2gxF4`z$EOjIOd?Gp6 zj%VjKV~u-#5K&10!m0EdDBa!L-`_>){9rMCDMHTl<;%9VQhFiZ_#w*9^gT`P?R|i{ z`HA3iEB(f1x4UD9``TOm?`WfRae`^36U^>-dO0AHzD!$x#d;B1O$A(wa7o<~ndWw% zzk6VC;HEVc$1xO+FEW&W3#oGOW+cnAmt|&F3S7;T2M4c|;-%AEbtcaBG93k2N95rg z)z>J~8!*gBcaE7Ef%6TH@Xj^vwJJSLDl*-*0U-2W;}*MZ0>{DOa^J4b%iOEen^%;q zQzTHwPFI>Pr%cPLB*sbyiK3P14Fcahe`;B!^yJE21<|EeT+)b$%*GWZKpYj))PvP0+ zqwr}EnqG(R*x}Cb7)~SU;irLgA}AjPC!FbX@!rlj6he!o*Tnr|tDlh_jJczrxN=|E z4y|+4x(DkWTHB~~4eEs3Db<;p$CKS4_gl|fI(W+l{`cAdUk~NE;LNELk6DmBH-)+5 zD6T>kMQ@Q#o7e;ef|ZC6mIRJCGMMqAA_T4Yl4Z-5P+ZYl2FGQR8I~?vR!sa90^#T7 zdn|w}w*Yy$h@6I$g|a9|q*R!E!v9VCS(?S=v#KgFo1-`Xx@4@7zxUV8yk41x?l?!r zkcKWfmw$PgW`Xp^-#i&Bk+LN~s%(n;S|fZxGd5GMC` zxj)7)Q}8`R&>^RM9+~CO_c0#0Kre+anS}ARlR;1m<4fcrP>$m3C^6)FhT~M>J26%4 zN<7u9pZ{*9iWMVoBXEZCF9iG)zG%+ae*Ui#R7UWfhDxL-K$qT0TxjCQkrKt%Q99sL zUUKaj_)&Hd@RdAGz)JG3rH5G@oWh{Xj|F5tFP9_=oEVqi%S%OEg5VM1*dged%9R4G z1gIq7i!kxO9JOfi+r@E`Ha^=>mTEkxXymE@ZaZ)WId#%P9OWS`4zeMn<6E*C%*#Fr z2{uEESAx<;wgt54@n)3$azK2n7Zf(4wyC6*ptqeBfoG@WLA6EYQu?8o(4^4&HZE(3 zjY3Ao-pSS>uMc@+(8mb$n98DiFZg*`59B9J3G6}SA#nZjNV6T}oX7G14u2Q0Sml)7`tv_o1D}U*vi@Bpr>g&i zw1djZP4@rEL}1fkAh5BfvbwTr6TUEp$R>P+W*|JcDZI6|W>et6AU=|Khf^cb&^3WrBD4h&eVGGa)H4u`ZN#^pqABmj0WJgtKZ! z5ZF=~_${g9PO3duet^DEm6L}GbQc!jJ4=IJ{%0gg-v9~)z4)*gQu3@60_2Y1Z#)WKcGX={&x^5cR_LppE2?}$>ioy<{W&EfVp!8Ndn{*68pTu4*pl97C3Vj z79{a$F8-k=cdiiAiQpT7AW2_)iiDHU$z$u0Bw$ehnfuR3a+4~)PZeP9 z&k{X5Y#(z!MZX-+E4+{YB_3uvKDjmsgr~=O(x3Cce_%tv_nX3rSd_V^h=j_v-vob$ zGAeLCQoKOpJ~D!x|b{y7O-B-Ij2#v}d|eXtB|+_eSU1$l*k;z-Bm z2$|c%lT;=&Uk)wHgbw>}gWbE=YVdHB;2StRAD`IjkL*QFvz%?j3-TpH_+(c&bpW3Q zi|q}ESngfdP(mz@54{btyoJR&qEdTbqIr9`g6?X@|(8cl-1mO12fLS)T2Ta+G)E)eTdHKmL zfaY8DUgPwv?5}fr3j0@t?RMvh7Q&m;kUu%J&mTeiDY%pho;!qZCF0X)_}W*&rcVPs zukf?@yX1ZN^A$YB^asFygtt^6hi+#DHQzxzukZ!@T|y%K3;~WR@bgWoMmz?-sK0{0 z{S)AbgwX94_>2Dp@G|k4!&uz#M+R9gul@7Cfwe(NG-jwY#L!dWgJKcpDoKRmMm!N3 z2#*?@jedLukGbuzY5~i!!)gUAwEGXHB56#6A zB34)u!8ZcAY#x?#5#%cT6)N@x1WH-XbMT7LC*V7CQr09sRPUlx3EpSHAK>uis!WuC zkKzp;V8x!V?7}jtjJ_G?dHN3Ml$DV(JpVv?$SsS;@Xf%%DDyl+`YvQSD}Zg$0cw&} zdnP|bG)AI(qp|%_gL*RN`7#k;Wg_y`$AFN_3W*m5xrODh9Ll!>;|6^~5ycL~lKAXf zh0F8xL%2L!<_`v$=bwd8_!K7dJWJu+vgA;B00SiYb)nW_XDJqh-UxqcX?$D90fV5Ly2FE5JC+0W#0a1UXWc zOon8hA5fm793b+%LR25+0Ga1U1UW`jC|fFql8Y~jdS)o|XUig+@m<%zP)OGHPYHUw zEV9J`{TV@@Dx($?N(F|De*OtS&#MHUglSL}`JjcVsisJZaB9rl%l}WUF4ExE{a7xPIa#7CHDuwhXQ*wC9 z8>`Gbs9~9u%afRE6`Hr5SRRMx3OJN3vr;o)o;Q) z5duz~j7(hVBJgUCPSac%ZZM4QMRf0;y{;?qIgOItAlB2{(QI@#wl+8Qwl~{hzWT17 zw)%FXt9ggf)a+~SXlm|gycxZ}m8-kim<7x2#On67xAhREZkA)EsJt!h^*cD5?wwdb?>b2X>2t_hI|X5r57?a?(`Y;9Zg0)HgQv?(A)^?`bwVdwX_twsq_<>boGvQVYk|)!E*;qqo`U?WSa?teGvdlAC>P zT5^e%O!{hXu1B6y3+U_ZZZ&ptU9;sDnCQ2o%8|;-EO1k2Z-WA_u)qqt)5lg?P=yTW zDhukXZ!)@DJG**}mfnuWp0>^ow%VE6)!ftD)zQt)vA`{zyJ-DWsW(=8oo;wno@AJJ&*OZ}dqsT4MoGc}>t{Q(Mc;Y^?<*WwmuQ zHZ?c0^DHQ8xOqo&m(kRM9L>A}y4m?wdZ(|)sNd1cF0erLyE@yNNbg2VXO~gm)YQe+ zS*hLaoi`b6sCWF?xpfQG1oO_4T+1%BQUqC`8=XGbKhbXF_ORZQN7A~@q z(5&j)+jev`Q`vW+SKv6%VW*+2sGDuD@JSur^{p-JVkfMzy{C!2%?WGhVV5`|t!-Rg zEo~5@!kN@x!K1qwV=(rdyLwut+iJejh!9n5ITCg*=1JpuI9#`&aSpw znhjxk>~bq@2P^{3l6Oo-Hb)Dv{Jig*I7yR?Kjom+-nW{mVoq^`l_U#9>SOH|SeR;iryL-=*-k5s zjB|Hm>y8H2;RK>YRjkv3Q8|RgQP0JER*En&X^j{jDEY?BX8oAp>tB>6m2OfzW%HDj9jy6cB( znpxk4nHUX5a*8SAK2p4#Q`$kb4cJnkIU5J=rj{Zmjy&GbeN(+p&J#I+%bX`<{ci>IYuysr7FG;Z=~>Xbz>-$2pIx>cpwxF0CBVX zmen*P-;BSj6QMyPfE@}owZ1xUdpXM3(J4%mm0%h|C2S(kUD8NiPDulKxh36gH!TxQsmLEx{y`$Qq3>rf!T77qXl_R4`wn-H0mX?ZC%! z>Jcsq_KzBWrkINJ{QYR2zE65KrZ7jw?1xSk5t`Mn(#9 z!%5nc-e-hHv0(*ki}m-zDuX5)w8X(e%DmdzMB;b;bCY9X~%1K_p+umtRB>)i?KwUFBDZ@@C4qX)KZ0VN^;Fl`N7jr~3g*wG0y ztcSr{phir0wqQYKK{}dS>)SC2_Sj(ESmE8=(ACzoqZ!LC3%9kt`=(}0;+EK84V@hw z1S__|y4%_@r{S_MwSjNwXh-|7$aUXvGnLV(-?e+&<}GL{RuWCL+Ocf1AUm3=<6mxr z+)#s+l7)a#31bjjVFTSzi{+6GV`o!K{m#u*Xg?OPp$VdH#*!!lv<1td4A53AgEBzX zSnk+BJ)G8>OwblAbS$h}Xu`J{s~ihNd!y&uh5_a|*YhJXQohMRoh#Z zGcI|Q#SeyZdv9#xO<>6g_3AmN38gd-rykK1re+8isIYl@h2x_fNE45QM!n+j$X#jz z)VY$^gg6eKN^=c^=i$V?IfkM3feltzfFtn_;o*fyvz$W+<18f}iwB?-+C1uKOMc)Y z6o3PfI|Sq>9FjXQ5)Bx1##4OhTAbz-ei>;0fv1!XPH1t1^)EVWgB5T37Qg^D*@0CA zET;@hmw7xnr4Q9%LmH5u5~KtWuY_=LmOrlLtTcbQtIU;CxVG@(!WFck?ON_aXbC|| z@zGNE2DW6$wTME~Z)0tEHGl4EmQ#4ulB<_cB(DU?6v(eOq1TBtzwtJ!$q;S z6)qrnE=2B4__T46#LR13AuyM$a$Rz+Kq}7TESA0vf-K90SVWw75rvWpus9DCN-sq6 z@)bN;C6+BU`PTwv1y9XWsVikFcv9*rTNGtpZR3$ zzJ9&i1x6nSnwwE?p|NhfYLmJ=ut^M)vtAxhh{E55E+;aA^A0-&=h3{vkj10x40B*3 z)qcNW4orDNVF6l$rV^=OXdqcRM2b$Pg2*ev!#IPB1w>d5_aZVFO5q3;VRRC7C@G+H zx`_Tofa&07BR?PVM^i)_2WfOC10fs)QfMPcPy(XrX2KBN6CCW1Y@{=bh2y;K(mhDwR2x?J!RnWl9(30)D<8{=E-Rg^Usm|#-*5f= zzL&0YAdIUu4EH_pB0SvSA9SH_3XO(vWy#@MWQ=Yp=SUlIARsb;hkk3qad_hG_1kS`fIER2!H4pbkfYUb*ohN}a<(a#rFA^O*R(<`LAaiiXEu}b>mTFm) zo!@psj3!4WgxikSgdo5QWOZ8_TMmeK4qlY&a^OrjB)k@oBlDC!0G#o8aNYwP41=~B z{6j!W=gIX2;Ec_K^CED3^Wda`Q#A+9usZ0+`y&9Jxqi)xepa2j_a= zl+L61KH#usL&&=2alaRuKrX8{VBhUP?>UEWF<~l z*^doD>3Hq9HmIoSSB_eQzyx%L@D~+hbCq!mhq&apTJg8ch9QD4pj_13(39DVD&&`e zz+M6O^=49s;)6(mlb0#J^a5D8#)cL^Lf(`(RE!q}gY zP^QysknGB-TD=Ad$LW-X4*?8Sqjtuf2#6^QZ$(&G_@fA2;=rp+Sgio+1|8n_IB+Pk z0R%)z3FjF=sCRTax|Laoq6m5Pbi%F&&P(v&fezsiSEutKankHbu5%_Lru1Bg=`(3A zhgF8D6dSIXz+0GL3>IYDY0z>&s>hJU0jW6&OOO!RN408{AJ-_)Xp~1a$|p3+K8^A* zjq)jta$KYQlty{CM)@&~^0Y>INTYmAqui%a9@i+J)F}69l#gnZhcwEMYLsb=bJSP$ zZjJIGjdEP0d{m=+LZj@{D5o^aqZ;K8X_O~5%B>pZphkI2qkKf8d|abkr%~?FC?D1+ zzfYq)p;4~YC^u=8k7$$+Ym|>^l&dt#)f(la8s$S8<#CO2g+_UUMtNMLJf=}Ts!{f8 zlviq$k7<SKzQ|1Yc@{~q-NTWQYQJ&E#KdMnasZkDU zl>0Qwr!>lkHOkW(MOV4I1TA zjWT_a*U<{LRcMqaG|DSA%HlOjq>|6%DXkn8#Kz(8s#C4 z@?9F`K8>mDHOfAXa$KWau2DXrQTAz+>om%v7G=5g z^dcAE&0uQfTL%ah{y+pVH=R@5f$>EW148hyzFLH4Wfb zKd4HZ8{)qL#qNqpXCO6%TOQa?uz1ND99N~Fp*X{--4!q3+@iAK$o<1Q;BXtuh9mmA zEtpD(Qn)AtbGJFU=TL3QEFs%$GRv~R8$4z!yV}k89hR$lR9Oo@6|xlyK~;(wrTN)$ zuBcX)>B6>HhEsctA27@APvNJ)qv4cuqw!XZU83yR$Ien0M3!dU@j3R@@l#XK#l9bI z719o#wHIe;^&de8rfhR(&06Gc^GZk$rD0#I)W$^w)n42Tq9_b-%JG-%&vHs*A2&6I z`wz6u=u{ZHkF=pmnnh#r$}6{QuBn0^RKH3*K7w2mDi;CRCjptpN|Wq5b7z}c4}RFs zH$A<$h9tB~xt>9;I{31GmaS6ALv+4@G-~;l)AfugAD>CvaI{tlHW^Osd2TTEOgeNr zrDs~75``Ss2K+@KZBFU=Q@|0eGU`w9a}Z0>s#VtUvJ)K}0U1=~szO}!HdHRBQzIIU zjI;JOvX^+mlxIKQ$Pw@-f{T-<4!ndsS#&7%ybyW5V#-7J{%}dr5{DP^TOcp=+Kh9pptD%z`6Nd@ctLrDofIu59e;*|58$E-o0;tb zAoKL(o2Gt(5&HG^KAV2<+68*Q!akpBJI-Pg8FL?7;1Dn82*YvY9H|cxp&x=eowBh$ zfM>Nash_$9=!f?GX4Q|h)tAk(58?*(NIZ^rmDq~Ura_=@jqJ0I^<#u1>LSZ{AwW?{qOUWeq5Z?L zpzV%{DtjF+TmlX9Sou}xOv*m3^qlSVR@%?G5L3vLiX>q_cH1GI?*YPhh`uR9;fMRoJ zq92-BiFR6QqacFHHiD=F;vnV;PBdj3dw?TsV*?N}#yH-OwKugQW!8R7;563Fg(Gb& zj%I@YkcX&&vCWA-v+0&Tz${)i1Oz2cy{Y+Zvr3o(~EJ z&t|=HguM$KM9ZaC>2#{)jsa)ZmMdG_nAzg$fSoa;mRh~R)GDgo7>IAKvNVe*en=Qf z)#2wEydMLVOeay8iDQm~0z%rLR_}CYVymU5#+)=Lruw>ZQ)AAy&T-_N?J-kh`keta z#@%9SgI{0A?+YXG5Uy1RByFA6;%&%3y1dEiOx%&E|>Z+krE!a5#_a z0NH>C|1ln*xzYQ@k`*R|kZwS%4lf53qbET^fOs7^Da2;bsgiYNjw#d%$^98{=uXaT zII4AvQV^Z5Lamqbyz0-H)Q%}(w)MMgm6PBg* z#=F&`!AQv7m46E~>bN|! z_u;PqXZk|BZMf03wJPph^yPr`;Q^5Anz6M6ao}7G$kcj8M_Gd4PTJ^4J;4NR?o6~% zr+B%>l`)!>R4u;*jf-~bHPci;)$%LAfgq44OD#)_ngS7FQ7BS|QTQ5bqm!{9qkwoXR$8T89|q)<1LqhZ;|`n?fYiNB zMqT(z>A`wTC?g|FBfriI<;D_Q*}YaGMtj>M2)+ zvN6i`PC%v{kbQuZzTJ-V9zc#bAP-Y62jodW;utiwqJycN7U3`x0_C_#bBN=qD^h?IcP{j?bW6^cHE|I$x4F zw4a}CwnO=UDsgBYo#VO_f1QO>I;^`)q~y5N@0TdIaA!-0MD0x}4-UK_54CP#SCppQ zQdxyKumDag{yK4#h5yjhq4hgFMMITq%q*8mvHY`HE~+o8B~f3OV>ruRUoEm|9!8aw6XJRdfO>22orz6*cPn*^<2^?zcmQy$Ur(trW zb^KBQ?ci1m0U>2k+f0iJ4u_Mr#iGRC2W^aXNNr?kO@wwIa3&NE&-Y={VW-j|fyM#x znGmxL@?0b#?Q1yu1*O#qfEMNI1^6sx-F4)YxD! z6?6WE#xOXHP5I{5*k3{$Q+PlMH@v%&o)c2xm~)n9`WY zMH-`XL8m)w8k4;`-G&m{D0MPIOlf1%)J8d=G%vKAvZy;uZDdsj`z~3FENQlph$#Mm zaPU($KJ(-(P6IOSfaGC1Fg_$S29|K=05T&XZZ?2dbkTeI#~isf0A~WYybbbPm4J+) zN49ufMRdZdE(oawkiOgOkRCuv_u3)v1fhD-O^aV`T+or$CBOUO*z>6f@PZ?>GOuStOB8)Zz&{8wxi z4GO9Hx(gK{>Z{I)Ksa*j9s`c3uPQ(xvdyVJ;ZM!Fz;73$D5{#?0#`y(4TFu5w_J>cIWv9|Pohti;S#~Nn*@Wd(**|TTeLps2V#EEmvJ>{z z$T}{|o~=KWxtMpq-8Nd0D{exRegqxT&sR{gjQ&~P#Fzxn`D|k$`k6HKQ$fXWIHe!b zA!#q`{-JEA^owuNPWK0r8MYxIERIbB-j8LgFS!B|IK=BBhb$C1+qQAaEIXCX=~UUL zkwmFkZ|O*x$9cJcqf2P^t7cuOn~hgYmzZ}`#XZk$RhaGIKhcr5+>gWR9FUr2%S?#m zer!?PpVzooXxt|>?rx3y#G<&L)VO;!?#DFlr_oJHZOF18UljM}H16dZ_i>H;k2LO+ zi{d_|aWB=lAJw?OsBu5BDDGd^xEE^NY0Q;+7Is*3QsX|gDDEdT?!?H!tNO6U{aKCs z$whI0TH}5iovV}kn8y8Sjr;VXxYL)J9lW;9Xxv9N?oVpmPc4f3QyTYE8h5%~dJ9$2IO18uvXK_u55q|B%N0 zsK(u^aqrQ%*DZ?s5smv1jeEJq{d$di>!P^7Pvd@A<6f$9r#H;V9!buue2e0KSmQpX zaWB-k*J|8%FN*sijr*v^ooU=FHST?j;y$Kvk89jdW69>|`D@l|+=mv${Vt9Bkj8yR z<6eH2xQ}Yw`!w#SH15kZ?(s#HJ*9Eqt#O~$xaVrzM;FCCu5tHi+)rxUU&qSd+2Y0) z#XX{NZ`HU@Y207YxF22=_aTjYoyPry#{I>!#675SuhqCuYTTdIxF1dG8AJw>zFRJW48uxOI`?$va{TlaU zi{kFnxR+|&k80cxo+a+>8uvnt`w@+MLgPNMsIs?e+?mFmPKup2AJn)XUljKyjr(bA zYdd&Vk7?XHEbelz@_X3O6ML1k=XVn6PS*vOeLrU!#pIn+X6@+_be}C3gn;`n zAXADChkV`!vCi=YFRUwFYk?*5a&-LB_dMxbF9S#Yn$p}4r}G`BQ)dD69!_xKW%Di?=r z1cc$2QPy3ECO~`+$gO}Jkr1I7T1AbU5Vfx^X>&@?Q-Bc-@$2dAdNyUw zc1u~FJG^S@*=yq>f@*J>-gheW+=>JoaN3+YBcn4-Oj#k%c3Y|ls=8PLI`WJRAKKWD zFQ5kY+K(shfi@YidKkf2iK@Fb07|zoFjnVCzZb3MkICL1lN!LNQhL8H7dI zv*5^9IcM4B8y@KFh08O?8y+l~)w>aFGRvOrU*{;hs?*mE@^MEebBm zo~1_;I9I%+dVUuUWiy;sF1>clQy8-@)CtQKmn+H&vTYhV6tyXq}oLUljLZjr+L9y+Y%DzsCL8 zqPQ1o+>dJ9y&Ct3#(iQ@+;cVVM>Ou`8u#5A_v4G=&NS|aHSVPv_Xdsodt9cz$9%qX?xhU>4 z8uuZM`;5l@dm8uYMR9*cdD7PhpiUxyzN*e<7Dx zQ`5JQqPBNnVf82H{MMHUGL|HWYV8y}Go89~YpzPN;m8#t<#pKVwi9Y?rB*K0zD%6z z!!h)T@j^y2?03{NUNkQ zAkP9K-rMYzINt$eH-020b{L4xDL{_O{X>!KRY1i1x>c?sNLMXqO#2II^P4t>wx?M5Y<16TuV{zLK6o!Z1Gl+SVG)7Q~J3OIOC?weCr40 z@yR_jL8lTpagz>`@g;ba)KM3-t4)qBeT!TMQHB{~-|1}_LOcA{P9uPIY3&_6_1X`utM*;Z}Af^1Ro2u-u0phg7r7%>w z)oIn~d4Pzw$WksNAl}d@PWlv`OUb-V9pWpC_{0_HjM{B=J8;G^7ZuvTjaGisTipCq zbZ#X&aF%|OfYdqUd4O>6SUBT|jXUb=Q%OJgjeu-29PQT<^Kx^Ija;e10vq{ zuIlSmK#rTWGaMQY#KA}0Y*aY;*p@wQ;t<7hKq?%Nw*yk=u*_OOPC3fe42XD7vZCAz zi0Z>dDZ+rLu|PoXrCg47`awXBIv{@ni2CA{$n|MJXgsxA?sI^UMd3fMX~upCh?)_J zT>03eoiHt2e1sHzoud_844i36L*%LjM9q-|q#ckWri2E)5@=@+>vQC~nQ$D|5(4C? z1F{d0K8IG{0|+^*6dwU()Y12T29Q$@$k!>CW3>7)AZqVImL2jQH)SRss}Xay(@H?p z3|8c-2gGY?jB@n?a?(-neSl0l>LLY*c$>PigoA(_b6C`e0bvfvCjmL?DAzN9)S+>S zS{@jP;5$^1$I(*02^_UIC}gIuG>keV{1G5@f@x{>*MRJX09MN_K|P2s8Yr312SmK9 zTtO-Ti8~~01f;^GV}z6bRO$fR%_UTJcq4E|!Oxl%>>&sqnhXhz5QKtqVWFJ=9hXGNuZXwS>K;j@xF#-7~Ak3kSPXkhCmXzf9N5Wxs zHVIDxGHLeV_|BR^A5Zr=xSs;fgrg0^^f2gtNpJ4j+51Y`=G zhNX>T1aXw&?*J)v%%8soNTGwy3xJ@yY;;}&WE_2~s2$7(!?bp&0*)nf-nkercuBon zN_@{9kRy(gZUTf1L-cG2vugoSyH7%^Hvls3u$G$usQ|83t{5Q1-O|PbfIvu_JRbq% zxWmRi1;`O+YXYRyQG+i5@&+KHq{%Qo?iaymK5Dn#9}^u@8(_-v*U(=(_i{kS95`JV`hX zZTu@B>Ng{zF8&jclMeg&B_QOiexqnDO6uTV0m!ri(g2A1C61un2}qs865avGX@`Wj z0Yc8I<$D3~0%BR+5u)R;vA+VO&mr?OL$ng~Hcr9X-My_LKE`ZrBfW#d&csucO)WtzS4m-FX0p#?Z_PY2eATy3!e+`Ip zuV50835UkM0m!(6&NLvcX4!dP=g;h0egYhyLx%-;Wm4QhXDuK{9QA%FAQO(V*8nnQ z<^rz(t+L5k7JdV8Y8_?oCy1k@F+fnyHfuQy$ZiLnM*yLISF{3SfZq3qUKEt^U)Ihs z;EXwBeu5}FMxrS|CLN`C5fHVXDQxUjKn^?T6V-xUfqAnQ~Cx4#-Kf z&m6#QqyX>=P0HNr0IDM>9>KjAxrPw4W+4559Cq}^Q9vdg-0vq|4(<;F5_ib_NkCo& zj-|0lK!`hW7TmuMNVy}|e*iM$fcyv$@mn*m%=H=|ag-GQC1kl5K{#vFyU+_gn^vdZ zvrRp%B~LAI`b_-{>`&lb!Qk!~zZ!@R$O_6dV;qDInQ8L~GaqoO9Ies-gq+oj?)Rb; z5ZIFNFd*bCoQJ)rQ&VP2dmNByhxL95kh;t4ZTVS1CLK600CLn(gFgpk)B!2E5G{_E zLg{cFAcq}vssZsjO42pAi z0#fea{+EDEo7Mu$Y&S4B_nGyDITGKU6Tc4javd=C6mpe1Z1rn^&|5C7)-(f%I(KlB zI4u7nXv3^i>RXZoc9ir2;8^uV$#sCl9d*$GNUH;KDx9doqzZvv;v)FHihg{X^PTq+&D z0Gt^Iub%)?Xy&3Et_|q19VJ~3$V`sv0V)uy42Jqg1`U709~}%ORWuwOh^er@zdsS$ zr(*PdZ519!;6vKGr(?-Ccm!LCom2yL6A~Xm<(kV z#UGFJH9H&jM}ZqeJQLB7{RyU2vnBD*y=#eLn9pEKLd6X^1;|X>+19b+CZnyRyQjXr z-DqlV=-pxLtnX=U?&=b{hqT$Wuf`+1FvOooi5hn*-U!Cjhi6dBDf;*i_7J-XeI!eX z@zI>{KD_@~q>BTPOks@-Bp_qF8}cWOKp+)MXzDP6sBhW|rxF;nrP1$4+tkPkQ;J1` zq0vCxd>65f$MxtB3il@S{sCwbD#zww^QGQ~fvA$l0+7OBu!=YpnM$3kigX;EtJQ0qjjD2`D zI>@p_h=02!j#{p)5+|&cvQFiDY^*51rfx-r`P^~G9Zc2-|Gbwbps;2+;9?suW(-DR z{r-r7H>M?$Ff|K8MuA=_!!C`+p3W|#y{)?kO_VGnjGpI?P>>BDFd~6ibe}MIgWg3x zf*I8TX+E*o-Z1rE^lf-sVqi|V5r_@PBcb6?GzHOwc8tN~fIl1=Nrc#rZlf_4rE}=6 zP#~5F5};YtQ5KLd24zFlR7`2xRBT%`Yh#t0_h^QZM2KD^M>&L+6aFyzDpl*rRBtkr z*oiM6G$$+^y!U?uV1;A^A!{&X#Dy>nOFw5^m6M-yf)k&Y!ghvZi363=H1GkaZoKms zHiN>%L*cEpHAZ42nhFnxkR4+vzVDV``_ZExuZ$%femB!42G$a?&emS3dIs;BT>rY{YXezJ_&~}M1&yR)-BsuXn4dB zzr3K3`2B#9z0pZ}c=tAy+1ML8Agos@$Ce&QGL>x53)CQ`vA?ET)E*J=2ZJU7RRn?u zgyuB-1gZfYts<(j7(MOXW)Eq9$Nxs+ik~4EyJ&z$hucO&TmwcpQ(7&B87<*x(AO+$ zI3wC{pu;}&?+B$NUP~-d9}MC<6J+1QJgvmW7_1|MQuAmCf&s_ZY)Zw9wq!>vN;c>3 zkA&KyjU$N!njc3n>4u^s!_<@DGy!VuPxuoDFuI_7Q`3`$O=2=;KF&FfnxBEGe3_~d z*hdwpVtj>bMfX!2-$tX-+fj6o9V0Z1u?^B1u-B+RYRk)068>O#R5aDhq=87xZx<&s zj;^LMoJ{!9ky`?Bq9K8r+8@J+5;ub3!ElNWocC33t3^w&VQ11D%mj>@TsGVCjYfj8 z5!4<)!VaW95Krtg_}PTfw51-=XhxhSy!7r&B;7s|gRyfwZ4AKOfTX05NDhSuQZkZ4 ze;B!gM~I4;hwxBiw-^^h1l9$kWhRahh=lx6Ol%CJ!4fZyJ`Xdc(G=!V;eBX3)Cm#_ zNhvuFk3>?`hDCA0Tl#Hq8H~sFZzD7wjaO}}+_t5vx*BvMcG0n&Eb)WkfdLup-=Zi7 zP0CnT1r(}$ubvUeG9}~F9b>rPz%YmrVibcV#vW{u1IDLbB*&FCcsE~?ztS(64PkTD z6)b7XuzAZ?2rgfvMr?5hgTII^DTjSr6@a4oYQVr)A(jJHPdsDY21(%fu^fx|lb8YdBO@V!K@6N2eBhV`tka{<{bdly!j;=5KhI)v)hQ$MjdE14 zJJ9KhsdgJyYna+$Unoxf|6U$U#c06i@2C^Y2XXGov2Z>e2@ekODS^a54~iiPfA_*Z zLXoXo3_mJBbn?Vat?g0sltWBdF+5`|YqCwo94i$OA&yW`Dn>1f8zf&|TZvfs(ITKN z+0@Y3-5&A>8xEvG&7%^#D;3!l#)88i>5cBkki@z}sotmv`6LZrYa}a@W#y{FaWs{yotGSI zdFqLI#nFkooVF{8MRS$RXhJI%=dcnS&eKrn51?W&{|I9?L*_z+hzm?Y%=!UKV)Aap z21t^?UYc*yfXIsZ6f*=NfLys{=AiFdB`_o66F~oM7)FyL{YkDHwXET327RXn zH{(0b)#ijpaHMuTYkz{-tg2%BibRkVyWi4wZyX6F4xki5g0Lxp zMSv?@`Ocv@g66xhpa&&upDA7LhFzqGwrtDJZ&Ui-<1~ z3t+AbDpU|?bRZmx1X)ic2^`F+F`V>Z)rF2U6_U6%g~=5+SuK8{YTAV0xD&I5F!h8z zv75rdP$Qa>)U9DO8p(Jl5FQ8ztWkBYAw#UB(37^Kn;MC?pheNLtvQ;4QIh1;qv1nB z*ax86SIu!KXNsegbA8^cdAXrO}&#CFlyN=aY5Dcmx_duXF7U64G<~osAJZffx!xD40COyV#=&wY@vkK$i-J&pYni^7 z7^?DEb>lpiKt~gfqC=L;PDk6M2Baow8GMJ6`)JxhlPGCk`_RfU+rTE7G%qnL2uO*t zsb0xu(%@5=5mY7}RSC6aZ((Bd9Q)!Hf0b8tu8KW}^L8L6dE$h^RwtQL0H{EYrq)0x z(6O+^+`-(3C9~58LV*byl-b%f%380s#%`%X?VnOdYMl)TvhpUFDLhO^^+Xx7U(7wF z0{JA(7fSFKs6+$kEwEo@t}4_h+!Hg0W|o*Lr7XB@Wu!)?As83$un` zC{A56pS@G-a854ew04+J*0YQ<=EfUkwe_^TADm^S2#upC;^;;V6;4B@9#$DS2)8?o zf^;E&l_KhH*3Mfbch9j7RW&KP65E`^tU<9*7nIa`PiqPan=S~;wYYt;O}e2@UJlym zH=HYFUPNlM{UZYdG`&`0Nw#t|mEEwZr_K;u+>f1Q9>mp0RYltlYR)bgk({`(k}^V62ibOj&#JS4b4xj(-{)NKKX`5ogEYjkxfK(@z`wI+N?Y4 z+>&dUk5sm~JYRUJQBN2kVr=f-C1ah)wMpiT{%6>^qPH!w9p|&60>aJ}Go8XjaMnW$ zH7jTxJrN6A*C|F!H6d`=i*0_z+Zh*3)GzFh--I>IOD$tX^A>Y6r6Ai3rCprSpian0 zLkVLb;vb}40+Wz6wYMzEv9X0|0@j^-jbXJ}L$-z*7E|zyeHS@aibkor;-R)Ex&-w0 zd_Tp$p=PnPH8Nh~*kTTZ_(8P5kv~FnW>9lL0mXz>>lQ8RG#8^5i2!B?DeS$7iI;u9 zhKxxPMVCCs?gZ&VO~&~MH0#C)*^d0(ibBj@YwB{0B`4{|S~dtHbo3pwPp3Fn6hYOU zy4i0MM+a?c+Oi3L>CzSzNA0r5pKC!e*G5maHNt$WDAKRB&&Xp|M{RADiQ-t5tu_K| zMWCPd+Nv~jhOD~B|9`MT1>UBGwJm{(05nt!PMk=imuO`rxJQPZu0T!(#UE@i6Di=4 Gs|^64ZaYQ* literal 0 HcmV?d00001 From db231fafeb7bac1dba6e9972dd17b892ff351621 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Thu, 28 Oct 2021 08:12:23 +0200 Subject: [PATCH 14/31] Docs build test --- Moose Development/Moose/Core/Menu.lua | 55 +++++++++++++++------------ 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/Moose Development/Moose/Core/Menu.lua b/Moose Development/Moose/Core/Menu.lua index e968b9350..e1efc83c9 100644 --- a/Moose Development/Moose/Core/Menu.lua +++ b/Moose Development/Moose/Core/Menu.lua @@ -33,15 +33,15 @@ -- -- ### To manage **main menus**, the classes begin with **MENU_**: -- --- * @{Core.Menu#MENU_MISSION}: Manages main menus for whole mission file. --- * @{Core.Menu#MENU_COALITION}: Manages main menus for whole coalition. --- * @{Core.Menu#MENU_GROUP}: Manages main menus for GROUPs. +-- * Core.Menu#MENU_MISSION: Manages main menus for whole mission file. +-- * Core.Menu#MENU_COALITION: Manages main menus for whole coalition. +-- * Core.Menu#MENU_GROUP: Manages main menus for GROUPs. -- -- ### To manage **command menus**, which are menus that allow the player to issue **functions**, the classes begin with **MENU_COMMAND_**: -- --- * @{Core.Menu#MENU_MISSION_COMMAND}: Manages command menus for whole mission file. --- * @{Core.Menu#MENU_COALITION_COMMAND}: Manages command menus for whole coalition. --- * @{Core.Menu#MENU_GROUP_COMMAND}: Manages command menus for GROUPs. +-- * Core.Menu#MENU_MISSION_COMMAND: Manages command menus for whole mission file. +-- * Core.Menu#MENU_COALITION_COMMAND: Manages command menus for whole coalition. +-- * Core.Menu#MENU_GROUP_COMMAND: Manages command menus for GROUPs. -- -- === --- @@ -205,9 +205,10 @@ end do -- MENU_BASE - - --- @type MENU_BASE - -- @extends Base#BASE + + --- + -- @type MENU_BASE + -- @extends Core.Base#BASE --- Defines the main MENU class where other MENU classes are derived from. -- This is an abstract class, so don't use it. @@ -325,8 +326,9 @@ do -- MENU_BASE end do -- MENU_COMMAND_BASE - - --- @type MENU_COMMAND_BASE + + --- + -- @type MENU_COMMAND_BASE -- @field #function MenuCallHandler -- @extends Core.Menu#MENU_BASE @@ -397,7 +399,8 @@ end do -- MENU_MISSION - --- @type MENU_MISSION + --- + -- @type MENU_MISSION -- @extends Core.Menu#MENU_BASE --- Manages the main menus for a complete mission. @@ -493,7 +496,8 @@ end do -- MENU_MISSION_COMMAND - --- @type MENU_MISSION_COMMAND + --- + -- @type MENU_MISSION_COMMAND -- @extends Core.Menu#MENU_COMMAND_BASE --- Manages the command menus for a complete mission, which allow players to execute functions during mission execution. @@ -579,7 +583,8 @@ end do -- MENU_COALITION - --- @type MENU_COALITION + --- + -- @type MENU_COALITION -- @extends Core.Menu#MENU_BASE --- Manages the main menus for @{DCS.coalition}s. @@ -716,7 +721,8 @@ end do -- MENU_COALITION_COMMAND - --- @type MENU_COALITION_COMMAND + --- + -- @type MENU_COALITION_COMMAND -- @extends Core.Menu#MENU_COMMAND_BASE --- Manages the command menus for coalitions, which allow players to execute functions during mission execution. @@ -813,8 +819,9 @@ do -- the same menus twice during initialization logic. -- These menu classes are handling this logic with this variable. local _MENUGROUPS = {} - - --- @type MENU_GROUP + + --- + -- @type MENU_GROUP -- @extends Core.Menu#MENU_BASE @@ -845,7 +852,7 @@ do -- MenuStatus[MenuGroupName]:Remove() -- end -- - -- --- @param Wrapper.Group#GROUP MenuGroup + -- -- @param Wrapper.Group#GROUP MenuGroup -- local function AddStatusMenu( MenuGroup ) -- local MenuGroupName = MenuGroup:GetName() -- -- This would create a menu for the red coalition under the MenuCoalitionRed menu object. @@ -971,8 +978,8 @@ do return self end - - --- @type MENU_GROUP_COMMAND + --- + -- @type MENU_GROUP_COMMAND -- @extends Core.Menu#MENU_COMMAND_BASE --- The @{Core.Menu#MENU_GROUP_COMMAND} class manages the command menus for coalitions, which allow players to execute functions during mission execution. @@ -1065,8 +1072,8 @@ end --- MENU_GROUP_DELAYED do - - --- @type MENU_GROUP_DELAYED + --- + -- @type MENU_GROUP_DELAYED -- @extends Core.Menu#MENU_BASE @@ -1200,8 +1207,8 @@ do return self end - - --- @type MENU_GROUP_COMMAND_DELAYED + --- + -- @type MENU_GROUP_COMMAND_DELAYED -- @extends Core.Menu#MENU_COMMAND_BASE --- Manages the command menus for coalitions, which allow players to execute functions during mission execution. From 80ced88ef13915496de863f425258e1a8a5dfc93 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Thu, 28 Oct 2021 08:31:14 +0200 Subject: [PATCH 15/31] Fix for docs build --- Moose Development/Moose/Core/Menu.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/Core/Menu.lua b/Moose Development/Moose/Core/Menu.lua index e968b9350..f0962380d 100644 --- a/Moose Development/Moose/Core/Menu.lua +++ b/Moose Development/Moose/Core/Menu.lua @@ -207,7 +207,7 @@ end do -- MENU_BASE --- @type MENU_BASE - -- @extends Base#BASE + -- @extends Core.Base#BASE --- Defines the main MENU class where other MENU classes are derived from. -- This is an abstract class, so don't use it. From 13c8cc90f2b631dde6c84fbb9f88249aa7ce594b Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Thu, 28 Oct 2021 08:41:10 +0200 Subject: [PATCH 16/31] doc build fix --- Moose Development/Moose/Core/Menu.lua | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Moose Development/Moose/Core/Menu.lua b/Moose Development/Moose/Core/Menu.lua index e1efc83c9..1a448a959 100644 --- a/Moose Development/Moose/Core/Menu.lua +++ b/Moose Development/Moose/Core/Menu.lua @@ -207,7 +207,7 @@ end do -- MENU_BASE --- - -- @type MENU_BASE + --- @type MENU_BASE -- @extends Core.Base#BASE --- Defines the main MENU class where other MENU classes are derived from. @@ -328,7 +328,7 @@ end do -- MENU_COMMAND_BASE --- - -- @type MENU_COMMAND_BASE + --- @type MENU_COMMAND_BASE -- @field #function MenuCallHandler -- @extends Core.Menu#MENU_BASE @@ -400,7 +400,7 @@ end do -- MENU_MISSION --- - -- @type MENU_MISSION + --- @type MENU_MISSION -- @extends Core.Menu#MENU_BASE --- Manages the main menus for a complete mission. @@ -497,7 +497,7 @@ end do -- MENU_MISSION_COMMAND --- - -- @type MENU_MISSION_COMMAND + ---- @type MENU_MISSION_COMMAND -- @extends Core.Menu#MENU_COMMAND_BASE --- Manages the command menus for a complete mission, which allow players to execute functions during mission execution. @@ -584,7 +584,7 @@ end do -- MENU_COALITION --- - -- @type MENU_COALITION + ---- @type MENU_COALITION -- @extends Core.Menu#MENU_BASE --- Manages the main menus for @{DCS.coalition}s. @@ -722,7 +722,7 @@ end do -- MENU_COALITION_COMMAND --- - -- @type MENU_COALITION_COMMAND + ---- @type MENU_COALITION_COMMAND -- @extends Core.Menu#MENU_COMMAND_BASE --- Manages the command menus for coalitions, which allow players to execute functions during mission execution. @@ -821,7 +821,7 @@ do local _MENUGROUPS = {} --- - -- @type MENU_GROUP + --- @type MENU_GROUP -- @extends Core.Menu#MENU_BASE @@ -979,7 +979,7 @@ do end --- - -- @type MENU_GROUP_COMMAND + --- @type MENU_GROUP_COMMAND -- @extends Core.Menu#MENU_COMMAND_BASE --- The @{Core.Menu#MENU_GROUP_COMMAND} class manages the command menus for coalitions, which allow players to execute functions during mission execution. @@ -1073,7 +1073,7 @@ end do --- - -- @type MENU_GROUP_DELAYED + --- @type MENU_GROUP_DELAYED -- @extends Core.Menu#MENU_BASE @@ -1208,7 +1208,7 @@ do end --- - -- @type MENU_GROUP_COMMAND_DELAYED + --- @type MENU_GROUP_COMMAND_DELAYED -- @extends Core.Menu#MENU_COMMAND_BASE --- Manages the command menus for coalitions, which allow players to execute functions during mission execution. From 0cf2a4353dd4f614def6d7a80cb1c36baba6c8cb Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Thu, 28 Oct 2021 09:13:36 +0200 Subject: [PATCH 17/31] doc build - next try --- Moose Development/Moose/Core/Menu.lua | 39 +++++++++++---------------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/Moose Development/Moose/Core/Menu.lua b/Moose Development/Moose/Core/Menu.lua index 1a448a959..f0962380d 100644 --- a/Moose Development/Moose/Core/Menu.lua +++ b/Moose Development/Moose/Core/Menu.lua @@ -33,15 +33,15 @@ -- -- ### To manage **main menus**, the classes begin with **MENU_**: -- --- * Core.Menu#MENU_MISSION: Manages main menus for whole mission file. --- * Core.Menu#MENU_COALITION: Manages main menus for whole coalition. --- * Core.Menu#MENU_GROUP: Manages main menus for GROUPs. +-- * @{Core.Menu#MENU_MISSION}: Manages main menus for whole mission file. +-- * @{Core.Menu#MENU_COALITION}: Manages main menus for whole coalition. +-- * @{Core.Menu#MENU_GROUP}: Manages main menus for GROUPs. -- -- ### To manage **command menus**, which are menus that allow the player to issue **functions**, the classes begin with **MENU_COMMAND_**: -- --- * Core.Menu#MENU_MISSION_COMMAND: Manages command menus for whole mission file. --- * Core.Menu#MENU_COALITION_COMMAND: Manages command menus for whole coalition. --- * Core.Menu#MENU_GROUP_COMMAND: Manages command menus for GROUPs. +-- * @{Core.Menu#MENU_MISSION_COMMAND}: Manages command menus for whole mission file. +-- * @{Core.Menu#MENU_COALITION_COMMAND}: Manages command menus for whole coalition. +-- * @{Core.Menu#MENU_GROUP_COMMAND}: Manages command menus for GROUPs. -- -- === --- @@ -205,8 +205,7 @@ end do -- MENU_BASE - - --- + --- @type MENU_BASE -- @extends Core.Base#BASE @@ -326,8 +325,7 @@ do -- MENU_BASE end do -- MENU_COMMAND_BASE - - --- + --- @type MENU_COMMAND_BASE -- @field #function MenuCallHandler -- @extends Core.Menu#MENU_BASE @@ -399,7 +397,6 @@ end do -- MENU_MISSION - --- --- @type MENU_MISSION -- @extends Core.Menu#MENU_BASE @@ -496,8 +493,7 @@ end do -- MENU_MISSION_COMMAND - --- - ---- @type MENU_MISSION_COMMAND + --- @type MENU_MISSION_COMMAND -- @extends Core.Menu#MENU_COMMAND_BASE --- Manages the command menus for a complete mission, which allow players to execute functions during mission execution. @@ -583,8 +579,7 @@ end do -- MENU_COALITION - --- - ---- @type MENU_COALITION + --- @type MENU_COALITION -- @extends Core.Menu#MENU_BASE --- Manages the main menus for @{DCS.coalition}s. @@ -721,8 +716,7 @@ end do -- MENU_COALITION_COMMAND - --- - ---- @type MENU_COALITION_COMMAND + --- @type MENU_COALITION_COMMAND -- @extends Core.Menu#MENU_COMMAND_BASE --- Manages the command menus for coalitions, which allow players to execute functions during mission execution. @@ -819,8 +813,7 @@ do -- the same menus twice during initialization logic. -- These menu classes are handling this logic with this variable. local _MENUGROUPS = {} - - --- + --- @type MENU_GROUP -- @extends Core.Menu#MENU_BASE @@ -852,7 +845,7 @@ do -- MenuStatus[MenuGroupName]:Remove() -- end -- - -- -- @param Wrapper.Group#GROUP MenuGroup + -- --- @param Wrapper.Group#GROUP MenuGroup -- local function AddStatusMenu( MenuGroup ) -- local MenuGroupName = MenuGroup:GetName() -- -- This would create a menu for the red coalition under the MenuCoalitionRed menu object. @@ -978,7 +971,7 @@ do return self end - --- + --- @type MENU_GROUP_COMMAND -- @extends Core.Menu#MENU_COMMAND_BASE @@ -1072,7 +1065,7 @@ end --- MENU_GROUP_DELAYED do - --- + --- @type MENU_GROUP_DELAYED -- @extends Core.Menu#MENU_BASE @@ -1207,7 +1200,7 @@ do return self end - --- + --- @type MENU_GROUP_COMMAND_DELAYED -- @extends Core.Menu#MENU_COMMAND_BASE From 2e4fd72781dd4c4b460a3e8984b8aa12cd294fa9 Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 28 Oct 2021 10:18:43 +0200 Subject: [PATCH 18/31] Update Fox.lua Removed incomplete line `--@field #boolean` --- Moose Development/Moose/Functional/Fox.lua | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Moose Development/Moose/Functional/Fox.lua b/Moose Development/Moose/Functional/Fox.lua index efcdc99a5..6624c352e 100644 --- a/Moose Development/Moose/Functional/Fox.lua +++ b/Moose Development/Moose/Functional/Fox.lua @@ -23,7 +23,6 @@ -- @module Functional.FOX -- @image Functional_FOX.png - --- FOX class. -- @type FOX -- @field #string ClassName Name of the class. @@ -47,8 +46,7 @@ -- @field #number dt10 Time step [sec] for missile position updates if distance to target > 10 km and < 50 km. Default 1 sec. -- @field #number dt05 Time step [sec] for missile position updates if distance to target > 5 km and < 10 km. Default 0.5 sec. -- @field #number dt01 Time step [sec] for missile position updates if distance to target > 1 km and < 5 km. Default 0.1 sec. --- @field #number dt00 Time step [sec] for missile position updates if distance to target < 1 km. Default 0.01 sec. --- @field #boolean +-- @field #number dt00 Time step [sec] for missile position updates if distance to target < 1 km. Default 0.01 sec. -- @extends Core.Fsm#FSM --- Fox 3! @@ -1813,4 +1811,4 @@ end --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- \ No newline at end of file From 801069f5ac14d5644f9812e05ba09d00cc53a7cf Mon Sep 17 00:00:00 2001 From: Applevangelist <72444570+Applevangelist@users.noreply.github.com> Date: Thu, 28 Oct 2021 15:39:57 +0200 Subject: [PATCH 19/31] Update Moose_Create.lua --- Moose Setup/Moose_Create.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Moose Setup/Moose_Create.lua b/Moose Setup/Moose_Create.lua index 41c41c22e..4b3e635ec 100644 --- a/Moose Setup/Moose_Create.lua +++ b/Moose Setup/Moose_Create.lua @@ -15,7 +15,7 @@ print( "Commit Hash ID : " .. MooseCommitHash ) print( "Moose development path : " .. MooseDevelopmentPath ) print( "Moose setup path : " .. MooseSetupPath ) print( "Moose target path : " .. MooseTargetPath ) -print( "isWidows : " .. isWindows) +print( "isWindows : " .. isWindows) function PathConvert(splatnixPath) @@ -90,4 +90,4 @@ LoaderFile:close() print("Moose include generation complete.") if MooseDynamicStatic == "D" then print("To enable dynamic moose loading, add a soft or hard link from \"\\Scripts\\Moose\" to the \"Moose Development\\Moose\" subdirectory of the Moose_Framework repository.") -end \ No newline at end of file +end From f70180eb66eb60ef8598ed65ecd2b06560927b59 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Fri, 29 Oct 2021 10:20:34 +0200 Subject: [PATCH 20/31] SET_BASE - cmpleted GetSetComplement --- Moose Development/Moose/Core/Set.lua | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index 44194929f..5230b20fe 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -308,17 +308,14 @@ do -- SET_BASE -- @return Core.Set#SET_BASE The set of objects that are in set *B* but **not** in this set *A*. function SET_BASE:GetSetComplement(SetB) - local complement=SET_BASE:New() + local complement = self:GetSetUnion(SetB) + local intersection = self:GetSetIntersection(SetB) - local union=self:GetSetUnion(SetA, SetB) - - for _,Object in pairs(union.Set) do - if SetA:IsIncludeObject(Object) and SetB:IsIncludeObject(Object) then - intersection:Add(intersection) - end + for _,Object in pairs(intersection.Set) do + complement:Remove(Object.ObjectName,true) end - return intersection + return complement end From 3e654570413bc7dd788248bd546ce46c9fb74307 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Fri, 29 Oct 2021 18:30:22 +0200 Subject: [PATCH 21/31] doc impro --- Moose Development/Moose/Functional/Mantis.lua | 2 +- Moose Development/Moose/Functional/Shorad.lua | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/Functional/Mantis.lua b/Moose Development/Moose/Functional/Mantis.lua index f9e10f545..2195df026 100644 --- a/Moose Development/Moose/Functional/Mantis.lua +++ b/Moose Development/Moose/Functional/Mantis.lua @@ -19,7 +19,7 @@ -- -- @module Functional.Mantis -- @image Functional.Mantis.jpg - +-- -- Date: July 2021 ------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Functional/Shorad.lua b/Moose Development/Moose/Functional/Shorad.lua index ce2d93d20..d6ddbbe7d 100644 --- a/Moose Development/Moose/Functional/Shorad.lua +++ b/Moose Development/Moose/Functional/Shorad.lua @@ -41,6 +41,7 @@ -- @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) -- -- Simple Class for a more intelligent Short Range Air Defense System From a1d67bf539bcf7ae4d479a3da61f85774349f98f Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sat, 30 Oct 2021 16:32:42 +0200 Subject: [PATCH 22/31] Speedmax returning 0 not nil --- Moose Development/Moose/Wrapper/Group.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index f1443b2d0..72c9215ab 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -568,12 +568,12 @@ function GROUP:GetSpeedMax() local Units=self:GetUnits() - local speedmax=nil + local speedmax=0 for _,unit in pairs(Units) do local unit=unit --Wrapper.Unit#UNIT local speed=unit:GetSpeedMax() - if speedmax==nil then + if speedmax==0 then speedmax=speed elseif speed Date: Sat, 30 Oct 2021 16:32:59 +0200 Subject: [PATCH 23/31] Speedmax returning 0 not nil --- Moose Development/Moose/Wrapper/Unit.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/Wrapper/Unit.lua b/Moose Development/Moose/Wrapper/Unit.lua index 3fb557504..098508f56 100644 --- a/Moose Development/Moose/Wrapper/Unit.lua +++ b/Moose Development/Moose/Wrapper/Unit.lua @@ -442,7 +442,7 @@ function UNIT:GetSpeedMax() return SpeedMax*3.6 end - return nil + return 0 end --- Returns the unit's max range in meters derived from the DCS descriptors. From 8d3910ea4c7819005657536815d2e3f1f1c4cd92 Mon Sep 17 00:00:00 2001 From: Ben Birch Date: Sun, 31 Oct 2021 19:37:26 +1100 Subject: [PATCH 24/31] fix cleanup of crates on load or build. --- Moose Development/Moose/Ops/CTLD.lua | 55 +++++++++++++++------------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/Moose Development/Moose/Ops/CTLD.lua b/Moose Development/Moose/Ops/CTLD.lua index 8cdfbcdd6..6abfd8477 100644 --- a/Moose Development/Moose/Ops/CTLD.lua +++ b/Moose Development/Moose/Ops/CTLD.lua @@ -2145,24 +2145,39 @@ function CTLD:_LoadCratesNearby(Group, Unit) self.Loaded_Cargo[unitname] = loaded self:_UpdateUnitCargoMass(Unit) -- clean up real world crates - local existingcrates = self.Spawned_Cargo -- #table - local newexcrates = {} - for _,_crate in pairs(existingcrates) do - local excrate = _crate -- #CTLD_CARGO - local ID = excrate:GetID() - for _,_ID in pairs(crateidsloaded) do - if ID ~= _ID then - table.insert(newexcrates,_crate) - end - end - end - self.Spawned_Cargo = nil - self.Spawned_Cargo = newexcrates + self:_CleanupTrackedCrates(crateidsloaded) end end return self end +--- (Internal) Function to clean up tracked cargo crates +function CTLD:_CleanupTrackedCrates(crateIdsToRemove) + local existingcrates = self.Spawned_Cargo -- #table + local newexcrates = {} + for _,_crate in pairs(existingcrates) do + local excrate = _crate -- #CTLD_CARGO + local ID = excrate:GetID() + local keep = true + for _,_ID in pairs(crateIdsToRemove) do + if ID == _ID then + keep = false + end + end + -- remove destroyed crates here too + local static = _crate:GetPositionable() -- Wrapper.Static#STATIC -- crates + if not static or not static:IsAlive() then + keep = false + end + if keep then + table.insert(newexcrates,_crate) + end + end + self.Spawned_Cargo = nil + self.Spawned_Cargo = newexcrates + return self +end + --- (Internal) Function to get current loaded mass -- @param #CTLD self -- @param Wrapper.Unit#UNIT Unit @@ -2849,19 +2864,7 @@ function CTLD:_CleanUpCrates(Crates,Build,Number) if found == numberdest then break end -- got enough end -- loop and remove from real world representation - for _,_crate in pairs(existingcrates) do - local excrate = _crate -- #CTLD_CARGO - local ID = excrate:GetID() - for _,_ID in pairs(destIDs) do - if ID ~= _ID then - table.insert(newexcrates,_crate) - end - end - end - - -- reset Spawned_Cargo - self.Spawned_Cargo = nil - self.Spawned_Cargo = newexcrates + self:_CleanupTrackedCrates(destIDs) return self end From cd62776be6b22f57b0fafa75aaca91e69d4782cf Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sun, 31 Oct 2021 11:53:24 +0100 Subject: [PATCH 25/31] AIRBOSS additions by Pene --- Moose Development/Moose/Ops/Airboss.lua | 257 +++++++++++++++--------- 1 file changed, 161 insertions(+), 96 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 8e49bbfec..3651c74ae 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -51,7 +51,7 @@ -- -- At the moment, optimized parameters are available for the F/A-18C Hornet (Lot 20) and A-4E community mod as aircraft and the USS John C. Stennis as carrier. -- --- The AV-8B Harrier, the USS Tarawa, USS America and Juan Carlos I are WIP. The AV-8B harrier and the LHA's and LHD can only be used together, i.e. these ships are the only carriers the harrier is supposed to land on and +-- The AV-8B Harrier, the USS Tarawa, USS America, HMAS Canberra and Juan Carlos I are WIP. The AV-8B harrier and the LHA's and LHD can only be used together, i.e. these ships are the only carriers the harrier is supposed to land on and -- no other fixed wing aircraft (human or AI controlled) are supposed to land on these ships. Currently only Case I is supported. Case II/III take slightly different steps from the CVN carrier. -- However, the two Case II/III pattern are very similar so this is not a big drawback. -- @@ -108,9 +108,10 @@ -- * [DCS: F/A-18C Hornet – Episode 16: CASE III Introduction](https://www.youtube.com/watch?v=DvlMHnLjbDQ) -- * [DCS: F/A-18C Hornet Case I Carrier Landing Training Lesson Recording](https://www.youtube.com/watch?v=D33uM9q4xgA) -- --- ### AV-8B Harrier at USS Tarawa +-- ### AV-8B Harrier and V/STOL Operations: -- -- * [Harrier Ship Landing Mission with Auto LSO!](https://www.youtube.com/watch?v=lqmVvpunk2c) +-- * [Updated Airboss V/STOL Features USS Tarawa](https://youtu.be/K7I4pU6j718) -- * [Harrier Practice pattern USS America](https://youtu.be/99NigITYmcI) -- -- === @@ -300,7 +301,7 @@ -- -- Once the aircraft reaches the Initial, the landing pattern begins. The important steps of the pattern are shown in the image above. -- The AV-8B Harrier pattern is very similar, the only differences are as there is no angled deck there is no wake check. from the ninety you wil fly a straight approach offset 26 ft to port (left) of the tram line. --- The aim is to arrive abeam the landing spot in a stable hover at 120 ft with forward speed matched to the boat. From there the LSO will call "cleared to land". You then level cross to the tram line at the designated landing spot at land vertcally. +-- The aim is to arrive abeam the landing spot in a stable hover at 120 ft with forward speed matched to the boat. From there the LSO will call "cleared to land". You then level cross to the tram line at the designated landing spot at land vertcally. When you stabalise over the landing spot LSO will call Stabalise to indicate you are centered at the correct spot. -- -- -- ## CASE III @@ -634,12 +635,12 @@ -- -- Furthermore, we have the cases: -- --- * 2.5 Points **B**: "Bolder", when the player landed but did not catch a wire. +-- * 2.5 Points **B**: "Bolter", when the player landed but did not catch a wire. -- * 2.0 Points **WOP**: "Pattern Wave-Off", when pilot was far away from where he should be in the pattern. -- * 2.0 Points **OWO**: "Own Wave-Off**, when pilot flies past the deck without touching it. -- * 1.0 Points **WO**: "Technique Wave-Off": Player got waved off in the final parts of the groove. -- * 1.0 Points **LIG**: "Long In the Groove", when pilot extents the downwind leg too far and screws up the timing for the following aircraft. --- * 0.0 Points **CUT**: "Cut pass", when player was waved off but landed anyway. +-- * 0.0 Points **CUT**: "Cut pass", when player was waved off but landed anyway. In addition if a V/STOL lands without having been Cleared to Land. -- -- ## Foul Deck Waveoff -- @@ -1298,9 +1299,10 @@ AIRBOSS.AircraftCarrier={ -- @field #string TRUMAN USS Harry S. Truman (CVN-75) [Super Carrier Module] -- @field #string FORRESTAL USS Forrestal (CV-59) [Heatblur Carrier Module] -- @field #string VINSON USS Carl Vinson (CVN-70) [Obsolete] --- @field #string TARAWA USS Tarawa (LHA-1) --- @field #string AMERICA USS America (LHA-6) --- @field #string JCARLOS Juan Carlos I (L61) +-- @field #string TARAWA USS Tarawa (LHA-1) [V/STOL Carrier] +-- @field #string AMERICA USS America (LHA-6) [V/STOL Carrier] +-- @field #string JCARLOS Juan Carlos I (L61) [V/STOL Carrier] +-- @field #string HMAS Canberra (L02) [V/STOL Carrier] -- @field #string KUZNETSOV Admiral Kuznetsov (CV 1143.5) AIRBOSS.CarrierType={ ROOSEVELT="CVN_71", @@ -1313,6 +1315,7 @@ AIRBOSS.CarrierType={ TARAWA="LHA_Tarawa", AMERICA="USS America LHA-6", JCARLOS="L61", + CANBERRA="L02", KUZNETSOV="KUZNECOW", } @@ -1726,7 +1729,7 @@ AIRBOSS.MenuF10Root=nil --- Airboss class version. -- @field #string version -AIRBOSS.version="1.2.0" +AIRBOSS.version="1.2.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1991,6 +1994,9 @@ function AIRBOSS:New(carriername, alias) elseif self.carriertype==AIRBOSS.CarrierType.JCARLOS then -- Use Juan Carlos parameters. self:_InitJcarlos() + elseif self.carriertype==AIRBOSS.CarrierType.CANBERRA then + -- Use Juan Carlos parameters at this stage --TODO Check primary Landing spot. + self:_InitJcarlos() elseif self.carriertype==AIRBOSS.CarrierType.KUZNETSOV then -- Kusnetsov parameters - maybe... self:_InitStennis() @@ -2082,7 +2088,7 @@ function AIRBOSS:New(carriername, alias) -- Carrier specific. - if self.carrier:GetTypeName()~=AIRBOSS.CarrierType.TARAWA or self.carrier:GetTypeName()~=AIRBOSS.CarrierType.AMERICA or self.carrier:GetTypeName()~=AIRBOSS.CarrierType.JCARLOS then + if self.carrier:GetTypeName()~=AIRBOSS.CarrierType.TARAWA or self.carrier:GetTypeName()~=AIRBOSS.CarrierType.AMERICA or self.carrier:GetTypeName()~=AIRBOSS.CarrierType.JCARLOS or self.carrier:GetTypeName()~=AIRBOSS.CarrierType.CANBERRA then -- Flare wires. local w1=stern:Translate(self.carrierparam.wire1, FB, true) @@ -6886,7 +6892,7 @@ function AIRBOSS:_GetMarshalAltitude(stack, case) p2=Carrier:Translate(UTILS.NMToMeters(1.5), hdg) -- Tarawa,LHA,LHD Delta patterns. - if self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS then + if self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS or self.carriertype==AIRBOSS.CarrierType.CANBERRA then -- Pattern is directly overhead the carrier. p1=Carrier:Translate(UTILS.NMToMeters(1.0), hdg+90) @@ -7714,6 +7720,8 @@ function AIRBOSS:_InitPlayer(playerData, step) playerData.wofd=false playerData.owo=false playerData.boltered=false + playerData.hover=false + playerData.stable=false playerData.landed=false playerData.Tlso=timer.getTime() playerData.Tgroove=nil @@ -8731,7 +8739,7 @@ function AIRBOSS:OnEventLand(EventData) self:T(self.lid..text) -- Check carrier type. - if self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS then + if self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS or self.carriertype==AIRBOSS.CarrierType.CANBERRA then -- Power "Idle". self:RadioTransmission(self.LSORadio, self.LSOCall.IDLE, false, 1, nil, true) @@ -8766,7 +8774,7 @@ function AIRBOSS:OnEventLand(EventData) -- AI unit landed -- -------------------- - if self.carriertype~=AIRBOSS.CarrierType.TARAWA or self.carriertype~=AIRBOSS.CarrierType.AMERICA or self.carriertype~=AIRBOSS.CarrierType.JCARLOS then + if self.carriertype~=AIRBOSS.CarrierType.TARAWA or self.carriertype~=AIRBOSS.CarrierType.AMERICA or self.carriertype~=AIRBOSS.CarrierType.JCARLOS or self.carriertype~=AIRBOSS.CarrierType.CANBERRA then -- Coordinate at landing event local coord=EventData.IniUnit:GetCoordinate() @@ -9676,6 +9684,8 @@ function AIRBOSS:_Bullseye(playerData) -- LSO expect spot 5 or 7.5 call if playerData.actype==AIRBOSS.AircraftCarrier.AV8B and self.carriertype==AIRBOSS.CarrierType.JCARLOS then self:RadioTransmission(self.LSORadio, self.LSOCall.EXPECTSPOT5, nil, nil, nil, true) + elseif playerData.actype==AIRBOSS.AircraftCarrier.AV8B and self.carriertype==AIRBOSS.CarrierType.CANBERRA then + self:RadioTransmission(self.LSORadio, self.LSOCall.EXPECTSPOT5, nil, nil, nil, true) elseif playerData.actype==AIRBOSS.AircraftCarrier.AV8B then self:RadioTransmission(self.LSORadio, self.LSOCall.EXPECTSPOT75, nil, nil, nil, true) end @@ -9813,7 +9823,7 @@ function AIRBOSS:_CheckForLongDownwind(playerData) local limit=UTILS.NMToMeters(-1.6) -- For the tarawa, other LHA and LHD we give a bit more space. - if self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS then + if self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS or self.carriertype==AIRBOSS.CarrierType.CANBERRA then limit=UTILS.NMToMeters(-2.0) end @@ -9861,7 +9871,9 @@ function AIRBOSS:_Abeam(playerData) -- LSO expect spot 5 or 7.5 call if playerData.actype==AIRBOSS.AircraftCarrier.AV8B and self.carriertype==AIRBOSS.CarrierType.JCARLOS then self:RadioTransmission(self.LSORadio, self.LSOCall.EXPECTSPOT5, false, 5, nil, true) - elseif playerData.actype==AIRBOSS.AircraftCarrier.AV8B then + elseif playerData.actype==AIRBOSS.AircraftCarrier.AV8B and self.carriertype==AIRBOSS.CarrierType.CANBERRA then + self:RadioTransmission(self.LSORadio, self.LSOCall.EXPECTSPOT5, false, 5, nil, true) + elseif playerData.actype==AIRBOSS.AircraftCarrier.AV8B then self:RadioTransmission(self.LSORadio, self.LSOCall.EXPECTSPOT75, false, 5, nil, true) end @@ -9898,7 +9910,7 @@ function AIRBOSS:_Ninety(playerData) self:_PlayerHint(playerData) -- Next step: wake. - if self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS then + if self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS or self.carriertype==AIRBOSS.CarrierType.CANBERRA then -- Harrier has no wake stop. It stays port of the boat. self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.FINAL) else @@ -10155,8 +10167,8 @@ function AIRBOSS:_Groove(playerData) -- Speed difference. local dv=math.abs(vplayer-vcarrier) - -- Stable when speed difference < 10 km/h. - local stable=dv<10 + -- Stable when speed difference < 20 km/h. + local stable=dv<20 -- Check if player is inside the zone. if playerData.unit:IsInZone(ZoneALS) and stable then @@ -10166,6 +10178,9 @@ function AIRBOSS:_Groove(playerData) -- Next step: Level cross. self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_LC) + -- Set Stable Hover + playerData.stable=true + playerData.hover=true end elseif rho<=RAR and playerData.step==AIRBOSS.PatternStep.GROOVE_LC then @@ -10185,13 +10200,14 @@ function AIRBOSS:_Groove(playerData) -- Speed difference. local dv=math.abs(vplayer-vcarrier) - -- Stable when v<7.5 km/h. - local stable=dv<7.5 + -- Stable when v<10 km/h. + local stable=dv<10 - -- Radio Transmission "Cleared to land" once the aircraft is inside the zone. - if playerData.unit:IsInZone(ZoneLS) and stable and playerData.warning==false then - self:RadioTransmission(self.LSORadio, self.LSOCall.STABILIZED, nil, nil, nil, true) - playerData.warning=true + -- Radio Transmission "Stabilized" once the aircraft has been cleared to cross and is over the Landing Spot and stable. + if playerData.unit:IsInZone(ZoneLS) and stable and playerData.stable==true then + self:RadioTransmission(self.LSORadio, self.LSOCall.STABILIZED, nil, nil, nil, false) + playerData.stable=false + playerData.warning=true end -- We keep it in this step until landed. @@ -10224,8 +10240,25 @@ function AIRBOSS:_Groove(playerData) -- Nothing else necessary. return end - - end + end + + -- Long V/STOL groove time Wave Off over 75 seconds to IC - TOPGUN level Only. --pene testing (WIP) + + --if rho>=RAR and rho<=RIC and not playerData.waveoff and playerData.difficulty==AIRBOSS.Difficulty.HARD and playerData.actype== AIRBOSS.AircraftCarrier.AV8B then + -- Get groove time + --local vSlow=groovedata.time + -- If too slow wave off. + --if vSlow >75 then + + -- LSO Wave off! + --self:RadioTransmission(self.LSORadio, self.LSOCall.WAVEOFF, nil, nil, nil, true) + --playerData.Tlso=timer.getTime() + + -- Player was waved Off + --playerData.waveoff=true + --return + --end + --end -- Groovedata step. groovedata.Step=playerData.step @@ -10249,25 +10282,25 @@ function AIRBOSS:_Groove(playerData) -- Distance in NM. local d=UTILS.MetersToNM(rho) - -- Drift on lineup. - if rho>=RAR and rho<=RIM then - if gd.LUE>0.22 and lineupError<-0.22 then - env.info" Drift Right across centre ==> DR-" - gd.Drift=" DR" - self:T(self.lid..string.format("Got Drift Right across centre step %s, d=%.3f: Max LUE=%.3f, lower LUE=%.3f", gs, d, gd.LUE, lineupError)) - elseif gd.LUE<-0.22 and lineupError>0.22 then - env.info" Drift Left ==> DL-" - gd.Drift=" DL" - self:T(self.lid..string.format("Got Drift Left across centre at step %s, d=%.3f: Min LUE=%.3f, lower LUE=%.3f", gs, d, gd.LUE, lineupError)) - elseif gd.LUE>0.13 and lineupError<-0.14 then - env.info" Little Drift Right across centre ==> (DR-)" - gd.Drift=" (DR)" - self:T(self.lid..string.format("Got Little Drift Right across centre at step %s, d=%.3f: Max LUE=%.3f, lower LUE=%.3f", gs, d, gd.LUE, lineupError)) - elseif gd.LUE<-0.13 and lineupError>0.14 then - env.info" Little Drift Left across centre ==> (DL-)" - gd.Drift=" (DL)" - self:E(self.lid..string.format("Got Little Drift Left across centre at step %s, d=%.3f: Min LUE=%.3f, lower LUE=%.3f", gs, d, gd.LUE, lineupError)) - end + -- Drift on lineup. + if rho>=RAR and rho<=RIM then + if gd.LUE>0.22 and lineupError<-0.22 then + env.info" Drift Right across centre ==> DR-" + gd.Drift=" DR" + self:T(self.lid..string.format("Got Drift Right across centre step %s, d=%.3f: Max LUE=%.3f, lower LUE=%.3f", gs, d, gd.LUE, lineupError)) + elseif gd.LUE<-0.22 and lineupError>0.22 then + env.info" Drift Left ==> DL-" + gd.Drift=" DL" + self:T(self.lid..string.format("Got Drift Left across centre at step %s, d=%.3f: Min LUE=%.3f, lower LUE=%.3f", gs, d, gd.LUE, lineupError)) + elseif gd.LUE>0.13 and lineupError<-0.14 then + env.info" Little Drift Right across centre ==> (DR-)" + gd.Drift=" (DR)" + self:T(self.lid..string.format("Got Little Drift Right across centre at step %s, d=%.3f: Max LUE=%.3f, lower LUE=%.3f", gs, d, gd.LUE, lineupError)) + elseif gd.LUE<-0.13 and lineupError>0.14 then + env.info" Little Drift Left across centre ==> (DL-)" + gd.Drift=" (DL)" + self:E(self.lid..string.format("Got Little Drift Left across centre at step %s, d=%.3f: Min LUE=%.3f, lower LUE=%.3f", gs, d, gd.LUE, lineupError)) + end end -- Update max deviation of line up error. @@ -10395,11 +10428,10 @@ function AIRBOSS:_CheckWaveOff(glideslopeError, lineupError, AoA, playerData) -- For the harrier, we allow a bit more room. if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then - glMax= 4.0 - glMin=-3.0 - luAbs= 5.0 - -- No waveoff for harrier pilots at the moment. - return false + glMax= 2.6 + glMin=-2.0 + luAbs= 4.1 -- Testing Pene (WIP) needs feedback to tighten up tolerences. + end -- Too high or too low? @@ -10423,9 +10455,10 @@ function AIRBOSS:_CheckWaveOff(glideslopeError, lineupError, AoA, playerData) waveoff=true end - -- Too slow or too fast? Only for pros. - if playerData.difficulty==AIRBOSS.Difficulty.HARD then - -- Get aircraft specific AoA values + -- Too slow or too fast? Only for pros. + + if playerData.difficulty==AIRBOSS.Difficulty.HARD and playerData.actype~=AIRBOSS.AircraftCarrier.AV8B then + -- Get aircraft specific AoA values. Not for AV-8B due to transition to Stable Hover. local aoaac=self:_GetAircraftAoA(playerData) -- Check too slow or too fast. if AoA 91 Seconds: SLOW V/STOL (Early hover stop selection) +-- * < 55 seconds: Fast V/STOL +-- * < 75 seconds: OK V/STOL +-- * > 76 Seconds: SLOW V/STOL (Early hover stop selection) +-- +-- If you manage to be between 60.0 and 65.0 seconds in the AV-8B, you will even get and okay underline "\_OK\_" -- -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. @@ -12269,9 +12324,9 @@ function AIRBOSS:_EvalGrooveTime(playerData) -- Time in groove for AV-8B elseif playerData.actype==AIRBOSS.AircraftCarrier.AV8B and t<55 then -- VSTOL Late Hover stop selection too fast to Abeam LDG Spot AV-8B. grade="FAST V/STOL Groove" - elseif playerData.actype==AIRBOSS.AircraftCarrier.AV8B and t<90 then -- VSTOL Operations with AV-8B. + elseif playerData.actype==AIRBOSS.AircraftCarrier.AV8B and t<75 then -- VSTOL Operations with AV-8B. grade="OK V/STOL Groove" - elseif playerData.actype==AIRBOSS.AircraftCarrier.AV8B and t>=91 then -- VSTOL Early Hover stop selection slow to Abeam LDG Spot AV-8B. + elseif playerData.actype==AIRBOSS.AircraftCarrier.AV8B and t>=76 then -- VSTOL Early Hover stop selection slow to Abeam LDG Spot AV-8B. grade="SLOW V/STOL Groove" else grade="LIG" @@ -12283,7 +12338,7 @@ function AIRBOSS:_EvalGrooveTime(playerData) end -- V/STOL Unicorn! - if playerData.actype==AIRBOSS.AircraftCarrier.AV8B and (t>=65.0 and t<=75.0) then + if playerData.actype==AIRBOSS.AircraftCarrier.AV8B and (t>=60.0 and t<=65.0) then grade="_OK_ V/STOL" end @@ -12312,7 +12367,7 @@ function AIRBOSS:_LSOgrade(playerData) -- Put everything together. local G=GXX.." "..GIM.." ".." "..GIC.." "..GAR - -- Count number of minor, normal and major deviations. TODO - work on Harrier counts due slower approach speed. + -- Count number of minor, normal and major deviations. local N=nXX+nIM+nIC+nAR local nL=count(G, '_')/2 local nS=count(G, '%(') @@ -12321,7 +12376,7 @@ function AIRBOSS:_LSOgrade(playerData) -- Groove time 15-18.99 sec for a unicorn. Or 65-70 for V/STOL unicorn. local Tgroove=playerData.Tgroove local TgrooveUnicorn=Tgroove and (Tgroove>=15.0 and Tgroove<=18.99) or false - local TgrooveVstolUnicorn=Tgroove and (Tgroove>=65.0 and Tgroove<=70.0)and playerData.actype==AIRBOSS.AircraftCarrier.AV8B or false + local TgrooveVstolUnicorn=Tgroove and (Tgroove>=60.0 and Tgroove<=65.0)and playerData.actype==AIRBOSS.AircraftCarrier.AV8B or false local grade local points @@ -12332,9 +12387,9 @@ function AIRBOSS:_LSOgrade(playerData) G="Unicorn" else - -- Add AV-8B Harrier devation allowances due to lower groundspeed and 3x conventional groove time, this allows to maintain LSO tolerances while respecting the deviations are not unsafe. (WIP requires feedback) + -- Add AV-8B Harrier devation allowances due to lower groundspeed and 3x conventional groove time, this allows to maintain LSO tolerances while respecting the deviations are not unsafe. (WIP requires feedback) -- Large devaitions still result in a No Grade, A Unicorn still requires a clean pass with no deviation. - if nL>3 and playerData.actype==AIRBOSS.AircraftCarrier.AV8B then + if nL>3 and playerData.actype==AIRBOSS.AircraftCarrier.AV8B then -- Larger deviations ==> "No grade" 2.0 points. grade="--" points=2.0 @@ -12371,7 +12426,7 @@ end text=text.."# of normal deviations = "..nN.."\n" text=text.."# of small deviations ( = "..nS.."\n" self:T2(self.lid..text) - + -- Special cases. if playerData.wop then --------------------- @@ -12427,8 +12482,18 @@ end -- Bolter grade="-- (BOLTER)" points=2.5 - end - + + elseif not playerData.hover and playerData.actype==AIRBOSS.AircraftCarrier.AV8B then + ------------------------------- + -- AV-8B not cleared to land -- -- Landing clearence is carrier from LC to Landing + ------------------------------- + if playerData.landed then + -- AIRBOSS wants your balls! + grade="CUT" + points=0.0 + end + + end return grade, points, G end @@ -12470,29 +12535,29 @@ function AIRBOSS:_Flightdata2Text(playerData, groovestep) --Angled Approach. local P=nil if step==AIRBOSS.PatternStep.GROOVE_XX and ROL<=4.0 and playerData.case<3 then - if LUE>self.lue.RIGHT then - P=underline("AA") - elseif - LUE>self.lue.RightMed then - P="AA " - elseif - LUE>self.lue.Right then - P=little("AA") - end + if LUE>self.lue.RIGHT then + P=underline("AA") + elseif + LUE>self.lue.RightMed then + P="AA " + elseif + LUE>self.lue.Right then + P=little("AA") + end end --Overshoot Start. local O=nil if step==AIRBOSS.PatternStep.GROOVE_XX then - if LUE Date: Sun, 31 Oct 2021 11:53:53 +0100 Subject: [PATCH 26/31] SRS Google addition by rollnthndr --- Moose Development/Moose/Sound/SRS.lua | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/Sound/SRS.lua b/Moose Development/Moose/Sound/SRS.lua index 9be975a78..8f27c6dbf 100644 --- a/Moose Development/Moose/Sound/SRS.lua +++ b/Moose Development/Moose/Sound/SRS.lua @@ -88,10 +88,19 @@ -- -- Use a specific "culture" with the @{#MSRS.SetCulture} function, e.g. `:SetCulture("en-US")` or `:SetCulture("de-DE")`. -- +-- ## Set Google +-- +-- Use Google's text-to-speech engine with the @{#MSRS.SetGoogle} function, e.g. ':SetGoogle()'. +-- By enabling this it also allows you to utilize SSML in your text for added flexibilty. +-- For more information on setting up a cloud account, visit: https://cloud.google.com/text-to-speech +-- Google's supported SSML reference: https://cloud.google.com/text-to-speech/docs/ssml +-- -- ## Set Voice -- -- Use a specifc voice with the @{#MSRS.SetVoice} function, e.g, `:SetVoice("Microsoft Hedda Desktop")`. -- Note that this must be installed on your windows system. +-- If enabling SetGoogle(), you can use voices provided by Google +-- Google's supported voices: https://cloud.google.com/text-to-speech/docs/voices -- -- ## Set Coordinate -- @@ -678,7 +687,7 @@ function MSRS:_GetCommand(freqs, modus, coal, gender, voice, culture, volume, sp -- Set google. if self.google then - command=command..string.format(' -G "%s"', self.google) + command=command..string.format(' --ssml -G "%s"', self.google) end -- Debug output. From e7e218476033b35d95d55a74e0a7c9853d60f49b Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Mon, 1 Nov 2021 19:46:56 +0100 Subject: [PATCH 27/31] Autolase - fixed error when not using a pilotset --- .../Moose/Functional/Autolase.lua | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/Moose Development/Moose/Functional/Autolase.lua b/Moose Development/Moose/Functional/Autolase.lua index 4c503d1da..fa1ec6efb 100644 --- a/Moose Development/Moose/Functional/Autolase.lua +++ b/Moose Development/Moose/Functional/Autolase.lua @@ -109,7 +109,7 @@ AUTOLASE = { --- AUTOLASE class version. -- @field #string version -AUTOLASE.version = "0.0.9" +AUTOLASE.version = "0.0.10" ------------------------------------------------------------------- -- Begin Functional.Autolase.lua @@ -297,13 +297,15 @@ end -- @param #AUTOLASE self -- @return #AUTOLASE self function AUTOLASE:SetPilotMenu() - local pilottable = self.pilotset:GetSetObjects() or {} - for _,_unit in pairs (pilottable) do - local Unit = _unit -- Wrapper.Unit#UNIT - if Unit and Unit:IsAlive() then - local Group = Unit:GetGroup() - local lasemenu = MENU_GROUP_COMMAND:New(Group,"Autolase Status",nil,self.ShowStatus,self,Group) - lasemenu:Refresh() + if self.usepilotset then + local pilottable = self.pilotset:GetSetObjects() or {} + for _,_unit in pairs (pilottable) do + local Unit = _unit -- Wrapper.Unit#UNIT + if Unit and Unit:IsAlive() then + local Group = Unit:GetGroup() + local lasemenu = MENU_GROUP_COMMAND:New(Group,"Autolase Status",nil,self.ShowStatus,self,Group) + lasemenu:Refresh() + end end end return self From f6f29db9f1bd494f86155c48eb8843540fa9b670 Mon Sep 17 00:00:00 2001 From: Applevangelist <72444570+Applevangelist@users.noreply.github.com> Date: Thu, 4 Nov 2021 17:23:29 +0100 Subject: [PATCH 28/31] CSAR additions by Shagrat Added functionality for Casevac --- Moose Development/Moose/Ops/CSAR.lua | 149 ++++++++++++++++++++++----- 1 file changed, 124 insertions(+), 25 deletions(-) diff --git a/Moose Development/Moose/Ops/CSAR.lua b/Moose Development/Moose/Ops/CSAR.lua index b166009df..bd874e608 100644 --- a/Moose Development/Moose/Ops/CSAR.lua +++ b/Moose Development/Moose/Ops/CSAR.lua @@ -45,6 +45,7 @@ -- * Object oriented refactoring of Ciribob\'s fantastic CSAR script. -- * No need for extra MIST loading. -- * Additional events to tailor your mission. +-- * Optional SpawnCASEVAC to create casualties without beacon (e.g. handling dead ground vehicles and create CASVAC requests). -- -- ## 0. Prerequisites -- @@ -105,7 +106,7 @@ -- self.countryblue= country.id.USA -- self.countryred = country.id.RUSSIA -- self.countryneutral = country.id.UN_PEACEKEEPERS --- +-- -- ## 2.1 Experimental Features -- -- WARNING - Here\'ll be dragons! @@ -115,7 +116,9 @@ -- self.SRSPath = "E:\\Progra~1\\DCS-SimpleRadio-Standalone\\" -- adjust your own path in your SRS installation -- server(!) -- self.SRSchannel = 300 -- radio channel -- self.SRSModulation = radio.modulation.AM -- modulation --- +-- -- +-- self.csarUsePara = false -- If set to true, will use the LandingAfterEjection Event instead of Ejection --shagrat +-- -- ## 3. Results -- -- Number of successful landings with save pilots and aggregated number of saved pilots is stored in these variables in the object: @@ -175,6 +178,8 @@ -- -- Create downed "Pilot Wagner" in #ZONE "CSAR_Start_1" at a random point for the blue coalition -- my_csar:SpawnCSARAtZone( "CSAR_Start_1", coalition.side.BLUE, "Pilot Wagner", true ) -- +-- --Create a casualty and CASEVAC request from a "Point" (VEC2) for the blue coalition --shagrat +-- my_csar:SpawnCASEVAC(Point, coalition.side.BLUE) -- -- @field #CSAR CSAR = { @@ -238,11 +243,11 @@ CSAR.AircraftType["Mi-8MTV2"] = 12 CSAR.AircraftType["Mi-8MT"] = 12 CSAR.AircraftType["Mi-24P"] = 8 CSAR.AircraftType["Mi-24V"] = 8 -CSAR.AircraftType["Bell-47"] = 2 +CSAR.AircraftType["Bell-47"] = 2 --- CSAR class version. -- @field #string version -CSAR.version="0.1.11r2" +CSAR.version="0.1.12r2" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToDo list @@ -250,7 +255,7 @@ CSAR.version="0.1.11r2" -- DONE: SRS Integration (to be tested) -- TODO: Maybe - add option to smoke/flare closest MASH - +-- TODO: shagrat Add cargoWeight to helicopter when pilot boarded ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Constructor ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -380,7 +385,10 @@ function CSAR:New(Coalition, Template, Alias) self.countryblue= country.id.USA self.countryred = country.id.RUSSIA self.countryneutral = country.id.UN_PEACEKEEPERS - + + -- added 0.1.3 + self.csarUsePara = true -- shagrat set to true, will use the LandingAfterEjection Event instead of Ejection + -- WARNING - here\'ll be dragons -- for this to work you need to de-sanitize your mission environment in \Scripts\MissionScripting.lua -- needs SRS => 1.9.6 to work (works on the *server* side) @@ -645,10 +653,14 @@ function CSAR:_AddCsar(_coalition , _country, _point, _typeName, _unitName, _pla local _typeName = _typeName or "Pilot" if not noMessage then - self:_DisplayToAllSAR("MAYDAY MAYDAY! " .. _typeName .. " is down. ", self.coalition, self.messageTime) + if _freq ~= 0 then --shagrat different CASEVAC msg + self:_DisplayToAllSAR("MAYDAY MAYDAY! " .. _typeName .. " is down. ", self.coalition, self.messageTime) + else + self:_DisplayToAllSAR("Troops In Contact. " .. _typeName .. " requests CASEVAC. ", self.coalition, self.messageTime) + end end - if _freq then + if (_freq and _freq ~= 0) then --shagrat only add beacon if _freq is NOT 0 self:_AddBeaconToGroup(_spawnedGroup, _freq) end @@ -657,10 +669,18 @@ function CSAR:_AddCsar(_coalition , _country, _point, _typeName, _unitName, _pla local _text = _description if not forcedesc then if _playerName ~= nil then - _text = "Pilot " .. _playerName + if _freq ~= 0 then --shagrat + _text = "Pilot " .. _playerName + else + _text = "TIC - " .. _playerName + end elseif _unitName ~= nil then - _text = "AI Pilot of " .. _unitName - end + if _freq ~= 0 then --shagrat + _text = "AI Pilot of " .. _unitName + else + _text = "TIC - " .. _unitName + end + end end self:T({_spawnedGroup, _alias}) @@ -668,7 +688,7 @@ function CSAR:_AddCsar(_coalition , _country, _point, _typeName, _unitName, _pla self:_CreateDownedPilotTrack(_spawnedGroup,_GroupName,_coalition,_unitName,_text,_typeName,_freq,_playerName) - self:_InitSARForPilot(_spawnedGroup, _GroupName, _freq, noMessage) + self:_InitSARForPilot(_spawnedGroup, _unitName, _freq, noMessage) --shagrat use unitName to have the aircraft callsign / descriptive "name" etc. return self end @@ -737,6 +757,58 @@ function CSAR:SpawnCSARAtZone(Zone, Coalition, Description, RandomPoint, Nomessa return self end +--- (Internal) Function to add a CSAR object into the scene at a Point coordinate (VEC_2). For mission designers wanting to add e.g. casualties to the scene, that don't use beacons. +-- @param #CSAR self +-- @param #string _Point a POINT_VEC2. +-- @param #number _coalition Coalition. +-- @param #string _description (optional) Description. +-- @param #boolean _nomessage (optional) If true, don\'t send a message to SAR. +-- @param #string unitname (optional) Name of the lost unit. +-- @param #string typename (optional) Type of plane. +-- @param #boolean forcedesc (optional) Force to use the description passed only for the pilot track entry. Use to have fully custom names. +function CSAR:_SpawnCASEVAC( _Point, _coalition, _description, _nomessage, unitname, typename, forcedesc) --shagrat added internal Function _SpawnCASEVAC + self:T(self.lid .. " _SpawnCASEVAC") + + local _description = _description or "CASEVAC" + local unitname = unitname or "CASEVAC" + local typename = typename or "Ground Commander" + + local pos = {} + pos = _Point + + local _country = 0 + if _coalition == coalition.side.BLUE then + _country = self.countryblue + elseif _coalition == coalition.side.RED then + _country = self.countryred + else + _country = self.countryneutral + end + --shagrat set frequency to 0 as "flag" for no beacon + self:_AddCsar(_coalition, _country, pos, typename, unitname, _description, 0, _nomessage, _description, forcedesc) + + return self +end + +--- Function to add a CSAR object into the scene at a zone coordinate. For mission designers wanting to add e.g. PoWs to the scene. +-- @param #CSAR self +-- @param #string Point a POINT_VEC2. +-- @param #number Coalition Coalition. +-- @param #string Description (optional) Description. +-- @param #boolean addBeacon (optional) yes or no. +-- @param #boolean Nomessage (optional) If true, don\'t send a message to SAR. +-- @param #string Unitname (optional) Name of the lost unit. +-- @param #string Typename (optional) Type of plane. +-- @param #boolean Forcedesc (optional) Force to use the **description passed only** for the pilot track entry. Use to have fully custom names. +-- @usage If missions designers want to spawn downed pilots into the field, e.g. at mission begin, to give the helicopter guys work, they can do this like so: +-- +-- -- Create casualty "CASEVAC" at Point #POINT_VEC2 for the blue coalition. +-- my_csar:SpawnCASEVAC( POINT_VEC2, coalition.side.BLUE ) +function CSAR:SpawnCASEVAC(Point, Coalition, Description, Nomessage, Unitname, Typename, Forcedesc) + self:_SpawnCASEVAC(Point, Coalition, Description, Nomessage, Unitname, Typename, Forcedesc) + return self +end --shagrat end added CASEVAC + --- (Internal) Event handler. -- @param #CSAR self function CSAR:_EventHandler(EventData) @@ -748,7 +820,7 @@ function CSAR:_EventHandler(EventData) -- no event if _event == nil or _event.initiator == nil then return false - + -- take off elseif _event.id == EVENTS.Takeoff then -- taken off self:T(self.lid .. " Event unit - Takeoff") @@ -824,12 +896,12 @@ function CSAR:_EventHandler(EventData) local _unit = _event.IniUnit local _unitname = _event.IniUnitName local _group = _event.IniGroup - + if _unit == nil then return -- error! end - - local _coalition = _unit:GetCoalition() + + local _coalition = _unit:GetCoalition() if _coalition ~= self.coalition then return --ignore! end @@ -852,11 +924,27 @@ function CSAR:_EventHandler(EventData) return end - -- all checks passed, get going. - local _freq = self:_GenerateADFFrequency() - self:_AddCsar(_coalition, _unit:GetCountry(), _unit:GetCoordinate() , _unit:GetTypeName(), _unit:GetName(), _event.IniPlayerName, _freq, false, "none") - - return true + -- all checks passed, get going. + if self.csarUsePara == false then --shagrat check parameter LandingAfterEjection, if true don't spawn a Pilot from EJECTION event, wait for the Chute to land + local _freq = self:_GenerateADFFrequency() + self:_AddCsar(_coalition, _unit:GetCountry(), _unit:GetCoordinate() , _unit:GetTypeName(), _unit:GetName(), _event.IniPlayerName, _freq, false, "none") + return true + end + ---- shagrat on event LANDING_AFTER_EJECTION spawn pilot at parachute location + elseif (_event.id == EVENTS.LandingAfterEjection and self.csarUsePara == true) then + self:I({EVENT=_event}) + local _LandingPos = COORDINATE:NewFromVec3(_event.initiator:getPosition().p) + local _unitname = "Aircraft" --_event.initiator:getName() or "Aircraft" --shagrat Optional use of Object name which is unfortunately 'f15_Pilot_Parachute' + local _typename = "Ejected Pilot" --_event.Initiator.getTypeName() or "Ejected Pilot" + local _country = _event.initiator:getCountry() + local _coalition = coalition.getCountryCoalition( _country ) + local _freq = self:_GenerateADFFrequency() + self:I({coalition=_coalition,country= _country, coord=_LandingPos, name=_unitname, player=_event.IniPlayerName, freq=_freq}) + self:_AddCsar(_coalition, _country, _LandingPos, nil, _unitname, _event.IniPlayerName, _freq, false, "none")--shagrat add CSAR at Parachute location. + + Unit.destroy(_event.initiator) -- shagrat remove static Pilot model + + return true elseif _event.id == EVENTS.Land then self:T(self.lid .. " Landing") @@ -921,8 +1009,13 @@ function CSAR:_InitSARForPilot(_downedGroup, _GroupName, _freq, _nomessage) local _leadername = _leader:GetName() if not _nomessage then - local _text = string.format("%s requests SAR at %s, beacon at %.2f KHz", _leadername, _coordinatesText, _freqk) - self:_DisplayToAllSAR(_text,self.coalition,self.messageTime) + if _freq ~= 0 then --shagrat + local _text = string.format("%s requests SAR at %s, beacon at %.2f KHz", _groupName, _coordinatesText, _freqk)--shagrat _groupName to prevent 'f15_Pilot_Parachute' + self:_DisplayToAllSAR(_text,self.coalition,self.messageTime) + else --shagrat CASEVAC msg + local _text = string.format("Pickup Zone at %s.", _coordinatesText ) + self:_DisplayToAllSAR(_text,self.coalition,self.messageTime) + end end for _,_heliName in pairs(self.csarUnits) do @@ -1060,7 +1153,7 @@ function CSAR:_PopSmokeForGroup(_woundedGroupName, _woundedLeader) if _lastSmoke == nil or timer.getTime() > _lastSmoke then local _smokecolor = self.smokecolor - local _smokecoord = _woundedLeader:GetCoordinate() + local _smokecoord = _woundedLeader:GetCoordinate():Translate( 6, math.random( 1, 360) ) --shagrat place smoke at a random 6 m distance, so smoke does not obscure the pilot _smokecoord:Smoke(_smokecolor) self.smokeMarkers[_woundedGroupName] = timer.getTime() + 300 -- next smoke time end @@ -1435,7 +1528,11 @@ function CSAR:_DisplayActiveSAR(_unitName) else 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) }) + if _value.frequency == 0 then--shagrat insert CASEVAC without Frequency + table.insert(_csarList, { dist = _distance, msg = string.format("%s at %s - %s ", _value.desc, _coordinatesText, distancetext) }) + else + table.insert(_csarList, { dist = _distance, msg = string.format("%s at %s - %.2f KHz ADF - %s ", _value.desc, _coordinatesText, _value.frequency / 1000, distancetext) }) + end end end @@ -1876,6 +1973,7 @@ function CSAR:onafterStart(From, Event, To) self:HandleEvent(EVENTS.Takeoff, self._EventHandler) self:HandleEvent(EVENTS.Land, self._EventHandler) self:HandleEvent(EVENTS.Ejection, self._EventHandler) + self:HandleEvent(EVENTS.LandingAfterEjection, self._EventHandler) --shagrat self:HandleEvent(EVENTS.PlayerEnterAircraft, self._EventHandler) self:HandleEvent(EVENTS.PlayerEnterUnit, self._EventHandler) self:HandleEvent(EVENTS.PilotDead, self._EventHandler) @@ -1986,6 +2084,7 @@ function CSAR:onafterStop(From, Event, To) self:UnHandleEvent(EVENTS.Takeoff) self:UnHandleEvent(EVENTS.Land) self:UnHandleEvent(EVENTS.Ejection) + self:UnHandleEvent(EVENTS.LandingAfterEjection) -- shagrat self:UnHandleEvent(EVENTS.PlayerEnterUnit) self:UnHandleEvent(EVENTS.PlayerEnterAircraft) self:UnHandleEvent(EVENTS.PilotDead) From a149ff17059c8fe23ad987807dfebc5a58972de5 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sat, 6 Nov 2021 15:15:30 +0100 Subject: [PATCH 29/31] Switch for fratricide and treason (coalition changes) --- .../Moose/Functional/Scoring.lua | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/Moose Development/Moose/Functional/Scoring.lua b/Moose Development/Moose/Functional/Scoring.lua index b92c7e789..2d6c263f9 100644 --- a/Moose Development/Moose/Functional/Scoring.lua +++ b/Moose Development/Moose/Functional/Scoring.lua @@ -284,9 +284,11 @@ function SCORING:New( GameName ) -- Default fratricide penalty level (maximum penalty that can be assigned to a player before he gets kicked). self:SetFratricide( self.ScaleDestroyPenalty * 3 ) + self.penaltyonfratricide = true -- Default penalty when a player changes coalition. self:SetCoalitionChangePenalty( self.ScaleDestroyPenalty ) + self.penaltyoncoalitionchange = true self:SetDisplayMessagePrefix() @@ -582,6 +584,23 @@ function SCORING:SetFratricide( Fratricide ) return self end +--- Decide if Fratricide is leading to penalties (true) or not (fals) +-- @param #SCORING self +-- @param #boolean OnOff Switch for Fratricide +-- @return #SCORING +function SCORING:SwitchFratricide( OnOff ) + self.penaltyonfratricide = OnOff + return self +end + +--- Decide if Coalition Changes is leading to penalties (true) or not (fals) +-- @param #SCORING self +-- @param #boolean OnOff Switch for Coalition Changes. +-- @return #SCORING +function SCORING:SwitchTreason( OnOff ) + self.penaltyoncoalitionchange = OnOff + return self +end --- When a player changes the coalition, he can receive a penalty score. -- Use the method @{#SCORING.SetCoalitionChangePenalty}() to define the penalty when a player changes coalition. @@ -647,8 +666,9 @@ function SCORING:_AddPlayerFromUnit( UnitData ) if not self.Players[PlayerName].UnitCoalition then self.Players[PlayerName].UnitCoalition = UnitCoalition else - if self.Players[PlayerName].UnitCoalition ~= UnitCoalition then - self.Players[PlayerName].Penalty = self.Players[PlayerName].Penalty + 50 + -- TODO: switch for coalition changes, make penalty alterable + if self.Players[PlayerName].UnitCoalition ~= UnitCoalition and self.penaltyoncoalitionchange then + self.Players[PlayerName].Penalty = self.Players[PlayerName].Penalty + self.CoalitionChangePenalty or 50 self.Players[PlayerName].PenaltyCoalition = self.Players[PlayerName].PenaltyCoalition + 1 MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' changed coalition from " .. _SCORINGCoalition[self.Players[PlayerName].UnitCoalition] .. " to " .. _SCORINGCoalition[UnitCoalition] .. "(changed " .. self.Players[PlayerName].PenaltyCoalition .. " times the coalition). 50 Penalty points added.", @@ -666,8 +686,9 @@ function SCORING:_AddPlayerFromUnit( UnitData ) self.Players[PlayerName].UNIT = UnitData self.Players[PlayerName].ThreatLevel = UnitThreatLevel self.Players[PlayerName].ThreatType = UnitThreatType - - if self.Players[PlayerName].Penalty > self.Fratricide * 0.50 then + + -- TODO: make fratricide switchable + if self.Players[PlayerName].Penalty > self.Fratricide * 0.50 and self.penaltyonfratricide then if self.Players[PlayerName].PenaltyWarning < 1 then MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "': WARNING! If you continue to commit FRATRICIDE and have a PENALTY score higher than " .. self.Fratricide .. ", you will be COURT MARTIALED and DISMISSED from this mission! \nYour total penalty is: " .. self.Players[PlayerName].Penalty, MESSAGE.Type.Information @@ -676,7 +697,7 @@ function SCORING:_AddPlayerFromUnit( UnitData ) end end - if self.Players[PlayerName].Penalty > self.Fratricide then + if self.Players[PlayerName].Penalty > self.Fratricide and self.penaltyonfratricide then MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' committed FRATRICIDE, he will be COURT MARTIALED and is DISMISSED from this mission!", MESSAGE.Type.Information ):ToAll() From eeeab869526f79cc0218ca89d8c0e4832513bfa5 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sat, 6 Nov 2021 18:07:42 +0100 Subject: [PATCH 30/31] added altered message --- Moose Development/Moose/Functional/Scoring.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Moose Development/Moose/Functional/Scoring.lua b/Moose Development/Moose/Functional/Scoring.lua index 2d6c263f9..9b91d6d7a 100644 --- a/Moose Development/Moose/Functional/Scoring.lua +++ b/Moose Development/Moose/Functional/Scoring.lua @@ -671,10 +671,10 @@ function SCORING:_AddPlayerFromUnit( UnitData ) self.Players[PlayerName].Penalty = self.Players[PlayerName].Penalty + self.CoalitionChangePenalty or 50 self.Players[PlayerName].PenaltyCoalition = self.Players[PlayerName].PenaltyCoalition + 1 MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' changed coalition from " .. _SCORINGCoalition[self.Players[PlayerName].UnitCoalition] .. " to " .. _SCORINGCoalition[UnitCoalition] .. - "(changed " .. self.Players[PlayerName].PenaltyCoalition .. " times the coalition). 50 Penalty points added.", + "(changed " .. self.Players[PlayerName].PenaltyCoalition .. " times the coalition). ".. self.CoalitionChangePenalty .."Penalty points added.", MESSAGE.Type.Information ):ToAll() - self:ScoreCSV( PlayerName, "", "COALITION_PENALTY", 1, -50, self.Players[PlayerName].UnitName, _SCORINGCoalition[self.Players[PlayerName].UnitCoalition], _SCORINGCategory[self.Players[PlayerName].UnitCategory], self.Players[PlayerName].UnitType, + self:ScoreCSV( PlayerName, "", "COALITION_PENALTY", 1, -1*self.CoalitionChangePenalty, self.Players[PlayerName].UnitName, _SCORINGCoalition[self.Players[PlayerName].UnitCoalition], _SCORINGCategory[self.Players[PlayerName].UnitCategory], self.Players[PlayerName].UnitType, UnitName, _SCORINGCoalition[UnitCoalition], _SCORINGCategory[UnitCategory], UnitData:GetTypeName() ) end end @@ -1142,9 +1142,9 @@ function SCORING:_EventOnHit( Event ) if InitCoalition then -- A coalition object was hit, probably a static. if InitCoalition == TargetCoalition then -- TODO: Penalty according scale - Player.Penalty = Player.Penalty + 10 - PlayerHit.Penalty = PlayerHit.Penalty + 10 - PlayerHit.PenaltyHit = PlayerHit.PenaltyHit + 1 + Player.Penalty = Player.Penalty + 10 --* self.ScaleDestroyPenalty + PlayerHit.Penalty = PlayerHit.Penalty + 10 --* self.ScaleDestroyPenalty + PlayerHit.PenaltyHit = PlayerHit.PenaltyHit + 1 * self.ScaleDestroyPenalty MESSAGE :NewType( self.DisplayMessagePrefix .. "Player '" .. Event.WeaponPlayerName .. "' hit friendly target " .. From 21a93652cd411f62703d8451370b0e31c8b8a0f3 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 7 Nov 2021 16:08:11 +0100 Subject: [PATCH 31/31] Update OpsGroup.lua - Fix for AUFTRAG carried out only after the group has passed all waypoints --- Moose Development/Moose/Ops/OpsGroup.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index a7a75c28f..e581c5f11 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -2903,9 +2903,12 @@ function OPSGROUP:RouteToMission(mission, delay) if self.isGround and mission.optionFormation then formation=mission.optionFormation end + + -- UID of this waypoint. + local uid=self:GetWaypointCurrent().uid -- Add waypoint. - local waypoint=self:AddWaypoint(waypointcoord, SpeedToMission, nil, formation, false) + local waypoint=self:AddWaypoint(waypointcoord, SpeedToMission, uid, formation, false) -- Add waypoint task. UpdateRoute is called inside. local waypointtask=self:AddTaskWaypoint(mission.DCStask, waypoint, mission.name, mission.prio, mission.duration)